Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown output #439

Closed
wants to merge 11 commits into from
8 changes: 7 additions & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ Top level keys
- ``issue_format`` -- A format string for rendering the issue/ticket number in newsfiles.
``#{issue}`` by default.
- ``underlines`` -- The characters used for underlining headers.
``["=", "-", "~"]`` by default.
``["=", "-", "~"]`` by default (unless ``filename`` has an ``.md`` extension, in which case it's ``["", "", ""]``).
- ``title_prefix`` -- Strings to prefix to the title, sections, and categories.
``["", "", ""]`` by default (unless ``filename`` has an ``.md`` extension, in which case it is ``["# ", "## ", "### "]``).
- ``bullet`` -- The bullet string to use before issues.
``"- "`` by default (unless ``filename`` has an ``.md`` extension, in which case it is ``" - "``).
- ``issues_spacing`` -- Add an empty newline between issues when rendering.
``False`` by default (unless ``filename`` has an ``.md`` extension, in which case it is ``True``).


Custom fragment types
Expand Down
5 changes: 4 additions & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Tutorial
========

This tutorial assumes you have a Python project with a *reStructuredText* (rst) news file (also known as changelog) file that you wish to use ``towncrier`` on, to generate its news file.
This tutorial assumes you have a Python project with a *reStructuredText* (rst) or *markdown* (md) news file (also known as changelog) file that you wish to use ``towncrier`` on, to generate its news file.
It will cover setting up your project with a basic configuration, which you can then feel free to `customize <customization/index.html>`_.

Install from PyPI::
Expand Down Expand Up @@ -145,6 +145,9 @@ You should get an output similar to this::
- #1, #2


Note: if you configure a markdown file (for example, ``filename = "CHANGES.md"`` in your configuration file), the titles will be output in markdown format instead.


Producing News Files In Production
----------------------------------

Expand Down
16 changes: 12 additions & 4 deletions src/towncrier/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def prefixed_lines() -> Iterator[str]:
def split_fragments(
fragments: Mapping[str, Mapping[tuple[str, str, int], str]],
definitions: Mapping[str, Mapping[str, Any]],
all_bullets: bool = True,
bullet_indent: int | None = 2,
) -> Mapping[str, Mapping[str, Mapping[str, Sequence[str]]]]:

output = OrderedDict()
Expand All @@ -173,11 +173,11 @@ def split_fragments(

for (ticket, category, counter), content in section_fragments.items():

if all_bullets:
# By default all fragmetns are append by "-" automatically,
if bullet_indent:
# By default all fragments are append by "- " automatically,
# and need to be indented because of that.
# (otherwise, assume they are formatted correctly)
content = indent(content.strip(), " ")[2:]
content = indent(content.strip(), " " * bullet_indent)[bullet_indent:]
else:
# Assume the text is formatted correctly
content = content.rstrip()
Expand Down Expand Up @@ -251,6 +251,9 @@ def render_fragments(
top_underline: str = "=",
all_bullets: bool = False,
render_title: bool = True,
title_prefixes: Sequence[str] = ["", "", ""],
bullet: str = "- ",
issues_spaced: bool = False,
) -> str:
"""
Render the fragments into a news file.
Expand Down Expand Up @@ -315,6 +318,11 @@ def get_indent(text: str) -> str:
versiondata=versiondata,
top_underline=top_underline,
get_indent=get_indent, # simplify indentation in the jinja template.
title_prefix=title_prefixes[0],
section_prefix=title_prefixes[1],
category_prefix=title_prefixes[2],
bullet=bullet,
issues_spaced=issues_spaced,
)

for line in res.split("\n"):
Expand Down
29 changes: 27 additions & 2 deletions src/towncrier/_settings/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ class Config:
title_format: str | Literal[False]
issue_format: str | None
underlines: list[str]
title_prefixes: list[str]
wrap: bool
all_bullets: bool
orphan_prefix: str
bullet: str
issues_spaced: bool


class ConfigError(Exception):
Expand All @@ -60,6 +63,21 @@ def __init__(self, *args: str, **kwargs: str):
_title_format = None
_template_fname = "towncrier:default"
_underlines = ["=", "-", "~"]
_md_underlines = ["", "", ""]
_prefixes = ["", "", ""]
_md_prefixes = ["# ", "## ", "### "]
_bullet = "- "
_md_bullet = " - "


# The default options differ when using a filename that has an extension of ``.md``:

# Firstly, no underlines are used, but instead markdown h1, h2 and h3 prefixes are used
# for the title, categories, and sections respectively.

# Secondly, Python's standard markdown implementation needs indentation and an empty
# line between bullets to render multi-line bullets correctly, so the bullet
# representation is ``" - "`` and the ``issues_spaced`` defaults to True.


def load_config_from_options(
Expand Down Expand Up @@ -164,11 +182,13 @@ def parse_toml(base_path: str, config: Mapping[str, Any]) -> Config:
failing_option="template",
)

filename = config.get("filename", "NEWS.rst")
is_md = filename.endswith(".md")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have all the if_md values and default selection in a common block.

Is ok to even move this to a separate function like `defaults = _get_missing_defaults(config)"

Suggested change
is_md = filename.endswith(".md")
underlines = config.get("underlines", "")
title_prefixes=config.get("title_prefixes", "")
if filename.endswith(".md"):
# Try to use markdown default.
if no underlines:
underlliens = ["", "", ""]
if no underlines:
# Fallback to rst defaults.
underlines = ["=", "-", "~"]

And I hope we don't need md_prefixes and md_bullet, as we can handle all this in the custom tempalates/default.md

What do you think?

return Config(
package=config.get("package", ""),
package_dir=config.get("package_dir", "."),
single_file=single_file,
filename=config.get("filename", "NEWS.rst"),
filename=filename,
directory=config.get("directory"),
version=config.get("version"),
name=config.get("name"),
Expand All @@ -178,8 +198,13 @@ def parse_toml(base_path: str, config: Mapping[str, Any]) -> Config:
start_string=config.get("start_string", _start_string),
title_format=config.get("title_format", _title_format),
issue_format=config.get("issue_format"),
underlines=config.get("underlines", _underlines),
underlines=config.get("underlines", _md_underlines if is_md else _underlines),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it easier to read.

Suggested change
underlines=config.get("underlines", _md_underlines if is_md else _underlines),
underlines=underlines,

title_prefixes=config.get(
"title_prefixes", _md_prefixes if is_md else _prefixes
),
wrap=wrap,
bullet=_md_bullet if is_md else _bullet,
issues_spaced=is_md,
all_bullets=all_bullets,
orphan_prefix=config.get("orphan_prefix", "+"),
)
7 changes: 6 additions & 1 deletion src/towncrier/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ def __main(

click.echo("Rendering news fragments...", err=to_err)
fragments = split_fragments(
fragment_contents, config.types, all_bullets=config.all_bullets
fragment_contents,
config.types,
bullet_indent=config.all_bullets and len(config.bullet),
)

if project_version is None:
Expand Down Expand Up @@ -193,6 +195,9 @@ def __main(
top_underline=config.underlines[0],
all_bullets=config.all_bullets,
render_title=render_title_with_fragments,
title_prefixes=config.title_prefixes,
bullet=config.bullet,
issues_spaced=config.issues_spaced,
)

if render_title_separately:
Expand Down
11 changes: 11 additions & 0 deletions src/towncrier/newsfragments/439.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
We now have easy markdown-compatible rendering!

When the configured filename has a ``.md`` extension: the title, sections, and
newsfragment categories are output respectively with #, ## and ### markdown
headings prefixed (and without any underlines). Bulleted issues are also
indented, with multi-line spaces between to rendered correctly with Python's
standard markdown implementation.

The default template no longer renders an empty newline for empty underlines
configurations, and templates are rendered with extra context for ``bullet``,
``title_prefix``, ``section_prefix``, and ``category_prefix``.
35 changes: 23 additions & 12 deletions src/towncrier/templates/default.rst
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
{% if render_title %}
{% set version_title %}
{% if versiondata.name %}
{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}}
{% else %}
{{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}
{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }}){% else %}
{{ versiondata.version }} ({{ versiondata.date }}){% endif %}
{% endset %}
{{ title_prefix }}{{ version_title }}
{% if top_underline %}
{{ top_underline * (version_title|length) }}
{% endif %}
{% endif %}
{% for section, _ in sections.items() %}
{% set underline = underlines[0] %}{% if section %}{{section}}
{{ underline * section|length }}{% set underline = underlines[1] %}

{% set underline = underlines[0] %}
{% if section %}
{{ section_prefix }}{{ section }}
{% if underline %}
{{ underline * section|length }}
{% endif %}
{% set underline = underlines[1] %}
{% endif %}

{% if sections[section] %}
{% for category, val in definitions.items() if category in sections[section]%}
{{ definitions[category]['name'] }}
{{ category_prefix }}{{ definitions[category]['name'] }}
{% if underline %}
{{ underline * definitions[category]['name']|length }}
{% endif %}

{% if definitions[category]['showcontent'] %}
{% for text, values in sections[section][category].items() %}
- {{ text }}{% if values %} ({{ values|join(', ') }}){% endif %}
{{ bullet }}{{ text }}{% if values %} ({{ values|join(', ') }}){% endif %}

{% if issues_spaced and not loop.last %}

{% endif %}
{% endfor %}

{% else %}
- {{ sections[section][category]['']|join(', ') }}
{{ bullet }}{{ sections[section][category]['']|join(', ') }}

{% endif %}
{% if sections[section][category]|length == 0 %}
{% if not sections[section][category] %}
No significant changes.

{% else %}
Expand Down
62 changes: 62 additions & 0 deletions src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1173,3 +1173,65 @@ def test_with_topline_and_template_and_draft(self):

self.assertEqual(0, result.exit_code, result.output)
self.assertEqual(expected_output, result.output)

def test_markdown_filename(self):
"""
When a filename ending with `.md` is configured, the configuration defaults for
underlines and title_prefixes change to be markdown compatible.

Test against both draft and build mode.
"""
runner = CliRunner()

with runner.isolated_filesystem():
setup_simple_project(extra_config='filename = "CHANGES.md"')
Path("foo/newsfragments/123.feature").write_text("Adds levitation")
Path("foo/newsfragments/456.feature").write_text(
"Revert levitation change\nIt was a bad idea...", newline=""
)

draft_result = runner.invoke(_main, ["--date=01-01-2001", "--draft"])
self.assertEqual(0, draft_result.exit_code, draft_result.output)
self.assertEqual(
draft_result.output,
dedent(
"""
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.

# Foo 1.2.3 (01-01-2001)

### Features

- Adds levitation (#123)

- Revert levitation change
It was a bad idea... (#456)

"""
).lstrip(),
)

result = runner.invoke(_main, ["--date=01-01-2001"])
changes_text = Path("CHANGES.md").read_text()

self.assertEqual(0, result.exit_code, result.output)

self.assertEqual(
changes_text,
dedent(
"""
# Foo 1.2.3 (01-01-2001)

### Features

- Adds levitation (#123)

- Revert levitation change
It was a bad idea... (#456)
"""
).lstrip(),
)
Loading