Skip to content

Commit

Permalink
Add single_file option
Browse files Browse the repository at this point in the history
Split from #158, contributed by @seberg
  • Loading branch information
hawkowl authored Sep 1, 2019
1 parent 83c33da commit 4068ee4
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 31 deletions.
6 changes: 5 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ source =
towncrier
branch = True


[paths]
source =
src/
.tox/*/lib/python*/site-packages/
.tox/pypy*/site-packages/

[report]
omit =
src/towncrier/__main__.py
src/towncrier/test/*
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ apidocs/
.eggs
.tox/
.coverage.*
.vscode
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ matrix:
env: TOX_ENV=py35-tests
- python: 3.6
env: TOX_ENV=py36-tests
- python: 3.7
env: TOX_ENV=py37-tests
31 changes: 31 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,34 @@ 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
```
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
```
28 changes: 22 additions & 6 deletions src/towncrier/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
from collections import OrderedDict


class ConfigError(Exception):
def __init__(self, *args, **kwargs):
self.failing_option = kwargs.get("failing_option")
super(ConfigError, self).__init__(*args)


_start_string = u".. towncrier release notes start\n"
_title_format = None
_template_fname = None
Expand Down Expand Up @@ -38,7 +44,7 @@ def load_config_from_file(from_file):

def parse_toml(config):
if "tool" not in config:
raise ValueError("No [tool.towncrier] section.")
raise ConfigError("No [tool.towncrier] section.", failing_option="all")

config = config["tool"]["towncrier"]

Expand All @@ -58,15 +64,25 @@ def parse_toml(config):
types = _default_types

wrap = config.get("wrap", False)
if isinstance(wrap, str):
if wrap in ["true", "True", "1"]:
wrap = True
else:
wrap = False

single_file_wrong = config.get("singlefile")
if single_file_wrong:
raise ConfigError(
"`singlefile` is not a valid option. Did you mean `single_file`?",
failing_option="singlefile",
)

single_file = config.get("single_file", True)
if not isinstance(single_file, bool):
raise ConfigError(
"`single_file` option must be a boolean: false or true.",
failing_option="single_file",
)

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,
Expand Down
16 changes: 9 additions & 7 deletions src/towncrier/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?")
Expand Down
39 changes: 28 additions & 11 deletions src/towncrier/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import os
import click
import pkg_resources
import traceback
import sys

from datetime import date

Expand Down Expand Up @@ -60,15 +62,19 @@ def _main(
project_date,
answer_yes,
):
return __main(
draft,
directory,
config_file,
project_name,
project_version,
project_date,
answer_yes,
)
try:
return __main(
draft,
directory,
config_file,
project_name,
project_version,
project_date,
answer_yes,
)
except Exception:
traceback.print_exc(file=sys.stderr)
raise


def __main(
Expand Down Expand Up @@ -167,12 +173,23 @@ def __main(
else:
click.echo("Writing to newsfile...", err=to_err)
start_line = config["start_line"]
news_file = config["filename"]

if config["single_file"]:
# When single_file is enabled, the news file name changes based on the version.
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)
Expand Down
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/161.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``single_file`` option can now be added to the configuration file. When set to ``true``, the filename key can now be formattable with the ``name``, ``version``, and ``project_date`` format variables. This allows subsequent versions to be written out to new files instead of appended to an existing one.
99 changes: 99 additions & 0 deletions src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,102 @@ def test_no_package_changelog(self):
"""
).lstrip(),
)

def test_single_file(self):
"""
Enabling the single file mode will write the changelog to a filename
that is formatted from the filename args.
"""
runner = CliRunner()

with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
'[tool.towncrier]\n single_file=true\n filename="{version}-notes.rst"'
)
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")

result = runner.invoke(
_main,
[
"--version",
"7.8.9",
"--name",
"foo",
"--date",
"01-01-2001",
"--yes",
],
)

self.assertEqual(0, result.exit_code, result.output)
self.assertTrue(os.path.exists("7.8.9-notes.rst"), os.listdir("."))
with open("7.8.9-notes.rst", "r") as f:
output = f.read()

self.assertEqual(
output,
dedent(
"""
foo 7.8.9 (01-01-2001)
======================
Features
--------
- Adds levitation (#123)
"""
).lstrip(),
)

def test_single_file_false(self):
"""
If formatting arguments are given in the filename arg and single_file is
false, the filename will not be formatted.
"""
runner = CliRunner()

with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
'[tool.towncrier]\n single_file=false\n filename="{version}-notes.rst"'
)
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")

result = runner.invoke(
_main,
[
"--version",
"7.8.9",
"--name",
"foo",
"--date",
"01-01-2001",
"--yes",
],
)

self.assertEqual(0, result.exit_code, result.output)
self.assertTrue(os.path.exists("{version}-notes.rst"), os.listdir("."))
self.assertFalse(os.path.exists("7.8.9-notes.rst"), os.listdir("."))
with open("{version}-notes.rst", "r") as f:
output = f.read()

self.assertEqual(
output,
dedent(
"""
foo 7.8.9 (01-01-2001)
======================
Features
--------
- Adds levitation (#123)
"""
).lstrip(),
)
69 changes: 68 additions & 1 deletion src/towncrier/test/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from twisted.trial.unittest import TestCase

import os
from textwrap import dedent

from .._settings import load_config
from .._settings import load_config, ConfigError


class TomlSettingsTests(TestCase):
Expand All @@ -28,3 +29,69 @@ def test_base(self):
self.assertEqual(config["package_dir"], ".")
self.assertEqual(config["filename"], "NEWS.rst")
self.assertEqual(config["underlines"], ["=", "-", "~"])

def test_missing(self):
"""
If the config file doesn't have the correct toml key, we error.
"""
temp = self.mktemp()
os.makedirs(temp)

with open(os.path.join(temp, "pyproject.toml"), "w") as f:
f.write(
dedent(
"""
[something.else]
blah='baz'
"""
)
)

with self.assertRaises(ConfigError) as e:
load_config(temp)

self.assertEqual(e.exception.failing_option, "all")

def test_incorrect_single_file(self):
"""
single_file must be a bool.
"""
temp = self.mktemp()
os.makedirs(temp)

with open(os.path.join(temp, "pyproject.toml"), "w") as f:
f.write(
dedent(
"""
[tool.towncrier]
single_file = "a"
"""
)
)

with self.assertRaises(ConfigError) as e:
load_config(temp)

self.assertEqual(e.exception.failing_option, "single_file")

def test_mistype_singlefile(self):
"""
singlefile is not accepted, single_file is.
"""
temp = self.mktemp()
os.makedirs(temp)

with open(os.path.join(temp, "pyproject.toml"), "w") as f:
f.write(
dedent(
"""
[tool.towncrier]
singlefile = "a"
"""
)
)

with self.assertRaises(ConfigError) as e:
load_config(temp)

self.assertEqual(e.exception.failing_option, "singlefile")
Loading

0 comments on commit 4068ee4

Please sign in to comment.