From 205da9cf80a01512bc49c324fceb592f3b4d0cae Mon Sep 17 00:00:00 2001 From: Nick Papior Date: Thu, 20 Feb 2025 09:56:51 +0100 Subject: [PATCH 1/6] enabled fallback of name|package values to project.name This fallback will only work if the pyproject.toml file is present. However, it will work for any combination of pyproject.toml or towncrier.toml configuration files. Note, that project.name is a required field in pyproject.toml files. Hence, the minimal requirement for using towncrier in python project is a no-op! Signed-off-by: Nick Papior --- docs/configuration.rst | 11 +++++---- src/towncrier/_settings/load.py | 35 ++++++++++++++++++++++++---- src/towncrier/test/test_settings.py | 36 +++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 771b3b12..6022cd9d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -17,9 +17,8 @@ A minimal configuration for a Python project looks like this: .. code-block:: toml # pyproject.toml - - [tool.towncrier] - package = "myproject" + [project] + name = "myproject" A minimal configuration for a non-Python project looks like this: @@ -36,9 +35,9 @@ Top level keys ``name`` The name of your project. - For Python projects that provide a ``package`` key, if left empty then the name will be automatically determined. + For Python projects that provide a ``package`` key, if left empty then the name will be automatically determined from the ``package`` key. - ``""`` by default. + Defaults to the key ``[project.name]`` in ``pyproject.toml`` (if present), otherwise defaults to the empty string ``""``. ``version`` The version of your project. @@ -167,6 +166,8 @@ Extra top level keys for Python projects Allows ``name`` and ``version`` to be automatically determined from the Python package. Changes the default ``directory`` to be a ``newsfragments`` directory within this package. + Defaults to the key ``[project.name]`` in ``pyproject.toml`` (if present), otherwise defaults to the empty string ``""``. + ``package_dir`` The folder your package lives. diff --git a/src/towncrier/_settings/load.py b/src/towncrier/_settings/load.py index c54f9798..857dbd4b 100644 --- a/src/towncrier/_settings/load.py +++ b/src/towncrier/_settings/load.py @@ -118,19 +118,44 @@ def load_config(directory: str) -> Config | None: towncrier_toml = os.path.join(directory, "towncrier.toml") pyproject_toml = os.path.join(directory, "pyproject.toml") + # In case the [tool.towncrier.name|package] is not specified + # we'll read it from [project.name] + + if os.path.exists(pyproject_toml): + pyproject_config = load_toml_from_file(pyproject_toml) + else: + # make it empty so it won't be used as a backup plan + pyproject_config = {} + if os.path.exists(towncrier_toml): - config_file = towncrier_toml + config_toml = towncrier_toml elif os.path.exists(pyproject_toml): - config_file = pyproject_toml + config_toml = pyproject_toml else: return None - return load_config_from_file(directory, config_file) + # Read the default configuration. Depending on which exists + config = load_config_from_file(directory, config_toml) + # Fallback certain values depending on the [project.name] + if project_name := pyproject_config.get("project", {}).get("name", ""): + # Fallback to the project name for the configuration name + # and the configuration package entries. + if not config.name: + config.name = project_name + if not config.package: + config.package = project_name -def load_config_from_file(directory: str, config_file: str) -> Config: + return config + + +def load_toml_from_file(config_file: str) -> Mapping[str, Any]: with open(config_file, "rb") as conffile: - config = tomllib.load(conffile) + return tomllib.load(conffile) + + +def load_config_from_file(directory: str, config_file: str) -> Config: + config = load_toml_from_file(config_file) return parse_toml(directory, config) diff --git a/src/towncrier/test/test_settings.py b/src/towncrier/test/test_settings.py index 14554086..1ad1ffd3 100644 --- a/src/towncrier/test/test_settings.py +++ b/src/towncrier/test/test_settings.py @@ -194,6 +194,42 @@ def test_towncrier_toml_preferred(self): config = load_config(project_dir) self.assertEqual(config.package, "a") + def test_pyproject_name_fallback_both(self): + """ + Towncrier will fallback to the [project.name] value in pyproject.toml. + """ + project_dir = self.mktemp_project( + pyproject_toml=""" + [project] + name = "a" + """, + ) + + config = load_config(project_dir) + self.assertEqual(config.package, "a") + self.assertEqual(config.name, "a") + + def test_pyproject_name_fallback_towncrier(self): + """ + Towncrier will fallback to the [project.name] value in pyproject.toml. + """ + project_dir = self.mktemp_project( + towncrier_toml=""" + [tool.towncrier] + package = "a" + """, + pyproject_toml=""" + [project] + name = "c" + [tool.towncrier] + name = "b" + """, + ) + + config = load_config(project_dir) + self.assertEqual(config.package, "a") + self.assertEqual(config.name, "c") + @with_isolated_runner def test_load_no_config(self, runner: CliRunner): """ From 8f8c9f51224dcd5dcb34546ea7dbce7104e99ce4 Mon Sep 17 00:00:00 2001 From: Nick Papior Date: Thu, 20 Feb 2025 09:57:42 +0100 Subject: [PATCH 2/6] added the news-fragment Signed-off-by: Nick Papior --- src/towncrier/newsfragments/687.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/towncrier/newsfragments/687.feature.rst diff --git a/src/towncrier/newsfragments/687.feature.rst b/src/towncrier/newsfragments/687.feature.rst new file mode 100644 index 00000000..42ed416c --- /dev/null +++ b/src/towncrier/newsfragments/687.feature.rst @@ -0,0 +1 @@ +Enabled fallback values of [tool.towncrier.name|package] to [project.name] if pyproject.toml files are used From 7eda8b3128b8a68c9da8663dab9397bfe311b186 Mon Sep 17 00:00:00 2001 From: Nick Papior Date: Thu, 20 Feb 2025 10:02:25 +0100 Subject: [PATCH 3/6] a minimal configuration now does not require tool.towncrier Signed-off-by: Nick Papior --- src/towncrier/_settings/load.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/towncrier/_settings/load.py b/src/towncrier/_settings/load.py index 857dbd4b..7ca18342 100644 --- a/src/towncrier/_settings/load.py +++ b/src/towncrier/_settings/load.py @@ -166,10 +166,7 @@ def load_config_from_file(directory: str, config_file: str) -> Config: def parse_toml(base_path: str, config: Mapping[str, Any]) -> Config: - if "towncrier" not in (config.get("tool") or {}): - raise ConfigError("No [tool.towncrier] section.", failing_option="all") - - config = config["tool"]["towncrier"] + config = config.get("tool", {}).get("towncrier", {}) parsed_data = {} # Check for misspelt options. From 45cc1bff988ef2bcbc6999b4bc0a21a37f7cb9bb Mon Sep 17 00:00:00 2001 From: Nick Papior Date: Thu, 20 Feb 2025 10:05:38 +0100 Subject: [PATCH 4/6] removed test that asserts tool.towncrier section Due to the lifting of minimal configuration in python projects the entry is not required any more. Signed-off-by: Nick Papior --- src/towncrier/test/test_settings.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/towncrier/test/test_settings.py b/src/towncrier/test/test_settings.py index 1ad1ffd3..7a5833ba 100644 --- a/src/towncrier/test/test_settings.py +++ b/src/towncrier/test/test_settings.py @@ -112,22 +112,6 @@ def test_template_extended(self): self.assertEqual(config.template, ("towncrier.templates", "default.rst")) - def test_missing(self): - """ - If the config file doesn't have the correct toml key, we error. - """ - project_dir = self.mktemp_project( - pyproject_toml=""" - [something.else] - blah='baz' - """ - ) - - with self.assertRaises(ConfigError) as e: - load_config(project_dir) - - self.assertEqual(e.exception.failing_option, "all") - def test_incorrect_single_file(self): """ single_file must be a bool. From afe1f4291cab71285a4104b46e72940aabff1960 Mon Sep 17 00:00:00 2001 From: Nick Papior Date: Mon, 24 Feb 2025 20:25:42 +0100 Subject: [PATCH 5/6] amended tests according to feedback in PR Signed-off-by: Nick Papior --- src/towncrier/newsfragments/687.feature.rst | 4 +- src/towncrier/test/test_settings.py | 91 +++++++++++++++++---- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/towncrier/newsfragments/687.feature.rst b/src/towncrier/newsfragments/687.feature.rst index 42ed416c..42d67ec8 100644 --- a/src/towncrier/newsfragments/687.feature.rst +++ b/src/towncrier/newsfragments/687.feature.rst @@ -1 +1,3 @@ -Enabled fallback values of [tool.towncrier.name|package] to [project.name] if pyproject.toml files are used +When used with an `pyproject.toml` file, when no explicit values are +defined for [tool.towncrier.name|package] they will now fallback to +the value of [project.name]. diff --git a/src/towncrier/test/test_settings.py b/src/towncrier/test/test_settings.py index 7a5833ba..f0747ba9 100644 --- a/src/towncrier/test/test_settings.py +++ b/src/towncrier/test/test_settings.py @@ -3,6 +3,8 @@ import os +from textwrap import dedent + from click.testing import CliRunner from twisted.trial.unittest import TestCase @@ -178,9 +180,12 @@ def test_towncrier_toml_preferred(self): config = load_config(project_dir) self.assertEqual(config.package, "a") - def test_pyproject_name_fallback_both(self): + def test_pyproject_only_pyproject_toml(self): """ Towncrier will fallback to the [project.name] value in pyproject.toml. + + This tests asserts that the minimal configuration is to do *nothing* + when using a pyproject.toml file. """ project_dir = self.mktemp_project( pyproject_toml=""" @@ -193,26 +198,78 @@ def test_pyproject_name_fallback_both(self): self.assertEqual(config.package, "a") self.assertEqual(config.name, "a") - def test_pyproject_name_fallback_towncrier(self): + def test_pyproject_assert_fallback(self): """ - Towncrier will fallback to the [project.name] value in pyproject.toml. + This test is an extensive test of the fallback scenarios + for the `package` and `name` keys in the towncrier section. + + It will fallback to pyproject.toml:name in any case. + And as such it checks the various fallback mechanisms + if the fields are not present in the towncrier.toml, nor + in the pyproject.toml files. + + This both tests when things are *only* in the pyproject.toml + and default usage of the data in the towncrier.toml file. + """ + pyproject_toml = dedent( + """ + [project] + name = "foo" + [tool.towncrier] """ - project_dir = self.mktemp_project( - towncrier_toml=""" - [tool.towncrier] - package = "a" - """, - pyproject_toml=""" - [project] - name = "c" - [tool.towncrier] - name = "b" - """, ) + towncrier_toml = dedent( + """ + [tool.towncrier] + """ + ) + tests = [ + "", + "name = '{name}'", + "package = '{package}'", + "name = '{name}'", + "package = '{package}'", + ] - config = load_config(project_dir) - self.assertEqual(config.package, "a") - self.assertEqual(config.name, "c") + def factory(name, package): + def func(test): + return dedent(test).format(name=name, package=package) + + return func + + for pp_fields in map(factory(name="a", package="b"), tests): + pp_toml = pyproject_toml + pp_fields + for tc_fields in map(factory(name="c", package="d"), tests): + tc_toml = towncrier_toml + tc_fields + + # Create the temporary project + project_dir = self.mktemp_project( + pyproject_toml=pp_toml, + towncrier_toml=tc_toml, + ) + + # Read the configuration file. + config = load_config(project_dir) + + # Now the values depend on where the fallback + # is. + # If something is in towncrier.toml, it will be preferred + # name fallsback to package + if "package" in tc_fields: + package = "d" + elif "package" in pp_fields: + package = "b" + else: + package = "foo" + self.assertEqual(config.package, package) + + if "name" in tc_fields: + self.assertEqual(config.name, "c") + elif "name" in pp_fields: + self.assertEqual(config.name, "a") + else: + # fall-back to package name + self.assertEqual(config.name, package) @with_isolated_runner def test_load_no_config(self, runner: CliRunner): From cfdb1f0923defad88f5a9544949a5f5f15c8fe59 Mon Sep 17 00:00:00 2001 From: Nick Papior Date: Mon, 24 Feb 2025 20:59:03 +0100 Subject: [PATCH 6/6] fixed order of field grabbing Signed-off-by: Nick Papior --- src/towncrier/_settings/load.py | 4 ++-- src/towncrier/test/test_settings.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/towncrier/_settings/load.py b/src/towncrier/_settings/load.py index 7ca18342..3a70a67a 100644 --- a/src/towncrier/_settings/load.py +++ b/src/towncrier/_settings/load.py @@ -141,10 +141,10 @@ def load_config(directory: str) -> Config | None: if project_name := pyproject_config.get("project", {}).get("name", ""): # Fallback to the project name for the configuration name # and the configuration package entries. - if not config.name: - config.name = project_name if not config.package: config.package = project_name + if not config.name: + config.name = config.package return config diff --git a/src/towncrier/test/test_settings.py b/src/towncrier/test/test_settings.py index f0747ba9..f46db2b2 100644 --- a/src/towncrier/test/test_settings.py +++ b/src/towncrier/test/test_settings.py @@ -257,16 +257,12 @@ def func(test): # name fallsback to package if "package" in tc_fields: package = "d" - elif "package" in pp_fields: - package = "b" else: package = "foo" self.assertEqual(config.package, package) if "name" in tc_fields: self.assertEqual(config.name, "c") - elif "name" in pp_fields: - self.assertEqual(config.name, "a") else: # fall-back to package name self.assertEqual(config.name, package)