pathseq 0.1.0__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.
@@ -0,0 +1,525 @@
1
+ from __future__ import annotations
2
+
3
+ import collections
4
+ from dataclasses import dataclass
5
+ from decimal import Decimal
6
+ import enum
7
+ import re
8
+
9
+ from statemachine import StateMachine, State
10
+
11
+ from ._error import NotASequenceError, ParseError
12
+ from ._file_num_seq import FileNumSequence
13
+ from ._ast import (
14
+ PaddedRange,
15
+ ParsedLooseSequence,
16
+ Ranges,
17
+ RangesEndName,
18
+ RangesInName,
19
+ RangesStartName,
20
+ )
21
+
22
+ _POSTFIX_SEPARATORS = {"_"}
23
+ _PREFIX_SEPARATORS = _POSTFIX_SEPARATORS | {"."}
24
+ RANGE_RE = re.compile(
25
+ r"""
26
+ -?\d+(?:\.\d+)?
27
+ (?: # optional range
28
+ - # range delimiter
29
+ -?\d+(?:\.\d+)? # end frame
30
+ (?: # optional stepping
31
+ x # step type
32
+ \d+(?:\.\d+)? # step value
33
+ )?
34
+ )?
35
+ """,
36
+ re.VERBOSE,
37
+ )
38
+ PAD_FORMAT_RE = re.compile(
39
+ r"""
40
+ (?:
41
+ \#+(?:\.\#+)?
42
+ | <UDIM>
43
+ | <UVTILE>
44
+ )
45
+ """,
46
+ flags=re.VERBOSE,
47
+ )
48
+ RANGES_RE = re.compile(
49
+ f"""
50
+ (
51
+ (?:{RANGE_RE.pattern} (?:, {RANGE_RE.pattern})*)?
52
+ {PAD_FORMAT_RE.pattern}
53
+ )
54
+ """,
55
+ flags=RANGE_RE.flags | PAD_FORMAT_RE.flags | re.VERBOSE,
56
+ )
57
+
58
+
59
+ class TokenType(enum.Enum):
60
+ RANGE = enum.auto()
61
+ INTER_RANGE = enum.auto()
62
+ STEM = enum.auto()
63
+ PREFIX = enum.auto()
64
+ POSTFIX = enum.auto()
65
+ SUFFIXES = enum.auto()
66
+
67
+
68
+ @dataclass
69
+ class Token:
70
+ type: TokenType
71
+ value: str
72
+ column: int
73
+
74
+ @property
75
+ def end_column(self) -> int:
76
+ return self.column + len(self.value)
77
+
78
+
79
+ def _tokenise(seq: str) -> list[Token]:
80
+ raw_tokens = re.split(RANGES_RE, seq)
81
+
82
+ starts_with_range = not bool(raw_tokens[0])
83
+ ends_with_range = not bool(raw_tokens[-1])
84
+ if ends_with_range:
85
+ raw_tokens.pop()
86
+
87
+ column = 0
88
+ tokens = []
89
+ for i, raw_token in enumerate(raw_tokens):
90
+ if i == 0:
91
+ if not raw_token:
92
+ pass
93
+ elif not ends_with_range:
94
+ separator = None
95
+ if raw_token != "." and any(
96
+ raw_token.endswith(sep) for sep in _PREFIX_SEPARATORS
97
+ ):
98
+ separator = Token(
99
+ TokenType.PREFIX,
100
+ raw_token[-1],
101
+ column + len(raw_token) - 1,
102
+ )
103
+ raw_token = raw_token[:-1]
104
+
105
+ token = Token(
106
+ TokenType.STEM,
107
+ raw_token,
108
+ column,
109
+ )
110
+ tokens.append(token)
111
+
112
+ if separator:
113
+ tokens.append(separator)
114
+ else:
115
+ separator = None
116
+ if any(raw_token.endswith(sep) for sep in _PREFIX_SEPARATORS):
117
+ separator = Token(
118
+ TokenType.PREFIX,
119
+ raw_token[-1],
120
+ column + len(raw_token) - 1,
121
+ )
122
+ raw_token = raw_token[:-1]
123
+
124
+ try:
125
+ suffix_dot = 0
126
+ if raw_token.startswith("."):
127
+ suffix_dot = 1
128
+ suffix_i = raw_token.index(".", suffix_dot)
129
+ except ValueError:
130
+ suffix_i = len(raw_token)
131
+
132
+ token = Token(
133
+ TokenType.STEM,
134
+ raw_token[:suffix_i],
135
+ column,
136
+ )
137
+ tokens.append(token)
138
+
139
+ if raw_token[suffix_i:]:
140
+ token = Token(
141
+ TokenType.SUFFIXES,
142
+ raw_token[suffix_i:],
143
+ column + suffix_i,
144
+ )
145
+ tokens.append(token)
146
+
147
+ if separator:
148
+ tokens.append(separator)
149
+ elif i % 2 == 1:
150
+ token = Token(
151
+ TokenType.RANGE,
152
+ raw_token,
153
+ column,
154
+ )
155
+ tokens.append(token)
156
+ elif i >= 2 and i != len(raw_tokens) - 1:
157
+ token = Token(
158
+ TokenType.INTER_RANGE,
159
+ raw_token,
160
+ column,
161
+ )
162
+ tokens.append(token)
163
+ elif i == len(raw_tokens) - 1:
164
+ if raw_token.startswith(".") and not raw_token.endswith("."):
165
+ token = Token(
166
+ TokenType.SUFFIXES,
167
+ raw_token,
168
+ column,
169
+ )
170
+ tokens.append(token)
171
+ elif starts_with_range:
172
+ if any(raw_token.startswith(sep) for sep in _POSTFIX_SEPARATORS):
173
+ token = Token(
174
+ TokenType.POSTFIX,
175
+ raw_token[0],
176
+ column,
177
+ )
178
+ tokens.append(token)
179
+ raw_token = raw_token[1:]
180
+ column += 1
181
+
182
+ if raw_token.endswith("."):
183
+ suffix_i = len(raw_token)
184
+ elif raw_token.startswith("."):
185
+ suffix_i = 0
186
+ else:
187
+ try:
188
+ suffix_i = raw_token.index(".")
189
+ except ValueError:
190
+ suffix_i = len(raw_token)
191
+
192
+ if raw_token[:suffix_i]:
193
+ token = Token(
194
+ TokenType.STEM,
195
+ raw_token[:suffix_i],
196
+ column,
197
+ )
198
+ tokens.append(token)
199
+
200
+ if raw_token[suffix_i:]:
201
+ token = Token(
202
+ TokenType.SUFFIXES,
203
+ raw_token[suffix_i:],
204
+ column + suffix_i,
205
+ )
206
+ tokens.append(token)
207
+ else:
208
+ if raw_token.endswith("."):
209
+ suffix_i = len(raw_token)
210
+ elif raw_token.startswith("."):
211
+ suffix_i = 0
212
+ else:
213
+ try:
214
+ suffix_i = raw_token.index(".")
215
+ except ValueError:
216
+ suffix_i = len(raw_token)
217
+
218
+ if raw_token[:suffix_i]:
219
+ token = Token(
220
+ TokenType.POSTFIX,
221
+ raw_token[:suffix_i],
222
+ column,
223
+ )
224
+ tokens.append(token)
225
+
226
+ if raw_token[suffix_i:]:
227
+ token = Token(
228
+ TokenType.SUFFIXES,
229
+ raw_token[suffix_i:],
230
+ column + suffix_i,
231
+ )
232
+ tokens.append(token)
233
+
234
+ column += len(raw_token)
235
+
236
+ assert all(isinstance(token, Token) for token in tokens)
237
+ if not any(token.type == TokenType.RANGE for token in tokens):
238
+ raise NotASequenceError(seq)
239
+
240
+ if __debug__:
241
+ type_counts = collections.Counter(token.type for token in tokens)
242
+ assert type_counts[TokenType.STEM] <= 1
243
+ num_pre_seps = type_counts[TokenType.PREFIX]
244
+ assert num_pre_seps <= 1
245
+ num_postfixes = type_counts[TokenType.POSTFIX]
246
+ assert num_postfixes <= 1
247
+ num_ranges = type_counts[TokenType.RANGE]
248
+ num_inter_ranges = type_counts[TokenType.INTER_RANGE]
249
+ assert num_inter_ranges == num_ranges - 1
250
+ assert type_counts[TokenType.SUFFIXES] <= 1
251
+
252
+ return tokens
253
+
254
+
255
+ class SeqParser(StateMachine):
256
+ init = State(initial=True)
257
+ range_starts_name = State()
258
+ starts_inter_range = State()
259
+ starts_postfix = State()
260
+ starts_stem = State()
261
+ starts_suffixes = State()
262
+
263
+ range_later = State() # stem
264
+
265
+ in_prefix = State()
266
+ range_in_name = State()
267
+ in_inter_range = State()
268
+ in_postfix = State()
269
+ in_suffixes = State()
270
+
271
+ range_ends_name = State() # suffixes
272
+ ends_prefix = State()
273
+ ends_range = State()
274
+ ends_inter_range = State()
275
+
276
+ parsed = State(final=True)
277
+
278
+ pump = (
279
+ init.to(range_starts_name, cond="is_range")
280
+ | init.to(range_later, validators="expect_range_or_stem")
281
+ | range_starts_name.to(starts_inter_range, cond="is_inter_range")
282
+ | starts_inter_range.to(range_starts_name, cond="expect_range")
283
+ | range_starts_name.to.itself(on="on_blank_inter_range", cond="is_range") # type: ignore[no-untyped-call]
284
+ | range_starts_name.to(in_suffixes, cond="is_suffixes", on="switch_to_in_range")
285
+ | range_starts_name.to(starts_postfix, cond="is_postfix")
286
+ | range_starts_name.to(
287
+ starts_stem, validators="expect_inter_or_pre_sep_or_stem"
288
+ )
289
+ | starts_postfix.to(starts_stem, validators="expect_stem")
290
+ | starts_stem.to(starts_suffixes, validators="expect_suffixes")
291
+ | range_later.to(in_prefix, cond="is_prefix")
292
+ | range_later.to(range_in_name, cond="is_range")
293
+ | in_prefix.to(range_in_name, validators="expect_range")
294
+ | range_in_name.to(in_inter_range, cond="is_inter_range")
295
+ | in_inter_range.to(range_in_name, validators="expect_range")
296
+ | range_in_name.to(in_postfix, cond="is_postfix")
297
+ | in_postfix.to(in_suffixes, validators="expect_suffixes")
298
+ | range_in_name.to(
299
+ in_suffixes, validators="expect_inter_or_post_sep_or_suffixes"
300
+ )
301
+ | range_later.to(
302
+ range_ends_name, validators="expect_pre_sep_or_range_or_suffixes"
303
+ )
304
+ | range_ends_name.to(ends_prefix, cond="is_prefix")
305
+ | range_ends_name.to(ends_range, validators="expect_pre_sep_or_range")
306
+ | ends_prefix.to(ends_range, validators="expect_range")
307
+ | ends_range.to(ends_inter_range, validators="expect_inter_range")
308
+ | ends_inter_range.to(ends_range, validators="expect_range")
309
+ )
310
+
311
+ finalise = (
312
+ range_starts_name.to(parsed)
313
+ | starts_stem.to(parsed)
314
+ | starts_suffixes.to(parsed)
315
+ | range_in_name.to(parsed)
316
+ | in_postfix.to(parsed)
317
+ | in_suffixes.to(parsed)
318
+ | ends_range.to(parsed)
319
+ )
320
+
321
+ def __init__(self, seq: str) -> None:
322
+ super().__init__()
323
+
324
+ self._seq = seq
325
+ self._range_type: type[ParsedLooseSequence] | None = None
326
+ self._stem = ""
327
+ self._prefix = ""
328
+ self._ranges: list[PaddedRange[int] | PaddedRange[Decimal]] = []
329
+ self._inter_ranges: list[str] = []
330
+ self._postfix = ""
331
+ self._suffixes: tuple[str, ...] = ()
332
+
333
+ def is_range(self, token: Token) -> bool:
334
+ return token.type == TokenType.RANGE
335
+
336
+ def is_inter_range(self, token: Token) -> bool:
337
+ return token.type == TokenType.INTER_RANGE
338
+
339
+ def is_stem(self, token: Token) -> bool:
340
+ return token.type == TokenType.STEM
341
+
342
+ def is_prefix(self, token: Token) -> bool:
343
+ return token.type == TokenType.PREFIX
344
+
345
+ def is_postfix(self, token: Token) -> bool:
346
+ return token.type == TokenType.POSTFIX
347
+
348
+ def is_suffixes(self, token: Token) -> bool:
349
+ return token.type == TokenType.SUFFIXES
350
+
351
+ def expect_range_or_stem(self, token: Token) -> bool:
352
+ return self._validate_stem(token, "Expected a range or a stem")
353
+
354
+ def expect_inter_or_pre_sep_or_stem(self, token: Token) -> bool:
355
+ return self._validate_stem(
356
+ token, "Expected an inter-range string, a prefix separator, or a stem"
357
+ )
358
+
359
+ def expect_inter_or_post_sep_or_suffixes(self, token: Token) -> bool:
360
+ return self._validate_suffixes(
361
+ token, "Expected an inter-range string, a postfix, or file suffixes"
362
+ )
363
+
364
+ def expect_stem(self, token: Token) -> bool:
365
+ return self._validate_stem(token, "Expected a stem")
366
+
367
+ def expect_suffixes(self, token: Token) -> bool:
368
+ return self._validate_suffixes(token, "Expected the file suffixes")
369
+
370
+ def expect_range(self, token: Token) -> bool:
371
+ return self._validate_range(token, "Expected the ranges")
372
+
373
+ def expect_pre_sep_or_range(self, token: Token) -> bool:
374
+ return self._validate_range(token, "Expected a prefix separator, or a range")
375
+
376
+ def expect_inter_range(self, token: Token) -> bool:
377
+ return self._validate_inter_range(token, "Expected an inter-range string")
378
+
379
+ def expect_pre_sep_or_range_or_suffixes(self, token: Token) -> bool:
380
+ return self._validate_suffixes(
381
+ token, "Expected a prefix separator, a ranges, or file suffixes"
382
+ )
383
+
384
+ def _validate_range(self, token: Token, reason: str) -> bool:
385
+ if token.type != TokenType.RANGE:
386
+ raise ParseError(self._seq, token.column, token.end_column, reason=reason)
387
+ return True
388
+
389
+ def _validate_inter_range(self, token: Token, reason: str) -> bool:
390
+ if token.type != TokenType.INTER_RANGE:
391
+ raise ParseError(self._seq, token.column, token.end_column, reason=reason)
392
+ return True
393
+
394
+ def _validate_stem(self, token: Token, reason: str) -> bool:
395
+ if token.type != TokenType.STEM:
396
+ raise ParseError(self._seq, token.column, token.end_column, reason=reason)
397
+ return True
398
+
399
+ def _validate_suffixes(self, token: Token, reason: str) -> bool:
400
+ if token.type != TokenType.SUFFIXES:
401
+ raise ParseError(self._seq, token.column, token.end_column, reason=reason)
402
+ return True
403
+
404
+ def switch_to_in_range(self) -> None:
405
+ self._range_type = RangesInName
406
+
407
+ def on_enter_range_starts_name(self, token: Token) -> None:
408
+ self._range_type = RangesStartName
409
+
410
+ self._ranges.append(self._parse_padded_range(token))
411
+
412
+ def on_enter_starts_inter_range(self, token: Token) -> None:
413
+ self._inter_ranges.append(token.value)
414
+
415
+ def on_enter_in_inter_range(self, token: Token) -> None:
416
+ self._inter_ranges.append(token.value)
417
+
418
+ def on_enter_ends_inter_range(self, token: Token) -> None:
419
+ self._inter_ranges.append(token.value)
420
+
421
+ def on_blank_inter_range(self) -> None:
422
+ self._inter_ranges.append("")
423
+
424
+ def on_enter_starts_postfix(self, token: Token) -> None:
425
+ self._postfix = token.value
426
+
427
+ def on_enter_starts_stem(self, token: Token) -> None:
428
+ self._stem = token.value
429
+
430
+ def on_enter_starts_suffixes(self, token: Token) -> None:
431
+ self._suffixes = self._parse_suffixes(token)
432
+
433
+ def on_enter_range_later(self, token: Token) -> None:
434
+ self._stem = token.value
435
+
436
+ def on_enter_in_prefix(self, token: Token) -> None:
437
+ self._prefix = token.value
438
+
439
+ def _parse_padded_range(
440
+ self, token: Token
441
+ ) -> PaddedRange[int] | PaddedRange[Decimal]:
442
+ match = PAD_FORMAT_RE.search(token.value)
443
+ if not match:
444
+ raise ParseError(
445
+ self._seq,
446
+ token.column,
447
+ token.end_column,
448
+ reason=f"Tokenised an invalid range: {token.value}",
449
+ )
450
+ pad_format = match.group(0)
451
+ seq_str = token.value[: -len(pad_format)]
452
+ file_nums = FileNumSequence.from_str(seq_str)
453
+ return PaddedRange(file_nums, pad_format) # type: ignore[misc]
454
+
455
+ def _parse_suffixes(self, token: Token) -> tuple[str, ...]:
456
+ if not token.value:
457
+ return ()
458
+
459
+ suffixes = []
460
+ buffer = token.value[0]
461
+ for char in token.value[1:]:
462
+ if char == ".":
463
+ suffixes.append(buffer)
464
+ buffer = char
465
+ else:
466
+ buffer += char
467
+
468
+ suffixes.append(buffer)
469
+ return tuple(suffixes)
470
+
471
+ def on_enter_range_in_name(self, token: Token) -> None:
472
+ self._range_type = RangesInName
473
+
474
+ self._ranges.append(self._parse_padded_range(token))
475
+
476
+ def on_enter_in_postfix(self, token: Token) -> None:
477
+ self._postfix = token.value
478
+
479
+ def on_enter_in_suffixes(self, token: Token) -> None:
480
+ self._suffixes = self._parse_suffixes(token)
481
+
482
+ def on_enter_range_ends_name(self, token: Token) -> None:
483
+ self._range_type = RangesEndName
484
+
485
+ self._suffixes = self._parse_suffixes(token)
486
+
487
+ def on_enter_ends_range(self, token: Token) -> None:
488
+ self._ranges.append(self._parse_padded_range(token))
489
+
490
+ def on_enter_ends_prefix(self, token: Token) -> None:
491
+ self._prefix = token.value
492
+
493
+ def on_finalise(self, source: State) -> ParsedLooseSequence:
494
+ if source == self.range_starts_name:
495
+ self._range_type = RangesInName
496
+
497
+ assert self._range_type is not None, "Failed to establish a range type"
498
+ assert self._range_type is not RangesStartName or self._prefix == ""
499
+ assert self._range_type is not RangesEndName or self._postfix == ""
500
+
501
+ return self._range_type(
502
+ stem=self._stem,
503
+ prefix=self._prefix, # type: ignore[arg-type]
504
+ ranges=Ranges(tuple(self._ranges), tuple(self._inter_ranges)),
505
+ postfix=self._postfix, # type: ignore[arg-type]
506
+ suffixes=self._suffixes,
507
+ )
508
+
509
+ @classmethod
510
+ def parse(cls, seq: str) -> ParsedLooseSequence:
511
+ machine = cls(seq)
512
+ tokens = collections.deque(_tokenise(seq))
513
+ while True:
514
+ try:
515
+ token = tokens.popleft()
516
+ except IndexError:
517
+ break
518
+ else:
519
+ machine.pump(token)
520
+
521
+ return machine.finalise() # type: ignore[no-any-return]
522
+
523
+
524
+ def parse_path_sequence(seq: str) -> ParsedLooseSequence:
525
+ return SeqParser.parse(seq)