4242log = logging .getLogger ('babel' )
4343
4444
45+ def _parse_toml (fileobj , style ):
46+ """Parse the Babel configuration from the given binary file object.
47+
48+ The ``style`` can be either "pyproject.toml" or "standalone", and dictates the
49+ sections/tables in the TOML file that will be used.
50+ """
51+ try :
52+ import tomllib
53+ except ImportError :
54+ try :
55+ import tomli as tomllib
56+ except ImportError as ie : # pragma: no cover
57+ raise ImportError ("tomli or tomllib is required to parse TOML files" ) from ie
58+
59+ try :
60+ parsed_data = tomllib .load (fileobj )
61+ except tomllib .TOMLDecodeError as e :
62+ raise ConfigurationError (f"{ filename } : Error parsing TOML file: { e } " ) from e
63+
64+ if style == "pyproject.toml" :
65+ try :
66+ babel_data = parsed_data ["tool" ]["babel" ]
67+ except (TypeError , KeyError ) as e :
68+ raise ConfigurationError (
69+ f"{ filename } : No 'tool.babel' section found in file" ,
70+ ) from e
71+ elif style == "standalone" :
72+ babel_data = parsed_data
73+ if "babel" in babel_data :
74+ raise ConfigurationError (
75+ f"{ filename } : 'babel' should not be present in a stand-alone configuration file" ,
76+ )
77+ else : # pragma: no cover
78+ raise ValueError (f"Unknown TOML style { style !r} " )
79+
80+ return babel_data
81+
82+
83+ def find_pyproject_toml (path : pathlib .Path | None , max_depth = 10 ) -> pathlib .Path | None :
84+ """Find the closest "pyproject.toml" file to the specified path.
85+
86+ If the path already points to a "pyproject.toml" file, return it.
87+ Otherwise, check if the path's directory contains one and return that, or walk
88+ up the directory hierarchy until one has been found or the maximum search depth
89+ has been exhausted.
90+ """
91+ if path is None :
92+ path = pathlib .Path ("." )
93+
94+ if not path .is_dir () and path .name == "pyproject.toml" :
95+ return path
96+
97+ tries = 0
98+ while tries < max_depth :
99+ pyproject_file = path / "pyproject.toml"
100+ if pyproject_file .exists ():
101+ return pyproject_file
102+
103+ parent = path .parent
104+ if path == parent :
105+ return None
106+
107+ path = parent
108+ tries += 1
109+
110+ return None
111+
112+
113+ def parse_pyproject_toml_config (path : pathlib .Path , table_name : str ) -> dict :
114+ """Parse the Babel config from a "pyproject.toml" file near the specified path."""
115+ path = find_pyproject_toml (path )
116+ if path and path .exists ():
117+ babel_data = _parse_toml (path .open ("rb" ), style = "pyproject.toml" )
118+ return babel_data [table_name ]
119+ else :
120+ return {}
121+
122+
45123class BaseError (Exception ):
46124 pass
47125
@@ -176,6 +254,7 @@ class CompileCatalog(CommandMixin):
176254 'print statistics about translations' ),
177255 ] # fmt: skip
178256 boolean_options = ['use-fuzzy' , 'statistics' ]
257+ toml_config_table = "compile_catalog"
179258
180259 def initialize_options (self ):
181260 self .domain = 'messages'
@@ -365,6 +444,7 @@ class ExtractMessages(CommandMixin):
365444 option_choices = {
366445 'add-location' : ('full' , 'file' , 'never' ),
367446 }
447+ toml_config_table = "extract_messages"
368448
369449 def initialize_options (self ):
370450 self .charset = 'utf-8'
@@ -561,7 +641,7 @@ def _get_mappings(self):
561641 if os .path .basename (self .mapping_file ) == "pyproject.toml"
562642 else "standalone"
563643 )
564- method_map , options_map = _parse_mapping_toml (
644+ method_map , options_map = _parse_mappings_from_toml (
565645 fileobj ,
566646 filename = self .mapping_file ,
567647 style = file_style ,
@@ -632,6 +712,7 @@ class InitCatalog(CommandMixin):
632712 'into several lines' ),
633713 ] # fmt: skip
634714 boolean_options = ['no-wrap' ]
715+ toml_config_table = "init_catalog"
635716
636717 def initialize_options (self ):
637718 self .output_dir = None
@@ -729,6 +810,7 @@ class UpdateCatalog(CommandMixin):
729810 'check' ,
730811 'ignore-pot-creation-date' ,
731812 ]
813+ toml_config_table = "update_catalog"
732814
733815 def initialize_options (self ):
734816 self .domain = 'messages'
@@ -972,6 +1054,7 @@ def run(self, argv=None):
9721054 if cmdname not in self .commands :
9731055 self .parser .error (f'unknown command "{ cmdname } "' )
9741056
1057+ # TODO add possibility to turn TOML config parsing off, e.g. new option above?
9751058 cmdinst = self ._configure_command (cmdname , args [1 :])
9761059 return cmdinst .run ()
9771060
@@ -1009,14 +1092,16 @@ def _configure_command(self, cmdname, argv):
10091092 assert isinstance (cmdinst , CommandMixin )
10101093 cmdinst .initialize_options ()
10111094
1095+ toml_config = parse_pyproject_toml_config (None , cmdinst .toml_config_table )
10121096 parser = optparse .OptionParser (
10131097 usage = self .usage % (cmdname , '' ),
10141098 description = self .commands [cmdname ],
10151099 )
10161100 as_args : str | None = getattr (cmdclass , "as_args" , None )
10171101 for long , short , help in cmdclass .user_options :
10181102 name = long .strip ("=" )
1019- default = getattr (cmdinst , name .replace ("-" , "_" ))
1103+ normalized_name = name .replace ("-" , "_" )
1104+ default = toml_config .get (normalized_name ) or getattr (cmdinst , normalized_name )
10201105 strs = [f"--{ name } " ]
10211106 if short :
10221107 strs .append (f"-{ short } " )
@@ -1030,9 +1115,13 @@ def _configure_command(self, cmdname, argv):
10301115 parser .add_option (* strs , action = "append" , help = help , choices = choices )
10311116 else :
10321117 parser .add_option (* strs , help = help , default = default , choices = choices )
1118+
1119+ parser .set_defaults (** toml_config )
10331120 options , args = parser .parse_args (argv )
10341121
10351122 if as_args :
1123+ # use the TOML config as fallback if args isn't set
1124+ args = args or toml_config .get (as_args .replace ("-" , "_" ), args )
10361125 setattr (options , as_args .replace ('-' , '_' ), args )
10371126
10381127 for key , value in vars (options ).items ():
@@ -1194,7 +1283,7 @@ def _parse_config_object(config: dict, *, filename="(unknown)"):
11941283 return method_map , options_map
11951284
11961285
1197- def _parse_mapping_toml (
1286+ def _parse_mappings_from_toml (
11981287 fileobj : BinaryIO ,
11991288 filename : str = "(unknown)" ,
12001289 style : Literal ["standalone" , "pyproject.toml" ] = "standalone" ,
@@ -1207,36 +1296,8 @@ def _parse_mapping_toml(
12071296 :param filename: the name of the file being parsed, for error messages
12081297 :param style: whether the file is in the style of a `pyproject.toml` file, i.e. whether to look for `tool.babel`.
12091298 """
1210- try :
1211- import tomllib
1212- except ImportError :
1213- try :
1214- import tomli as tomllib
1215- except ImportError as ie : # pragma: no cover
1216- raise ImportError ("tomli or tomllib is required to parse TOML files" ) from ie
1217-
1218- try :
1219- parsed_data = tomllib .load (fileobj )
1220- except tomllib .TOMLDecodeError as e :
1221- raise ConfigurationError (f"{ filename } : Error parsing TOML file: { e } " ) from e
1222-
1223- if style == "pyproject.toml" :
1224- try :
1225- babel_data = parsed_data ["tool" ]["babel" ]
1226- except (TypeError , KeyError ) as e :
1227- raise ConfigurationError (
1228- f"{ filename } : No 'tool.babel' section found in file" ,
1229- ) from e
1230- elif style == "standalone" :
1231- babel_data = parsed_data
1232- if "babel" in babel_data :
1233- raise ConfigurationError (
1234- f"{ filename } : 'babel' should not be present in a stand-alone configuration file" ,
1235- )
1236- else : # pragma: no cover
1237- raise ValueError (f"Unknown TOML style { style !r} " )
1238-
1239- return _parse_config_object (babel_data , filename = filename )
1299+ parsed_data = _parse_toml (fileobj , style )
1300+ return _parse_config_object (parsed_data , filename = filename )
12401301
12411302
12421303def _parse_spec (s : str ) -> tuple [int | None , tuple [int | tuple [int , str ], ...]]:
0 commit comments