omdev 0.0.0.dev267__py3-none-any.whl → 0.0.0.dev269__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -911,75 +911,6 @@ class WheelFile(zipfile.ZipFile):
911
911
  ##
912
912
 
913
913
 
914
- _TOML_TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
915
-
916
- TOML_RE_NUMBER = re.compile(
917
- r"""
918
- 0
919
- (?:
920
- x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
921
- |
922
- b[01](?:_?[01])* # bin
923
- |
924
- o[0-7](?:_?[0-7])* # oct
925
- )
926
- |
927
- [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
928
- (?P<floatpart>
929
- (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
930
- (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
931
- )
932
- """,
933
- flags=re.VERBOSE,
934
- )
935
- TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
936
- TOML_RE_DATETIME = re.compile(
937
- rf"""
938
- ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
939
- (?:
940
- [Tt ]
941
- {_TOML_TIME_RE_STR}
942
- (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
943
- )?
944
- """,
945
- flags=re.VERBOSE,
946
- )
947
-
948
-
949
- def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
950
- """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
951
-
952
- Raises ValueError if the match does not correspond to a valid date or datetime.
953
- """
954
- (
955
- year_str,
956
- month_str,
957
- day_str,
958
- hour_str,
959
- minute_str,
960
- sec_str,
961
- micros_str,
962
- zulu_time,
963
- offset_sign_str,
964
- offset_hour_str,
965
- offset_minute_str,
966
- ) = match.groups()
967
- year, month, day = int(year_str), int(month_str), int(day_str)
968
- if hour_str is None:
969
- return datetime.date(year, month, day)
970
- hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
971
- micros = int(micros_str.ljust(6, '0')) if micros_str else 0
972
- if offset_sign_str:
973
- tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
974
- offset_hour_str, offset_minute_str, offset_sign_str,
975
- )
976
- elif zulu_time:
977
- tz = datetime.UTC
978
- else: # local date-time
979
- tz = None
980
- return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
981
-
982
-
983
914
  @functools.lru_cache() # noqa
984
915
  def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
985
916
  sign = 1 if sign_str == '+' else -1
@@ -991,47 +922,25 @@ def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.ti
991
922
  )
992
923
 
993
924
 
994
- def toml_match_to_localtime(match: re.Match) -> datetime.time:
995
- hour_str, minute_str, sec_str, micros_str = match.groups()
996
- micros = int(micros_str.ljust(6, '0')) if micros_str else 0
997
- return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
998
-
999
-
1000
- def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
1001
- if match.group('floatpart'):
1002
- return parse_float(match.group())
1003
- return int(match.group(), 0)
1004
-
1005
-
1006
- TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
1007
-
1008
- # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
1009
- # functions.
1010
- TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
1011
- TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
925
+ def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
926
+ """
927
+ A decorator to make `parse_float` safe.
1012
928
 
1013
- TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
1014
- TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
929
+ `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
930
+ thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
931
+ """
1015
932
 
1016
- TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
933
+ # The default `float` callable never returns illegal types. Optimize it.
934
+ if parse_float is float:
935
+ return float
1017
936
 
1018
- TOML_WS = frozenset(' \t')
1019
- TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
1020
- TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
1021
- TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
1022
- TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
937
+ def safe_parse_float(float_str: str) -> ta.Any:
938
+ float_value = parse_float(float_str)
939
+ if isinstance(float_value, (dict, list)):
940
+ raise ValueError('parse_float must not return dicts or lists') # noqa
941
+ return float_value
1023
942
 
1024
- TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
1025
- {
1026
- '\\b': '\u0008', # backspace
1027
- '\\t': '\u0009', # tab
1028
- '\\n': '\u000A', # linefeed
1029
- '\\f': '\u000C', # form feed
1030
- '\\r': '\u000D', # carriage return
1031
- '\\"': '\u0022', # quote
1032
- '\\\\': '\u005C', # backslash
1033
- },
1034
- )
943
+ return safe_parse_float
1035
944
 
1036
945
 
1037
946
  class TomlDecodeError(ValueError):
@@ -1056,63 +965,15 @@ def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str
1056
965
  src = s.replace('\r\n', '\n')
1057
966
  except (AttributeError, TypeError):
1058
967
  raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
1059
- pos = 0
1060
- out = TomlOutput(TomlNestedDict(), TomlFlags())
1061
- header: TomlKey = ()
1062
- parse_float = toml_make_safe_parse_float(parse_float)
1063
-
1064
- # Parse one statement at a time (typically means one line in TOML source)
1065
- while True:
1066
- # 1. Skip line leading whitespace
1067
- pos = toml_skip_chars(src, pos, TOML_WS)
1068
-
1069
- # 2. Parse rules. Expect one of the following:
1070
- # - end of file
1071
- # - end of line
1072
- # - comment
1073
- # - key/value pair
1074
- # - append dict to list (and move to its namespace)
1075
- # - create dict (and move to its namespace)
1076
- # Skip trailing whitespace when applicable.
1077
- try:
1078
- char = src[pos]
1079
- except IndexError:
1080
- break
1081
- if char == '\n':
1082
- pos += 1
1083
- continue
1084
- if char in TOML_KEY_INITIAL_CHARS:
1085
- pos = toml_key_value_rule(src, pos, out, header, parse_float)
1086
- pos = toml_skip_chars(src, pos, TOML_WS)
1087
- elif char == '[':
1088
- try:
1089
- second_char: ta.Optional[str] = src[pos + 1]
1090
- except IndexError:
1091
- second_char = None
1092
- out.flags.finalize_pending()
1093
- if second_char == '[':
1094
- pos, header = toml_create_list_rule(src, pos, out)
1095
- else:
1096
- pos, header = toml_create_dict_rule(src, pos, out)
1097
- pos = toml_skip_chars(src, pos, TOML_WS)
1098
- elif char != '#':
1099
- raise toml_suffixed_err(src, pos, 'Invalid statement')
1100
968
 
1101
- # 3. Skip comment
1102
- pos = toml_skip_comment(src, pos)
969
+ parse_float = toml_make_safe_parse_float(parse_float)
1103
970
 
