bracex 2.6__tar.gz → 3.0__tar.gz
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.
- {bracex-2.6 → bracex-3.0}/.gitignore +3 -0
- {bracex-2.6 → bracex-3.0}/LICENSE.md +1 -1
- {bracex-2.6 → bracex-3.0}/PKG-INFO +2 -6
- {bracex-2.6 → bracex-3.0}/README.md +0 -3
- {bracex-2.6 → bracex-3.0}/bracex/__init__.py +181 -121
- {bracex-2.6 → bracex-3.0}/bracex/__meta__.py +1 -1
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/changelog.md +15 -0
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/development.md +4 -5
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/index.md +20 -13
- bracex-3.0/docs/theme/announce.html +10 -0
- bracex-3.0/docs/theme/assets/pymdownx-extras/extra-95634471d6.css +2 -0
- bracex-3.0/docs/theme/assets/pymdownx-extras/extra-loader-Ccztcqfq.js +4 -0
- bracex-3.0/docs/theme/main.html +17 -0
- {bracex-2.6 → bracex-3.0}/hatch_build.py +0 -1
- {bracex-2.6 → bracex-3.0}/pyproject.toml +3 -3
- bracex-3.0/requirements/docs.txt +4 -0
- {bracex-2.6 → bracex-3.0}/tests/brace-cases.txt +7 -0
- {bracex-2.6 → bracex-3.0}/tests/brace-results.txt +9 -1
- {bracex-2.6 → bracex-3.0}/tests/generate.sh +1 -1
- {bracex-2.6 → bracex-3.0}/tests/test_brace.py +123 -2
- bracex-2.6/docs/theme/announce.html +0 -5
- bracex-2.6/mkdocs.yml +0 -157
- bracex-2.6/requirements/docs.txt +0 -4
- {bracex-2.6 → bracex-3.0}/.coveragerc +0 -0
- {bracex-2.6 → bracex-3.0}/.pyspelling.yml +0 -0
- {bracex-2.6 → bracex-3.0}/bracex/__main__.py +0 -0
- {bracex-2.6 → bracex-3.0}/bracex/py.typed +0 -0
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/.snippets/abbr.md +0 -0
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/.snippets/links.md +0 -0
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/.snippets/refs.md +0 -0
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/contributing.md +0 -0
- {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/license.md +0 -0
- {bracex-2.6 → bracex-3.0}/requirements/lint.txt +0 -0
- {bracex-2.6 → bracex-3.0}/requirements/test.txt +0 -0
- {bracex-2.6 → bracex-3.0}/tests/__init__.py +0 -0
- {bracex-2.6 → bracex-3.0}/tests/test_main.py +0 -0
- {bracex-2.6 → bracex-3.0}/tests/test_versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bracex
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0
|
|
4
4
|
Summary: Bash style brace expander.
|
|
5
5
|
Project-URL: Homepage, https://github.com/facelessuser/bracex
|
|
6
6
|
Author-email: Isaac Muse <Isaac.Muse@gmail.com>
|
|
@@ -13,7 +13,6 @@ Classifier: Intended Audience :: Developers
|
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -21,11 +20,10 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
22
|
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
25
24
|
Description-Content-Type: text/markdown
|
|
26
25
|
|
|
27
26
|
[![Donate via PayPal][donate-image]][donate-link]
|
|
28
|
-
[![Build][github-ci-image]][github-ci-link]
|
|
29
27
|
[![Coverage Status][codecov-image]][codecov-link]
|
|
30
28
|
[![PyPI Version][pypi-image]][pypi-link]
|
|
31
29
|
[![PyPI - Python Version][python-image]][pypi-link]
|
|
@@ -84,8 +82,6 @@ Documentation is found here: https://facelessuser.github.io/bracex/.
|
|
|
84
82
|
|
|
85
83
|
MIT License
|
|
86
84
|
|
|
87
|
-
[github-ci-image]: https://github.com/facelessuser/bracex/actions/workflows/build.yml/badge.svg
|
|
88
|
-
[github-ci-link]: https://github.com/facelessuser/bracex/actions?query=workflow%3Abuild+branch%3Amaster
|
|
89
85
|
[codecov-image]: https://img.shields.io/codecov/c/github/facelessuser/bracex/master.svg?logo=codecov&logoColor=aaaaaa&labelColor=333333
|
|
90
86
|
[codecov-link]: https://codecov.io/github/facelessuser/bracex
|
|
91
87
|
[pypi-image]: https://img.shields.io/pypi/v/bracex.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
[![Donate via PayPal][donate-image]][donate-link]
|
|
2
|
-
[![Build][github-ci-image]][github-ci-link]
|
|
3
2
|
[![Coverage Status][codecov-image]][codecov-link]
|
|
4
3
|
[![PyPI Version][pypi-image]][pypi-link]
|
|
5
4
|
[![PyPI - Python Version][python-image]][pypi-link]
|
|
@@ -58,8 +57,6 @@ Documentation is found here: https://facelessuser.github.io/bracex/.
|
|
|
58
57
|
|
|
59
58
|
MIT License
|
|
60
59
|
|
|
61
|
-
[github-ci-image]: https://github.com/facelessuser/bracex/actions/workflows/build.yml/badge.svg
|
|
62
|
-
[github-ci-link]: https://github.com/facelessuser/bracex/actions?query=workflow%3Abuild+branch%3Amaster
|
|
63
60
|
[codecov-image]: https://img.shields.io/codecov/c/github/facelessuser/bracex/master.svg?logo=codecov&logoColor=aaaaaa&labelColor=333333
|
|
64
61
|
[codecov-link]: https://codecov.io/github/facelessuser/bracex
|
|
65
62
|
[pypi-image]: https://img.shields.io/pypi/v/bracex.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333
|
|
@@ -22,10 +22,9 @@ from __future__ import annotations
|
|
|
22
22
|
import itertools
|
|
23
23
|
import math
|
|
24
24
|
import re
|
|
25
|
-
from typing import Iterator, Pattern, Match, Iterable, AnyStr
|
|
25
|
+
from typing import Iterator, Pattern, Match, Iterable, AnyStr, Any
|
|
26
26
|
from . import __meta__
|
|
27
27
|
|
|
28
|
-
|
|
29
28
|
__all__ = ('expand', 'iexpand')
|
|
30
29
|
|
|
31
30
|
__version__ = __meta__.__version__
|
|
@@ -40,24 +39,41 @@ RE_CHR_ITER = re.compile(r'([A-Za-z])\.{2}([A-Za-z])(?:\.{2}(-?\d+))?(?=\})')
|
|
|
40
39
|
DEFAULT_LIMIT = 1000
|
|
41
40
|
|
|
42
41
|
|
|
42
|
+
class Sentinel(str):
|
|
43
|
+
"""A sentinel string value."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
EMPTY = Sentinel('')
|
|
47
|
+
|
|
48
|
+
|
|
43
49
|
class ExpansionLimitException(Exception):
|
|
44
50
|
"""Brace expansion limit exception."""
|
|
45
51
|
|
|
46
52
|
|
|
47
|
-
def expand(
|
|
53
|
+
def expand(
|
|
54
|
+
string: AnyStr,
|
|
55
|
+
keep_escapes: bool = False,
|
|
56
|
+
limit: int = DEFAULT_LIMIT,
|
|
57
|
+
return_empty: bool = False
|
|
58
|
+
) -> list[AnyStr]:
|
|
48
59
|
"""Expand braces."""
|
|
49
60
|
|
|
50
|
-
return list(iexpand(string, keep_escapes, limit))
|
|
61
|
+
return list(iexpand(string, keep_escapes, limit, return_empty))
|
|
51
62
|
|
|
52
63
|
|
|
53
|
-
def iexpand(
|
|
64
|
+
def iexpand(
|
|
65
|
+
string: AnyStr,
|
|
66
|
+
keep_escapes: bool = False,
|
|
67
|
+
limit: int = DEFAULT_LIMIT,
|
|
68
|
+
return_empty: bool = False
|
|
69
|
+
) -> Iterator[AnyStr]:
|
|
54
70
|
"""Expand braces and return an iterator."""
|
|
55
71
|
|
|
56
72
|
if isinstance(string, bytes):
|
|
57
|
-
for entry in ExpandBrace(keep_escapes, limit).expand(string.decode('latin-1')):
|
|
73
|
+
for entry in ExpandBrace(keep_escapes, limit, return_empty).expand(string.decode('latin-1')):
|
|
58
74
|
yield entry.encode('latin-1')
|
|
59
75
|
else:
|
|
60
|
-
for entry in ExpandBrace(keep_escapes, limit).expand(string):
|
|
76
|
+
for entry in ExpandBrace(keep_escapes, limit, return_empty).expand(string):
|
|
61
77
|
yield entry
|
|
62
78
|
|
|
63
79
|
|
|
@@ -127,32 +143,27 @@ class StringIter:
|
|
|
127
143
|
class ExpandBrace:
|
|
128
144
|
"""Expand braces like in Bash."""
|
|
129
145
|
|
|
130
|
-
def __init__(
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
keep_escapes: bool = False,
|
|
149
|
+
limit: int = DEFAULT_LIMIT,
|
|
150
|
+
return_empty: bool = False
|
|
151
|
+
) -> None:
|
|
131
152
|
"""Initialize."""
|
|
132
153
|
|
|
133
154
|
self.max_limit = limit
|
|
134
|
-
self.count = 0
|
|
135
155
|
self.expanding = False
|
|
136
156
|
self.keep_escapes = keep_escapes
|
|
157
|
+
self.return_empty = return_empty
|
|
137
158
|
|
|
138
|
-
def
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
self.count -= sum(count)
|
|
142
|
-
prod = 1
|
|
143
|
-
for c in count:
|
|
144
|
-
prod *= c
|
|
145
|
-
self.update_count(prod)
|
|
146
|
-
|
|
147
|
-
def update_count(self, count: int) -> None:
|
|
148
|
-
"""Update the count and assert if count exceeds the max limit."""
|
|
159
|
+
def account(self, count: int) -> int:
|
|
160
|
+
"""Ensure count is not exceeding the expectation."""
|
|
149
161
|
|
|
150
|
-
self.count
|
|
151
|
-
|
|
152
|
-
if self.max_limit > 0 and self.count > self.max_limit:
|
|
162
|
+
if self.max_limit > 0 and count > self.max_limit:
|
|
153
163
|
raise ExpansionLimitException(
|
|
154
164
|
f'Brace expansion has exceeded the limit of {self.max_limit:d}'
|
|
155
165
|
)
|
|
166
|
+
return count
|
|
156
167
|
|
|
157
168
|
def set_expanding(self) -> bool:
|
|
158
169
|
"""Set that we are expanding a sequence, and return whether a release is required by the caller."""
|
|
@@ -192,9 +203,33 @@ class ExpandBrace:
|
|
|
192
203
|
"""
|
|
193
204
|
|
|
194
205
|
for x in itertools.product(a, b):
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
if all(i is EMPTY for i in x):
|
|
207
|
+
yield EMPTY
|
|
208
|
+
else:
|
|
209
|
+
yield ''.join(x)
|
|
210
|
+
|
|
211
|
+
def chain(self, *iterables: Any) -> Iterator[str]:
|
|
212
|
+
"""Chain iterables."""
|
|
213
|
+
|
|
214
|
+
for iterable in iterables:
|
|
215
|
+
yield from iterable
|
|
216
|
+
|
|
217
|
+
def flatten(self, iterables: Any) -> Iterator[str]:
|
|
218
|
+
"""Flatten out results."""
|
|
219
|
+
|
|
220
|
+
for item in iterables:
|
|
221
|
+
if isinstance(item, list):
|
|
222
|
+
yield from self.flatten(item)
|
|
223
|
+
continue
|
|
224
|
+
yield item
|
|
225
|
+
|
|
226
|
+
def get_literals(
|
|
227
|
+
self,
|
|
228
|
+
c: str,
|
|
229
|
+
i: StringIter,
|
|
230
|
+
depth: int,
|
|
231
|
+
ignore_end: bool = False
|
|
232
|
+
) -> tuple[list[str | Iterator[str]], int]:
|
|
198
233
|
"""
|
|
199
234
|
Get a string literal.
|
|
200
235
|
|
|
@@ -202,66 +237,63 @@ class ExpandBrace:
|
|
|
202
237
|
Also gather chars between braces and commas within a group (is_expanding).
|
|
203
238
|
"""
|
|
204
239
|
|
|
205
|
-
result =
|
|
240
|
+
result = [] # type: list[str | Iterator[str]]
|
|
206
241
|
is_dollar = False
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
seq_count = []
|
|
242
|
+
count = 1
|
|
243
|
+
literal = ''
|
|
210
244
|
|
|
211
245
|
try:
|
|
212
246
|
while c:
|
|
213
|
-
value = [c] # type: Iterable[str]
|
|
214
247
|
ignore_brace = is_dollar
|
|
215
248
|
is_dollar = False
|
|
216
249
|
|
|
217
250
|
if c == '$':
|
|
218
251
|
is_dollar = True
|
|
252
|
+
literal += c
|
|
219
253
|
|
|
220
254
|
elif c == '\\':
|
|
221
|
-
|
|
255
|
+
literal += self.get_escape(c, i)
|
|
222
256
|
|
|
223
257
|
elif not ignore_brace and c == '{':
|
|
258
|
+
|
|
259
|
+
if literal:
|
|
260
|
+
result.append(literal)
|
|
261
|
+
literal = ''
|
|
262
|
+
|
|
224
263
|
# Try and get the group
|
|
225
|
-
index = i.index
|
|
226
264
|
try:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if self.max_limit > 0:
|
|
231
|
-
diff = self.count - current_count
|
|
232
|
-
seq_count.append(diff)
|
|
233
|
-
count = False
|
|
234
|
-
value = seq
|
|
265
|
+
seq, scount = self.get_sequence(next(i), i, depth + 1)
|
|
266
|
+
count *= scount
|
|
267
|
+
result.append(seq)
|
|
235
268
|
except StopIteration:
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
|
|
269
|
+
# There are no characters after `{`
|
|
270
|
+
# Save `{` and stop parsing.
|
|
271
|
+
literal += c
|
|
272
|
+
raise
|
|
239
273
|
|
|
240
|
-
elif self.is_expanding() and c
|
|
274
|
+
elif self.is_expanding() and (c == ',' or (c == '}' and not ignore_end)):
|
|
241
275
|
# We are Expanding within a group and found a group delimiter
|
|
242
276
|
# Return what we gathered before the group delimiters.
|
|
243
|
-
i.rewind(1)
|
|
244
|
-
if count:
|
|
245
|
-
self.update_count(1)
|
|
246
|
-
else:
|
|
247
|
-
self.update_count_seq(seq_count)
|
|
248
|
-
return result
|
|
249
277
|
|
|
250
|
-
|
|
251
|
-
|
|
278
|
+
ignore_end = False
|
|
279
|
+
|
|
280
|
+
if literal:
|
|
281
|
+
result.append(literal)
|
|
282
|
+
|
|
283
|
+
i.rewind(1)
|
|
284
|
+
break
|
|
285
|
+
else:
|
|
286
|
+
literal += c
|
|
252
287
|
|
|
253
288
|
c = next(i)
|
|
254
289
|
except StopIteration:
|
|
255
|
-
|
|
256
|
-
|
|
290
|
+
# Ensure we store any remaining literals
|
|
291
|
+
if literal:
|
|
292
|
+
result.append(literal)
|
|
257
293
|
|
|
258
|
-
|
|
259
|
-
self.update_count(1)
|
|
260
|
-
else:
|
|
261
|
-
self.update_count_seq(seq_count)
|
|
262
|
-
return result
|
|
294
|
+
return result, self.account(count)
|
|
263
295
|
|
|
264
|
-
def get_sequence(self, c: str, i: StringIter, depth: int) -> Iterator[str]
|
|
296
|
+
def get_sequence(self, c: str, i: StringIter, depth: int) -> tuple[Iterator[str], int]:
|
|
265
297
|
"""
|
|
266
298
|
Get the sequence.
|
|
267
299
|
|
|
@@ -269,69 +301,100 @@ class ExpandBrace:
|
|
|
269
301
|
It will basically crawl to the end or find a valid series.
|
|
270
302
|
"""
|
|
271
303
|
|
|
272
|
-
result =
|
|
304
|
+
result = [] # type: list[str | Iterator[str] | Iterable[str | Iterator[str]]]
|
|
273
305
|
release = self.set_expanding()
|
|
274
306
|
has_comma = False # Used to indicate validity of group (`{1..2}` are an exception).
|
|
275
307
|
is_empty = True # Tracks whether the current slot is empty `{slot,slot,slot}`.
|
|
308
|
+
counts = []
|
|
276
309
|
|
|
277
310
|
# Detect numerical and alphabetic series: `{1..2}` etc.
|
|
278
311
|
i.rewind(1)
|
|
279
|
-
item = self.get_range(i)
|
|
312
|
+
item, count = self.get_range(i)
|
|
280
313
|
i.advance(1)
|
|
281
314
|
if item is not None:
|
|
282
315
|
self.release_expanding(release)
|
|
283
|
-
return item
|
|
316
|
+
return item, self.account(count)
|
|
284
317
|
|
|
285
318
|
try:
|
|
286
319
|
while True:
|
|
287
320
|
# Bash has some special top level logic. if `}` follows `{` but hasn't matched
|
|
288
321
|
# a group yet, keep going except when the first 2 bytes are `{}` which gets
|
|
289
322
|
# completely ignored.
|
|
290
|
-
keep_looking = depth == 1 and not has_comma
|
|
323
|
+
keep_looking = depth == 1 and not has_comma
|
|
291
324
|
if (c == '}' and (not keep_looking or i.index == 2)):
|
|
292
|
-
|
|
325
|
+
self.release_expanding(release)
|
|
326
|
+
|
|
327
|
+
# Handle empty slot
|
|
293
328
|
if is_empty:
|
|
294
|
-
result
|
|
329
|
+
result.append(EMPTY)
|
|
330
|
+
if has_comma:
|
|
331
|
+
counts.append(1)
|
|
332
|
+
|
|
333
|
+
# Sequence is not valid
|
|
295
334
|
if not has_comma:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
335
|
+
count = 1
|
|
336
|
+
temp = iter(['{'])
|
|
337
|
+
for r in self.flatten(result):
|
|
338
|
+
temp = self.squash(temp, [r] if isinstance(r, str) else r)
|
|
339
|
+
temp = self.squash(temp, ['}'])
|
|
340
|
+
return temp, self.account(math.prod(counts, start=1))
|
|
341
|
+
|
|
342
|
+
# Format return for a sequence
|
|
343
|
+
fin = iter([]) # type: Iterator[str]
|
|
344
|
+
start = 0
|
|
345
|
+
l = len(result)
|
|
346
|
+
for e, x in enumerate(result):
|
|
347
|
+
if not isinstance(x, str):
|
|
348
|
+
if e != start:
|
|
349
|
+
fin = self.chain(fin, result[start:e])
|
|
350
|
+
if isinstance(x, list):
|
|
351
|
+
temp = iter([EMPTY])
|
|
352
|
+
for y in x:
|
|
353
|
+
temp = self.squash(temp, [y] if isinstance(y, str) else y)
|
|
354
|
+
fin = self.chain(fin, temp)
|
|
355
|
+
else:
|
|
356
|
+
fin = self.chain(fin, result[e])
|
|
357
|
+
start = e + 1
|
|
358
|
+
if start < l:
|
|
359
|
+
fin = self.chain(fin, result[start:])
|
|
360
|
+
return fin, self.account(sum(counts))
|
|
299
361
|
|
|
300
362
|
elif c == ',':
|
|
301
363
|
# Must be the first element in the list.
|
|
302
364
|
has_comma = True
|
|
303
365
|
if is_empty:
|
|
304
|
-
result
|
|
366
|
+
result.append(EMPTY)
|
|
367
|
+
counts.append(1)
|
|
305
368
|
else:
|
|
306
369
|
is_empty = True
|
|
307
370
|
|
|
308
371
|
else:
|
|
309
|
-
if
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if
|
|
314
|
-
result
|
|
372
|
+
# Lower level: Try to find group, but give up if cannot acquire.
|
|
373
|
+
value, lcount = self.get_literals(c, i, depth, keep_looking)
|
|
374
|
+
counts.append(lcount)
|
|
375
|
+
if value is not None:
|
|
376
|
+
if len(value) > 1:
|
|
377
|
+
result.append(value)
|
|
315
378
|
else:
|
|
316
|
-
result
|
|
317
|
-
|
|
318
|
-
if value is not None:
|
|
319
|
-
result = self.squash(result, value)
|
|
320
|
-
is_empty = False
|
|
321
|
-
else:
|
|
322
|
-
# Lower level: Try to find group, but give up if cannot acquire.
|
|
323
|
-
value = self.get_literals(c, i, depth)
|
|
324
|
-
if value is not None:
|
|
325
|
-
result = itertools.chain(result, value)
|
|
326
|
-
is_empty = False
|
|
379
|
+
result.extend(value)
|
|
380
|
+
is_empty = False
|
|
327
381
|
|
|
328
382
|
c = next(i)
|
|
329
383
|
|
|
330
384
|
except StopIteration:
|
|
331
385
|
self.release_expanding(release)
|
|
332
|
-
raise
|
|
333
386
|
|
|
334
|
-
|
|
387
|
+
# Sequence is not valid
|
|
388
|
+
temp2 = iter(['{']) # type: Iterator[str]
|
|
389
|
+
l = len(result)
|
|
390
|
+
last_str = False
|
|
391
|
+
for r in self.flatten(result):
|
|
392
|
+
is_str = isinstance(r, str)
|
|
393
|
+
temp2 = self.squash(temp2, [(',' if last_str else '') + r] if is_str else r)
|
|
394
|
+
last_str = is_str
|
|
395
|
+
return temp2, self.account(math.prod(counts, start=1))
|
|
396
|
+
|
|
397
|
+
def get_range(self, i: StringIter) -> tuple[Iterator[str] | None, int]:
|
|
335
398
|
"""
|
|
336
399
|
Check and retrieve range if value is a valid range.
|
|
337
400
|
|
|
@@ -347,8 +410,6 @@ class ExpandBrace:
|
|
|
347
410
|
m = i.match(RE_CHR_ITER)
|
|
348
411
|
if m:
|
|
349
412
|
return self.get_char_range(*m.groups())
|
|
350
|
-
except ExpansionLimitException:
|
|
351
|
-
raise
|
|
352
413
|
except Exception: # pragma: no cover
|
|
353
414
|
# TODO: We really should never fail here,
|
|
354
415
|
# but if we do, assume the sequence range
|
|
@@ -356,7 +417,7 @@ class ExpandBrace:
|
|
|
356
417
|
# be removed in the future with more testing.
|
|
357
418
|
pass
|
|
358
419
|
|
|
359
|
-
return None
|
|
420
|
+
return None, 0
|
|
360
421
|
|
|
361
422
|
def format_values(self, values: Iterable[int], padding: int) -> Iterator[str]:
|
|
362
423
|
"""Get padding adjusting for negative values."""
|
|
@@ -364,7 +425,7 @@ class ExpandBrace:
|
|
|
364
425
|
for value in values:
|
|
365
426
|
yield "{:0{pad}d}".format(value, pad=padding) if padding else str(value)
|
|
366
427
|
|
|
367
|
-
def get_int_range(self, start: str, end: str, increment: str | None = None) -> Iterator[str]:
|
|
428
|
+
def get_int_range(self, start: str, end: str, increment: str | None = None) -> tuple[Iterator[str], int]:
|
|
368
429
|
"""Get an integer range between start and end and increments of increment."""
|
|
369
430
|
|
|
370
431
|
first, last = int(start), int(end)
|
|
@@ -389,15 +450,15 @@ class ExpandBrace:
|
|
|
389
450
|
padding = 0
|
|
390
451
|
|
|
391
452
|
if first < last:
|
|
392
|
-
|
|
453
|
+
count = math.ceil(abs(((last + 1) - first) / inc))
|
|
393
454
|
r = range(first, last + 1, -inc if inc < 0 else inc)
|
|
394
455
|
else:
|
|
395
|
-
|
|
456
|
+
count = math.ceil(abs(((first + 1) - last) / inc))
|
|
396
457
|
r = range(first, last - 1, inc if inc < 0 else -inc)
|
|
397
458
|
|
|
398
|
-
return self.format_values(r, padding)
|
|
459
|
+
return self.format_values(r, padding), count
|
|
399
460
|
|
|
400
|
-
def get_char_range(self, start: str, end: str, increment: str | None = None) -> Iterator[str]:
|
|
461
|
+
def get_char_range(self, start: str, end: str, increment: str | None = None) -> tuple[Iterator[str], int]:
|
|
401
462
|
"""Get a range of alphabetic characters."""
|
|
402
463
|
|
|
403
464
|
inc = int(increment) if increment else 1
|
|
@@ -416,35 +477,34 @@ class ExpandBrace:
|
|
|
416
477
|
last = alpha.index(end)
|
|
417
478
|
|
|
418
479
|
if first < last:
|
|
419
|
-
|
|
420
|
-
return itertools.islice(alpha, first, last + 1, inc)
|
|
480
|
+
count = math.ceil(((last + 1) - first) / inc)
|
|
481
|
+
return itertools.islice(alpha, first, last + 1, inc), count
|
|
482
|
+
count = math.ceil(((first + 1) - last) / inc)
|
|
483
|
+
return itertools.islice(alpha, last, first + 1, inc), count
|
|
421
484
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
485
|
+
def expand_str(self, string: str) -> Iterator[str]:
|
|
486
|
+
"""Expand the string."""
|
|
487
|
+
|
|
488
|
+
i = StringIter(string)
|
|
489
|
+
values, _ = self.get_literals(next(i), i, 0)
|
|
490
|
+
|
|
491
|
+
# Squash the nested list by calculating the combinations
|
|
492
|
+
results = iter([EMPTY]) # type: Iterator[str]
|
|
493
|
+
for v in values:
|
|
494
|
+
results = self.squash(results, [v] if isinstance(v, str) else v)
|
|
495
|
+
return results
|
|
425
496
|
|
|
426
497
|
def expand(self, string: str) -> Iterator[str]:
|
|
427
498
|
"""Expand."""
|
|
428
499
|
|
|
429
500
|
self.expanding = False
|
|
430
|
-
empties = []
|
|
431
501
|
found_literal = False
|
|
432
502
|
if string:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
empties.append(x)
|
|
441
|
-
continue
|
|
442
|
-
found_literal = True
|
|
443
|
-
while empties:
|
|
444
|
-
yield empties.pop(0)
|
|
445
|
-
yield x
|
|
446
|
-
empties = []
|
|
447
|
-
|
|
448
|
-
# We found no literals so return an empty string
|
|
449
|
-
if not found_literal:
|
|
503
|
+
for x in self.expand_str(string):
|
|
504
|
+
if x is EMPTY:
|
|
505
|
+
continue
|
|
506
|
+
found_literal = True
|
|
507
|
+
yield x
|
|
508
|
+
|
|
509
|
+
if not found_literal and self.return_empty:
|
|
450
510
|
yield ""
|
|
@@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
|
|
|
193
193
|
return Version(major, minor, micro, release, pre, post, dev)
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
__version_info__ = Version(
|
|
196
|
+
__version_info__ = Version(3, 0, 0, "final")
|
|
197
197
|
__version__ = __version_info__._get_canonical()
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 3.0
|
|
4
|
+
|
|
5
|
+
- **BREAK**: If there are no expansions, `bracex` will not return an empty string by default in the list. This matches
|
|
6
|
+
Bash. To get the old behavior, enable the new `return_empty` parameter to always return at least an empty string if
|
|
7
|
+
expansion yields no expansions.
|
|
8
|
+
- **FIX**: To better match Bash, empty expansions are not returned as empty strings.
|
|
9
|
+
- **FIX**: No longer backtrack if a sequence is evaluated and is deemed to be invalid.
|
|
10
|
+
|
|
11
|
+
## 2.7
|
|
12
|
+
|
|
13
|
+
- **NEW**: Drop support for Python 3.9.
|
|
14
|
+
- **FIX**: Fix issue where the parsing of literal strings was slow.
|
|
15
|
+
- **FIX**: Fix issue related to tracking the count of expansions when a set was specified with an empty slot.
|
|
16
|
+
- **FIX**: Fix issue related to chaining expansions.
|
|
17
|
+
|
|
3
18
|
## 2.6
|
|
4
19
|
|
|
5
20
|
- **NEW**: Drop support for Python 3.8.
|
|
@@ -19,7 +19,7 @@ Directory | Description
|
|
|
19
19
|
--------------------- | -----------
|
|
20
20
|
`docs/src/dictionary` | Contains the spell check wordlist(s) for the project.
|
|
21
21
|
`docs/src/markdown` | Contains the content for the documentation.
|
|
22
|
-
`
|
|
22
|
+
`bracex` | Contains the source code for the project.
|
|
23
23
|
`requirements` | Contains files with lists of dependencies that are required for the project, and required for continuous integration.
|
|
24
24
|
`tests` | Contains unit test files.
|
|
25
25
|
|
|
@@ -37,8 +37,7 @@ Documents are in Markdown (with some additional syntax provided by extensions) a
|
|
|
37
37
|
Markdown. If you would like to build and preview the documentation, you must have these packages installed:
|
|
38
38
|
|
|
39
39
|
- @Python-Markdown/markdown: the Markdown parser.
|
|
40
|
-
- @
|
|
41
|
-
- @squidfunk/mkdocs-material: a material theme for MkDocs.
|
|
40
|
+
- @zensical/zensical: the document site generator.
|
|
42
41
|
- @facelessuser/pymdown-extensions: this Python Markdown extension bundle.
|
|
43
42
|
|
|
44
43
|
In order to build and preview the documents, just run the command below from the root of the project and you should be
|
|
@@ -46,7 +45,7 @@ able to view the documents at `localhost:8000` in your browser. After that, you
|
|
|
46
45
|
and have your browser preview update live.
|
|
47
46
|
|
|
48
47
|
```
|
|
49
|
-
|
|
48
|
+
zensical serve -f zensical.yml
|
|
50
49
|
```
|
|
51
50
|
|
|
52
51
|
## Spell Checking Documents
|
|
@@ -65,7 +64,7 @@ run the following command from the root of the project.
|
|
|
65
64
|
You will need to make sure the documents are built first:
|
|
66
65
|
|
|
67
66
|
```
|
|
68
|
-
|
|
67
|
+
zensical build --clean -f zensical.yml
|
|
69
68
|
```
|
|
70
69
|
|
|
71
70
|
And then run the spell checker.
|