diff --git a/README.rst b/README.rst index cf47bee0..8bbff14d 100644 --- a/README.rst +++ b/README.rst @@ -105,3 +105,36 @@ These are: The start of the filename is the ticket number, and the content is what will end up in the news file. For example, if ticket #850 is about adding a new widget, the filename would be ``myproject/newsfragments/850.feature`` and the content would be ``myproject.widget has been added``. + + +Further Options +--------------- + +Towncrier has the following global options, which can be specified in the toml file: + +``` +[tool.towncrier] + package = "" + package_dir = "." + single_file = true # if false, filename is formatted like `title_format`. + filename = "NEWS.rst" + directory = "directory/of/news/fragments" + template = "path/to/template.rst" + start_line = "start of generated content" + title_format = "{name} {version} ({project_date})" # or false if template includes title + issue_format = "format string for {issue} (issue is the first part of fragment name)" + underlines: "=-~" + wrap = false # Wrap text to 79 characters + all_bullets = true # make all fragments bullet points +``` +If a single file is used, the content of this file are overwritten each time. + +Furthermore, you can add your own fragment types using: +``` +[tool.towncrier] + [[tool.towncrier.type]] + directory = "deprecation" + name = "Deprecations" + showcontent = true +``` + diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index d862cec5..02bea66b 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -117,7 +117,7 @@ def prefixed_lines(): # Takes the output from find_fragments above. Probably it would be useful to # add an example output here. Next time someone digs deep enough to figure it # out, please do so... -def split_fragments(fragments, definitions): +def split_fragments(fragments, definitions, all_bullets=True): output = OrderedDict() @@ -126,7 +126,14 @@ def split_fragments(fragments, definitions): for (ticket, category, counter), content in section_fragments.items(): - content = indent(content.strip(), u" ")[2:] + if all_bullets: + # By default all fragmetns are append by "-" automatically, + # and need to be indented because of that. + # (otherwise, assume they are formatted correctly) + content = indent(content.strip(), u" ")[2:] + else: + # Assume the text is formatted correctly + content = content.rstrip() if definitions[category]["showcontent"] is False: content = u"" @@ -161,6 +168,17 @@ def entry_key(entry): return [issue_key(issue) for issue in issues] +def bullet_key(entry): + text, _ = entry + if text[0] == u"-": + return 0 + elif text[0] == "*": + return 1 + elif text[:2] == u"#.": + return 2 + return 3 + + def render_issue(issue_format, issue): if issue_format is None: try: @@ -181,6 +199,7 @@ def render_fragments( wrap, versiondata, top_underline="=", + all_bullets=False, ): """ Render the fragments into a news file. @@ -213,6 +232,8 @@ def render_fragments( # - Fix the other thing (#1) # - Fix the thing (#2, #7, #123) entries.sort(key=entry_key) + if not all_bullets: + entries.sort(key=bullet_key) # Then we put these nicely sorted entries back in an ordered dict # for the template, after formatting each issue number @@ -225,12 +246,25 @@ def render_fragments( done = [] + def get_indent(text): + # If bullets are not assumed and we wrap, the subsequent + # indentation depends on whether or not this is a bullet point. + # (it is probably usually best to disable wrapping in that case) + if not text: + return u"" + if all_bullets or text[0] in "-*": + return u" " + elif text[:2] == "#.": + return u" " + return u"" + res = jinja_template.render( sections=data, definitions=definitions, underlines=underlines, versiondata=versiondata, top_underline=top_underline, + get_indent=get_indent, # simplify indentation in the jinja template. ) for line in res.split(u"\n"): @@ -239,7 +273,7 @@ def render_fragments( textwrap.fill( line, width=79, - subsequent_indent=u" ", + subsequent_indent=get_indent(line), break_long_words=False, break_on_hyphens=False, ) diff --git a/src/towncrier/_settings.py b/src/towncrier/_settings.py index d5093deb..2795babc 100644 --- a/src/towncrier/_settings.py +++ b/src/towncrier/_settings.py @@ -64,9 +64,18 @@ def parse_toml(config): else: wrap = False + single_file = config.get("single_file", True) + if not isinstance(single_file, bool): + raise ValueError("`single_file` option must be a boolean: false or true.") + + all_bullets = config.get("all_bullets", True) + if not isinstance(all_bullets, bool): + raise ValueError("`all_bullets` must be boolean: false or true.") + return { "package": config.get("package", ""), "package_dir": config.get("package_dir", "."), + "single_file": single_file, "filename": config.get("filename", "NEWS.rst"), "directory": config.get("directory"), "sections": sections, @@ -77,4 +86,5 @@ def parse_toml(config): "issue_format": config.get("issue_format"), "underlines": config.get("underlines", _underlines), "wrap": wrap, + "all_bullets": all_bullets, } diff --git a/src/towncrier/_writer.py b/src/towncrier/_writer.py index d0da0317..5f877dff 100644 --- a/src/towncrier/_writer.py +++ b/src/towncrier/_writer.py @@ -11,17 +11,19 @@ import os -def append_to_newsfile(directory, filename, start_line, top_line, content): +def append_to_newsfile(directory, filename, start_line, top_line, content, single_file=True): news_file = os.path.join(directory, filename) - if not os.path.exists(news_file): - existing_content = u"" + if single_file: + if not os.path.exists(news_file): + existing_content = u"" + else: + with open(news_file, "rb") as f: + existing_content = f.read().decode("utf8") + existing_content = existing_content.split(start_line, 1) else: - with open(news_file, "rb") as f: - existing_content = f.read().decode("utf8") - - existing_content = existing_content.split(start_line, 1) + existing_content = [u""] if top_line and top_line in existing_content: raise ValueError("It seems you've already produced newsfiles for this version?") diff --git a/src/towncrier/build.py b/src/towncrier/build.py index 3869afcf..e1fe9b43 100644 --- a/src/towncrier/build.py +++ b/src/towncrier/build.py @@ -114,7 +114,7 @@ def __main( ) click.echo("Rendering news fragments...", err=to_err) - fragments = split_fragments(fragments, definitions) + fragments = split_fragments(fragments, definitions, all_bullets=config["all_bullets"]) if project_version is None: project_version = get_version( @@ -152,6 +152,7 @@ def __main( config["wrap"], {"name": project_name, "version": project_version, "date": project_date}, top_underline=config["underlines"][0], + all_bullets=config["all_bullets"], ) if draft: @@ -167,12 +168,20 @@ def __main( else: click.echo("Writing to newsfile...", err=to_err) start_line = config["start_line"] + news_file = config["filename"] + if not config["single_file"]: + news_file = news_file.format( + name=project_name, + version=project_version, + project_date=project_date, + ) append_to_newsfile( - directory, config["filename"], start_line, top_line, rendered + directory, news_file, start_line, top_line, rendered, + single_file=config["single_file"] ) click.echo("Staging newsfile...", err=to_err) - stage_newsfile(directory, config["filename"]) + stage_newsfile(directory, news_file) click.echo("Removing news fragments...", err=to_err) remove_files(fragment_filenames, answer_yes) diff --git a/src/towncrier/newsfragments/158.feature b/src/towncrier/newsfragments/158.feature new file mode 100644 index 00000000..4d31938b --- /dev/null +++ b/src/towncrier/newsfragments/158.feature @@ -0,0 +1,8 @@ +There is now the option for ``single_file = false`` and ``all_bullets = false`` +in the configuration. Disabling ``single_file`` causes the filename to be +formatable using `name`, `version` and `project_date`. Setting ``all_bullets`` +to false means that news fragments have to include the bullet point if they +should be rendered as enumerations, otherwise they are rendered directly +(this means fragments can include a header.). +The ``template-single-file-no-bullets.rst`` file gives an example template +using these options. diff --git a/src/towncrier/templates/template-single-file-no-bullets.rst b/src/towncrier/templates/template-single-file-no-bullets.rst new file mode 100644 index 00000000..dcb1812d --- /dev/null +++ b/src/towncrier/templates/template-single-file-no-bullets.rst @@ -0,0 +1,38 @@ +{% set title = "{} {} Release Notes".format(versiondata.name, versiondata.version) %} +{{ "=" * title|length }} +{{ title }} +{{ "=" * title|length }} + +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %}{% if section %}{{ section }} +{{ underline * section|length }}{% set underline = underlines[1] %} + +{% endif %} +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section] %} + +{{ definitions[category]['name'] }} +{{ underline * definitions[category]['name']|length }} + +{% if definitions[category]['showcontent'] %} +{% for text, values in sections[section][category].items() %} +{{ text }} +{{ get_indent(text) }}({{values|join(', ') }}) + +{% endfor %} +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% if sections[section][category]|length == 0 %} +No significant changes. + +{% else %} +{% endif %} +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %}