From 3cddf1d45732d48e308518974e730c5a55365c63 Mon Sep 17 00:00:00 2001 From: Matthias Dellweg Date: Tue, 7 Nov 2023 12:36:26 +0100 Subject: [PATCH] Refactored test_build.py Made use of more generic with_[git_]project decorators. Co-authored-by: Adi Roiban --- src/towncrier/newsfragments/562.misc | 1 + src/towncrier/test/helpers.py | 76 ++- src/towncrier/test/test_build.py | 933 ++++++++++++--------------- 3 files changed, 497 insertions(+), 513 deletions(-) create mode 100644 src/towncrier/newsfragments/562.misc diff --git a/src/towncrier/newsfragments/562.misc b/src/towncrier/newsfragments/562.misc new file mode 100644 index 00000000..88d21020 --- /dev/null +++ b/src/towncrier/newsfragments/562.misc @@ -0,0 +1 @@ +Improved structure and readability of some tests for building the changelog. diff --git a/src/towncrier/test/helpers.py b/src/towncrier/test/helpers.py index ddc9ffc6..0cb1d9d7 100644 --- a/src/towncrier/test/helpers.py +++ b/src/towncrier/test/helpers.py @@ -5,6 +5,7 @@ from functools import wraps from pathlib import Path +from subprocess import call from typing import Any, Callable from click.testing import CliRunner @@ -62,10 +63,83 @@ def setup_simple_project( ) -> None: if config is None: config = "[tool.towncrier]\n" 'package = "foo"\n' + extra_config - + else: + config = textwrap.dedent(config) Path(pyproject_path).write_text(config) Path("foo").mkdir() Path("foo/__init__.py").write_text('__version__ = "1.2.3"\n') if mkdir_newsfragments: Path("foo/newsfragments").mkdir() + + +def with_project( + *, + config: str | None = None, + pyproject_path: str = "pyproject.toml", +) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """Decorator to run a test with an isolated directory containing a simple + project. + + The files are not managed by git. + + `config` is the content of the config file. + It will be automatically dedented. + + `pyproject_path` is the path where to store the config file. + """ + + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: + @wraps(fn) + def test(*args: Any, **kw: Any) -> Any: + runner = CliRunner() + with runner.isolated_filesystem(): + setup_simple_project( + config=config, + pyproject_path=pyproject_path, + ) + + return fn(*args, runner=runner, **kw) + + return test + + return decorator + + +def with_git_project( + *, + config: str | None = None, + pyproject_path: str = "pyproject.toml", +) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """Decorator to run a test with an isolated directory containing a simple + project checked into git. + Use `config` to tweak the content of the config file. + Use `pyproject_path` to tweak the location of the config file. + """ + + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: + def _commit() -> None: + call(["git", "add", "."]) + call(["git", "commit", "-m", "Second Commit"]) + + @wraps(fn) + def test(*args: Any, **kw: Any) -> Any: + runner = CliRunner() + with runner.isolated_filesystem(): + setup_simple_project( + config=config, + pyproject_path=pyproject_path, + ) + + call(["git", "init"]) + call(["git", "config", "user.name", "user"]) + call(["git", "config", "user.email", "user@example.com"]) + call(["git", "config", "commit.gpgSign", "false"]) + call(["git", "add", "."]) + call(["git", "commit", "-m", "Initial Commit"]) + + return fn(*args, runner=runner, commit=_commit, **kw) + + return test + + return decorator diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py index 938814cc..76f6e8aa 100644 --- a/src/towncrier/test/test_build.py +++ b/src/towncrier/test/test_build.py @@ -6,7 +6,6 @@ from datetime import date from pathlib import Path -from subprocess import call from textwrap import dedent from unittest.mock import patch @@ -15,50 +14,48 @@ from .._shell import cli from ..build import _main -from .helpers import read, setup_simple_project, with_isolated_runner, write +from .helpers import read, with_git_project, with_project, write class TestCli(TestCase): maxDiff = None - def _test_command(self, command): - runner = CliRunner() - - with runner.isolated_filesystem(): - setup_simple_project() - with open("foo/newsfragments/123.feature", "w") as f: - f.write("Adds levitation") - # Towncrier treats this as 124.feature, ignoring .rst extension - with open("foo/newsfragments/124.feature.rst", "w") as f: - f.write("Extends levitation") - # Towncrier supports non-numeric newsfragment names. - with open("foo/newsfragments/baz.feature.rst", "w") as f: - f.write("Baz levitation") - # Towncrier supports files that have a dot in the name of the - # newsfragment - with open("foo/newsfragments/fix-1.2.feature", "w") as f: - f.write("Baz fix levitation") - # Towncrier supports fragments not linked to a feature - with open("foo/newsfragments/+anything.feature", "w") as f: - f.write("Orphaned feature") - with open("foo/newsfragments/+xxx.feature", "w") as f: - f.write("Another orphaned feature") - with open("foo/newsfragments/+123_orphaned.feature", "w") as f: - f.write("An orphaned feature starting with a number") - with open("foo/newsfragments/+12.3_orphaned.feature", "w") as f: - f.write("An orphaned feature starting with a dotted number") - with open("foo/newsfragments/+orphaned_123.feature", "w") as f: - f.write("An orphaned feature ending with a number") - with open("foo/newsfragments/+orphaned_12.3.feature", "w") as f: - f.write("An orphaned feature ending with a dotted number") - # Towncrier ignores files that don't have a dot - with open("foo/newsfragments/README", "w") as f: - f.write("Blah blah") - # And files that don't have a valid category - with open("foo/newsfragments/README.rst", "w") as f: - f.write("**Blah blah**") - - result = runner.invoke(command, ["--draft", "--date", "01-01-2001"]) + @with_project() + def _test_command(self, command, runner): + # Off the shelf newsfragment + with open("foo/newsfragments/123.feature", "w") as f: + f.write("Adds levitation") + # Towncrier treats this as 124.feature, ignoring .rst extension + with open("foo/newsfragments/124.feature.rst", "w") as f: + f.write("Extends levitation") + # Towncrier supports non-numeric newsfragment names. + with open("foo/newsfragments/baz.feature.rst", "w") as f: + f.write("Baz levitation") + # Towncrier supports files that have a dot in the name of the + # newsfragment + with open("foo/newsfragments/fix-1.2.feature", "w") as f: + f.write("Baz fix levitation") + # Towncrier supports fragments not linked to a feature + with open("foo/newsfragments/+anything.feature", "w") as f: + f.write("Orphaned feature") + with open("foo/newsfragments/+xxx.feature", "w") as f: + f.write("Another orphaned feature") + with open("foo/newsfragments/+123_orphaned.feature", "w") as f: + f.write("An orphaned feature starting with a number") + with open("foo/newsfragments/+12.3_orphaned.feature", "w") as f: + f.write("An orphaned feature starting with a dotted number") + with open("foo/newsfragments/+orphaned_123.feature", "w") as f: + f.write("An orphaned feature ending with a number") + with open("foo/newsfragments/+orphaned_12.3.feature", "w") as f: + f.write("An orphaned feature ending with a dotted number") + # Towncrier ignores files that don't have a dot + with open("foo/newsfragments/README", "w") as f: + f.write("Blah blah") + # And files that don't have a valid category + with open("foo/newsfragments/README.rst", "w") as f: + f.write("**Blah blah**") + + result = runner.invoke(command, ["--draft", "--date", "01-01-2001"]) self.assertEqual(0, result.exit_code, result.output) self.assertEqual( @@ -100,7 +97,7 @@ def test_command(self): def test_subcommand(self): self._test_command(_main) - @with_isolated_runner + @with_project() def test_in_different_dir_dir_option(self, runner): """ The current working directory doesn't matter as long as we pass @@ -108,7 +105,6 @@ def test_in_different_dir_dir_option(self, runner): """ project_dir = Path(".").resolve() - setup_simple_project() Path("foo/newsfragments/123.feature").write_text("Adds levitation") # Ensure our assetion below is meaningful. self.assertFalse((project_dir / "NEWS.rst").exists()) @@ -125,7 +121,7 @@ def test_in_different_dir_dir_option(self, runner): self.assertEqual(0, result.exit_code) self.assertTrue((project_dir / "NEWS.rst").exists()) - @with_isolated_runner + @with_project() def test_in_different_dir_config_option(self, runner): """ The current working directory and the location of the configuration @@ -134,7 +130,6 @@ def test_in_different_dir_config_option(self, runner): """ project_dir = Path(".").resolve() - setup_simple_project() Path("foo/newsfragments/123.feature").write_text("Adds levitation") # Ensure our assetion below is meaningful. self.assertFalse((project_dir / "NEWS.rst").exists()) @@ -157,7 +152,12 @@ def test_in_different_dir_config_option(self, runner): self.assertEqual(0, result.exit_code) self.assertTrue((project_dir / "NEWS.rst").exists()) - @with_isolated_runner + @with_project( + config=""" + [tool.towncrier] + directory = "changelog.d" + """ + ) def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): """ Using the `--dir` CLI argument, the NEWS file can @@ -167,9 +167,6 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): The path passed to `--dir` becomes the working directory. """ - Path("pyproject.toml").write_text( - "[tool.towncrier]\n" + 'directory = "changelog.d"\n' - ) Path("foo/foo").mkdir(parents=True) Path("foo/foo/__init__.py").write_text("") Path("foo/changelog.d").mkdir() @@ -184,12 +181,11 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): self.assertEqual(0, result.exit_code) self.assertTrue(Path("foo/NEWS.rst").exists()) - @with_isolated_runner + @with_project() def test_no_newsfragment_directory(self, runner): """ A missing newsfragment directory acts as if there are no changes. """ - setup_simple_project() os.rmdir("foo/newsfragments") result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"]) @@ -197,49 +193,38 @@ def test_no_newsfragment_directory(self, runner): self.assertEqual(0, result.exit_code) self.assertIn("No significant changes.\n", result.output) - def test_no_newsfragments_draft(self): + @with_project() + def test_no_newsfragments_draft(self, runner): """ An empty newsfragment directory acts as if there are no changes. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - setup_simple_project() - - result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"]) + result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"]) self.assertEqual(0, result.exit_code) self.assertIn("No significant changes.\n", result.output) - def test_no_newsfragments(self): + @with_project() + def test_no_newsfragments(self, runner): """ An empty newsfragment directory acts as if there are no changes and removing files handles it gracefully. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - setup_simple_project() + result = runner.invoke(_main, ["--date", "01-01-2001"]) - result = runner.invoke(_main, ["--date", "01-01-2001"]) - - news = read("NEWS.rst") + news = read("NEWS.rst") self.assertEqual(0, result.exit_code) self.assertIn("No significant changes.\n", news) - def test_collision(self): - runner = CliRunner() - - with runner.isolated_filesystem(): - setup_simple_project() - # Note that both are 123.feature - with open("foo/newsfragments/123.feature", "w") as f: - f.write("Adds levitation") - with open("foo/newsfragments/123.feature.rst", "w") as f: - f.write("Extends levitation") + @with_project() + def test_collision(self, runner): + # Note that both are 123.feature + with open("foo/newsfragments/123.feature", "w") as f: + f.write("Adds levitation") + with open("foo/newsfragments/123.feature.rst", "w") as f: + f.write("Extends levitation") - result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"]) + result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"]) # This should fail self.assertEqual(type(result.exception), ValueError) @@ -399,69 +384,52 @@ def run_order_scenario(sections, types): ), ) - def test_draft_no_date(self): + @with_git_project() + def test_draft_no_date(self, runner, commit): """ If no date is passed, today's date is used. """ - runner = CliRunner() + fragment_path1 = "foo/newsfragments/123.feature" + fragment_path2 = "foo/newsfragments/124.feature.rst" + with open(fragment_path1, "w") as f: + f.write("Adds levitation") + with open(fragment_path2, "w") as f: + f.write("Extends levitation") - with runner.isolated_filesystem(): - setup_simple_project() - fragment_path1 = "foo/newsfragments/123.feature" - fragment_path2 = "foo/newsfragments/124.feature.rst" - with open(fragment_path1, "w") as f: - f.write("Adds levitation") - with open(fragment_path2, "w") as f: - f.write("Extends levitation") - - call(["git", "init"]) - call(["git", "config", "user.name", "user"]) - call(["git", "config", "user.email", "user@example.com"]) - call(["git", "config", "commit.gpgSign", "false"]) - call(["git", "add", "."]) - call(["git", "commit", "-m", "Initial Commit"]) - - today = date.today() - result = runner.invoke(_main, ["--draft"]) + commit() - self.assertEqual(0, result.exit_code) - self.assertIn(f"Foo 1.2.3 ({today.isoformat()})", result.output) + today = date.today() + result = runner.invoke(_main, ["--draft"]) - def test_no_confirmation(self): - runner = CliRunner() + self.assertEqual(0, result.exit_code) + self.assertIn(f"Foo 1.2.3 ({today.isoformat()})", result.output) - with runner.isolated_filesystem(): - setup_simple_project() - fragment_path1 = "foo/newsfragments/123.feature" - fragment_path2 = "foo/newsfragments/124.feature.rst" - with open(fragment_path1, "w") as f: - f.write("Adds levitation") - with open(fragment_path2, "w") as f: - f.write("Extends levitation") - - call(["git", "init"]) - call(["git", "config", "user.name", "user"]) - call(["git", "config", "user.email", "user@example.com"]) - call(["git", "config", "commit.gpgSign", "false"]) - call(["git", "add", "."]) - call(["git", "commit", "-m", "Initial Commit"]) - - result = runner.invoke(_main, ["--date", "01-01-2001", "--yes"]) - - self.assertEqual(0, result.exit_code) - path = "NEWS.rst" - self.assertTrue(os.path.isfile(path)) - self.assertFalse(os.path.isfile(fragment_path1)) - self.assertFalse(os.path.isfile(fragment_path2)) - - @with_isolated_runner - def test_keep_fragments(self, runner): + @with_git_project() + def test_no_confirmation(self, runner, commit): + fragment_path1 = "foo/newsfragments/123.feature" + fragment_path2 = "foo/newsfragments/124.feature.rst" + with open(fragment_path1, "w") as f: + f.write("Adds levitation") + with open(fragment_path2, "w") as f: + f.write("Extends levitation") + + commit() + + result = runner.invoke(_main, ["--date", "01-01-2001", "--yes"]) + + self.assertEqual(0, result.exit_code) + path = "NEWS.rst" + self.assertTrue(os.path.isfile(path)) + self.assertFalse(os.path.isfile(fragment_path1)) + self.assertFalse(os.path.isfile(fragment_path2)) + + @with_git_project() + def test_keep_fragments(self, runner, commit): """ The `--keep` option will build the full final news file without deleting the fragment files and without any extra CLI interaction or confirmation. """ - setup_simple_project() fragment_path1 = "foo/newsfragments/123.feature" fragment_path2 = "foo/newsfragments/124.feature.rst" with open(fragment_path1, "w") as f: @@ -469,12 +437,7 @@ def test_keep_fragments(self, runner): with open(fragment_path2, "w") as f: f.write("Extends levitation") - call(["git", "init"]) - call(["git", "config", "user.name", "user"]) - call(["git", "config", "user.email", "user@example.com"]) - call(["git", "config", "commit.gpgSign", "false"]) - call(["git", "add", "."]) - call(["git", "commit", "-m", "Initial Commit"]) + commit() result = runner.invoke(_main, ["--date", "01-01-2001", "--keep"]) @@ -485,8 +448,8 @@ def test_keep_fragments(self, runner): self.assertTrue(os.path.isfile(fragment_path1)) self.assertTrue(os.path.isfile(fragment_path2)) - @with_isolated_runner - def test_yes_keep_error(self, runner): + @with_git_project() + def test_yes_keep_error(self, runner, commit): """ It will fail to perform any action when the conflicting --keep and --yes options are provided. @@ -495,7 +458,6 @@ def test_yes_keep_error(self, runner): to make sure both orders are validated since click triggers the validator in the order it parses the command line. """ - setup_simple_project() fragment_path1 = "foo/newsfragments/123.feature" fragment_path2 = "foo/newsfragments/124.feature.rst" with open(fragment_path1, "w") as f: @@ -503,12 +465,7 @@ def test_yes_keep_error(self, runner): with open(fragment_path2, "w") as f: f.write("Extends levitation") - call(["git", "init"]) - call(["git", "config", "user.name", "user"]) - call(["git", "config", "user.email", "user@example.com"]) - call(["git", "config", "commit.gpgSign", "false"]) - call(["git", "add", "."]) - call(["git", "commit", "-m", "Initial Commit"]) + commit() result = runner.invoke(_main, ["--date", "01-01-2001", "--yes", "--keep"]) self.assertEqual(1, result.exit_code) @@ -516,38 +473,30 @@ def test_yes_keep_error(self, runner): result = runner.invoke(_main, ["--date", "01-01-2001", "--keep", "--yes"]) self.assertEqual(1, result.exit_code) - def test_confirmation_says_no(self): + @with_git_project() + def test_confirmation_says_no(self, runner, commit): """ If the user says "no" to removing the newsfragements, we end up with a NEWS.rst AND the newsfragments. """ - runner = CliRunner() + fragment_path1 = "foo/newsfragments/123.feature" + fragment_path2 = "foo/newsfragments/124.feature.rst" + with open(fragment_path1, "w") as f: + f.write("Adds levitation") + with open(fragment_path2, "w") as f: + f.write("Extends levitation") - with runner.isolated_filesystem(): - setup_simple_project() - fragment_path1 = "foo/newsfragments/123.feature" - fragment_path2 = "foo/newsfragments/124.feature.rst" - with open(fragment_path1, "w") as f: - f.write("Adds levitation") - with open(fragment_path2, "w") as f: - f.write("Extends levitation") - - call(["git", "init"]) - call(["git", "config", "user.name", "user"]) - call(["git", "config", "user.email", "user@example.com"]) - call(["git", "config", "commit.gpgSign", "false"]) - call(["git", "add", "."]) - call(["git", "commit", "-m", "Initial Commit"]) - - with patch("towncrier.build.click.confirm") as m: - m.return_value = False - result = runner.invoke(_main, []) - - self.assertEqual(0, result.exit_code) - path = "NEWS.rst" - self.assertTrue(os.path.isfile(path)) - self.assertTrue(os.path.isfile(fragment_path1)) - self.assertTrue(os.path.isfile(fragment_path2)) + commit() + + with patch("towncrier.build.click.confirm") as m: + m.return_value = False + result = runner.invoke(_main, []) + + self.assertEqual(0, result.exit_code) + path = "NEWS.rst" + self.assertTrue(os.path.isfile(path)) + self.assertTrue(os.path.isfile(fragment_path1)) + self.assertTrue(os.path.isfile(fragment_path2)) def test_needs_config(self): """ @@ -561,50 +510,45 @@ def test_needs_config(self): self.assertEqual(1, result.exit_code, result.output) self.assertTrue(result.output.startswith("No configuration file found.")) - @with_isolated_runner + @with_project(config="[tool.towncrier]") def test_needs_version(self, runner: CliRunner): """ If the configuration file doesn't specify a version or a package, the version option is required. """ - write("towncrier.toml", "[tool.towncrier]") - result = runner.invoke(_main, ["--draft"], catch_exceptions=False) self.assertEqual(2, result.exit_code) self.assertIn("Error: '--version' is required", result.output) - def test_projectless_changelog(self): + @with_project() + def test_projectless_changelog(self, runner): """In which a directory containing news files is built into a changelog - without a Python project or version number. We override the project title from the commandline. """ - runner = CliRunner() + # Remove the version from the project + Path("foo/__init__.py").unlink() - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write("[tool.towncrier]\n" 'package = "foo"\n') - os.mkdir("foo") - os.mkdir("foo/newsfragments") - with open("foo/newsfragments/123.feature", "w") as f: - f.write("Adds levitation") - # Towncrier ignores .rst extension - with open("foo/newsfragments/124.feature.rst", "w") as f: - f.write("Extends levitation") + with open("foo/newsfragments/123.feature", "w") as f: + f.write("Adds levitation") + # Towncrier ignores .rst extension + with open("foo/newsfragments/124.feature.rst", "w") as f: + f.write("Extends levitation") - result = runner.invoke( - _main, - [ - "--name", - "FooBarBaz", - "--version", - "7.8.9", - "--date", - "01-01-2001", - "--draft", - ], - ) + result = runner.invoke( + _main, + [ + "--name", + "FooBarBaz", + "--version", + "7.8.9", + "--date", + "01-01-2001", + "--draft", + ], + ) self.assertEqual(0, result.exit_code) self.assertEqual( @@ -632,22 +576,23 @@ def test_projectless_changelog(self): ).lstrip(), ) - def test_version_in_config(self): - """The calling towncrier with version defined in configfile. + @with_project( + config=""" + [tool.towncrier] + version = "7.8.9" + """ + ) + def test_version_in_config(self, runner): + """Calling towncrier with version defined in configfile. Specifying a version in toml file will be helpful if version is maintained by i.e. bumpversion and it's not a python project. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write("[tool.towncrier]\n" 'version = "7.8.9"\n') - os.mkdir("newsfragments") - with open("newsfragments/123.feature", "w") as f: - f.write("Adds levitation") + os.mkdir("newsfragments") + with open("newsfragments/123.feature", "w") as f: + f.write("Adds levitation") - result = runner.invoke(_main, ["--date", "01-01-2001", "--draft"]) + result = runner.invoke(_main, ["--date", "01-01-2001", "--draft"]) self.assertEqual(0, result.exit_code, result.output) self.assertEqual( @@ -674,24 +619,25 @@ def test_version_in_config(self): ).lstrip(), ) - def test_project_name_in_config(self): + @with_project( + config=""" + [tool.towncrier] + name = "ImGoProject" + """ + ) + def test_project_name_in_config(self, runner): """The calling towncrier with project name defined in configfile. Specifying a project name in toml file will be helpful to keep the project name consistent as part of the towncrier configuration, not call. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write("[tool.towncrier]\n" 'name = "ImGoProject"\n') - os.mkdir("newsfragments") - with open("newsfragments/123.feature", "w") as f: - f.write("Adds levitation") + os.mkdir("newsfragments") + with open("newsfragments/123.feature", "w") as f: + f.write("Adds levitation") - result = runner.invoke( - _main, ["--version", "7.8.9", "--date", "01-01-2001", "--draft"] - ) + result = runner.invoke( + _main, ["--version", "7.8.9", "--date", "01-01-2001", "--draft"] + ) self.assertEqual(0, result.exit_code, result.output) self.assertEqual( @@ -718,7 +664,8 @@ def test_project_name_in_config(self): ).lstrip(), ) - def test_no_package_changelog(self): + @with_project(config="[tool.towncrier]") + def test_no_package_changelog(self, runner): """The calling towncrier with any package argument. Specifying a package in the toml file or the command line @@ -727,18 +674,13 @@ def test_no_package_changelog(self): so we do not need the package for that. - we don't need to include the package in the changelog header. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write("[tool.towncrier]") - os.mkdir("newsfragments") - with open("newsfragments/123.feature", "w") as f: - f.write("Adds levitation") + os.mkdir("newsfragments") + with open("newsfragments/123.feature", "w") as f: + f.write("Adds levitation") - result = runner.invoke( - _main, ["--version", "7.8.9", "--date", "01-01-2001", "--draft"] - ) + result = runner.invoke( + _main, ["--version", "7.8.9", "--date", "01-01-2001", "--draft"] + ) self.assertEqual(0, result.exit_code, result.output) self.assertEqual( @@ -765,13 +707,19 @@ def test_no_package_changelog(self): ).lstrip(), ) - def test_release_notes_in_separate_files(self): + @with_project( + config=""" + [tool.towncrier] + single_file=false + filename="{version}-notes.rst" + """ + ) + def test_release_notes_in_separate_files(self, runner): """ When `single_file = false` the release notes for each version are stored in a separate file. The name of the file is defined by the `filename` configuration value. """ - runner = CliRunner() def do_build_once_with(version, fragment_file, fragment): with open(f"newsfragments/{fragment_file}", "w") as f: @@ -794,78 +742,65 @@ def do_build_once_with(version, fragment_file, fragment): return result results = [] - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write( - "\n".join( - [ - "[tool.towncrier]", - " single_file=false", - ' filename="{version}-notes.rst"', - ] - ) - ) - os.mkdir("newsfragments") - results.append( - do_build_once_with("7.8.9", "123.feature", "Adds levitation") - ) - results.append(do_build_once_with("7.9.0", "456.bugfix", "Adds catapult")) + os.mkdir("newsfragments") + results.append(do_build_once_with("7.8.9", "123.feature", "Adds levitation")) + results.append(do_build_once_with("7.9.0", "456.bugfix", "Adds catapult")) - self.assertEqual(0, results[0].exit_code, results[0].output) - self.assertEqual(0, results[1].exit_code, results[1].output) - self.assertEqual( - 2, - len(list(Path.cwd().glob("*-notes.rst"))), - "one newfile for each build", - ) - self.assertTrue(os.path.exists("7.8.9-notes.rst"), os.listdir(".")) - self.assertTrue(os.path.exists("7.9.0-notes.rst"), os.listdir(".")) + self.assertEqual(0, results[0].exit_code, results[0].output) + self.assertEqual(0, results[1].exit_code, results[1].output) + self.assertEqual( + 2, + len(list(Path.cwd().glob("*-notes.rst"))), + "one newfile for each build", + ) + self.assertTrue(os.path.exists("7.8.9-notes.rst"), os.listdir(".")) + self.assertTrue(os.path.exists("7.9.0-notes.rst"), os.listdir(".")) - outputs = [] - outputs.append(read("7.8.9-notes.rst")) - outputs.append(read("7.9.0-notes.rst")) + outputs = [] + outputs.append(read("7.8.9-notes.rst")) + outputs.append(read("7.9.0-notes.rst")) - self.assertEqual( - outputs[0], - dedent( - """ - foo 7.8.9 (01-01-2001) - ====================== + self.assertEqual( + outputs[0], + dedent( + """ + foo 7.8.9 (01-01-2001) + ====================== - Features - -------- + Features + -------- - - Adds levitation (#123) + - Adds levitation (#123) + """ + ).lstrip(), + ) + self.assertEqual( + outputs[1], + dedent( """ - ).lstrip(), - ) - self.assertEqual( - outputs[1], - dedent( - """ - foo 7.9.0 (01-01-2001) - ====================== + foo 7.9.0 (01-01-2001) + ====================== - Bugfixes - -------- + Bugfixes + -------- - - Adds catapult (#456) - """ - ).lstrip(), - ) + - Adds catapult (#456) + """ + ).lstrip(), + ) - def test_singlefile_errors_and_explains_cleanly(self): + @with_project( + config=""" + [tool.towncrier] + singlefile="fail!" + """ + ) + def test_singlefile_errors_and_explains_cleanly(self, runner): """ Failure to find the configuration file results in a clean explanation without a traceback. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write('[tool.towncrier]\n singlefile="fail!"\n') - - result = runner.invoke(_main) + result = runner.invoke(_main) self.assertEqual(1, result.exit_code) self.assertEqual( @@ -958,123 +893,115 @@ def do_build_once_with(version, fragment_file, fragment): ).lstrip(), ) - def test_bullet_points_false(self): + @with_project( + config=""" + [tool.towncrier] + template="towncrier:single-file-no-bullets" + all_bullets=false + """ + ) + def test_bullet_points_false(self, runner): """ When all_bullets is false, subsequent lines are not indented. The automatic ticket number inserted by towncrier will align with the manual bullet. """ - runner = CliRunner() + os.mkdir("newsfragments") + with open("newsfragments/123.feature", "w") as f: + f.write("wow!\n~~~~\n\nNo indentation at all.") + with open("newsfragments/124.bugfix", "w") as f: + f.write("#. Numbered bullet list.") + with open("newsfragments/125.removal", "w") as f: + f.write("- Hyphen based bullet list.") + with open("newsfragments/126.doc", "w") as f: + f.write("* Asterisk based bullet list.") - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write( - "[tool.towncrier]\n" - 'template="towncrier:single-file-no-bullets"\n' - "all_bullets=false" - ) - os.mkdir("newsfragments") - with open("newsfragments/123.feature", "w") as f: - f.write("wow!\n~~~~\n\nNo indentation at all.") - with open("newsfragments/124.bugfix", "w") as f: - f.write("#. Numbered bullet list.") - with open("newsfragments/125.removal", "w") as f: - f.write("- Hyphen based bullet list.") - with open("newsfragments/126.doc", "w") as f: - f.write("* Asterisk based bullet list.") - - result = runner.invoke( - _main, - [ - "--version", - "7.8.9", - "--name", - "foo", - "--date", - "01-01-2001", - "--yes", - ], - ) + result = runner.invoke( + _main, + [ + "--version", + "7.8.9", + "--name", + "foo", + "--date", + "01-01-2001", + "--yes", + ], + ) - self.assertEqual(0, result.exit_code, result.output) - output = read("NEWS.rst") + self.assertEqual(0, result.exit_code, result.output) + output = read("NEWS.rst") self.assertEqual( output, - """ -foo 7.8.9 (01-01-2001) -====================== + dedent( + """ + foo 7.8.9 (01-01-2001) + ====================== -Features --------- + Features + -------- -wow! -~~~~ + wow! + ~~~~ -No indentation at all. -(#123) + No indentation at all. + (#123) -Bugfixes --------- + Bugfixes + -------- -#. Numbered bullet list. - (#124) + #. Numbered bullet list. + (#124) -Improved Documentation ----------------------- + Improved Documentation + ---------------------- -* Asterisk based bullet list. - (#126) + * Asterisk based bullet list. + (#126) -Deprecations and Removals -------------------------- + Deprecations and Removals + ------------------------- -- Hyphen based bullet list. - (#125) -""".lstrip(), + - Hyphen based bullet list. + (#125) + """ + ).lstrip(), ) - def test_title_format_custom(self): + @with_project( + config=""" + [tool.towncrier] + package = "foo" + title_format = "[{project_date}] CUSTOM RELEASE for {name} version {version}" + """ + ) + def test_title_format_custom(self, runner): """ A non-empty title format adds the specified title. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write( - dedent( - """\ - [tool.towncrier] - package = "foo" - title_format = "[{project_date}] CUSTOM RELEASE for {name} version {version}" - """ - ) - ) - os.mkdir("foo") - os.mkdir("foo/newsfragments") - with open("foo/newsfragments/123.feature", "w") as f: - f.write("Adds levitation") - # Towncrier ignores .rst extension - with open("foo/newsfragments/124.feature.rst", "w") as f: - f.write("Extends levitation") + with open("foo/newsfragments/123.feature", "w") as f: + f.write("Adds levitation") + # Towncrier ignores .rst extension + with open("foo/newsfragments/124.feature.rst", "w") as f: + f.write("Extends levitation") - result = runner.invoke( - _main, - [ - "--name", - "FooBarBaz", - "--version", - "7.8.9", - "--date", - "20-01-2001", - "--draft", - ], - ) + result = runner.invoke( + _main, + [ + "--name", + "FooBarBaz", + "--version", + "7.8.9", + "--date", + "20-01-2001", + "--draft", + ], + ) expected_output = dedent( """\ @@ -1101,61 +1028,53 @@ def test_title_format_custom(self): self.assertEqual(0, result.exit_code) self.assertEqual(expected_output, result.output) - def test_title_format_false(self): + @with_project( + config=""" + [tool.towncrier] + package = "foo" + title_format = false + template = "template.rst" + """ + ) + def test_title_format_false(self, runner): """ Setting the title format to false disables the explicit title. This would be used, for example, when the template creates the title itself. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write( - dedent( - """\ - [tool.towncrier] - package = "foo" - title_format = false - template = "template.rst" - """ - ) - ) - os.mkdir("foo") - os.mkdir("foo/newsfragments") - with open("template.rst", "w") as f: - f.write( - dedent( - """\ - Here's a hardcoded title added by the template - ============================================== - {% for section in sections %} - {% set underline = "-" %} - {% for category, val in definitions.items() if category in sections[section] %} - - {% for text, values in sections[section][category]|dictsort(by='value') %} - - {{ text }} - - {% endfor %} - {% endfor %} - {% endfor %} - """ - ) + with open("template.rst", "w") as f: + f.write( + dedent( + """\ + Here's a hardcoded title added by the template + ============================================== + {% for section in sections %} + {% set underline = "-" %} + {% for category, val in definitions.items() if category in sections[section] %} + + {% for text, values in sections[section][category]|dictsort(by='value') %} + - {{ text }} + + {% endfor %} + {% endfor %} + {% endfor %} + """ ) - - result = runner.invoke( - _main, - [ - "--name", - "FooBarBaz", - "--version", - "7.8.9", - "--date", - "20-01-2001", - "--draft", - ], - catch_exceptions=False, ) + result = runner.invoke( + _main, + [ + "--name", + "FooBarBaz", + "--version", + "7.8.9", + "--date", + "20-01-2001", + "--draft", + ], + catch_exceptions=False, + ) + expected_output = dedent( """\ Loading template... @@ -1173,46 +1092,40 @@ def test_title_format_false(self): self.assertEqual(0, result.exit_code) self.assertEqual(expected_output, result.output) - def test_start_string(self): + @with_project( + config=""" + [tool.towncrier] + start_string="Release notes start marker" + """ + ) + def test_start_string(self, runner): """ The `start_string` configuration is used to detect the starting point for inserting the generated release notes. A newline is automatically added to the configured value. """ - runner = CliRunner() - - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write( - dedent( - """\ - [tool.towncrier] - start_string="Release notes start marker" - """ - ) - ) - os.mkdir("newsfragments") - with open("newsfragments/123.feature", "w") as f: - f.write("Adds levitation") - with open("NEWS.rst", "w") as f: - f.write("a line\n\nanother\n\nRelease notes start marker\na footer!\n") + os.mkdir("newsfragments") + with open("newsfragments/123.feature", "w") as f: + f.write("Adds levitation") + with open("NEWS.rst", "w") as f: + f.write("a line\n\nanother\n\nRelease notes start marker\na footer!\n") - result = runner.invoke( - _main, - [ - "--version", - "7.8.9", - "--name", - "foo", - "--date", - "01-01-2001", - "--yes", - ], - ) + 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("NEWS.rst"), os.listdir(".")) - output = read("NEWS.rst") + self.assertEqual(0, result.exit_code, result.output) + self.assertTrue(os.path.exists("NEWS.rst"), os.listdir(".")) + output = read("NEWS.rst") expected_output = dedent( """\ @@ -1236,13 +1149,11 @@ def test_start_string(self): self.assertEqual(expected_output, output) - @with_isolated_runner + @with_project() def test_default_start_string(self, runner): """ The default start string is ``.. towncrier release notes start``. """ - setup_simple_project() - write("foo/newsfragments/123.feature", "Adds levitation") write( "NEWS.rst", @@ -1285,14 +1196,18 @@ def test_default_start_string(self, runner): self.assertEqual(expected_output, output) - @with_isolated_runner + @with_project( + config=""" + [tool.towncrier] + package = "foo" + filename = "NEWS.md" + """ + ) def test_default_start_string_markdown(self, runner): """ The default start string is ```` for Markdown. """ - setup_simple_project(extra_config='filename = "NEWS.md"') - write("foo/newsfragments/123.feature", "Adds levitation") write( "NEWS.md", @@ -1333,59 +1248,53 @@ def test_default_start_string_markdown(self, runner): self.assertEqual(expected_output, output) - def test_with_topline_and_template_and_draft(self): + @with_project( + config=""" + [tool.towncrier] + title_format = "{version} - {project_date}" + template = "template.rst" + + [[tool.towncrier.type]] + directory = "feature" + name = "" + showcontent = true + """ + ) + def test_with_topline_and_template_and_draft(self, runner): """ Spacing is proper when drafting with a topline and a template. """ - runner = CliRunner() + os.mkdir("newsfragments") + with open("newsfragments/123.feature", "w") as f: + f.write("Adds levitation") + with open("template.rst", "w") as f: + f.write( + dedent( + """\ + {% for section in sections %} + {% set underline = "-" %} + {% for category, val in definitions.items() if category in sections[section] %} - with runner.isolated_filesystem(): - with open("pyproject.toml", "w") as f: - f.write( - dedent( - """\ - [tool.towncrier] - title_format = "{version} - {project_date}" - template = "template.rst" + {% for text, values in sections[section][category]|dictsort(by='value') %} + - {{ text }} - [[tool.towncrier.type]] - directory = "feature" - name = "" - showcontent = true - """ - ) - ) - os.mkdir("newsfragments") - with open("newsfragments/123.feature", "w") as f: - f.write("Adds levitation") - with open("template.rst", "w") as f: - f.write( - dedent( - """\ - {% for section in sections %} - {% set underline = "-" %} - {% for category, val in definitions.items() if category in sections[section] %} - - {% for text, values in sections[section][category]|dictsort(by='value') %} - - {{ text }} - - {% endfor %} - {% endfor %} - {% endfor %} - """ - ) + {% endfor %} + {% endfor %} + {% endfor %} + """ ) - - result = runner.invoke( - _main, - [ - "--version=7.8.9", - "--name=foo", - "--date=20-01-2001", - "--draft", - ], ) + result = runner.invoke( + _main, + [ + "--version=7.8.9", + "--name=foo", + "--date=20-01-2001", + "--draft", + ], + ) + expected_output = dedent( """\ Loading template...