19
19
import shutil
20
20
import sys
21
21
import tempfile
22
+ import warnings
22
23
from collections import OrderedDict
23
24
from configparser import RawConfigParser
24
25
from io import StringIO
25
- from typing import Iterable
26
+ from typing import BinaryIO , Iterable , Literal
26
27
27
28
from babel import Locale , localedata
28
29
from babel import __version__ as VERSION
@@ -53,6 +54,12 @@ class SetupError(BaseError):
53
54
pass
54
55
55
56
57
+ class ConfigurationError (BaseError ):
58
+ """
59
+ Raised for errors in configuration files.
60
+ """
61
+
62
+
56
63
def listify_value (arg , split = None ):
57
64
"""
58
65
Make a list out of an argument.
@@ -534,16 +541,29 @@ def _get_mappings(self):
534
541
mappings = []
535
542
536
543
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 )
539
559
for path in self .input_paths :
540
560
mappings .append ((path , method_map , options_map ))
541
561
542
562
elif getattr (self .distribution , 'message_extractors' , None ):
543
563
message_extractors = self .distribution .message_extractors
544
564
for path , mapping in message_extractors .items ():
545
565
if isinstance (mapping , str ):
546
- method_map , options_map = parse_mapping (StringIO (mapping ))
566
+ method_map , options_map = parse_mapping_cfg (StringIO (mapping ))
547
567
else :
548
568
method_map , options_map = [], {}
549
569
for pattern , method , options in mapping :
@@ -980,53 +1000,19 @@ def main():
980
1000
981
1001
982
1002
def 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 )
984
1009
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.
1026
1013
1027
1014
:param fileobj: a readable file-like object containing the configuration
1028
1015
text to parse
1029
- :see: `extract_from_directory`
1030
1016
"""
1031
1017
extractors = {}
1032
1018
method_map = []
@@ -1053,6 +1039,94 @@ def parse_mapping(fileobj, filename=None):
1053
1039
return method_map , options_map
1054
1040
1055
1041
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
+
1056
1130
def _parse_spec (s : str ) -> tuple [int | None , tuple [int | tuple [int , str ], ...]]:
1057
1131
inds = []
1058
1132
number = None
0 commit comments