1919import shutil
2020import sys
2121import tempfile
22+ import warnings
2223from collections import OrderedDict
2324from configparser import RawConfigParser
2425from io import StringIO
25- from typing import Iterable
26+ from typing import BinaryIO , Iterable , Literal
2627
2728from babel import Locale , localedata
2829from babel import __version__ as VERSION
@@ -53,6 +54,12 @@ class SetupError(BaseError):
5354 pass
5455
5556
57+ class ConfigurationError (BaseError ):
58+ """
59+ Raised for errors in configuration files.
60+ """
61+
62+
5663def listify_value (arg , split = None ):
5764 """
5865 Make a list out of an argument.
@@ -534,16 +541,29 @@ def _get_mappings(self):
534541 mappings = []
535542
536543 if self .mapping_file :
537- with open (self .mapping_file ) as fileobj :
538- method_map , options_map = parse_mapping (fileobj )
544+ if self .mapping_file .endswith (".toml" ):
545+ with open (self .mapping_file , "rb" ) as fileobj :
546+ file_style = (
547+ "pyproject.toml"
548+ if os .path .basename (self .mapping_file ) == "pyproject.toml"
549+ else "standalone"
550+ )
551+ method_map , options_map = _parse_mapping_toml (
552+ fileobj ,
553+ filename = self .mapping_file ,
554+ style = file_style ,
555+ )
556+ else :
557+ with open (self .mapping_file ) as fileobj :
558+ method_map , options_map = parse_mapping_cfg (fileobj , filename = self .mapping_file )
539559 for path in self .input_paths :
540560 mappings .append ((path , method_map , options_map ))
541561
542562 elif getattr (self .distribution , 'message_extractors' , None ):
543563 message_extractors = self .distribution .message_extractors
544564 for path , mapping in message_extractors .items ():
545565 if isinstance (mapping , str ):
546- method_map , options_map = parse_mapping (StringIO (mapping ))
566+ method_map , options_map = parse_mapping_cfg (StringIO (mapping ))
547567 else :
548568 method_map , options_map = [], {}
549569 for pattern , method , options in mapping :
@@ -980,53 +1000,19 @@ def main():
9801000
9811001
9821002def parse_mapping (fileobj , filename = None ):
983- """Parse an extraction method mapping from a file-like object.
1003+ warnings .warn (
1004+ "parse_mapping is deprecated, use parse_mapping_cfg instead" ,
1005+ DeprecationWarning ,
1006+ stacklevel = 2 ,
1007+ )
1008+ return parse_mapping_cfg (fileobj , filename )
9841009
985- >>> buf = StringIO('''
986- ... [extractors]
987- ... custom = mypackage.module:myfunc
988- ...
989- ... # Python source files
990- ... [python: **.py]
991- ...
992- ... # Genshi templates
993- ... [genshi: **/templates/**.html]
994- ... include_attrs =
995- ... [genshi: **/templates/**.txt]
996- ... template_class = genshi.template:TextTemplate
997- ... encoding = latin-1
998- ...
999- ... # Some custom extractor
1000- ... [custom: **/custom/*.*]
1001- ... ''')
1002-
1003- >>> method_map, options_map = parse_mapping(buf)
1004- >>> len(method_map)
1005- 4
1006-
1007- >>> method_map[0]
1008- ('**.py', 'python')
1009- >>> options_map['**.py']
1010- {}
1011- >>> method_map[1]
1012- ('**/templates/**.html', 'genshi')
1013- >>> options_map['**/templates/**.html']['include_attrs']
1014- ''
1015- >>> method_map[2]
1016- ('**/templates/**.txt', 'genshi')
1017- >>> options_map['**/templates/**.txt']['template_class']
1018- 'genshi.template:TextTemplate'
1019- >>> options_map['**/templates/**.txt']['encoding']
1020- 'latin-1'
1021-
1022- >>> method_map[3]
1023- ('**/custom/*.*', 'mypackage.module:myfunc')
1024- >>> options_map['**/custom/*.*']
1025- {}
1010+
1011+ def parse_mapping_cfg (fileobj , filename = None ):
1012+ """Parse an extraction method mapping from a file-like object.
10261013
10271014 :param fileobj: a readable file-like object containing the configuration
10281015 text to parse
1029- :see: `extract_from_directory`
10301016 """
10311017 extractors = {}
10321018 method_map = []
@@ -1053,6 +1039,94 @@ def parse_mapping(fileobj, filename=None):
10531039 return method_map , options_map
10541040
10551041
1042+ def _parse_config_object (config : dict , * , filename = "(unknown)" ):
1043+ extractors = {}
1044+ method_map = []
1045+ options_map = {}
1046+
1047+ extractors_read = config .get ("extractors" , {})
1048+ if not isinstance (extractors_read , dict ):
1049+ raise ConfigurationError (f"{ filename } : extractors: Expected a dictionary, got { type (extractors_read )!r} " )
1050+ for method , callable_spec in extractors_read .items ():
1051+ if not isinstance (method , str ):
1052+ # Impossible via TOML, but could happen with a custom object.
1053+ raise ConfigurationError (f"{ filename } : extractors: Extraction method must be a string, got { method !r} " )
1054+ if not isinstance (callable_spec , str ):
1055+ raise ConfigurationError (f"{ filename } : extractors: Callable specification must be a string, got { callable_spec !r} " )
1056+ extractors [method ] = callable_spec
1057+
1058+ if "mapping" in config :
1059+ raise ConfigurationError (f"{ filename } : 'mapping' is not a valid key, did you mean 'mappings'?" )
1060+
1061+ mappings_read = config .get ("mappings" , [])
1062+ if not isinstance (mappings_read , list ):
1063+ raise ConfigurationError (f"{ filename } : mappings: Expected a list, got { type (mappings_read )!r} " )
1064+ for idx , entry in enumerate (mappings_read ):
1065+ if not isinstance (entry , dict ):
1066+ raise ConfigurationError (f"{ filename } : mappings[{ idx } ]: Expected a dictionary, got { type (entry )!r} " )
1067+ entry = entry .copy ()
1068+
1069+ method = entry .pop ("method" , None )
1070+ if not isinstance (method , str ):
1071+ raise ConfigurationError (f"{ filename } : mappings[{ idx } ]: 'method' must be a string, got { method !r} " )
1072+ method = extractors .get (method , method ) # Map the extractor name to the callable now
1073+
1074+ pattern = entry .pop ("pattern" , None )
1075+ if not isinstance (pattern , (list , str )):
1076+ raise ConfigurationError (f"{ filename } : mappings[{ idx } ]: 'pattern' must be a list or a string, got { pattern !r} " )
1077+ if not isinstance (pattern , list ):
1078+ pattern = [pattern ]
1079+
1080+ for pat in pattern :
1081+ if not isinstance (pat , str ):
1082+ raise ConfigurationError (f"{ filename } : mappings[{ idx } ]: 'pattern' elements must be strings, got { pat !r} " )
1083+ method_map .append ((pat , method ))
1084+ options_map [pat ] = entry
1085+
1086+ return method_map , options_map
1087+
1088+
1089+ def _parse_mapping_toml (
1090+ fileobj : BinaryIO ,
1091+ filename : str = "(unknown)" ,
1092+ style : Literal ["standalone" , "pyproject.toml" ] = "standalone" ,
1093+ ):
1094+ """Parse an extraction method mapping from a binary file-like object.
1095+
1096+ .. warning: As of this version of Babel, this is a private API subject to changes.
1097+
1098+ :param fileobj: a readable binary file-like object containing the configuration TOML to parse
1099+ :param filename: the name of the file being parsed, for error messages
1100+ :param style: whether the file is in the style of a `pyproject.toml` file, i.e. whether to look for `tool.babel`.
1101+ """
1102+ try :
1103+ import tomllib
1104+ except ImportError :
1105+ try :
1106+ import tomli as tomllib
1107+ except ImportError as ie : # pragma: no cover
1108+ raise ImportError ("tomli or tomllib is required to parse TOML files" ) from ie
1109+
1110+ try :
1111+ parsed_data = tomllib .load (fileobj )
1112+ except tomllib .TOMLDecodeError as e :
1113+ raise ConfigurationError (f"{ filename } : Error parsing TOML file: { e } " ) from e
1114+
1115+ if style == "pyproject.toml" :
1116+ try :
1117+ babel_data = parsed_data ["tool" ]["babel" ]
1118+ except (TypeError , KeyError ) as e :
1119+ raise ConfigurationError (f"{ filename } : No 'tool.babel' section found in file" ) from e
1120+ elif style == "standalone" :
1121+ babel_data = parsed_data
1122+ if "babel" in babel_data :
1123+ raise ConfigurationError (f"{ filename } : 'babel' should not be present in a stand-alone configuration file" )
1124+ else : # pragma: no cover
1125+ raise ValueError (f"Unknown TOML style { style !r} " )
1126+
1127+ return _parse_config_object (babel_data , filename = filename )
1128+
1129+
10561130def _parse_spec (s : str ) -> tuple [int | None , tuple [int | tuple [int , str ], ...]]:
10571131 inds = []
10581132 number = None
0 commit comments