Skip to content

Commit 0a06855

Browse files
committed
Provide a more correct definition of stem
1 parent 89d5b75 commit 0a06855

6 files changed

Lines changed: 99 additions & 30 deletions

File tree

doc/source/format.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ Compatibility with VFX Software
340340
This format maximises compatibility with software commonly used in
341341
the VFX industry.
342342
Any sequence string that is not directly compatible with a DCC
343-
can be converted to a compatible sequence string using :ref:`format conversion <convert>`.
343+
can be converted to a compatible sequence string using :doc:`format conversion <convert>`.
344344

345345
The "**File Compatible**" column notes whether pathseq can represent
346346
a file sequence output by the DCC.
@@ -572,15 +572,15 @@ a stem may or may not be present in the name of a loose path sequence.
572572

573573
For ranges that start or end the name of the sequence,
574574
there is ambiguity in how to interpret the stem and suffixes.
575-
Unlike :attr:`pathlib.PurePath.stem`, this will never contain a suffix
575+
Like :attr:`pathlib.PurePath.stem`, this will contain a suffix
576576
if the paths have multiple suffixes:
577577

578578
.. code-block:: pycon
579579
580580
>>> LoosePathSequence('file.tar.gz.1-5#').stem
581-
'file'
581+
'file.tar'
582582
>>> LoosePathSequence('1-5#file.tar.gz').stem
583-
'file'
583+
'file.tar'
584584
585585
586586
.. _format-loose-prerange:

doc/source/quickstart.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,4 @@ Like :class:`pathlib.Path`, the name of a path sequence can be split into its pa
217217
.. seealso::
218218

219219
:doc:`/format`
220-
:doc:`/user/convert`
220+
:doc:`/convert`

