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,327 @@
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
+ ParsedSequence,
16
+ Ranges,
17
+ )
18
+
19
+ _PREFIX_SEPARATORS = {".", "_"}
20
+ RANGE_RE = re.compile(
21
+ r"""
22
+ -?\d+(?:\.\d+)?
23
+ (?: # optional range
24
+ - # range delimiter
25
+ -?\d+(?:\.\d+)? # end frame
26
+ (?: # optional stepping
27
+ x # step type
28
+ \d+(?:\.\d+)? # step value
29
+ )?
30
+ )?
31
+ """,
32
+ re.VERBOSE,
33
+ )
34
+ PAD_FORMAT_RE = re.compile(
35
+ r"""
36
+ (?:
37
+ \#+(?:\.\#+)?
38
+ | <UDIM>
39
+ | <UVTILE>
40
+ )
41
+ """,
42
+ flags=re.VERBOSE,
43
+ )
44
+ RANGES_RE = re.compile(
45
+ f"""
46
+ (
47
+ (?:{RANGE_RE.pattern} (?:, {RANGE_RE.pattern})*)?
48
+ {PAD_FORMAT_RE.pattern}
49
+ )
50
+ """,
51
+ flags=RANGE_RE.flags | PAD_FORMAT_RE.flags | re.VERBOSE,
52
+ )
53
+
54
+
55
+ class TokenType(enum.Enum):
56
+ RANGE = enum.auto()
57
+ INTER_RANGE = enum.auto()
58
+ STEM = enum.auto()
59
+ PREFIX = enum.auto()
60
+ SUFFIXES = enum.auto()
61
+
62
+
63
+ @dataclass
64
+ class Token:
65
+ type: TokenType
66
+ value: str
67
+ column: int
68
+
69
+ @property
70
+ def end_column(self) -> int:
71
+ return self.column + len(self.value)
72
+
73
+
74
+ def _tokenise_seq(seq: str) -> list[Token]:
75
+ raw_tokens = re.split(RANGES_RE, seq)
76
+ if len(raw_tokens) == 1:
77
+ raise NotASequenceError(seq)
78
+
79
+ return process_tokens(seq, raw_tokens)
80
+
81
+
82
+ def process_tokens(seq: str, raw_tokens: list[str]) -> list[Token]:
83
+ if seq.endswith("."):
84
+ raise ParseError(seq, len(seq) - 1, reason="Suffixes cannot end with a '.'")
85
+
86
+ starts_with_range = not bool(raw_tokens[0])
87
+ if starts_with_range:
88
+ raise ParseError(seq, 0, reason="Expected a stem but got a range")
89
+
90
+ ends_with_range = not bool(raw_tokens[-1])
91
+ if ends_with_range:
92
+ raise ParseError(
93
+ seq, len(seq), reason="Expected file suffixes but path ends with a range"
94
+ )
95
+
96
+ column = 0
97
+ tokens: list[Token] = []
98
+ for i, raw_token in enumerate(raw_tokens):
99
+ if i == 0:
100
+ separator = None
101
+ if raw_token != "." and any(
102
+ raw_token.endswith(sep) for sep in _PREFIX_SEPARATORS
103
+ ):
104
+ separator = Token(
105
+ TokenType.PREFIX,
106
+ raw_token[-1],
107
+ column + len(raw_token) - 1,
108
+ )
109
+ raw_token = raw_token[:-1]
110
+
111
+ token = Token(
112
+ TokenType.STEM,
113
+ raw_token,
114
+ column,
115
+ )
116
+ tokens.append(token)
117
+
118
+ if separator:
119
+ tokens.append(separator)
120
+ elif i % 2 == 1:
121
+ token = Token(
122
+ TokenType.RANGE,
123
+ raw_token,
124
+ column,
125
+ )
126
+ tokens.append(token)
127
+ elif i >= 2 and i != len(raw_tokens) - 1:
128
+ if not raw_token:
129
+ raise ParseError(
130
+ seq,
131
+ column + 1,
132
+ column + 2,
133
+ "Expected a non-empty inter-range separator",
134
+ )
135
+ if raw_token == ".":
136
+ raise ParseError(
137
+ seq,
138
+ column + 1,
139
+ column + 2,
140
+ "Cannot use '.' as an inter-range separator",
141
+ )
142
+ token = Token(
143
+ TokenType.INTER_RANGE,
144
+ raw_token,
145
+ column,
146
+ )
147
+ tokens.append(token)
148
+ elif i == len(raw_tokens) - 1:
149
+ if raw_token.startswith("."):
150
+ token = Token(
151
+ TokenType.SUFFIXES,
152
+ raw_token,
153
+ column,
154
+ )
155
+ tokens.append(token)
156
+ else:
157
+ raise ParseError(
158
+ seq,
159
+ column + 1,
160
+ column + 2,
161
+ "Expected a '.' to begin file suffixes",
162
+ )
163
+ else:
164
+ assert False, "Found a token that should be impossible"
165
+
166
+ column += len(raw_token)
167
+
168
+ assert all(isinstance(token, Token) for token in tokens)
169
+ if not any(token.type == TokenType.RANGE for token in tokens):
170
+ raise NotASequenceError(seq)
171
+
172
+ if __debug__:
173
+ type_counts = collections.Counter(token.type for token in tokens)
174
+ assert type_counts[TokenType.STEM] <= 1
175
+ num_pre_seps = type_counts[TokenType.PREFIX]
176
+ assert num_pre_seps <= 1
177
+ num_ranges = type_counts[TokenType.RANGE]
178
+ num_inter_ranges = type_counts[TokenType.INTER_RANGE]
179
+ assert num_inter_ranges == num_ranges - 1
180
+ assert type_counts[TokenType.SUFFIXES] <= 1
181
+
182
+ return tokens
183
+
184
+
185
+ class _SeqParser(StateMachine):
186
+ init = State(initial=True)
187
+ stem = State()
188
+
189
+ in_prefix = State()
190
+ range_in_name = State()
191
+ in_inter_range = State()
192
+ in_suffixes = State()
193
+
194
+ parsed = State(final=True)
195
+
196
+ pump = (
197
+ init.to(stem, validators="expect_stem")
198
+ | stem.to(in_prefix, cond="is_prefix")
199
+ | stem.to(range_in_name, validators="expect_prefix_or_range")
200
+ | in_prefix.to(range_in_name, validators="expect_range")
201
+ | range_in_name.to(in_inter_range, cond="is_inter_range")
202
+ | in_inter_range.to(range_in_name, validators="expect_range")
203
+ | range_in_name.to(in_suffixes, validators="expect_inter_range_or_suffixes")
204
+ )
205
+
206
+ finalise = in_suffixes.to(parsed)
207
+
208
+ def __init__(self, seq: str) -> None:
209
+ super().__init__()
210
+
211
+ self._seq = seq
212
+ self._stem: str = ""
213
+ self._prefix = ""
214
+ self._ranges: list[PaddedRange[int] | PaddedRange[Decimal]] = []
215
+ self._inter_ranges: list[str] = []
216
+ self._suffixes: tuple[str, ...] = ()
217
+
218
+ def is_inter_range(self, token: Token) -> bool:
219
+ return token.type == TokenType.INTER_RANGE
220
+
221
+ def is_prefix(self, token: Token) -> bool:
222
+ return token.type == TokenType.PREFIX
223
+
224
+ def expect_stem(self, token: Token) -> bool:
225
+ return self._validate_stem(token, "Expected a stem")
226
+
227
+ def expect_prefix_or_range(self, token: Token) -> bool:
228
+ return self._validate_range(token, "Expected a prefix separator, or the ranges")
229
+
230
+ def expect_range(self, token: Token) -> bool:
231
+ return self._validate_range(token, "Expected the ranges")
232
+
233
+ def expect_inter_range_or_suffixes(self, token: Token) -> bool:
234
+ return self._validate_suffixes(
235
+ token, "Expected an inter-range string, or file suffixes"
236
+ )
237
+
238
+ def _validate_range(self, token: Token, reason: str) -> bool:
239
+ if token.type != TokenType.RANGE:
240
+ raise ParseError(self._seq, token.column, token.end_column, reason=reason)
241
+ return True
242
+
243
+ def _validate_stem(self, token: Token, reason: str) -> bool:
244
+ if token.type != TokenType.STEM:
245
+ raise ParseError(self._seq, token.column, token.end_column, reason=reason)
246
+ return True
247
+
248
+ def _validate_suffixes(self, token: Token, reason: str) -> bool:
249
+ if token.type != TokenType.SUFFIXES:
250
+ raise ParseError(self._seq, token.column, token.end_column, reason=reason)
251
+ return True
252
+
253
+ def on_enter_stem(self, token: Token) -> None:
254
+ self._stem = token.value
255
+
256
+ def on_enter_in_prefix(self, token: Token) -> None:
257
+ self._prefix = token.value
258
+
259
+ def _parse_padded_range(
260
+ self, token: Token
261
+ ) -> PaddedRange[int] | PaddedRange[Decimal]:
262
+ match = PAD_FORMAT_RE.search(token.value)
263
+ if not match:
264
+ raise ParseError(
265
+ self._seq,
266
+ token.column,
267
+ token.end_column,
268
+ reason=f"Tokenised an invalid range: {token.value}",
269
+ )
270
+ pad_format = match.group(0)
271
+ seq_str = token.value[: -len(pad_format)]
272
+ file_nums = FileNumSequence.from_str(seq_str)
273
+ return PaddedRange(file_nums, pad_format) # type: ignore[misc]
274
+
275
+ def _parse_suffixes(self, token: Token) -> tuple[str, ...]:
276
+ if not token.value:
277
+ return ()
278
+
279
+ suffixes = []
280
+ buffer = token.value[0]
281
+ for char in token.value[1:]:
282
+ if char == ".":
283
+ if buffer == ".":
284
+ column = token.column + len(buffer)
285
+ raise ParseError(
286
+ self._seq,
287
+ column,
288
+ column + 1,
289
+ "Cannot have an empty file extension",
290
+ )
291
+ suffixes.append(buffer)
292
+ buffer = char
293
+ else:
294
+ buffer += char
295
+
296
+ suffixes.append(buffer)
297
+ return tuple(suffixes)
298
+
299
+ def on_enter_range_in_name(self, token: Token) -> None:
300
+ self._ranges.append(self._parse_padded_range(token))
301
+
302
+ def on_enter_in_inter_range(self, token: Token) -> None:
303
+ self._inter_ranges.append(token.value or "")
304
+
305
+ def on_enter_in_suffixes(self, token: Token) -> None:
306
+ self._suffixes = self._parse_suffixes(token)
307
+
308
+ def on_finalise(self) -> ParsedSequence:
309
+ return ParsedSequence(
310
+ stem=self._stem,
311
+ prefix=self._prefix,
312
+ ranges=Ranges(tuple(self._ranges), tuple(self._inter_ranges)),
313
+ suffixes=self._suffixes,
314
+ )
315
+
316
+ @classmethod
317
+ def parse(cls, seq: str) -> ParsedSequence:
318
+ machine = cls(seq)
319
+ tokens = _tokenise_seq(seq)
320
+ for token in tokens:
321
+ machine.pump(token)
322
+
323
+ return machine.finalise() # type: ignore[no-any-return]
324
+
325
+
326
+ def parse_path_sequence(seq: str) -> ParsedSequence:
327
+ return _SeqParser.parse(seq)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from ._base import BasePathSequence, PathT_co
4
+ from ._pure_path_sequence import PurePathSequence
5
+
6
+
7
+ class PathSequence(PurePathSequence[PathT_co], BasePathSequence[PathT_co]):
8
+ """A sequence of Path objects.
9
+
10
+ Raises:
11
+ NotASequenceError: When the given path does not represent a sequence,
12
+ but a regular path.
13
+ ParseError: When the given path is not a valid path sequence.
14
+ """
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal
4
+
5
+ from typing_extensions import (
6
+ Self, # PY311
7
+ )
8
+
9
+ from ._ast import PaddedRange, ParsedSequence, Ranges
10
+ from ._base import BasePurePathSequence, PathT_co
11
+ from ._file_num_seq import FileNumSequence
12
+ from ._parse_path_sequence import parse_path_sequence
13
+
14
+
15
+ class PurePathSequence(BasePurePathSequence[PathT_co]):
16
+ """A sequence of PurePath objects.
17
+
18
+ Raises:
19
+ NotASequenceError: When the given path does not represent a sequence,
20
+ but a regular path.
21
+ ParseError: When the given path is not a valid path sequence.
22
+ """
23
+
24
+ _parsed: ParsedSequence
25
+
26
+ def _parse(self, name: str) -> ParsedSequence:
27
+ return parse_path_sequence(name)
28
+
29
+ @property
30
+ def parsed(self) -> ParsedSequence:
31
+ """The parsed sequence string, as a tree of objects."""
32
+ return self._parsed
33
+
34
+ def with_file_num_seqs(
35
+ self, *seqs: FileNumSequence[int] | FileNumSequence[Decimal]
36
+ ) -> Self:
37
+ """Return a new sequence with the file number sequences changed.
38
+
39
+ Raises:
40
+ TypeError: If the given number of file number sequences does not match
41
+ the sequence's number of file number sequences.
42
+ """
43
+ if len(seqs) != len(self._parsed.ranges.ranges):
44
+ raise TypeError(
45
+ f"Need {len(self._parsed.ranges.ranges)} sequences, but got {len(seqs)}"
46
+ )
47
+
48
+ new_ranges = tuple(
49
+ PaddedRange(seq, range_.pad_format)
50
+ for seq, range_ in zip(seqs, self._parsed.ranges.ranges)
51
+ )
52
+ new = self._parsed.__class__(
53
+ self._parsed.stem,
54
+ self._parsed.prefix,
55
+ Ranges(new_ranges, self._parsed.ranges.inter_ranges),
56
+ self._parsed.suffixes,
57
+ )
58
+ return self.__class__(self._path.with_name(str(new)))
pathseq/py.typed ADDED
File without changes
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.3
2
+ Name: pathseq
3
+ Version: 0.1.0
4
+ Summary: A pathlib-like library for working with file sequences.
5
+ Author: Ashley Whetter
6
+ Author-email: Ashley Whetter <ashley@awhetter.co.uk>
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Plugins
9
+ Classifier: Framework :: Sphinx :: Extension
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Dist: lark
21
+ Requires-Dist: python-statemachine
22
+ Requires-Dist: typing-extensions
23
+ Requires-Python: >=3.10
24
+ Project-URL: Documentation, https://pathseq.readthedocs.org
25
+ Project-URL: Source, https://github.com/AWhetter/pathseq
26
+ Description-Content-Type: text/x-rst
27
+
28
+ pathseq
29
+ =======
30
+
31
+ .. image:: https://readthedocs.org/projects/pathseq/badge/?version=latest
32
+ :target: https://pathseq.readthedocs.org
33
+ :alt: Documentation
34
+
35
+ .. image:: https://github.com/AWhetter/pathseq/actions/workflows/main.yml/badge.svg?branch=main
36
+ :target: https://github.com/AWhetter/pathseq/actions/workflows/main.yml?query=branch%3Amain
37
+ :alt: Github Build Status
38
+
39
+ .. image:: https://img.shields.io/pypi/v/pathseq.svg
40
+ :target: https://pypi.org/project/pathseq/
41
+ :alt: PyPI Version
42
+
43
+ .. image:: https://img.shields.io/pypi/pyversions/pathseq.svg
44
+ :target: https://pypi.org/project/pathseq/
45
+ :alt: Supported Python Versions
46
+
47
+ A pathlib-first library for working with file sequences.
48
+
49
+ TODO: Note special features
50
+
51
+ * Multi-dimension ranges (eg animated udims)
52
+ * pathlib-first API
53
+ * Support for UDIM tokens
54
+ * Sequence compression?
55
+
56
+ Getting Started
57
+ ---------------
58
+
59
+ The following section demonstrates how to install ``pathseq`` and some basic usage.
60
+ Full documentation is available here: https://pathseq.readthedocs.io/en/latest/
61
+
62
+
63
+ Installation
64
+ ~~~~~~~~~~~~
65
+
66
+ ``pathseq`` can be installed through pip:
67
+
68
+ .. code-block:: bash
69
+
70
+ pip install pathseq
71
+
72
+ Usage
73
+ ~~~~~
74
+
75
+ TODO
76
+
77
+ For more detailed usage, see the documentation:
78
+ https://pathseq.readthedocs.io/en/latest/
79
+
80
+
81
+ Contributing
82
+ ------------
83
+
84
+ Running the tests
85
+ ~~~~~~~~~~~~~~~~~
86
+
87
+ Tests are executed through `tox <https://tox.readthedocs.io/en/latest/>`_.
88
+
89
+ .. code-block:: bash
90
+
91
+ tox
92
+
93
+
94
+ Code Style
95
+ ~~~~~~~~~~
96
+
97
+ Code is formatted using `black <https://github.com/python/black>`_.
98
+
99
+ You can check your formatting using black's check mode:
100
+
101
+ .. code-block:: bash
102
+
103
+ tox -e format
104
+
105
+ You can also get black to format your changes for you:
106
+
107
+ .. code-block:: bash
108
+
109
+ tox -e format -- src/ tests/
110
+
111
+
112
+ Release Notes
113
+ ~~~~~~~~~~~~~
114
+
115
+ Release notes are managed through `towncrier <https://towncrier.readthedocs.io/en/stable/index.html>`_.
116
+ When making a pull request you will need to create a news fragment to document your change:
117
+
118
+ .. code-block:: bash
119
+
120
+ tox -e release_notes -- create --help
121
+
122
+
123
+ Versioning
124
+ ----------
125
+
126
+ We use `SemVer <https://semver.org/>`_ for versioning.
127
+ For the versions available, see the `tags on this repository <https://github.com/AWhetter/pathseq/tags>`_.
128
+
129
+
130
+ License
131
+ -------
132
+
133
+ This project is licensed under the MIT License.
134
+ See the `LICENSE.rst <LICENSE.rst>`_ file for details.
@@ -0,0 +1,24 @@
1
+ pathseq/__init__.py,sha256=Vfq5BvdPoY0rpQ_JLgGa6HEn69RmlMvBAbMAIfPpOPU,1121
2
+ pathseq/_arithmetic_sequence.py,sha256=9FiiHMRHPiSVuKh1IWOkHhapiJoeFDl6LKr15p_t790,4630
3
+ pathseq/_ast/__init__.py,sha256=vzv_VxfY8HFmKuVbPhzVrZ3Ii1DVskSlo7LhIHxtdcY,447
4
+ pathseq/_ast/_base.py,sha256=rDgCUF8m4RUBdYU7Q5C5r6SDWHzgC6e1yf3-dUSyN50,8241
5
+ pathseq/_ast/_formatter.py,sha256=xy6UKTZ8Nx2DAlzoe9VQzqAMskugDYNoWSD2AKVgU-A,1146
6
+ pathseq/_ast/_loose_type.py,sha256=yg0T1Xw-KWALTqmx7ZrHvw9RCR7cD2sI8A_9ek_HeWE,7574
7
+ pathseq/_ast/_type.py,sha256=VqIXf1Au-RMXrmCyDu5MofjCI1UPEydpPJbB4aQhR1A,1958
8
+ pathseq/_base.py,sha256=EOcxeb3Wv4v_DKXtkiFysv1eyKvO8VKke-9MDWhJwJM,17096
9
+ pathseq/_decimal_range.py,sha256=T6eqqVzRe5JfvnraO5UzqslGKke-64T2714Q08boyZM,5138
10
+ pathseq/_error.py,sha256=MNK5kazk3PsuE6hs9Hpcv9wnYqZZIqoIMuzj9IErUYw,2644
11
+ pathseq/_file_num_seq.py,sha256=VSqYAX4-tIJn3h5AD038_xpAP31Oj3ocoZfch-tClTY,7293
12
+ pathseq/_formatter.py,sha256=Sevv_B-AsOSbx0D4oYq-edgHQEySLpMoyZ94CK5ZMOY,2569
13
+ pathseq/_from_disk.py,sha256=iUqK3ma1m9vhIUso2vnLLpAa9tF_eQXYOZL4ch7oewU,2445
14
+ pathseq/_loose_path_sequence.py,sha256=0YoJM5H7nXO0zFFK09cCx3GYBvDlOcwk8VZLUaTqpxk,467
15
+ pathseq/_loose_pure_path_sequence.py,sha256=cc4dfStsCqtLrvJ40RuNQRO5OjUQLN2r-QkB581_0lc,4196
16
+ pathseq/_parse_file_num_seq.py,sha256=6Sd__7SEzjQaxXvVXj-0Yad8NsElHl7JzQukYpvS-KA,2102
17
+ pathseq/_parse_loose_path_sequence.py,sha256=8_v05ty-gn4S8TD0djduD1Sj7JPVkw9SNKBeLwEx_sI,17539
18
+ pathseq/_parse_path_sequence.py,sha256=FumrJ02XOpjfm_UMVEi786E3ZkWDXu1PhqW1qrfl55M,9957
19
+ pathseq/_path_sequence.py,sha256=glss3zoC3lMTxGrPIGQXputLOG65e-XSFM50BwhdpXk,446
20
+ pathseq/_pure_path_sequence.py,sha256=3-MgYD4peH-uNpm7xkTdLxYP1Kkz9ERiU4DRy9S32dA,1869
21
+ pathseq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ pathseq-0.1.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
23
+ pathseq-0.1.0.dist-info/METADATA,sha256=8XaFeU9rKHVPkgtdpW2_BvPG-s7M5uX4pASwe0zbIa8,3421
24
+ pathseq-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any