1104
- # 4. Expect end of line or end of file
1105
- try:
1106
- char = src[pos]
1107
- except IndexError:
1108
- break
1109
- if char != '\n':
1110
- raise toml_suffixed_err(
1111
- src, pos, 'Expected newline or end of document after a statement',
1112
- )
1113
- pos += 1
971
+ parser = TomlParser(
972
+ src,
973
+ parse_float=parse_float,
974
+ )
1114
975
 
1115
- return out.data.dict
976
+ return parser.parse()
1116
977
 
1117
978
 
1118
979
  class TomlFlags:
@@ -1124,6 +985,8 @@ class TomlFlags:
1124
985
  EXPLICIT_NEST = 1
1125
986
 
1126
987
  def __init__(self) -> None:
988
+ super().__init__()
989
+
1127
990
  self._flags: ta.Dict[str, dict] = {}
1128
991
  self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
1129
992
 
@@ -1174,6 +1037,8 @@ class TomlFlags:
1174
1037
 
1175
1038
  class TomlNestedDict:
1176
1039
  def __init__(self) -> None:
1040
+ super().__init__()
1041
+
1177
1042
  # The parsed content of the TOML document
1178
1043
  self.dict: ta.Dict[str, ta.Any] = {}
1179
1044
 
@@ -1206,479 +1071,613 @@ class TomlNestedDict:
1206
1071
  cont[last_key] = [{}]
1207
1072
 
1208
1073
 
1209
- class TomlOutput(ta.NamedTuple):
1210
- data: TomlNestedDict
1211
- flags: TomlFlags
1212
-
1074
+ class TomlParser:
1075
+ def __init__(
1076
+ self,
1077
+ src: str,
1078
+ *,
1079
+ parse_float: TomlParseFloat = float,
1080
+ ) -> None:
1081
+ super().__init__()
1213
1082
 
1214
- def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
1215
- try:
1216
- while src[pos] in chars:
1217
- pos += 1
1218
- except IndexError:
1219
- pass
1220
- return pos
1083
+ self.src = src
1221
1084
 
1085
+ self.parse_float = parse_float
1222
1086
 
1223
- def toml_skip_until(
1224
- src: str,
1225
- pos: TomlPos,
1226
- expect: str,
1227
- *,
1228
- error_on: ta.FrozenSet[str],
1229
- error_on_eof: bool,
1230
- ) -> TomlPos:
1231
- try:
1232
- new_pos = src.index(expect, pos)
1233
- except ValueError:
1234
- new_pos = len(src)
1235
- if error_on_eof:
1236
- raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
1087
+ self.data = TomlNestedDict()
1088
+ self.flags = TomlFlags()
1089
+ self.pos = 0
1237
1090
 
1238
- if not error_on.isdisjoint(src[pos:new_pos]):
1239
- while src[pos] not in error_on:
1240
- pos += 1
1241
- raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
1242
- return new_pos
1091
+ ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
1243
1092
 
1093
+ # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the
1094
+ # parser functions.
1095
+ ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset('\t')
1096
+ ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset('\t\n')
1244
1097
 
1245
- def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
1246
- try:
1247
- char: ta.Optional[str] = src[pos]
1248
- except IndexError:
1249
- char = None
1250
- if char == '#':
1251
- return toml_skip_until(
1252
- src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
1253
- )
1254
- return pos
1098
+ ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS
1099
+ ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS
1255
1100
 
1101
+ ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS
1256
1102
 
1257
- def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
1258
- while True:
1259
- pos_before_skip = pos
1260
- pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
1261
- pos = toml_skip_comment(src, pos)
1262
- if pos == pos_before_skip:
1263
- return pos
1103
+ WS = frozenset(' \t')
1104
+ WS_AND_NEWLINE = WS | frozenset('\n')
1105
+ BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
1106
+ KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")
1107
+ HEXDIGIT_CHARS = frozenset(string.hexdigits)
1264
1108
 
1109
+ BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType({
1110
+ '\\b': '\u0008', # backspace
1111
+ '\\t': '\u0009', # tab
1112
+ '\\n': '\u000A', # linefeed
1113
+ '\\f': '\u000C', # form feed
1114
+ '\\r': '\u000D', # carriage return
1115
+ '\\"': '\u0022', # quote
1116
+ '\\\\': '\u005C', # backslash
1117
+ })
1118
+
1119
+ def parse(self) -> ta.Dict[str, ta.Any]: # noqa: C901
1120
+ header: TomlKey = ()
1121
+
1122
+ # Parse one statement at a time (typically means one line in TOML source)
1123
+ while True:
1124
+ # 1. Skip line leading whitespace
1125
+ self.skip_chars(self.WS)
1126
+
1127
+ # 2. Parse rules. Expect one of the following:
1128
+ # - end of file
1129
+ # - end of line
1130
+ # - comment
1131
+ # - key/value pair
1132
+ # - append dict to list (and move to its namespace)
1133
+ # - create dict (and move to its namespace)
1134
+ # Skip trailing whitespace when applicable.
1135
+ try:
1136
+ char = self.src[self.pos]
1137
+ except IndexError:
1138
+ break
1139
+ if char == '\n':
1140
+ self.pos += 1
1141
+ continue
1142
+ if char in self.KEY_INITIAL_CHARS:
1143
+ self.key_value_rule(header)
1144
+ self.skip_chars(self.WS)
1145
+ elif char == '[':
1146
+ try:
1147
+ second_char: ta.Optional[str] = self.src[self.pos + 1]
1148
+ except IndexError:
1149
+ second_char = None
1150
+ self.flags.finalize_pending()
1151
+ if second_char == '[':
1152
+ header = self.create_list_rule()
1153
+ else:
1154
+ header = self.create_dict_rule()
1155
+ self.skip_chars(self.WS)
1156
+ elif char != '#':
1157
+ raise self.suffixed_err('Invalid statement')
1265
1158
 
1266
- def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
1267
- pos += 1 # Skip "["
1268
- pos = toml_skip_chars(src, pos, TOML_WS)
1269
- pos, key = toml_parse_key(src, pos)
1159
+ # 3. Skip comment
1160
+ self.skip_comment()
1270
1161
 
