Skip to content

Implement parsing of default command configuration from pyproject.toml - #1290

Open
max-moser wants to merge 2 commits into
python-babel:masterfrom
max-moser:mm/pyproject-config
Open

max-moser wants to merge 2 commits into
python-babel:masterfrom
max-moser:mm/pyproject-config

Conversation

@max-moser

Copy link
Copy Markdown

I believe this should close #777

This PR adds support of pyproject.toml as a source for default configuration values for the various CLI commands (compile, extract, init, update).
The configuration will be read from pyproject.toml, under the [tool.babel.<command_name>] table, where <command_name> is one of compile_catalog, extract_messages, init_catalog, update_catalog (taken from frontend_setuptools).

The motivation for this PR here is that I've recently migrated most of the inveniosoftware packages from setuptools to hatchling and we included the old configuration from setup.cfg in pyproject.toml, only to find that pybabel doesn't actually read any command config from pyproject.toml 😅
(Luckily for us that was only a minor issue in one package that we fixed otherwise)


The mechanism for finding pyproject.toml is similar to that used in other tools, like isort.
I could also imagine adding a global CLI flag for disabling reading pyproject.toml altogether, or for letting users specify another TOML config file instead.

The parsed values are inserted as default values for the optparse parser, so user-provided CLI flags should still take precedence.

Please let me know if this goes in an acceptable direction, then I can polish up the implementation a bit 🙂

@max-moser
max-moser force-pushed the mm/pyproject-config branch from 65a5cdb to ceb1f4b Compare July 7, 2026 14:34
@akx akx self-assigned this Jul 7, 2026
@akx akx added the feature label Jul 7, 2026
@max-moser
max-moser force-pushed the mm/pyproject-config branch from ceb1f4b to 511f10d Compare July 11, 2026 08:47
@akx akx added this to the Babel 2.19 milestone Aug 14, 2026
@akx
akx force-pushed the mm/pyproject-config branch from 511f10d to da57261 Compare October 30, 2026 09:11

@akx akx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would require tests -- oh and what happens if this finds a pyproject.toml that doesn't have tool.babel?

@max-moser

Copy link
Copy Markdown
Author

Thanks for the review, I'll spruce up the PR a bit. 🙂

@max-moser
max-moser force-pushed the mm/pyproject-config branch from da57261 to a39395c Compare October 30, 2026 10:46
If present, use a "pyproject.toml" file for default configuration of
`pybabel` commands.
@max-moser
max-moser force-pushed the mm/pyproject-config branch from a39395c to af58d33 Compare October 30, 2026 11:07

@max-moser max-moser left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've touched up the PR a bit to address your feedback (especially the case when the section is missing from pyproject.toml) and generally touched up the implementation a little bit (with type hints and such).

There's now a test case for the config parsing itself, and one for the extract_messages CLI command that basically emulates what another test did, but with the config coming from pyproject.toml instead of CLI args.

if cmdname not in self.commands:
self.parser.error(f'unknown command "{cmdname}"')

# TODO add possibility to turn TOML config parsing off, e.g. new option above?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akx thoughts & prayers?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... at the very least I think we should fail gracefully here if there's e.g. a malformed pyproject.toml somewhere up the directory hierarchy?

@akx akx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Some comments -- this is looking pretty good.

I think we might want a BABEL_NO_PYPROJECT_TOML escape hatch for now?

Comment on lines +57 to +62
The ``style`` can be either "pyproject.toml" or "standalone", and dictates
the sections/tables in the TOML file that will be used.
The ``filename`` will be used in error messages.
If ``allow_missing_sections`` is set and the TOML file doesn't have a
``[tool.babel]`` section, an empty dictionary will be returned instead of raising
a ``ConfigurationError``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the standard :param: , etc. docstring markers here?

Comment on lines +51 to +53
style: Literal["standalone", "pyproject.toml"],
filename: str = "(unknown)",
allow_missing_sections: bool = False,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the rest of these args kwarg-only:

Suggested change
style: Literal["standalone", "pyproject.toml"],
filename: str = "(unknown)",
allow_missing_sections: bool = False,
*,
style: Literal["standalone", "pyproject.toml"],
filename: str = "(unknown)",
allow_missing_sections: bool = False,

return babel_data


def find_pyproject_toml(path: pathlib.Path | None, max_depth=10) -> pathlib.Path | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def find_pyproject_toml(path: pathlib.Path | None, max_depth=10) -> pathlib.Path | None:
def find_pyproject_toml(path: pathlib.Path | None, max_depth: int = 10) -> pathlib.Path | None:

def find_pyproject_toml(path: pathlib.Path | None, max_depth=10) -> pathlib.Path | None:
"""Find the closest "pyproject.toml" file to the specified path.

If the path already points to a "pyproject.toml" file, return it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function ever expected to actually be called with a direct pyproject.toml path? Currently doesn't look like it, so I'd maybe get rid of that logic?

tries = 0
while tries < max_depth:
pyproject_file = path / "pyproject.toml"
if pyproject_file.exists():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

Suggested change
if pyproject_file.exists():
if pyproject_file.is_file():

else:
parser.add_option(*strs, help=help, default=default, choices=choices)

parser.set_defaults(**toml_config)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this separately necessary? 🤔

Comment on lines +1386 to +1387
# use the TOML config as fallback if args isn't set
args = args or toml_config.get(as_args.replace("-", "_"), args)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Comment on lines +50 to +56
"""
Fixture to create ``${data_dir}/pyproject.toml`` and clean it after the test.
"""
pyproject_toml = (pathlib.Path(data_dir) / "pyproject.toml")
pyproject_toml.touch()
yield pyproject_toml
pyproject_toml.unlink()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause race conditions when tests are being run in parallel with e.g. pytest-xdist.

See e.g. the pot_file fixture that uses tmp_path - could we do something like that here?


def test_parse_pyproject_toml_config_without_pyproject_toml_file(tmp_path):
"""
Test that trying to parse the config from a missing ``pyproject.toml`` doesn't crash.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This too could fail in the esoteric case that someone has a stray pyproject.toml somewhere in their tmp hierarchy... but maybe we'll take that chance.

"""
(tmp_path / "pyproject.toml").write_text(pyproject_toml_content)
assert frontend.parse_pyproject_toml_config(tmp_path, "extract_messages") == expected
(tmp_path / "pyproject.toml").unlink()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(tmp_path / "pyproject.toml").unlink()

No need, tmp_path will be nuked by pytest anyhow.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parse settings from pyproject.toml

2 participants