Skip to content

Commit

Permalink
Check if any news fragments have invalid filenames (#622)
Browse files Browse the repository at this point in the history
* Check for invalid fragment names

* Refactor in response to review

- Change `build_ignore_filenames` to `ignore` in config
- Remove `--strict` option from `towncrier build`

* Tidy docs

* Make `config.ignore` filename list case-insensitive & add more automatically ignored filenames

* Improve test

* Improve test coverage
  • Loading branch information
MetRonnie authored Jul 29, 2024
1 parent a5a51b1 commit 4b58be5
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 4 deletions.
9 changes: 9 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ Top level keys

``true`` by default.

``ignore``
A case-insensitive list of filenames in the news fragments directory to ignore.

``towncrier check`` will fail if there are any news fragment files that have invalid filenames, except for those in the list. ``towncrier build`` will likewise fail, but only if this list has been configured (set to an empty list if there are no files to ignore).

Some filenames such as ``.gitignore`` and ``README`` are automatically ignored. However, if a custom template is stored in the news fragment directory, you should add it to this list.

``None`` by default.

Extra top level keys for Python projects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 0 additions & 2 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ Create this folder::
# This makes sure that Git will never delete the empty folder
$ echo '!.gitignore' > src/myproject/newsfragments/.gitignore

The ``.gitignore`` will remain and keep Git from not tracking the directory.


Detecting Version
-----------------
Expand Down
17 changes: 17 additions & 0 deletions src/towncrier/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pathlib import Path
from typing import Any, DefaultDict, Iterable, Iterator, Mapping, NamedTuple, Sequence

from click import ClickException
from jinja2 import Template

from towncrier._settings.load import Config
Expand Down Expand Up @@ -106,10 +107,17 @@ def __call__(self, section_directory: str = "") -> str:
def find_fragments(
base_directory: str,
config: Config,
strict: bool,
) -> tuple[Mapping[str, Mapping[tuple[str, str, int], str]], list[tuple[str, str]]]:
"""
Sections are a dictonary of section names to paths.
If strict, raise ClickException if any fragments have an invalid name.
"""
ignored_files = {".gitignore", ".keep", "readme", "readme.md", "readme.rst"}
if config.ignore:
ignored_files.update(filename.lower() for filename in config.ignore)

get_section_path = FragmentsPath(base_directory, config)

content = {}
Expand All @@ -129,10 +137,19 @@ def find_fragments(
file_content = {}

for basename in files:
if basename.lower() in ignored_files:
continue

issue, category, counter = parse_newfragment_basename(
basename, config.types
)
if category is None:
if strict and issue is None:
raise ClickException(
f"Invalid news fragment name: {basename}\n"
"If this filename is deliberate, add it to "
"'ignore' in your configuration."
)
continue
assert issue is not None
assert counter is not None
Expand Down
1 change: 1 addition & 0 deletions src/towncrier/_settings/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class Config:
orphan_prefix: str = "+"
create_eof_newline: bool = True
create_add_extension: bool = True
ignore: list[str] | None = None


class ConfigError(ClickException):
Expand Down
8 changes: 7 additions & 1 deletion src/towncrier/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,13 @@ def __main(

click.echo("Finding news fragments...", err=to_err)

fragment_contents, fragment_files = find_fragments(base_directory, config)
fragment_contents, fragment_files = find_fragments(
base_directory,
config,
# Fail if any fragment filenames are invalid only if ignore list is set
# (this maintains backward compatibility):
strict=(config.ignore is not None),
)
fragment_filenames = [filename for (filename, _category) in fragment_files]

click.echo("Rendering news fragments...", err=to_err)
Expand Down
4 changes: 3 additions & 1 deletion src/towncrier/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,14 @@ def __main(
click.echo(f"{n}. {change}")
click.echo("----")

# This will fail if any fragment files have an invalid name:
all_fragment_files = find_fragments(base_directory, config, strict=True)[1]

news_file = os.path.normpath(os.path.join(base_directory, config.filename))
if news_file in files:
click.echo("Checks SKIPPED: news file changes detected.")
sys.exit(0)

all_fragment_files = find_fragments(base_directory, config)[1]
fragments = set() # will only include fragments of types that are checked
unchecked_fragments = set() # will include fragments of types that are not checked
for fragment_filename, category in all_fragment_files:
Expand Down
3 changes: 3 additions & 0 deletions src/towncrier/newsfragments/622.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
``towncrier check`` will now fail if any news fragments have invalid filenames.

Added a new configuration option called ``ignore`` that allows you to specify a list of filenames that should be ignored. If this is set, ``towncrier build`` will also fail if any filenames are invalid, except for those in the list.
63 changes: 63 additions & 0 deletions src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1583,3 +1583,66 @@ def test_uncommitted_files(self, runner, commit):
"""
),
)

@with_project(
config="""
[tool.towncrier]
package = "foo"
ignore = ["template.jinja", "CAPYBARAS.md"]
"""
)
def test_ignored_files(self, runner):
"""
When `ignore` is set in config, files with those names are ignored.
"""
with open("foo/newsfragments/123.feature", "w") as f:
f.write("This has valid filename (control case)")
with open("foo/newsfragments/template.jinja", "w") as f:
f.write("This template has been manually ignored")
with open("foo/newsfragments/CAPYBARAS.md", "w") as f:
f.write("This markdown file has been manually ignored")
with open("foo/newsfragments/.gitignore", "w") as f:
f.write("gitignore is automatically ignored")

result = runner.invoke(
_main, ["--draft", "--date", "01-01-2001", "--version", "1.0.0"]
)
self.assertEqual(0, result.exit_code, result.output)

@with_project(
config="""
[tool.towncrier]
package = "foo"
ignore = []
"""
)
def test_invalid_fragment_name(self, runner):
"""
When `ignore` is set in config, invalid filenames cause failure.
"""
with open("foo/newsfragments/123.feature", "w") as f:
f.write("This has valid filename (control case)")
with open("foo/newsfragments/feature.124", "w") as f:
f.write("This has the issue and category the wrong way round")

result = runner.invoke(
_main, ["--draft", "--date", "01-01-2001", "--version", "1.0.0"]
)
self.assertEqual(1, result.exit_code, result.output)
self.assertIn("Invalid news fragment name: feature.124", result.output)

@with_project()
def test_no_ignore_configured(self, runner):
"""
When `ignore` is not set in config, invalid filenames are skipped.
This maintains backward compatibility with before we added `ignore`
to the configuration spec.
"""
with open("foo/newsfragments/feature.124", "w") as f:
f.write("This has the issue and category the wrong way round")

result = runner.invoke(
_main, ["--draft", "--date", "01-01-2001", "--version", "1.0.0"]
)
self.assertEqual(0, result.exit_code, result.output)
4 changes: 4 additions & 0 deletions src/towncrier/test/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def test_invalid_category(self):
parse_newfragment_basename("README.ext", ["feature"]),
(None, None, None),
)
self.assertEqual(
parse_newfragment_basename("README", ["feature"]),
(None, None, None),
)

def test_counter(self):
"""<number>.<category>.<counter> generates a custom counter value."""
Expand Down
44 changes: 44 additions & 0 deletions src/towncrier/test/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,47 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner):
self.assertTrue(
result.output.endswith("No new newsfragments found on this branch.\n")
)

@with_isolated_runner
def test_ignored_files(self, runner):
"""
When `ignore` is set in config, files with those names are ignored.
"""
create_project("pyproject.toml", extra_config='ignore = ["template.jinja"]')

write(
"foo/newsfragments/124.feature",
"This fragment has valid name (control case)",
)
write("foo/newsfragments/template.jinja", "This is manually ignored")
write("foo/newsfragments/.gitignore", "gitignore is automatically ignored")
commit("add stuff")

result = runner.invoke(towncrier_check, ["--compare-with", "main"])
self.assertEqual(0, result.exit_code, result.output)

@with_isolated_runner
def test_invalid_fragment_name(self, runner):
"""
Fails if a news fragment has an invalid name, even if `ignore` is not set in
the config.
"""
create_project("pyproject.toml")

write(
"foo/newsfragments/124.feature",
"This fragment has valid name (control case)",
)
write(
"foo/newsfragments/feature.125",
"This has issue and category wrong way round",
)
write(
"NEWS.rst",
"Modification of news file should not skip check of invalid names",
)
commit("add stuff")

result = runner.invoke(towncrier_check, ["--compare-with", "main"])
self.assertEqual(1, result.exit_code, result.output)
self.assertIn("Invalid news fragment name: feature.125", result.output)

0 comments on commit 4b58be5

Please sign in to comment.