1271
- if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
1272
- raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
1273
- out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1274
- try:
1275
- out.data.get_or_create_nest(key)
1276
- except KeyError:
1277
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1162
+ # 4. Expect end of line or end of file
1163
+ try:
1164
+ char = self.src[self.pos]
1165
+ except IndexError:
1166
+ break
1167
+ if char != '\n':
1168
+ raise self.suffixed_err('Expected newline or end of document after a statement')
1169
+ self.pos += 1
1278
1170
 
1279
- if not src.startswith(']', pos):
1280
- raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
1281
- return pos + 1, key
1171
+ return self.data.dict
1282
1172
 
1173
+ def skip_chars(self, chars: ta.Iterable[str]) -> None:
1174
+ try:
1175
+ while self.src[self.pos] in chars:
1176
+ self.pos += 1
1177
+ except IndexError:
1178
+ pass
1283
1179
 
1284
- def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
1285
- pos += 2 # Skip "[["
1286
- pos = toml_skip_chars(src, pos, TOML_WS)
1287
- pos, key = toml_parse_key(src, pos)
1180
+ def skip_until(
1181
+ self,
1182
+ expect: str,
1183
+ *,
1184
+ error_on: ta.FrozenSet[str],
1185
+ error_on_eof: bool,
1186
+ ) -> None:
1187
+ try:
1188
+ new_pos = self.src.index(expect, self.pos)
1189
+ except ValueError:
1190
+ new_pos = len(self.src)
1191
+ if error_on_eof:
1192
+ raise self.suffixed_err(f'Expected {expect!r}', pos=new_pos) from None
1288
1193
 
1289
- if out.flags.is_(key, TomlFlags.FROZEN):
1290
- raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
1291
- # Free the namespace now that it points to another empty list item...
1292
- out.flags.unset_all(key)
1293
- # ...but this key precisely is still prohibited from table declaration
1294
- out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1295
- try:
1296
- out.data.append_nest_to_list(key)
1297
- except KeyError:
1298
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1299
-
1300
- if not src.startswith(']]', pos):
1301
- raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
1302
- return pos + 2, key
1303
-
1304
-
1305
- def toml_key_value_rule(
1306
- src: str,
1307
- pos: TomlPos,
1308
- out: TomlOutput,
1309
- header: TomlKey,
1310
- parse_float: TomlParseFloat,
1311
- ) -> TomlPos:
1312
- pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
1313
- key_parent, key_stem = key[:-1], key[-1]
1314
- abs_key_parent = header + key_parent
1315
-
1316
- relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
1317
- for cont_key in relative_path_cont_keys:
1318
- # Check that dotted key syntax does not redefine an existing table
1319
- if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
1320
- raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
1321
- # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
1322
- # table sections.
1323
- out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
1324
-
1325
- if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
1326
- raise toml_suffixed_err(
1327
- src,
1328
- pos,
1329
- f'Cannot mutate immutable namespace {abs_key_parent}',
1330
- )
1194
+ if not error_on.isdisjoint(self.src[self.pos:new_pos]):
1195
+ while self.src[self.pos] not in error_on:
1196
+ self.pos += 1
1197
+ raise self.suffixed_err(f'Found invalid character {self.src[self.pos]!r}')
1198
+ self.pos = new_pos
1331
1199
 
1332
- try:
1333
- nest = out.data.get_or_create_nest(abs_key_parent)
1334
- except KeyError:
1335
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1336
- if key_stem in nest:
1337
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
1338
- # Mark inline table and array namespaces recursively immutable
1339
- if isinstance(value, (dict, list)):
1340
- out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
1341
- nest[key_stem] = value
1342
- return pos
1343
-
1344
-
1345
- def toml_parse_key_value_pair(
1346
- src: str,
1347
- pos: TomlPos,
1348
- parse_float: TomlParseFloat,
1349
- ) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
1350
- pos, key = toml_parse_key(src, pos)
1351
- try:
1352
- char: ta.Optional[str] = src[pos]
1353
- except IndexError:
1354
- char = None
1355
- if char != '=':
1356
- raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
1357
- pos += 1
1358
- pos = toml_skip_chars(src, pos, TOML_WS)
1359
- pos, value = toml_parse_value(src, pos, parse_float)
1360
- return pos, key, value
1361
-
1362
-
1363
- def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
1364
- pos, key_part = toml_parse_key_part(src, pos)
1365
- key: TomlKey = (key_part,)
1366
- pos = toml_skip_chars(src, pos, TOML_WS)
1367
- while True:
1200
+ def skip_comment(self) -> None:
1368
1201
  try:
1369
- char: ta.Optional[str] = src[pos]
1202
+ char: ta.Optional[str] = self.src[self.pos]
1370
1203
  except IndexError:
1371
1204
  char = None
1372
- if char != '.':
1373
- return pos, key
1374
- pos += 1
1375
- pos = toml_skip_chars(src, pos, TOML_WS)
1376
- pos, key_part = toml_parse_key_part(src, pos)
1377
- key += (key_part,)
1378
- pos = toml_skip_chars(src, pos, TOML_WS)
1205
+ if char == '#':
1206
+ self.pos += 1
1207
+ self.skip_until(
1208
+ '\n',
1209
+ error_on=self.ILLEGAL_COMMENT_CHARS,
1210
+ error_on_eof=False,
1211
+ )
1379
1212
 
1213
+ def skip_comments_and_array_ws(self) -> None:
1214
+ while True:
1215
+ pos_before_skip = self.pos
1216
+ self.skip_chars(self.WS_AND_NEWLINE)
1217
+ self.skip_comment()
1218
+ if self.pos == pos_before_skip:
1219
+ return
1380
1220
 
