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.
Files changed (37) hide show
  1. {bracex-2.6 → bracex-3.0}/.gitignore +3 -0
  2. {bracex-2.6 → bracex-3.0}/LICENSE.md +1 -1
  3. {bracex-2.6 → bracex-3.0}/PKG-INFO +2 -6
  4. {bracex-2.6 → bracex-3.0}/README.md +0 -3
  5. {bracex-2.6 → bracex-3.0}/bracex/__init__.py +181 -121
  6. {bracex-2.6 → bracex-3.0}/bracex/__meta__.py +1 -1
  7. {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/changelog.md +15 -0
  8. {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/development.md +4 -5
  9. {bracex-2.6 → bracex-3.0}/docs/src/markdown/index.md +20 -13
  10. bracex-3.0/docs/theme/announce.html +10 -0
  11. bracex-3.0/docs/theme/assets/pymdownx-extras/extra-95634471d6.css +2 -0
  12. bracex-3.0/docs/theme/assets/pymdownx-extras/extra-loader-Ccztcqfq.js +4 -0
  13. bracex-3.0/docs/theme/main.html +17 -0
  14. {bracex-2.6 → bracex-3.0}/hatch_build.py +0 -1
  15. {bracex-2.6 → bracex-3.0}/pyproject.toml +3 -3
  16. bracex-3.0/requirements/docs.txt +4 -0
  17. {bracex-2.6 → bracex-3.0}/tests/brace-cases.txt +7 -0
  18. {bracex-2.6 → bracex-3.0}/tests/brace-results.txt +9 -1
  19. {bracex-2.6 → bracex-3.0}/tests/generate.sh +1 -1
  20. {bracex-2.6 → bracex-3.0}/tests/test_brace.py +123 -2
  21. bracex-2.6/docs/theme/announce.html +0 -5
  22. bracex-2.6/mkdocs.yml +0 -157
  23. bracex-2.6/requirements/docs.txt +0 -4
  24. {bracex-2.6 → bracex-3.0}/.coveragerc +0 -0
  25. {bracex-2.6 → bracex-3.0}/.pyspelling.yml +0 -0
  26. {bracex-2.6 → bracex-3.0}/bracex/__main__.py +0 -0
  27. {bracex-2.6 → bracex-3.0}/bracex/py.typed +0 -0
  28. {bracex-2.6 → bracex-3.0}/docs/src/markdown/.snippets/abbr.md +0 -0
  29. {bracex-2.6 → bracex-3.0}/docs/src/markdown/.snippets/links.md +0 -0
  30. {bracex-2.6 → bracex-3.0}/docs/src/markdown/.snippets/refs.md +0 -0
  31. {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/contributing.md +0 -0
  32. {bracex-2.6 → bracex-3.0}/docs/src/markdown/about/license.md +0 -0
  33. {bracex-2.6 → bracex-3.0}/requirements/lint.txt +0 -0
  34. {bracex-2.6 → bracex-3.0}/requirements/test.txt +0 -0
  35. {bracex-2.6 → bracex-3.0}/tests/__init__.py +0 -0
  36. {bracex-2.6 → bracex-3.0}/tests/test_main.py +0 -0
  37. {bracex-2.6 → bracex-3.0}/tests/test_versions.py +0 -0
@@ -1,5 +1,8 @@
1
1
  .DS_Store
2
2
 
3
+ manifest-css.json
4
+ manifest-js.json
5
+
3
6
  # Byte-compiled / optimized / DLL files
4
7
  __pycache__/
5
8
  *.py[cod]
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 - 2025 Isaac Muse
3
+ Copyright (c) 2018 - 2026 Isaac Muse
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bracex
3
- Version: 2.6
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.9
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(string: AnyStr, keep_escapes: bool = False, limit: int = DEFAULT_LIMIT) -> list[AnyStr]:
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(string: AnyStr, keep_escapes: bool = False, limit: int = DEFAULT_LIMIT) -> Iterator[AnyStr]:
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__(self, keep_escapes: bool = False, limit: int = DEFAULT_LIMIT) -> None:
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 update_count_seq(self, count: list[int]) -> None:
139
- """Update the count from a list after evaluating a brace sequence and assert if count exceeds the max limit."""
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 += 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
- yield ''.join(x) if isinstance(x, tuple) else x
196
-
197
- def get_literals(self, c: str, i: StringIter, depth: int) -> Iterator[str] | None:
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 = iter([''])
240
+ result = [] # type: list[str | Iterator[str]]
206
241
  is_dollar = False
207
-
208
- count = True
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
- value = [self.get_escape(c, i)]
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
- current_count = self.count
228
- seq = self.get_sequence(next(i), i, depth + 1)
229
- if seq:
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
- # Searched to end of string
237
- # and still didn't find it.
238
- i.rewind(i.index - index)
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 in (',', '}'):
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
- # Squash the current set of literals.
251
- result = self.squash(result, value)
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
- if self.is_expanding():
256
- return None
290
+ # Ensure we store any remaining literals
291
+ if literal:
292
+ result.append(literal)
257
293
 
258
- if count:
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] | None:
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 = iter([]) # type: Iterator[str]
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 # and i.index not in self.skip_index
323
+ keep_looking = depth == 1 and not has_comma
291
324
  if (c == '}' and (not keep_looking or i.index == 2)):
292
- # If there is no comma, we know the sequence is bogus.
325
+ self.release_expanding(release)
326
+
327
+ # Handle empty slot
293
328
  if is_empty:
294
- result = itertools.chain(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
- result = (''.join(['{', literal, '}']) for literal in result)
297
- self.release_expanding(release)
298
- return result
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 = itertools.chain(result, [''])
366
+ result.append(EMPTY)
367
+ counts.append(1)
305
368
  else:
306
369
  is_empty = True
307
370
 
308
371
  else:
309
- if c == '}':
310
- # Top level: If we didn't find a comma, we haven't
311
- # completed the top level group. Request more and
312
- # append to what we already have for the first slot.
313
- if is_empty and not has_comma:
314
- result = itertools.chain(result, [c])
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 = self.squash(result, [c])
317
- value = self.get_literals(next(i), i, depth)
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
- def get_range(self, i: StringIter) -> Iterator[str] | None:
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
- self.update_count(math.ceil(abs(((last + 1) - first) / inc)))
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
- self.update_count(math.ceil(abs(((first + 1) - last) / inc)))
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
- self.update_count(math.ceil(((last + 1) - first) / inc))
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
- else:
423
- self.update_count(math.ceil(((first + 1) - last) / inc))
424
- return itertools.islice(alpha, last, first + 1, inc)
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
- i = StringIter(string)
434
- value = self.get_literals(next(i), i, 0)
435
- if value is not None:
436
- for x in value:
437
- # We don't want to return trailing empty strings.
438
- # Store empty strings and output only when followed by a literal.
439
- if not x:
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(2, 6, 0, "final")
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
- `soupsieve` | Contains the source code for the project.
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
- - @mkdocs/mkdocs: the document site generator.
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
- mkdocs serve
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
- mkdocs build --clean
67
+ zensical build --clean -f zensical.yml
69
68
  ```
70
69
 
71
70
  And then run the spell checker.