From 0c1de45d3aa22b575f737bc7518d4c371dd7557f Mon Sep 17 00:00:00 2001 From: Ben Rowland Date: Fri, 8 Nov 2024 16:10:57 +0000 Subject: [PATCH] fix: make compatible with towncrier>=24.7 towncrier 24.7 changed the way that its find_fragments() function works to accept a Config dataclass instead of specific components of the config. This commit adds a new version of lookup_towncrier_fragments() to use the new API and chooses which one to use based on the version of towncrier obtained from importlib.metadata. It also slightly changes the way that towncrier config can be obtained: the previous get_towncrier_config() function (which returned a dict) is now renamed as get_towncrier_config_as_dict() and get_towncrier_config() returns the new Config dataclass object for towncrier versions above 22.12.0rc1 and raises NotImplementedError for older versions. This API change should be ok as get_towncrier_config() was only called in one place before. --- setup.cfg | 1 + .../towncrier/_fragment_discovery.py | 52 ++++++++++++++++++- src/sphinxcontrib/towncrier/_towncrier.py | 17 ++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 00e7576..742252e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ classifiers = [options] include_package_data = True install_requires = + packaging sphinx towncrier >= 23 package_dir = diff --git a/src/sphinxcontrib/towncrier/_fragment_discovery.py b/src/sphinxcontrib/towncrier/_fragment_discovery.py index 4903366..7547de4 100644 --- a/src/sphinxcontrib/towncrier/_fragment_discovery.py +++ b/src/sphinxcontrib/towncrier/_fragment_discovery.py @@ -2,11 +2,14 @@ from functools import lru_cache +from importlib.metadata import version from pathlib import Path from typing import Optional, Set from sphinx.util import logging +from packaging.version import Version + try: # pylint: disable=no-name-in-module @@ -17,7 +20,9 @@ find_fragments, ) -from ._towncrier import get_towncrier_config # noqa: WPS436 +from ._towncrier import ( # noqa: WPS436 + get_towncrier_config, get_towncrier_config_as_dict, +) logger = logging.getLogger(__name__) @@ -42,11 +47,21 @@ def _find_config_file(base: Path) -> Path: # pylint: disable=fixme # FIXME: refactor `lookup_towncrier_fragments` to drop noqas @lru_cache(maxsize=1, typed=True) # noqa: WPS210 -def lookup_towncrier_fragments( # noqa: WPS210 +def lookup_towncrier_fragments( working_dir: Optional[str] = None, config_path: Optional[str] = None, ) -> Set[Path]: """Emit RST-formatted Towncrier changelog fragment paths.""" + tc_version = Version(version('towncrier')) + if tc_version >= Version('24.7.0'): + return _lookup_towncrier_fragments_post24_7(working_dir, config_path) + return _lookup_towncrier_fragments_pre24_7(working_dir, config_path) + + +# used for towncrier version 24.7 and above +def _lookup_towncrier_fragments_post24_7( # noqa: WPS210 + working_dir: Optional[str] = None, config_path: Optional[str] = None, +) -> Set[Path]: project_path = Path.cwd() if working_dir is None else Path(working_dir) final_config_path = ( @@ -66,6 +81,39 @@ def lookup_towncrier_fragments( # noqa: WPS210 ) return set() + fragment_dir = (towncrier_config.directory or 'newsfragments') + fragment_base_directory = project_path / fragment_dir + + _fragments, fragment_filenames = find_fragments( + str(fragment_base_directory), towncrier_config, strict=False, + ) + + return {Path(fname[0]) for fname in fragment_filenames} + + +# used for versions of towncrier before 24.7 +def _lookup_towncrier_fragments_pre24_7( # noqa: WPS210 + working_dir: Optional[str] = None, config_path: Optional[str] = None, +) -> Set[Path]: + project_path = Path.cwd() if working_dir is None else Path(working_dir) + + final_config_path = ( + _resolve_spec_config(project_path, config_path) + or _find_config_file(project_path) + ) + + try: + towncrier_config = get_towncrier_config_as_dict( + project_path, + final_config_path, + ) + except KeyError as key_err: + # NOTE: The error is missing key 'towncrier' or similar + logger.warning( + f'Missing key {key_err!s} in file {final_config_path!s}', + ) + return set() + fragment_directory: Optional[str] = 'newsfragments' try: fragment_base_directory = project_path / towncrier_config['directory'] diff --git a/src/sphinxcontrib/towncrier/_towncrier.py b/src/sphinxcontrib/towncrier/_towncrier.py index 4c114c3..f2d0000 100644 --- a/src/sphinxcontrib/towncrier/_towncrier.py +++ b/src/sphinxcontrib/towncrier/_towncrier.py @@ -1,13 +1,30 @@ """Towncrier related shims.""" from dataclasses import asdict as _dataclass_to_dict +from importlib.metadata import version from pathlib import Path from typing import Any, Dict, Union from towncrier._settings.load import load_config_from_file # noqa: WPS436 +from packaging.version import Version + def get_towncrier_config( + project_path: Path, + final_config_path: Union[Path, None], +) -> Any: + """Return the towncrier config in native format.""" + tc_version = Version(version('towncrier')) + if tc_version >= Version('22.12.0rc1'): + return load_config_from_file(str(project_path), str(final_config_path)) + raise NotImplementedError( + 'Towncrier Config is not available before version 22.12.0rc1, consider ' + 'using "get_towncrier_config_as_dict()" instead', + ) + + +def get_towncrier_config_as_dict( project_path: Path, final_config_path: Union[Path, None], ) -> Dict[str, Any]: # FIXME: add a better type # pylint: disable=fixme