1381
- def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1382
- try:
1383
- char: ta.Optional[str] = src[pos]
1384
- except IndexError:
1385
- char = None
1386
- if char in TOML_BARE_KEY_CHARS:
1387
- start_pos = pos
1388
- pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
1389
- return pos, src[start_pos:pos]
1390
- if char == "'":
1391
- return toml_parse_literal_str(src, pos)
1392
- if char == '"':
1393
- return toml_parse_one_line_basic_str(src, pos)
1394
- raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
1395
-
1396
-
1397
- def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1398
- pos += 1
1399
- return toml_parse_basic_str(src, pos, multiline=False)
1400
-
1401
-
1402
- def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
1403
- pos += 1
1404
- array: list = []
1405
-
1406
- pos = toml_skip_comments_and_array_ws(src, pos)
1407
- if src.startswith(']', pos):
1408
- return pos + 1, array
1409
- while True:
1410
- pos, val = toml_parse_value(src, pos, parse_float)
1411
- array.append(val)
1412
- pos = toml_skip_comments_and_array_ws(src, pos)
1413
-
1414
- c = src[pos:pos + 1]
1415
- if c == ']':
1416
- return pos + 1, array
1417
- if c != ',':
1418
- raise toml_suffixed_err(src, pos, 'Unclosed array')
1419
- pos += 1
1420
-
1421
- pos = toml_skip_comments_and_array_ws(src, pos)
1422
- if src.startswith(']', pos):
1423
- return pos + 1, array
1424
-
1425
-
1426
- def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
1427
- pos += 1
1428
- nested_dict = TomlNestedDict()
1429
- flags = TomlFlags()
1430
-
1431
- pos = toml_skip_chars(src, pos, TOML_WS)
1432
- if src.startswith('}', pos):
1433
- return pos + 1, nested_dict.dict
1434
- while True:
1435
- pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
1436
- key_parent, key_stem = key[:-1], key[-1]
1437
- if flags.is_(key, TomlFlags.FROZEN):
1438
- raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
1221
+ def create_dict_rule(self) -> TomlKey:
1222
+ self.pos += 1 # Skip "["
1223
+ self.skip_chars(self.WS)
1224
+ key = self.parse_key()
1225
+
1226
+ if self.flags.is_(key, TomlFlags.EXPLICIT_NEST) or self.flags.is_(key, TomlFlags.FROZEN):
1227
+ raise self.suffixed_err(f'Cannot declare {key} twice')
1228
+ self.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1439
1229
  try:
1440
- nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
1230
+ self.data.get_or_create_nest(key)
1441
1231
  except KeyError:
1442
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1443
- if key_stem in nest:
1444
- raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
1445
- nest[key_stem] = value
1446
- pos = toml_skip_chars(src, pos, TOML_WS)
1447
- c = src[pos:pos + 1]
1448
- if c == '}':
1449
- return pos + 1, nested_dict.dict
1450
- if c != ',':
1451
- raise toml_suffixed_err(src, pos, 'Unclosed inline table')
1452
- if isinstance(value, (dict, list)):
1453
- flags.set(key, TomlFlags.FROZEN, recursive=True)
1454
- pos += 1
1455
- pos = toml_skip_chars(src, pos, TOML_WS)
1456
-
1457
-
1458
- def toml_parse_basic_str_escape(
1459
- src: str,
1460
- pos: TomlPos,
1461
- *,
1462
- multiline: bool = False,
1463
- ) -> ta.Tuple[TomlPos, str]:
1464
- escape_id = src[pos:pos + 2]
1465
- pos += 2
1466
- if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
1467
- # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
1468
- # newline.
1469
- if escape_id != '\\\n':
1470
- pos = toml_skip_chars(src, pos, TOML_WS)
1471
- try:
1472
- char = src[pos]
1473
- except IndexError:
1474
- return pos, ''
1475
- if char != '\n':
1476
- raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
1477
- pos += 1
1478
- pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
1479
- return pos, ''
1480
- if escape_id == '\\u':
1481
- return toml_parse_hex_char(src, pos, 4)
1482
- if escape_id == '\\U':
1483
- return toml_parse_hex_char(src, pos, 8)
1484
- try:
1485
- return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
1486
- except KeyError:
1487
- raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
1232
+ raise self.suffixed_err('Cannot overwrite a value') from None
1233
+
1234
+ if not self.src.startswith(']', self.pos):
1235
+ raise self.suffixed_err("Expected ']' at the end of a table declaration")
1236
+ self.pos += 1
1237
+ return key
1238
+
1239
+ def create_list_rule(self) -> TomlKey:
1240
+ self.pos += 2 # Skip "[["
1241
+ self.skip_chars(self.WS)
1242
+ key = self.parse_key()
1243
+
1244
+ if self.flags.is_(key, TomlFlags.FROZEN):
1245
+ raise self.suffixed_err(f'Cannot mutate immutable namespace {key}')
1246
+ # Free the namespace now that it points to another empty list item...
1247
+ self.flags.unset_all(key)
1248
+ # ...but this key precisely is still prohibited from table declaration
1249
+ self.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1250
+ try:
1251
+ self.data.append_nest_to_list(key)
1252
+ except KeyError:
1253
+ raise self.suffixed_err('Cannot overwrite a value') from None
1488
1254
 
1255
+ if not self.src.startswith(']]', self.pos):
1256
+ raise self.suffixed_err("Expected ']]' at the end of an array declaration")
1257
+ self.pos += 2
1258
+ return key
1489
1259
 
1490
- def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1491
- return toml_parse_basic_str_escape(src, pos, multiline=True)
1260
+ def key_value_rule(self, header: TomlKey) -> None:
1261
+ key, value = self.parse_key_value_pair()
1262
+ key_parent, key_stem = key[:-1], key[-1]
1263
+ abs_key_parent = header + key_parent
1492
1264
 
1265
+ relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
1266
+ for cont_key in relative_path_cont_keys:
1267
+ # Check that dotted key syntax does not redefine an existing table
1268
+ if self.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
1269
+ raise self.suffixed_err(f'Cannot redefine namespace {cont_key}')
1270
+ # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in
1271
+ # following table sections.
1272
+ self.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
1493
1273
 
1494
- def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
1495
- hex_str = src[pos:pos + hex_len]
1496
- if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
1497
- raise toml_suffixed_err(src, pos, 'Invalid hex value')
1498
- pos += hex_len
1499
- hex_int = int(hex_str, 16)
1500
- if not toml_is_unicode_scalar_value(hex_int):
1501
- raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
1502
- return pos, chr(hex_int)
1274
+ if self.flags.is_(abs_key_parent, TomlFlags.FROZEN):
1275
+ raise self.suffixed_err(f'Cannot mutate immutable namespace {abs_key_parent}')
1503
1276
 
