Skip to content

Commit

Permalink
Section aware create (#603)
Browse files Browse the repository at this point in the history
* Abstract the fragments path generation

* Update the create option to work with sections

* Add fragment

* Update newsfraghment

* No need to mention new behaviour in news fragment, since that case wouldn't have worked previously anyway

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add some test docstrings

* Default section

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Typing improvement

* Skip an invalid branch to cover

* Add test for multiple sections all with paths

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
SmileyChris and pre-commit-ci[bot] authored Jun 13, 2024
1 parent be32e6b commit e3e73bd
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 61 deletions.
5 changes: 5 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ If that is the entire fragment name, a random hash will be added for you::
Whether to start ``$EDITOR`` to edit the news fragment right away.
Default: ``$EDITOR`` will be started unless you also provided content.

.. option:: --section SECTION

The section to use for the news fragment.
Default: the section with no path, or if all sections have a path then the first defined section.


``towncrier check``
-------------------
Expand Down
49 changes: 38 additions & 11 deletions src/towncrier/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from jinja2 import Template

from towncrier._settings.load import Config


# Returns issue, category and counter or (None, None, None) if the basename
# could not be parsed or doesn't contain a valid category.
Expand Down Expand Up @@ -54,6 +56,35 @@ def parse_newfragment_basename(
return invalid


class FragmentsPath:
"""
A helper to get the full path to a fragments directory.
This is a callable that optionally takes a section directory and returns the full
path to the fragments directory for that section (or the default if no section is
provided).
"""

def __init__(self, base_directory: str, config: Config):
self.base_directory = base_directory
self.config = config
if config.directory is not None:
self.base_directory = os.path.abspath(
os.path.join(base_directory, config.directory)
)
self.append_directory = ""
else:
self.base_directory = os.path.abspath(
os.path.join(base_directory, config.package_dir, config.package)
)
self.append_directory = "newsfragments"

def __call__(self, section_directory: str = "") -> str:
return os.path.join(
self.base_directory, section_directory, self.append_directory
)


# Returns a structure like:
#
# {
Expand All @@ -70,25 +101,21 @@ def parse_newfragment_basename(
# Also returns a list of the paths that the fragments were taken from.
def find_fragments(
base_directory: str,
sections: Mapping[str, str],
fragment_directory: str | None,
frag_type_names: Iterable[str],
orphan_prefix: str | None = None,
config: Config,
) -> tuple[Mapping[str, Mapping[tuple[str, str, int], str]], list[str]]:
"""
Sections are a dictonary of section names to paths.
"""
get_section_path = FragmentsPath(base_directory, config)

content = {}
fragment_filenames = []
# Multiple orphan news fragments are allowed per section, so initialize a counter
# that can be incremented automatically.
orphan_fragment_counter: DefaultDict[str | None, int] = defaultdict(int)

for key, val in sections.items():
if fragment_directory is not None:
section_dir = os.path.join(base_directory, val, fragment_directory)
else:
section_dir = os.path.join(base_directory, val)
for key, section_dir in config.sections.items():
section_dir = get_section_path(section_dir)

try:
files = os.listdir(section_dir)
Expand All @@ -99,13 +126,13 @@ def find_fragments(

for basename in files:
issue, category, counter = parse_newfragment_basename(
basename, frag_type_names
basename, config.types
)
if category is None:
continue
assert issue is not None
assert counter is not None
if orphan_prefix and issue.startswith(orphan_prefix):
if config.orphan_prefix and issue.startswith(config.orphan_prefix):
issue = ""
# Use and increment the orphan news fragment counter.
counter = orphan_fragment_counter[category]
Expand Down
19 changes: 1 addition & 18 deletions src/towncrier/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,24 +178,7 @@ def __main(

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

if config.directory is not None:
fragment_base_directory = os.path.abspath(
os.path.join(base_directory, config.directory)
)
fragment_directory = None
else:
fragment_base_directory = os.path.abspath(
os.path.join(base_directory, config.package_dir, config.package)
)
fragment_directory = "newsfragments"

fragment_contents, fragment_filenames = find_fragments(
fragment_base_directory,
config.sections,
fragment_directory,
config.types,
config.orphan_prefix,
)
fragment_contents, fragment_filenames = find_fragments(base_directory, config)

click.echo("Rendering news fragments...", err=to_err)
fragments = split_fragments(
Expand Down
19 changes: 1 addition & 18 deletions src/towncrier/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,8 @@ def __main(
click.echo("Checks SKIPPED: news file changes detected.")
sys.exit(0)

if config.directory:
fragment_base_directory = os.path.abspath(
os.path.join(base_directory, config.directory)
)
fragment_directory = None
else:
fragment_base_directory = os.path.abspath(
os.path.join(base_directory, config.package_dir, config.package)
)
fragment_directory = "newsfragments"

fragments = {
os.path.abspath(path)
for path in find_fragments(
fragment_base_directory,
config.sections,
fragment_directory,
config.types.keys(),
)[1]
os.path.abspath(path) for path in find_fragments(base_directory, config)[1]
}
fragments_in_branch = fragments & files

Expand Down
73 changes: 59 additions & 14 deletions src/towncrier/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import os

from pathlib import Path
from typing import cast

import click

from ._builder import FragmentsPath
from ._settings import config_option_help, load_config_from_options


Expand Down Expand Up @@ -47,6 +49,11 @@
default=DEFAULT_CONTENT,
help="Sets the content of the new fragment.",
)
@click.option(
"--section",
type=str,
help="The section to create the fragment for.",
)
@click.argument("filename", default="")
def _main(
ctx: click.Context,
Expand All @@ -55,6 +62,7 @@ def _main(
filename: str,
edit: bool | None,
content: str,
section: str | None,
) -> None:
"""
Create a new news fragment.
Expand All @@ -75,7 +83,7 @@ def _main(
If the FILENAME base is just '+' (to create a fragment not tied to an
issue), it will be appended with a random hex string.
"""
__main(ctx, directory, config, filename, edit, content)
__main(ctx, directory, config, filename, edit, content, section)


def __main(
Expand All @@ -85,6 +93,7 @@ def __main(
filename: str,
edit: bool | None,
content: str,
section: str | None,
) -> None:
"""
The main entry point.
Expand All @@ -97,7 +106,54 @@ def __main(
if ext.lower() in (".rst", ".md"):
filename_ext = ext

section_provided = section is not None
if not section_provided:
# Get the default section.
if len(config.sections) == 1:
section = next(iter(config.sections))
else:
# If there are multiple sections then the first without a path is the default
# section, otherwise it's the first defined section.
for (
section_name,
section_dir,
) in config.sections.items(): # pragma: no branch
if not section_dir:
section = section_name
break
if section is None:
section = list(config.sections.keys())[0]

if section not in config.sections:
# Raise a click exception with the correct parameter.
section_param = None
for p in ctx.command.params: # pragma: no branch
if p.name == "section":
section_param = p
break
expected_sections = ", ".join(f"'{s}'" for s in config.sections)
raise click.BadParameter(
f"expected one of {expected_sections}",
param=section_param,
)
section = cast(str, section)

if not filename:
if not section_provided:
sections = list(config.sections)
if len(sections) > 1:
click.echo("Pick a section:")
default_section_index = None
for i, s in enumerate(sections):
click.echo(f" {i+1}: {s or '(primary)'}")
if not default_section_index and s == section:
default_section_index = str(i + 1)
section_index = click.prompt(
"Section",
type=click.Choice([str(i + 1) for i in range(len(sections))]),
default=default_section_index,
)
section = sections[int(section_index) - 1]
prompt = "Issue number"
# Add info about adding orphan if config is set.
if config.orphan_prefix:
Expand Down Expand Up @@ -134,19 +190,8 @@ def __main(
if filename_parts[-1] in config.types and filename_ext:
filename += filename_ext

if config.directory:
fragments_directory = os.path.abspath(
os.path.join(base_directory, config.directory)
)
else:
fragments_directory = os.path.abspath(
os.path.join(
base_directory,
config.package_dir,
config.package,
"newsfragments",
)
)
get_fragments_path = FragmentsPath(base_directory, config)
fragments_directory = get_fragments_path(section_directory=config.sections[section])

if not os.path.exists(fragments_directory):
os.makedirs(fragments_directory)
Expand Down
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/603.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``towncrier create`` action now uses sections defined in your config (either interactively, or via the new ``--section`` option).
Loading

0 comments on commit e3e73bd

Please sign in to comment.