Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 78 additions & 33 deletions babel/messages/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,68 @@
log = logging.getLogger('babel')


def _parse_toml(fileobj, style):
try:
import tomllib
except ImportError:
try:
import tomli as tomllib
except ImportError as ie: # pragma: no cover
raise ImportError("tomli or tomllib is required to parse TOML files") from ie

try:
parsed_data = tomllib.load(fileobj)
except tomllib.TOMLDecodeError as e:
raise ConfigurationError(f"{filename}: Error parsing TOML file: {e}") from e

if style == "pyproject.toml":
try:
babel_data = parsed_data["tool"]["babel"]
except (TypeError, KeyError) as e:
raise ConfigurationError(
f"{filename}: No 'tool.babel' section found in file",
) from e
elif style == "standalone":
babel_data = parsed_data
if "babel" in babel_data:
raise ConfigurationError(
f"{filename}: 'babel' should not be present in a stand-alone configuration file",
)
else: # pragma: no cover
raise ValueError(f"Unknown TOML style {style!r}")

return babel_data


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

if not path.is_dir() and path.name == "pyproject.toml":
return path

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

parent = path.parent
if path == parent:
return None

path = parent
tries += 1

return None


def parse_pyproject_toml_config(path: pathlib.Path, table_name: str) -> dict:
path = find_pyproject_toml(path)
babel_data = _parse_toml(path.open("rb"), style="pyproject.toml")
return babel_data[table_name]


class BaseError(Exception):
pass

Expand Down Expand Up @@ -176,6 +238,7 @@ class CompileCatalog(CommandMixin):
'print statistics about translations'),
] # fmt: skip
boolean_options = ['use-fuzzy', 'statistics']
toml_config_table = "compile_catalog"

def initialize_options(self):
self.domain = 'messages'
Expand Down Expand Up @@ -365,6 +428,7 @@ class ExtractMessages(CommandMixin):
option_choices = {
'add-location': ('full', 'file', 'never'),
}
toml_config_table = "extract_messages"

def initialize_options(self):
self.charset = 'utf-8'
Expand Down Expand Up @@ -561,7 +625,7 @@ def _get_mappings(self):
if os.path.basename(self.mapping_file) == "pyproject.toml"
else "standalone"
)
method_map, options_map = _parse_mapping_toml(
method_map, options_map = _parse_mappings_from_toml(
fileobj,
filename=self.mapping_file,
style=file_style,
Expand Down Expand Up @@ -632,6 +696,7 @@ class InitCatalog(CommandMixin):
'into several lines'),
] # fmt: skip
boolean_options = ['no-wrap']
toml_config_table = "init_catalog"

def initialize_options(self):
self.output_dir = None
Expand Down Expand Up @@ -729,6 +794,7 @@ class UpdateCatalog(CommandMixin):
'check',
'ignore-pot-creation-date',
]
toml_config_table = "update_catalog"

def initialize_options(self):
self.domain = 'messages'
Expand Down Expand Up @@ -972,6 +1038,7 @@ def run(self, argv=None):
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?
cmdinst = self._configure_command(cmdname, args[1:])
return cmdinst.run()

Expand Down Expand Up @@ -1009,14 +1076,16 @@ def _configure_command(self, cmdname, argv):
assert isinstance(cmdinst, CommandMixin)
cmdinst.initialize_options()

toml_config = parse_pyproject_toml_config(None, cmdinst.toml_config_table)
parser = optparse.OptionParser(
usage=self.usage % (cmdname, ''),
description=self.commands[cmdname],
)
as_args: str | None = getattr(cmdclass, "as_args", None)
for long, short, help in cmdclass.user_options:
name = long.strip("=")
default = getattr(cmdinst, name.replace("-", "_"))
normalized_name = name.replace("-", "_")
default = toml_config.get(normalized_name) or getattr(cmdinst, normalized_name)
strs = [f"--{name}"]
if short:
strs.append(f"-{short}")
Expand All @@ -1030,9 +1099,13 @@ def _configure_command(self, cmdname, argv):
parser.add_option(*strs, action="append", help=help, choices=choices)
else:
parser.add_option(*strs, help=help, default=default, choices=choices)

parser.set_defaults(**toml_config)
options, args = parser.parse_args(argv)

if as_args:
# use the TOML config as fallback if args isn't set
args = args or toml_config.get(as_args.replace("-", "_"), args)
setattr(options, as_args.replace('-', '_'), args)

for key, value in vars(options).items():
Expand Down Expand Up @@ -1194,7 +1267,7 @@ def _parse_config_object(config: dict, *, filename="(unknown)"):
return method_map, options_map


def _parse_mapping_toml(
def _parse_mappings_from_toml(
fileobj: BinaryIO,
filename: str = "(unknown)",
style: Literal["standalone", "pyproject.toml"] = "standalone",
Expand All @@ -1207,36 +1280,8 @@ def _parse_mapping_toml(
:param filename: the name of the file being parsed, for error messages
:param style: whether the file is in the style of a `pyproject.toml` file, i.e. whether to look for `tool.babel`.
"""
try:
import tomllib
except ImportError:
try:
import tomli as tomllib
except ImportError as ie: # pragma: no cover
raise ImportError("tomli or tomllib is required to parse TOML files") from ie

try:
parsed_data = tomllib.load(fileobj)
except tomllib.TOMLDecodeError as e:
raise ConfigurationError(f"{filename}: Error parsing TOML file: {e}") from e

if style == "pyproject.toml":
try:
babel_data = parsed_data["tool"]["babel"]
except (TypeError, KeyError) as e:
raise ConfigurationError(
f"{filename}: No 'tool.babel' section found in file",
) from e
elif style == "standalone":
babel_data = parsed_data
if "babel" in babel_data:
raise ConfigurationError(
f"{filename}: 'babel' should not be present in a stand-alone configuration file",
)
else: # pragma: no cover
raise ValueError(f"Unknown TOML style {style!r}")

return _parse_config_object(babel_data, filename=filename)
parsed_data = _parse_toml(fileobj, style)
return _parse_config_object(parsed_data, filename=filename)


def _parse_spec(s: str) -> tuple[int | None, tuple[int | tuple[int, str], ...]]:
Expand Down