1277
+ try:
1278
+ nest = self.data.get_or_create_nest(abs_key_parent)
1279
+ except KeyError:
1280
+ raise self.suffixed_err('Cannot overwrite a value') from None
1281
+ if key_stem in nest:
1282
+ raise self.suffixed_err('Cannot overwrite a value')
1283
+ # Mark inline table and array namespaces recursively immutable
1284
+ if isinstance(value, (dict, list)):
1285
+ self.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
1286
+ nest[key_stem] = value
1504
1287
 
1505
- def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1506
- pos += 1 # Skip starting apostrophe
1507
- start_pos = pos
1508
- pos = toml_skip_until(
1509
- src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
1510
- )
1511
- return pos + 1, src[start_pos:pos] # Skip ending apostrophe
1288
+ def parse_key_value_pair(self) -> ta.Tuple[TomlKey, ta.Any]:
1289
+ key = self.parse_key()
1290
+ try:
1291
+ char: ta.Optional[str] = self.src[self.pos]
1292
+ except IndexError:
1293
+ char = None
1294
+ if char != '=':
1295
+ raise self.suffixed_err("Expected '=' after a key in a key/value pair")
1296
+ self.pos += 1
1297
+ self.skip_chars(self.WS)
1298
+ value = self.parse_value()
1299
+ return key, value
1300
+
1301
+ def parse_key(self) -> TomlKey:
1302
+ key_part = self.parse_key_part()
1303
+ key: TomlKey = (key_part,)
1304
+ self.skip_chars(self.WS)
1305
+ while True:
1306
+ try:
1307
+ char: ta.Optional[str] = self.src[self.pos]
1308
+ except IndexError:
1309
+ char = None
1310
+ if char != '.':
1311
+ return key
1312
+ self.pos += 1
1313
+ self.skip_chars(self.WS)
1314
+ key_part = self.parse_key_part()
1315
+ key += (key_part,)
1316
+ self.skip_chars(self.WS)
1317
+
1318
+ def parse_key_part(self) -> str:
1319
+ try:
1320
+ char: ta.Optional[str] = self.src[self.pos]
1321
+ except IndexError:
1322
+ char = None
1323
+ if char in self.BARE_KEY_CHARS:
1324
+ start_pos = self.pos
1325
+ self.skip_chars(self.BARE_KEY_CHARS)
1326
+ return self.src[start_pos:self.pos]
1327
+ if char == "'":
1328
+ return self.parse_literal_str()
1329
+ if char == '"':
1330
+ return self.parse_one_line_basic_str()
1331
+ raise self.suffixed_err('Invalid initial character for a key part')
1332
+
1333
+ def parse_one_line_basic_str(self) -> str:
1334
+ self.pos += 1
1335
+ return self.parse_basic_str(multiline=False)
1336
+
1337
+ def parse_array(self) -> list:
1338
+ self.pos += 1
1339
+ array: list = []
1340
+
1341
+ self.skip_comments_and_array_ws()
1342
+ if self.src.startswith(']', self.pos):
1343
+ self.pos += 1
1344
+ return array
1345
+ while True:
1346
+ val = self.parse_value()
1347
+ array.append(val)
1348
+ self.skip_comments_and_array_ws()
1349
+
1350
+ c = self.src[self.pos:self.pos + 1]
1351
+ if c == ']':
1352
+ self.pos += 1
1353
+ return array
1354
+ if c != ',':
1355
+ raise self.suffixed_err('Unclosed array')
1356
+ self.pos += 1
1357
+
1358
+ self.skip_comments_and_array_ws()
1359
+ if self.src.startswith(']', self.pos):
1360
+ self.pos += 1
1361
+ return array
1362
+
1363
+ def parse_inline_table(self) -> dict:
1364
+ self.pos += 1
1365
+ nested_dict = TomlNestedDict()
1366
+ flags = TomlFlags()
1367
+
1368
+ self.skip_chars(self.WS)
1369
+ if self.src.startswith('}', self.pos):
1370
+ self.pos += 1
1371
+ return nested_dict.dict
1372
+ while True:
1373
+ key, value = self.parse_key_value_pair()
1374
+ key_parent, key_stem = key[:-1], key[-1]
1375
+ if flags.is_(key, TomlFlags.FROZEN):
1376
+ raise self.suffixed_err(f'Cannot mutate immutable namespace {key}')
1377
+ try:
1378
+ nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
1379
+ except KeyError:
1380
+ raise self.suffixed_err('Cannot overwrite a value') from None
1381
+ if key_stem in nest:
1382
+ raise self.suffixed_err(f'Duplicate inline table key {key_stem!r}')
1383
+ nest[key_stem] = value
1384
+ self.skip_chars(self.WS)
1385
+ c = self.src[self.pos:self.pos + 1]
1386
+ if c == '}':
1387
+ self.pos += 1
1388
+ return nested_dict.dict
1389
+ if c != ',':
1390
+ raise self.suffixed_err('Unclosed inline table')
1391
+ if isinstance(value, (dict, list)):
1392
+ flags.set(key, TomlFlags.FROZEN, recursive=True)
1393
+ self.pos += 1
1394
+ self.skip_chars(self.WS)
1395
+
1396
+ def parse_basic_str_escape(self, multiline: bool = False) -> str:
1397
+ escape_id = self.src[self.pos:self.pos + 2]
1398
+ self.pos += 2
1399
+ if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
1400
+ # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found
1401
+ # before newline.
1402
+ if escape_id != '\\\n':
1403
+ self.skip_chars(self.WS)
1404
+ try:
1405
+ char = self.src[self.pos]
1406
+ except IndexError:
1407
+ return ''
1408
+ if char != '\n':
1409
+ raise self.suffixed_err("Unescaped '\\' in a string")
1410
+ self.pos += 1
1411
+ self.skip_chars(self.WS_AND_NEWLINE)
1412
+ return ''
1413
+ if escape_id == '\\u':
1414
+ return self.parse_hex_char(4)
1415
+ if escape_id == '\\U':
1416
+ return self.parse_hex_char(8)
1417
+ try:
1418
+ return self.BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
1419
+ except KeyError:
1420
+ raise self.suffixed_err("Unescaped '\\' in a string") from None
1512
1421
 
1422
+ def parse_basic_str_escape_multiline(self) -> str:
1423
+ return self.parse_basic_str_escape(multiline=True)
1513
1424
 
