Skip to content

Commit cd14c9c

Browse files
authored
Allowed fallback usage of project.name for name and package if pyproject.toml exists (#687)
2 parents cd0d2df + cfdb1f0 commit cd14c9c

File tree

4 files changed

+129
-30
lines changed

4 files changed

+129
-30
lines changed

docs/configuration.rst

+6-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ A minimal configuration for a Python project looks like this:
1717
.. code-block:: toml
1818
1919
# pyproject.toml
20-
21-
[tool.towncrier]
22-
package = "myproject"
20+
[project]
21+
name = "myproject"
2322
2423
A minimal configuration for a non-Python project looks like this:
2524

@@ -36,9 +35,9 @@ Top level keys
3635
``name``
3736
The name of your project.
3837

39-
For Python projects that provide a ``package`` key, if left empty then the name will be automatically determined.
38+
For Python projects that provide a ``package`` key, if left empty then the name will be automatically determined from the ``package`` key.
4039

41-
``""`` by default.
40+
Defaults to the key ``[project.name]`` in ``pyproject.toml`` (if present), otherwise defaults to the empty string ``""``.
4241

4342
``version``
4443
The version of your project.
@@ -167,6 +166,8 @@ Extra top level keys for Python projects
167166
Allows ``name`` and ``version`` to be automatically determined from the Python package.
168167
Changes the default ``directory`` to be a ``newsfragments`` directory within this package.
169168

169+
Defaults to the key ``[project.name]`` in ``pyproject.toml`` (if present), otherwise defaults to the empty string ``""``.
170+
170171
``package_dir``
171172
The folder your package lives.
172173

src/towncrier/_settings/load.py

+31-9
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,44 @@ def load_config(directory: str) -> Config | None:
118118
towncrier_toml = os.path.join(directory, "towncrier.toml")
119119
pyproject_toml = os.path.join(directory, "pyproject.toml")
120120

121+
# In case the [tool.towncrier.name|package] is not specified
122+
# we'll read it from [project.name]
123+
124+
if os.path.exists(pyproject_toml):
125+
pyproject_config = load_toml_from_file(pyproject_toml)
126+
else:
127+
# make it empty so it won't be used as a backup plan
128+
pyproject_config = {}
129+
121130
if os.path.exists(towncrier_toml):
122-
config_file = towncrier_toml
131+
config_toml = towncrier_toml
123132
elif os.path.exists(pyproject_toml):
124-
config_file = pyproject_toml
133+
config_toml = pyproject_toml
125134
else:
126135
return None
127136

128-
return load_config_from_file(directory, config_file)
137+
# Read the default configuration. Depending on which exists
138+
config = load_config_from_file(directory, config_toml)
129139

140+
# Fallback certain values depending on the [project.name]
141+
if project_name := pyproject_config.get("project", {}).get("name", ""):
142+
# Fallback to the project name for the configuration name
143+
# and the configuration package entries.
144+
if not config.package:
145+
config.package = project_name
146+
if not config.name:
147+
config.name = config.package
130148

131-
def load_config_from_file(directory: str, config_file: str) -> Config:
149+
return config
150+
151+
152+
def load_toml_from_file(config_file: str) -> Mapping[str, Any]:
132153
with open(config_file, "rb") as conffile:
133-
config = tomllib.load(conffile)
154+
return tomllib.load(conffile)
155+
156+
157+
def load_config_from_file(directory: str, config_file: str) -> Config:
158+
config = load_toml_from_file(config_file)
134159

135160
return parse_toml(directory, config)
136161

@@ -141,10 +166,7 @@ def load_config_from_file(directory: str, config_file: str) -> Config:
141166

142167

143168
def parse_toml(base_path: str, config: Mapping[str, Any]) -> Config:
144-
if "towncrier" not in (config.get("tool") or {}):
145-
raise ConfigError("No [tool.towncrier] section.", failing_option="all")
146-
147-
config = config["tool"]["towncrier"]
169+
config = config.get("tool", {}).get("towncrier", {})
148170
parsed_data = {}
149171

150172
# Check for misspelt options.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When used with an `pyproject.toml` file, when no explicit values are
2+
defined for [tool.towncrier.name|package] they will now fallback to
3+
the value of [project.name].

src/towncrier/test/test_settings.py

+89-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import os
55

6+
from textwrap import dedent
7+
68
from click.testing import CliRunner
79
from twisted.trial.unittest import TestCase
810

@@ -112,22 +114,6 @@ def test_template_extended(self):
112114

113115
self.assertEqual(config.template, ("towncrier.templates", "default.rst"))
114116

115-
def test_missing(self):
116-
"""
117-
If the config file doesn't have the correct toml key, we error.
118-
"""
119-
project_dir = self.mktemp_project(
120-
pyproject_toml="""
121-
[something.else]
122-
blah='baz'
123-
"""
124-
)
125-
126-
with self.assertRaises(ConfigError) as e:
127-
load_config(project_dir)
128-
129-
self.assertEqual(e.exception.failing_option, "all")
130-
131117
def test_incorrect_single_file(self):
132118
"""
133119
single_file must be a bool.
@@ -194,6 +180,93 @@ def test_towncrier_toml_preferred(self):
194180
config = load_config(project_dir)
195181
self.assertEqual(config.package, "a")
196182

183+
def test_pyproject_only_pyproject_toml(self):
184+
"""
185+
Towncrier will fallback to the [project.name] value in pyproject.toml.
186+
187+
This tests asserts that the minimal configuration is to do *nothing*
188+
when using a pyproject.toml file.
189+
"""
190+
project_dir = self.mktemp_project(
191+
pyproject_toml="""
192+
[project]
193+
name = "a"
194+
""",
195+
)
196+
197+
config = load_config(project_dir)
198+
self.assertEqual(config.package, "a")
199+
self.assertEqual(config.name, "a")
200+
201+
def test_pyproject_assert_fallback(self):
202+
"""
203+
This test is an extensive test of the fallback scenarios
204+
for the `package` and `name` keys in the towncrier section.
205+
206+
It will fallback to pyproject.toml:name in any case.
207+
And as such it checks the various fallback mechanisms
208+
if the fields are not present in the towncrier.toml, nor
209+
in the pyproject.toml files.
210+
211+
This both tests when things are *only* in the pyproject.toml
212+
and default usage of the data in the towncrier.toml file.
213+
"""
214+
pyproject_toml = dedent(
215+
"""
216+
[project]
217+
name = "foo"
218+
[tool.towncrier]
219+
"""
220+
)
221+
towncrier_toml = dedent(
222+
"""
223+
[tool.towncrier]
224+
"""
225+
)
226+
tests = [
227+
"",
228+
"name = '{name}'",
229+
"package = '{package}'",
230+
"name = '{name}'",
231+
"package = '{package}'",
232+
]
233+
234+
def factory(name, package):
235+
def func(test):
236+
return dedent(test).format(name=name, package=package)
237+
238+
return func
239+
240+
for pp_fields in map(factory(name="a", package="b"), tests):
241+
pp_toml = pyproject_toml + pp_fields
242+
for tc_fields in map(factory(name="c", package="d"), tests):
243+
tc_toml = towncrier_toml + tc_fields
244+
245+
# Create the temporary project
246+
project_dir = self.mktemp_project(
247+
pyproject_toml=pp_toml,
248+
towncrier_toml=tc_toml,
249+
)
250+
251+
# Read the configuration file.
252+
config = load_config(project_dir)
253+
254+
# Now the values depend on where the fallback
255+
# is.
256+
# If something is in towncrier.toml, it will be preferred
257+
# name fallsback to package
258+
if "package" in tc_fields:
259+
package = "d"
260+
else:
261+
package = "foo"
262+
self.assertEqual(config.package, package)
263+
264+
if "name" in tc_fields:
265+
self.assertEqual(config.name, "c")
266+
else:
267+
# fall-back to package name
268+
self.assertEqual(config.name, package)
269+
197270
@with_isolated_runner
198271
def test_load_no_config(self, runner: CliRunner):
199272
"""

0 commit comments

Comments
 (0)