src/pathseq/_ast/_formatter.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ class Formatter:
6767
'image.$F.exr'
6868
"""
6969

70+
def __init__(self) -> None:
71+
self.include_all_suffixes = False
72+
"""Whether to include all suffixes in the formatted sequence.
73+
74+
This is set by :meth:`~.Formatter.format` to indicate whether the
75+
suffixes have been cleanly separated from the stem or not.
76+
If this is ``False``, then the ``stem`` and ``suffixes`` will overlap
77+
and both include all but the last suffix.
78+
"""
79+
7080
def stem(self, stem: str) -> str:
7181
return stem
7282

@@ -89,7 +99,7 @@ def post_range(self, post_range: str) -> str:
8999
return post_range
90100

91101
def suffixes(self, suffixes: tuple[str, ...]) -> str:
92-
return "".join(suffixes)
102+
return "".join(suffixes if self.include_all_suffixes else suffixes[-1:])
93103

94104
def format(self, seq: ParsedSequence | ParsedLooseSequence) -> str:
95105
"""Format the given path sequence into a string.
@@ -103,6 +113,14 @@ def format(self, seq: ParsedSequence | ParsedLooseSequence) -> str:
103113
Returns:
104114
The formatter path sequence.
105115
"""
116+
from ._loose_type import RangesInName
117+
from ._type import ParsedSequence
118+
119+
self.include_all_suffixes = bool(
120+
isinstance(seq, ParsedSequence)
121+
or (isinstance(seq, RangesInName) and seq.ranges.ranges)
122+
)
123+
106124
return "".join(
107125
getattr(self, field.name)(getattr(seq, field.name))
108126
for field in dataclasses.fields(seq)

src/pathseq/_loose_pure_path_sequence.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,18 @@ def stem(self) -> str:
7878
>>> LoosePurePathSequence('/path/to/images.exr.1-3####').stem
7979
'images'
8080
81-
Unlike :attr:`pathlib.PurePath.stem`, this will never contain a suffix
82-
if the paths have multiple suffixes:
81+
If the ranges do not start or end the path then,
82+
unlike :attr:`pathlib.PurePath.stem`, this will never contain a suffix
83+
if the range is in the path and the paths have multiple suffixes:
8384
8485
.. code-block:: pycon
8586
86-
>>> LoosePurePathSequence('/path/to/images.tar.gz.1-3####').stem
87+
>>> LoosePurePathSequence('/path/to/images.1-3####.tar.gz').stem
8788
'images'
89+
>>> LoosePurePathSequence('/path/to/1-3####_images.tar.gz').stem
90+
'images.tar'
91+
>>> LoosePurePathSequence('/path/to/images.tar.gz.1-3####').stem
92+
'images.tar'
8893
8994
If the paths have no stem, then the empty string is returned:
9095

src/pathseq/_parse_loose_path_sequence.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ def _tokenise(seq: str) -> list[Token]:
125125
suffix_dot = 0
126126
if raw_token.startswith("."):
127127
suffix_dot = 1
128+
post_i = raw_token.rindex(".", suffix_dot)
128129
suffix_i = raw_token.index(".", suffix_dot)
129130
except ValueError:
131+
post_i = len(raw_token)
130132
suffix_i = len(raw_token)
131133

132134
token = Token(
133135
TokenType.STEM,
134-
raw_token[:suffix_i],
136+
raw_token[:post_i],
135137
column,
136138
)
137139
tokens.append(token)
@@ -161,14 +163,7 @@ def _tokenise(seq: str) -> list[Token]:
161163
)
162164
tokens.append(token)
163165
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:
166+
if starts_with_range:
172167
if any(raw_token.startswith(sep) for sep in _POST_RANGE_SEPARATORS):
173168
token = Token(
174169
TokenType.POST_RANGE,
@@ -180,19 +175,23 @@ def _tokenise(seq: str) -> list[Token]:
180175
column += 1
181176

182177
if raw_token.endswith("."):
178+
post_i = 0
183179
suffix_i = len(raw_token)
184180
elif raw_token.startswith("."):
181+
post_i = 0
185182
suffix_i = 0
186183
else:
187184
try:
185+
post_i = raw_token.rindex(".")
188186
suffix_i = raw_token.index(".")
189187
except ValueError:
188+
post_i = len(raw_token)
190189
suffix_i = len(raw_token)
191190

192-
if raw_token[:suffix_i]:
191+
if raw_token[:post_i]:
193192
token = Token(
194193
TokenType.STEM,
195-
raw_token[:suffix_i],
194+
raw_token[:post_i],
196195
column,
197196
)
198197
tokens.append(token)
@@ -204,10 +203,8 @@ def _tokenise(seq: str) -> list[Token]:
204203
column + suffix_i,
205204
)
206205
tokens.append(token)
207-
else:
208-
if raw_token.endswith("."):
209-
suffix_i = len(raw_token)
210-
elif raw_token.startswith("."):
206+
elif not ends_with_range:
207+
if raw_token.startswith("."):
211208
suffix_i = 0
212209
else:
213210
try:
@@ -230,6 +227,55 @@ def _tokenise(seq: str) -> list[Token]:
230227
column + suffix_i,
231228
)
232229
tokens.append(token)
230+
else:
231+
if any(raw_token.startswith(sep) for sep in _PRE_RANGE_SEPARATORS):
232+
token = Token(
233+
TokenType.PRE_RANGE,
234+
raw_token[0],
235+
column,
236+
)
237+
tokens.append(token)
238+
raw_token = raw_token[1:]
239+
column += 1
240+
241+
prerange = None
242+
if any(raw_token.endswith(sep) for sep in _PRE_RANGE_SEPARATORS):
243+
prerange = Token(
244+
TokenType.PRE_RANGE,
245+
raw_token[-1],
246+
column + len(raw_token) - 1,
247+
)
248+
raw_token = raw_token[:-1]
249+
250+
if raw_token.startswith("."):
251+
stem_i = len(raw_token)
252+
suffix_i = 0
253+
else:
254+
try:
255+
stem_i = raw_token.rindex(".")
256+
suffix_i = raw_token.index(".")
257+
except ValueError:
258+
stem_i = 0
259+
suffix_i = len(raw_token)
260+
261+
token = Token(
262+
TokenType.STEM,
263+
raw_token[:stem_i],
264+
column,
265+
)
266+
tokens.append(token)
267+
268+
if raw_token[suffix_i:]:
269+
token = Token(
270+
TokenType.SUFFIXES,
271+
raw_token[suffix_i:],
272+
column + suffix_i,
273+
)
274+
tokens.append(token)
275+
276+
if prerange:
277+
tokens.append(prerange)
278+
column += 1
233279

234280
column += len(raw_token)
235281

tests/test_loose_parsing.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class TestPathSequence:
108108
(),
109109
),
110110
"",
111-
"file",
111+
"file.tar",
112112
(".tar", ".gz"),
113113
),
114114
id="#file.tar.gz",
@@ -369,8 +369,8 @@ def test_in_without_stem(self, seq, expected):
369369
),
370370
(),
371371
),
372-
".",
373-
(),
372+
"",
373+
(".",),
374374
),
375375
id="file.#.",
376376
),
@@ -445,10 +445,10 @@ def test_in_without_stem(self, seq, expected):
445445
),
446446
(),
447447
),
448-
".exr.",
449-
(),
448+
"",
449+
(".exr", "."),
450450
),
451-
id="file.#.exr",
451+
id="file.#.exr.",
452452
),
453453
],
454454
)

0 commit comments

Comments
 (0)