1514
- def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
1515
- pos += 3
1516
- if src.startswith('\n', pos):
1517
- pos += 1
1425
+ @classmethod
1426
+ def is_unicode_scalar_value(cls, codepoint: int) -> bool:
1427
+ return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
1428
+
1429
+ def parse_hex_char(self, hex_len: int) -> str:
1430
+ hex_str = self.src[self.pos:self.pos + hex_len]
1431
+ if len(hex_str) != hex_len or not self.HEXDIGIT_CHARS.issuperset(hex_str):
1432
+ raise self.suffixed_err('Invalid hex value')
1433
+ self.pos += hex_len
1434
+ hex_int = int(hex_str, 16)
1435
+ if not self.is_unicode_scalar_value(hex_int):
1436
+ raise self.suffixed_err('Escaped character is not a Unicode scalar value')
1437
+ return chr(hex_int)
1438
+
1439
+ def parse_literal_str(self) -> str:
1440
+ self.pos += 1 # Skip starting apostrophe
1441
+ start_pos = self.pos
1442
+ self.skip_until("'", error_on=self.ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True)
1443
+ end_pos = self.pos
1444
+ self.pos += 1
1445
+ return self.src[start_pos:end_pos] # Skip ending apostrophe
1446
+
1447
+ def parse_multiline_str(self, *, literal: bool) -> str:
1448
+ self.pos += 3
1449
+ if self.src.startswith('\n', self.pos):
1450
+ self.pos += 1
1451
+
1452
+ if literal:
1453
+ delim = "'"
1454
+ start_pos = self.pos
1455
+ self.skip_until(
1456
+ "'''",
1457
+ error_on=self.ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
1458
+ error_on_eof=True,
1459
+ )
1460
+ result = self.src[start_pos:self.pos]
1461
+ self.pos += 3
1462
+ else:
1463
+ delim = '"'
1464
+ result = self.parse_basic_str(multiline=True)
1465
+
1466
+ # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
1467
+ if not self.src.startswith(delim, self.pos):
1468
+ return result
1469
+ self.pos += 1
1470
+ if not self.src.startswith(delim, self.pos):
1471
+ return result + delim
1472
+ self.pos += 1
1473
+ return result + (delim * 2)
1474
+
1475
+ def parse_basic_str(self, *, multiline: bool) -> str:
1476
+ if multiline:
1477
+ error_on = self.ILLEGAL_MULTILINE_BASIC_STR_CHARS
1478
+ parse_escapes = self.parse_basic_str_escape_multiline
1479
+ else:
1480
+ error_on = self.ILLEGAL_BASIC_STR_CHARS
1481
+ parse_escapes = self.parse_basic_str_escape
1482
+ result = ''
1483
+ start_pos = self.pos
1484
+ while True:
1485
+ try:
1486
+ char = self.src[self.pos]
1487
+ except IndexError:
1488
+ raise self.suffixed_err('Unterminated string') from None
1489
+ if char == '"':
1490
+ if not multiline:
1491
+ end_pos = self.pos
1492
+ self.pos += 1
1493
+ return result + self.src[start_pos:end_pos]
1494
+ if self.src.startswith('"""', self.pos):
1495
+ end_pos = self.pos
1496
+ self.pos += 3
1497
+ return result + self.src[start_pos:end_pos]
1498
+ self.pos += 1
1499
+ continue
1500
+ if char == '\\':
1501
+ result += self.src[start_pos:self.pos]
1502
+ parsed_escape = parse_escapes()
1503
+ result += parsed_escape
1504
+ start_pos = self.pos
1505
+ continue
1506
+ if char in error_on:
1507
+ raise self.suffixed_err(f'Illegal character {char!r}')
1508
+ self.pos += 1
1518
1509
 
1519
- if literal:
1520
- delim = "'"
1521
- end_pos = toml_skip_until(
1522
- src,
1523
- pos,
1524
- "'''",
1525
- error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
1526
- error_on_eof=True,
1527
- )
1528
- result = src[pos:end_pos]
1529
- pos = end_pos + 3
1530
- else:
1531
- delim = '"'
1532
- pos, result = toml_parse_basic_str(src, pos, multiline=True)
1533
-
1534
- # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
1535
- if not src.startswith(delim, pos):
1536
- return pos, result
1537
- pos += 1
1538
- if not src.startswith(delim, pos):
1539
- return pos, result + delim
1540
- pos += 1
1541
- return pos, result + (delim * 2)
1542
-
1543
-
1544
- def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
1545
- if multiline:
1546
- error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
1547
- parse_escapes = toml_parse_basic_str_escape_multiline
1548
- else:
1549
- error_on = TOML_ILLEGAL_BASIC_STR_CHARS
1550
- parse_escapes = toml_parse_basic_str_escape
1551
- result = ''
1552
- start_pos = pos
1553
- while True:
1510
+ def parse_value(self) -> ta.Any: # noqa: C901
1554
1511
  try:
1555
- char = src[pos]
1512
+ char: ta.Optional[str] = self.src[self.pos]
1556
1513
  except IndexError:
1557
- raise toml_suffixed_err(src, pos, 'Unterminated string') from None
1514
+ char = None
1515
+
1516
+ # IMPORTANT: order conditions based on speed of checking and likelihood
1517
+
1518
+ # Basic strings
1558
1519
  if char == '"':
1559
- if not multiline:
1560
- return pos + 1, result + src[start_pos:pos]
1561
- if src.startswith('"""', pos):
1562
- return pos + 3, result + src[start_pos:pos]
1563
- pos += 1
1564
- continue
1565
- if char == '\\':
1566
- result += src[start_pos:pos]
1567
- pos, parsed_escape = parse_escapes(src, pos)
1568
- result += parsed_escape
1569
- start_pos = pos
1570
- continue
1571
- if char in error_on:
1572
- raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
1573
- pos += 1
1520
+ if self.src.startswith('"""', self.pos):
1521
+ return self.parse_multiline_str(literal=False)
1522
+ return self.parse_one_line_basic_str()
1523
+
1524
+ # Literal strings
1525
+ if char == "'":
1526
+ if self.src.startswith("'''", self.pos):
1527
+ return self.parse_multiline_str(literal=True)
1528
+ return self.parse_literal_str()
1529
+
1530
+ # Booleans
1531
+ if char == 't':
1532
+ if self.src.startswith('true', self.pos):
1533
+ self.pos += 4
1534
+ return True
1535
+ if char == 'f':
1536
+ if self.src.startswith('false', self.pos):
1537
+ self.pos += 5
1538
+ return False
1574
1539
 
1540
+ # Arrays
1541
+ if char == '[':
1542
+ return self.parse_array()
1575
1543
 
1576
- def toml_parse_value( # noqa: C901
1577
- src: str,
1578
- pos: TomlPos,
1579
- parse_float: TomlParseFloat,
1580
- ) -> ta.Tuple[TomlPos, ta.Any]:
1581
- try:
1582
- char: ta.Optional[str] = src[pos]
1583
- except IndexError:
1584
- char = None
1585
-
1586
- # IMPORTANT: order conditions based on speed of checking and likelihood
1587
-
1588
- # Basic strings
1589
- if char == '"':
1590
- if src.startswith('"""', pos):
1591
- return toml_parse_multiline_str(src, pos, literal=False)
1592
- return toml_parse_one_line_basic_str(src, pos)
1593
-
1594
- # Literal strings
1595
- if char == "'":
1596
- if src.startswith("'''", pos):
1597
- return toml_parse_multiline_str(src, pos, literal=True)
1598
- return toml_parse_literal_str(src, pos)
1599
-
1600
- # Booleans
1601
- if char == 't':
1602
- if src.startswith('true', pos):
1603
- return pos + 4, True
1604
- if char == 'f':
1605
- if src.startswith('false', pos):
1606
- return pos + 5, False
1607
-
1608
- # Arrays
1609
- if char == '[':
1610
- return toml_parse_array(src, pos, parse_float)
1611
-
1612
- # Inline tables
1613
- if char == '{':
1614
- return toml_parse_inline_table(src, pos, parse_float)
1615
-
1616
- # Dates and times
1617
- datetime_match = TOML_RE_DATETIME.match(src, pos)
1618
- if datetime_match:
1619
- try:
1620
- datetime_obj = toml_match_to_datetime(datetime_match)
1621
- except ValueError as e:
1622
- raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
1623
- return datetime_match.end(), datetime_obj
1624
- localtime_match = TOML_RE_LOCALTIME.match(src, pos)
1625
- if localtime_match:
1626
- return localtime_match.end(), toml_match_to_localtime(localtime_match)
1627
-
1628
- # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
1629
- # located after handling of dates and times.
1630
- number_match = TOML_RE_NUMBER.match(src, pos)
1631
- if number_match:
1632
- return number_match.end(), toml_match_to_number(number_match, parse_float)
1633
-
1634
- # Special floats
1635
- first_three = src[pos:pos + 3]
1636
- if first_three in {'inf', 'nan'}:
1637
- return pos + 3, parse_float(first_three)
1638
- first_four = src[pos:pos + 4]
1639
- if first_four in {'-inf', '+inf', '-nan', '+nan'}:
1640
- return pos + 4, parse_float(first_four)
1641
-
1642
- raise toml_suffixed_err(src, pos, 'Invalid value')
1643
-
1644
-
1645
- def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
1646
- """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
1647
-
1648
- def coord_repr(src: str, pos: TomlPos) -> str:
1649
- if pos >= len(src):
1544
+ # Inline tables
1545
+ if char == '{':
1546
+ return self.parse_inline_table()
1547
+
1548
+ # Dates and times
1549
+ datetime_match = self.RE_DATETIME.match(self.src, self.pos)
1550
+ if datetime_match:
1551
+ try:
1552
+ datetime_obj = self.match_to_datetime(datetime_match)
1553
+ except ValueError as e:
1554
+ raise self.suffixed_err('Invalid date or datetime') from e
1555
+ self.pos = datetime_match.end()
1556
+ return datetime_obj
1557
+ localtime_match = self.RE_LOCALTIME.match(self.src, self.pos)
1558
+ if localtime_match:
1559
+ self.pos = localtime_match.end()
1560
+ return self.match_to_localtime(localtime_match)
1561
+
1562
+ # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to
1563
+ # be located after handling of dates and times.
1564
+ number_match = self.RE_NUMBER.match(self.src, self.pos)
1565
+ if number_match:
1566
+ self.pos = number_match.end()
1567
+ return self.match_to_number(number_match, self.parse_float)
1568
+
1569
+ # Special floats
1570
+ first_three = self.src[self.pos:self.pos + 3]
1571
+ if first_three in {'inf', 'nan'}:
1572
+ self.pos += 3
1573
+ return self.parse_float(first_three)
1574
+ first_four = self.src[self.pos:self.pos + 4]
1575
+ if first_four in {'-inf', '+inf', '-nan', '+nan'}:
1576
+ self.pos += 4
1577
+ return self.parse_float(first_four)
1578
+
1579
+ raise self.suffixed_err('Invalid value')
1580
+
1581
+ def coord_repr(self, pos: TomlPos) -> str:
1582
+ if pos >= len(self.src):
1650
1583
  return 'end of document'
1651
- line = src.count('\n', 0, pos) + 1
1584
+ line = self.src.count('\n', 0, pos) + 1
1652
1585
  if line == 1:
1653
1586
  column = pos + 1
1654
1587
  else:
1655
- column = pos - src.rindex('\n', 0, pos)
1588
+ column = pos - self.src.rindex('\n', 0, pos)
1656
1589
  return f'line {line}, column {column}'
1657
1590
 
1658
- return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
1591
+ def suffixed_err(self, msg: str, *, pos: ta.Optional[TomlPos] = None) -> TomlDecodeError:
1592
+ """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
1593
+
1594
+ if pos is None:
1595
+ pos = self.pos
1596
+ return TomlDecodeError(f'{msg} (at {self.coord_repr(pos)})')
1597
+
1598
+ _TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
1599
+
1600
+ RE_NUMBER = re.compile(
1601
+ r"""
1602
+ 0
1603
+ (?:
1604
+ x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
1605
+ |
1606
+ b[01](?:_?[01])* # bin
1607
+ |
1608
+ o[0-7](?:_?[0-7])* # oct
1609
+ )
1610
+ |
1611
+ [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
1612
+ (?P<floatpart>
1613
+ (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
1614
+ (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
1615
+ )
1616
+ """,
1617
+ flags=re.VERBOSE,
1618
+ )
1659
1619
 
1620
+ RE_LOCALTIME = re.compile(_TIME_RE_STR)
1660
1621
 
1661
- def toml_is_unicode_scalar_value(codepoint: int) -> bool:
1662
- return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
1622
+ RE_DATETIME = re.compile(
1623
+ rf"""
1624
+ ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
1625
+ (?:
1626
+ [Tt ]
1627
+ {_TIME_RE_STR}
1628
+ (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
1629
+ )?
1630
+ """,
1631
+ flags=re.VERBOSE,
1632
+ )
1663
1633
 
1634
+ @classmethod
1635
+ def match_to_datetime(cls, match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
1636
+ """
1637
+ Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
1664
1638
 
1665
- def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
1666
- """A decorator to make `parse_float` safe.
1639
+ Raises ValueError if the match does not correspond to a valid date or datetime.
1640
+ """
1667
1641
 
1668
- `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
1669
- thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
1670
- """
1671
- # The default `float` callable never returns illegal types. Optimize it.
1672
- if parse_float is float:
1673
- return float
1642
+ (
1643
+ year_str,
1644
+ month_str,
1645
+ day_str,
1646
+ hour_str,
1647
+ minute_str,
1648
+ sec_str,
1649
+ micros_str,
1650
+ zulu_time,
1651
+ offset_sign_str,
1652
+ offset_hour_str,
1653
+ offset_minute_str,
1654
+ ) = match.groups()
1655
+ year, month, day = int(year_str), int(month_str), int(day_str)
1656
+ if hour_str is None:
1657
+ return datetime.date(year, month, day)
1658
+ hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
1659
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
1660
+ if offset_sign_str:
1661
+ tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
1662
+ offset_hour_str, offset_minute_str, offset_sign_str,
1663
+ )
1664
+ elif zulu_time:
1665
+ tz = datetime.UTC
1666
+ else: # local date-time
1667
+ tz = None
1668
+ return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
1674
1669
 
1675
- def safe_parse_float(float_str: str) -> ta.Any:
1676
- float_value = parse_float(float_str)
1677
- if isinstance(float_value, (dict, list)):
1678
- raise ValueError('parse_float must not return dicts or lists') # noqa
1679
- return float_value
1670
+ @classmethod
1671
+ def match_to_localtime(cls, match: re.Match) -> datetime.time:
1672
+ hour_str, minute_str, sec_str, micros_str = match.groups()
1673
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
1674
+ return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
1680
1675
 
1681
- return safe_parse_float
1676
+ @classmethod
1677
+ def match_to_number(cls, match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
1678
+ if match.group('floatpart'):
1679
+ return parse_float(match.group())
1680
+ return int(match.group(), 0)
1682
1681
 
1683
1682
 
1684
1683
  ########################################
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omdev
3
- Version: 0.0.0.dev267
3
+ Version: 0.0.0.dev269
4
4
  Summary: omdev
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,7 +12,7 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omlish==0.0.0.dev267
15
+ Requires-Dist: omlish==0.0.0.dev269
16
16
  Provides-Extra: all
17
17
  Requires-Dist: black~=25.1; extra == "all"
18
18
  Requires-Dist: pycparser~=2.22; extra == "all"
@@ -215,7 +215,7 @@ omdev/scripts/execrss.py,sha256=mR0G0wERBYtQmVIn63lCIIFb5zkCM6X_XOENDFYDBKc,651
215
215
  omdev/scripts/exectime.py,sha256=S2O4MgtzTsFOY2IUJxsrnOIame9tEFc6aOlKP-F1JSg,1541
216
216
  omdev/scripts/importtrace.py,sha256=NjRilVNBugswrNflIhPCPHRTd-vIijqSZ8C7J8lariI,14038
217
217
  omdev/scripts/interp.py,sha256=B3YviHFRbJoom4dvmGdJ7Y0LiWWpE6p1igEme4IWspU,151083
218
- omdev/scripts/pyproject.py,sha256=Lbv3rAX8Pu5izapEKqNtibwYnBsasWwt6YscfDI1Kvw,258940
218
+ omdev/scripts/pyproject.py,sha256=_WVVYRdyzgMKnuNYyjwduerjYFugZjdHgwAA-eUgCrQ,260133
219
219
  omdev/scripts/slowcat.py,sha256=lssv4yrgJHiWfOiHkUut2p8E8Tq32zB-ujXESQxFFHY,2728
220
220
  omdev/scripts/tmpexec.py,sha256=WTYcf56Tj2qjYV14AWmV8SfT0u6Y8eIU6cKgQRvEK3c,1442
221
221
  omdev/tokens/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -253,9 +253,9 @@ omdev/tools/json/rendering.py,sha256=tMcjOW5edfozcMSTxxvF7WVTsbYLoe9bCKFh50qyaGw
253
253
  omdev/tools/pawk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
254
254
  omdev/tools/pawk/__main__.py,sha256=VCqeRVnqT1RPEoIrqHFSu4PXVMg4YEgF4qCQm90-eRI,66
255
255
  omdev/tools/pawk/pawk.py,sha256=zsEkfQX0jF5bn712uqPAyBSdJt2dno1LH2oeSMNfXQI,11424
256
- omdev-0.0.0.dev267.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
257
- omdev-0.0.0.dev267.dist-info/METADATA,sha256=BD1w15gK8LEmpX5QXuAeNO2BIkXtGIbT6bxj-t8tXCE,1658
258
- omdev-0.0.0.dev267.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
259
- omdev-0.0.0.dev267.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
260
- omdev-0.0.0.dev267.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
261
- omdev-0.0.0.dev267.dist-info/RECORD,,
256
+ omdev-0.0.0.dev269.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
257
+ omdev-0.0.0.dev269.dist-info/METADATA,sha256=AqDahcQlEANe8r3cGhe2SVIQ97W8LuvioG1A1bKaQlI,1658
258
+ omdev-0.0.0.dev269.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
259
+ omdev-0.0.0.dev269.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
260
+ omdev-0.0.0.dev269.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
261
+ omdev-0.0.0.dev269.dist-info/RECORD,,