unstd 2.0.0__tar.gz → 2.1.1__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.
- {unstd-2.0.0 → unstd-2.1.1}/.github/workflows/ci.yml +12 -8
- {unstd-2.0.0 → unstd-2.1.1}/.github/workflows/release.yml +6 -1
- {unstd-2.0.0 → unstd-2.1.1}/CHANGELOG.md +49 -0
- {unstd-2.0.0 → unstd-2.1.1}/PKG-INFO +2 -2
- unstd-2.1.1/changelog.d/README.md +14 -0
- {unstd-2.0.0 → unstd-2.1.1}/pyproject.toml +11 -2
- unstd-2.1.1/src/unstd/rex/README.md +86 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/rex/__init__.py +76 -113
- unstd-2.1.1/src/unstd/rex/_faithful.py +304 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/serde/structs.py +7 -1
- unstd-2.1.1/tests/rex/test_fidelity.py +330 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/serde/test_structs.py +54 -0
- {unstd-2.0.0 → unstd-2.1.1}/uv.lock +602 -549
- unstd-2.0.0/src/unstd/rex/README.md +0 -132
- {unstd-2.0.0 → unstd-2.1.1}/.gitignore +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/LICENSE +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/NOTICE +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/bench/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/bench/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/bench/__main__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/bench/baseline.json +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/bench/cases.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/bench/report.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/bench/timing.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/_extra.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/audio/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/audio/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/audio/wavx.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/clone/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/clone/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/clone/structural.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/containers/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/containers/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/containers/persistent.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/containers/sorted.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/crypto/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/crypto/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/crypto/_blake3.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/crypto/digest.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/crypto/tls.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/crypto/token.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/fs/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/fs/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/ids/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/ids/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/ids/uid.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/iters/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/iters/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/iters/consecutive.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/iters/distinct.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/iters/reshape.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/iters/select.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/model/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/model/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/model/base.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/model/settings.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/pack/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/pack/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/pack/binpack.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/proc/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/proc/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/py.typed +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/rand/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/rand/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/rand/crypto.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/rand/draw.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/serde/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/serde/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/serde/b64.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/serde/jsonx.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/serde/ndjson.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/text/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/text/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/text/fuzz.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/time/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/time/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/time/timeutil.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/time/zoned.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/toml/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/src/unstd/toml/__init__.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/audio/test_audio.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/clone/test_clone.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/conftest.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/containers/test_containers.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/crypto/blake3_reference_vectors.json +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/crypto/test_adverse.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/crypto/test_vectors.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/fs/test_fs.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/ids/test_uid.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/iters/test_iters.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/model/test_model.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/pack/test_pack.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/proc/test_proc.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/properties/README.md +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/properties/test_clone_invariants.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/properties/test_pack_invariants.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/properties/test_serde_parity.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/properties/test_toml_invariants.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/properties/test_uuid7.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/properties/test_wav_quant.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/rand/test_rand.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/rex/test_rex.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/serde/test_b64.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/serde/test_jsonx.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/serde/test_ndjson.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/test_bench.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/test_package.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/text/test_text.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/time/test_timeutil.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/time/test_zoned.py +0 -0
- {unstd-2.0.0 → unstd-2.1.1}/tests/toml/test_toml.py +0 -0
|
@@ -14,7 +14,7 @@ permissions:
|
|
|
14
14
|
contents: read
|
|
15
15
|
|
|
16
16
|
concurrency:
|
|
17
|
-
group: ${{ github.workflow }}-${{ github.ref }}
|
|
17
|
+
group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}
|
|
18
18
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
19
19
|
|
|
20
20
|
jobs:
|
|
@@ -29,11 +29,12 @@ jobs:
|
|
|
29
29
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
30
30
|
with:
|
|
31
31
|
persist-credentials: false
|
|
32
|
-
- uses: astral-sh/setup-uv@
|
|
32
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
33
33
|
with:
|
|
34
34
|
enable-cache: false
|
|
35
|
+
version: "latest-known"
|
|
35
36
|
python-version: "3.13"
|
|
36
|
-
- run: uv sync --all-extras --group dev --python 3.13
|
|
37
|
+
- run: uv sync --locked --all-extras --group dev --python 3.13
|
|
37
38
|
- run: uv run --python 3.13 ruff check .
|
|
38
39
|
- run: uv run --python 3.13 ruff format --check .
|
|
39
40
|
- run: uv run --python 3.13 mypy
|
|
@@ -52,11 +53,12 @@ jobs:
|
|
|
52
53
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
53
54
|
with:
|
|
54
55
|
persist-credentials: false
|
|
55
|
-
- uses: astral-sh/setup-uv@
|
|
56
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
56
57
|
with:
|
|
57
58
|
enable-cache: false
|
|
59
|
+
version: "latest-known"
|
|
58
60
|
python-version: ${{ matrix.python-version }}
|
|
59
|
-
- run: uv sync --all-extras --group dev --python ${{ matrix.python-version }}
|
|
61
|
+
- run: uv sync --locked --all-extras --group dev --python ${{ matrix.python-version }}
|
|
60
62
|
- run: uv run --python ${{ matrix.python-version }} pytest -q
|
|
61
63
|
|
|
62
64
|
floors:
|
|
@@ -75,9 +77,10 @@ jobs:
|
|
|
75
77
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
76
78
|
with:
|
|
77
79
|
persist-credentials: false
|
|
78
|
-
- uses: astral-sh/setup-uv@
|
|
80
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
79
81
|
with:
|
|
80
82
|
enable-cache: false
|
|
83
|
+
version: "latest-known"
|
|
81
84
|
python-version: "3.13"
|
|
82
85
|
- run: uv sync --all-extras --group dev --python 3.13 --resolution lowest-direct
|
|
83
86
|
# Which versions the floors admitted — the first thing anyone reading a
|
|
@@ -105,11 +108,12 @@ jobs:
|
|
|
105
108
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
106
109
|
with:
|
|
107
110
|
persist-credentials: false
|
|
108
|
-
- uses: astral-sh/setup-uv@
|
|
111
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
109
112
|
with:
|
|
110
113
|
enable-cache: false
|
|
114
|
+
version: "latest-known"
|
|
111
115
|
python-version: "3.13"
|
|
112
|
-
- run: uv sync --group dev --python 3.13
|
|
116
|
+
- run: uv sync --locked --group dev --python 3.13
|
|
113
117
|
# A diagnostic, not a gate — the zero-closure claim is asserted offline by
|
|
114
118
|
# `tests/test_package.py`. This is here because a failure that reproduces
|
|
115
119
|
# only on the fallback leg is nearly unreadable without knowing which
|
|
@@ -36,8 +36,13 @@ jobs:
|
|
|
36
36
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
37
37
|
with:
|
|
38
38
|
persist-credentials: false
|
|
39
|
-
- uses: astral-sh/setup-uv@
|
|
39
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
40
40
|
with:
|
|
41
41
|
enable-cache: false
|
|
42
|
+
version: "latest-known"
|
|
43
|
+
- name: The tag agrees with package metadata
|
|
44
|
+
env:
|
|
45
|
+
TAG: ${{ github.ref_name }}
|
|
46
|
+
run: test "$TAG" = "v$(uv version --short)"
|
|
42
47
|
- run: uv build
|
|
43
48
|
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -1,7 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc_radar:
|
|
3
|
+
sentinels:
|
|
4
|
+
- file: src/unstd/rex/_faithful.py
|
|
5
|
+
contains: ['_parser.parse(pattern, flags)', 'if started:']
|
|
6
|
+
- file: pyproject.toml
|
|
7
|
+
contains: ['"msgspec>=0.22"', 'exclude-newer = "P2D"']
|
|
8
|
+
- file: .github/workflows/ci.yml
|
|
9
|
+
contains: ['github.sha || github.ref', 'version: "latest-known"', 'uv sync --locked']
|
|
10
|
+
- file: .github/workflows/release.yml
|
|
11
|
+
contains: ['TAG: ${{ github.ref_name }}', 'uv version --short']
|
|
12
|
+
- file: src/unstd/serde/structs.py
|
|
13
|
+
contains: ['Callable[[Decimal], object]', 'decimal_format: _DecimalFormat']
|
|
14
|
+
---
|
|
15
|
+
|
|
1
16
|
# Changelog
|
|
2
17
|
|
|
3
18
|
<!-- towncrier release notes start -->
|
|
4
19
|
|
|
20
|
+
## [2.1.1] - 2026-10-04
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
We keep Python's Unicode classification, scoped flags, empty-match iteration and
|
|
25
|
+
nullable capture behavior across native engine upgrades. Eligible ASCII and byte
|
|
26
|
+
work still run natively; surrogate recovery and typed native refusals preserve
|
|
27
|
+
outputs without retrying replacement callbacks.
|
|
28
|
+
|
|
29
|
+
## [2.1.0] - 2026-10-04
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `Codec(decimal_format=...)` accepts a callable over each `Decimal`, so exact
|
|
34
|
+
decimal policies stay exact instead of passing through a float. Callback
|
|
35
|
+
errors propagate, and the same encoder remains usable afterward. This uses
|
|
36
|
+
[msgspec 0.22's encoder support](https://github.com/msgspec/msgspec/releases/tag/0.22.0).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- We refreshed the optional-backend and development lock, including uuid-utils
|
|
41
|
+
1.0 and msgspec 0.22. Existing UUID generation and codec defaults keep their
|
|
42
|
+
behavior; the serde extra's msgspec floor moves to 0.22.
|
|
43
|
+
- We quarantine new resolver and isolated-build releases for two days. CI uses
|
|
44
|
+
the reviewed lock and checksum-backed uv versions from the pinned
|
|
45
|
+
[setup-uv 10.2 action](https://github.com/astral-sh/setup-uv/releases/tag/v10.2.0).
|
|
46
|
+
The separate floor job still proves the oldest supported direct dependencies.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- We keep each main commit's CI run in its own concurrency group. Newer pushes
|
|
51
|
+
cannot replace pending checks for a release commit, and the release tag must
|
|
52
|
+
agree with the package version before publication.
|
|
53
|
+
|
|
5
54
|
## [2.0.0] - 2026-09-27
|
|
6
55
|
|
|
7
56
|
### Breaking
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unstd
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: stdlib-faithful accelerated replacements behind optional extras
|
|
5
5
|
Project-URL: Homepage, https://github.com/The-Billy-Company/unstd
|
|
6
6
|
Project-URL: Repository, https://github.com/The-Billy-Company/unstd
|
|
@@ -43,7 +43,7 @@ Requires-Dist: numpy>=2.5; extra == 'rand'
|
|
|
43
43
|
Provides-Extra: rex
|
|
44
44
|
Requires-Dist: irregex>=2.6.0; extra == 'rex'
|
|
45
45
|
Provides-Extra: serde
|
|
46
|
-
Requires-Dist: msgspec>=0.
|
|
46
|
+
Requires-Dist: msgspec>=0.22; extra == 'serde'
|
|
47
47
|
Requires-Dist: orjson>=3.12; extra == 'serde'
|
|
48
48
|
Requires-Dist: pybase64>=1.5; extra == 'serde'
|
|
49
49
|
Provides-Extra: text
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc_radar:
|
|
3
|
+
sentinels:
|
|
4
|
+
- file: CHANGELOG.md
|
|
5
|
+
contains: ['## [2.1.1]', 'Codec(decimal_format=...)']
|
|
6
|
+
- file: pyproject.toml
|
|
7
|
+
contains: ['exclude-newer = "P2D"']
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Unreleased changes
|
|
11
|
+
|
|
12
|
+
We keep one fragment per change here; release history stays in the root changelog.
|
|
13
|
+
Fragments describe what changed for a caller. On release, we fold their bodies
|
|
14
|
+
into the root changelog and move their source assertions with them.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unstd"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.1"
|
|
8
8
|
description = "stdlib-faithful accelerated replacements behind optional extras"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -45,7 +45,7 @@ dependencies = []
|
|
|
45
45
|
[project.optional-dependencies]
|
|
46
46
|
serde = [
|
|
47
47
|
"orjson>=3.12",
|
|
48
|
-
"msgspec>=0.
|
|
48
|
+
"msgspec>=0.22",
|
|
49
49
|
"pybase64>=1.5",
|
|
50
50
|
]
|
|
51
51
|
ids = [
|
|
@@ -114,6 +114,15 @@ dev = [
|
|
|
114
114
|
"cryptography>=44.0",
|
|
115
115
|
]
|
|
116
116
|
|
|
117
|
+
[tool.uv]
|
|
118
|
+
# Keep development, optional backends and isolated builds behind the same
|
|
119
|
+
# two-day release quarantine as the applications that consume this package.
|
|
120
|
+
exclude-newer = "P2D"
|
|
121
|
+
|
|
122
|
+
[tool.uv.exclude-newer-package]
|
|
123
|
+
# Our engine is released by the same verified publisher; upstreams keep P2D.
|
|
124
|
+
irregex = false
|
|
125
|
+
|
|
117
126
|
[tool.pytest.ini_options]
|
|
118
127
|
testpaths = ["tests"]
|
|
119
128
|
# The repo root, so `tests/test_bench.py` can import the `bench` harness. `bench`
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc_radar:
|
|
3
|
+
sentinels:
|
|
4
|
+
- file: src/unstd/rex/_faithful.py
|
|
5
|
+
contains: ['_parser.parse(pattern, flags)', 'parsed.getwidth()[0] == 0', 'started = True', 'if started:']
|
|
6
|
+
- file: src/unstd/rex/__init__.py
|
|
7
|
+
contains: ['python = _re.compile(pattern, flags)', 'profile, _irgx.error', '_MAXCACHE = 512']
|
|
8
|
+
- file: pyproject.toml
|
|
9
|
+
contains: ['"irregex>=2.6.0"']
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# `unstd.rex`
|
|
13
|
+
|
|
14
|
+
We put our [irregex](https://github.com/The-Billy-Company/irregex) engine behind
|
|
15
|
+
Python's `re` surface. Install `pip install 'unstd[rex]'`, then swap the import:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from unstd import rex
|
|
19
|
+
|
|
20
|
+
rx = rex.compile(r"(?a:\w+)")
|
|
21
|
+
assert rx.findall("café 123") == ["caf", "123"]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`compile`, `search`, `match`, `fullmatch`, `findall`, `finditer`, `sub`, `subn`,
|
|
25
|
+
`split`, `escape`, `purge`, flags and compile errors keep their familiar shape.
|
|
26
|
+
A base installation has zero dependencies and uses stdlib `re`.
|
|
27
|
+
|
|
28
|
+
## Python owns the meaning
|
|
29
|
+
|
|
30
|
+
We validate with Python's compiler and inspect its parsed operations, including
|
|
31
|
+
scoped ASCII, Unicode and case flags. The native engine keeps supported regular
|
|
32
|
+
patterns, bytes, faithful ASCII classes and verbose mode; the compiled facade
|
|
33
|
+
retains your source and Python flag metadata.
|
|
34
|
+
|
|
35
|
+
Unicode word/digit classes and boundaries run natively on ASCII subjects and
|
|
36
|
+
use Python on non-ASCII subjects. Unicode whitespace and cased Unicode
|
|
37
|
+
`IGNORECASE` patterns use Python even on ASCII input; their semantics differ
|
|
38
|
+
between the engines, and we use the running interpreter's Unicode version.
|
|
39
|
+
|
|
40
|
+
Empty-match patterns and nullable repeats use Python's iteration and capture
|
|
41
|
+
rules. Backreferences, lookaround, unsupported operations and `LOCALE`/`DEBUG`
|
|
42
|
+
also fall back, as do native operations that decline a requested match shape.
|
|
43
|
+
|
|
44
|
+
## Safety follows execution
|
|
45
|
+
|
|
46
|
+
Native matching uses finite automata, so hostile patterns cannot trigger
|
|
47
|
+
catastrophic backtracking there. `rex.search(r"(a+)+$", "a" * 40 + "!")` keeps
|
|
48
|
+
that native safety win; the tests prove the same call hangs on stdlib `re` in a
|
|
49
|
+
separate process.
|
|
50
|
+
|
|
51
|
+
Every fallback uses Python's backtracking engine and loses that guarantee.
|
|
52
|
+
Installing the extra alone is insufficient: use supported regular patterns and
|
|
53
|
+
bytes or faithful ASCII expressions over encodable input when linear matching
|
|
54
|
+
is required.
|
|
55
|
+
|
|
56
|
+
## Recovery keeps side effects intact
|
|
57
|
+
|
|
58
|
+
Python strings can contain lone surrogates that UTF-8 cannot encode. We recover
|
|
59
|
+
on Python before producing a match or starting a replacement callback; a
|
|
60
|
+
callback's own exception propagates unchanged, and callbacks are never retried.
|
|
61
|
+
|
|
62
|
+
`findall` converts absent groups to empty strings for compiled and module calls;
|
|
63
|
+
`groups()` still reports `None`. Negative substitution or split limits leave the
|
|
64
|
+
input unchanged, just as Python does.
|
|
65
|
+
|
|
66
|
+
## Existing boundaries
|
|
67
|
+
|
|
68
|
+
On the native path, `$` means absolute end rather than the position before a
|
|
69
|
+
final newline. Use `(?m)…$` for line ends or `…\Z` for absolute end; POSIX bracket
|
|
70
|
+
expressions also retain their native class meaning instead of Python's
|
|
71
|
+
nested-set spelling.
|
|
72
|
+
|
|
73
|
+
Native matches provide `group`, `groups`, `groupdict`, `span`, `expand`, `start`
|
|
74
|
+
and `end`; `.pos` and `.endpos` remain stdlib-only. A callable replacement sees
|
|
75
|
+
the active backend's match, whose `.re.flags` uses native bookkeeping.
|
|
76
|
+
|
|
77
|
+
`error` and `escape` are stdlib's own objects, native-only syntax is refused by
|
|
78
|
+
Python, and compiled patterns round-trip through every module verb. Compiles and
|
|
79
|
+
declines share a bounded cache of 512 entries; `purge` clears it and both engines.
|
|
80
|
+
|
|
81
|
+
## Where it comes from
|
|
82
|
+
|
|
83
|
+
We replaced RE2 once irregex supplied the existing API, retiring that dependency.
|
|
84
|
+
The safety primitive is [Thompson's automaton construction, explained by Russ
|
|
85
|
+
Cox](https://swtch.com/~rsc/regexp/regexp1.html); the language contract comes from
|
|
86
|
+
[Python's regex documentation](https://docs.python.org/3/library/re.html).
|
|
@@ -1,102 +1,63 @@
|
|
|
1
|
-
r"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
- Compiles are cached like stdlib's own ``re._cache`` (bounded, oldest out), so
|
|
62
|
-
a module-level call is one dict lookup — including for a pattern irgx
|
|
63
|
-
*declined*, which irgx itself does not remember and would re-attempt at the
|
|
64
|
-
cost of a full compile plus a raise.
|
|
65
|
-
- ``findall``'s non-participating groups are rewritten from irgx's ``None`` to
|
|
66
|
-
stdlib's ``""`` (``.groups()`` already reports ``None`` in both libraries, so
|
|
67
|
-
only this one projection needed adapting).
|
|
68
|
-
|
|
69
|
-
Divergences (deliberate, documented, pinned as tests):
|
|
70
|
-
|
|
71
|
-
- **PCRE-only patterns degrade to stdlib** (backref / lookaround) — losing the
|
|
72
|
-
linear-time guarantee (above). ``compile`` returns a stdlib ``re.Pattern`` for
|
|
73
|
-
these even when the backend is present.
|
|
74
|
-
- **``$`` is the end of the text, not the byte before a trailing newline.**
|
|
75
|
-
Stdlib inherits Perl's rule where ``$`` also matches just before a final
|
|
76
|
-
``\\n``; irgx follows Rust's ``regex`` and Go's ``regexp`` in reading it as the
|
|
77
|
-
absolute end. So ``rex.findall(r"[a-z]+$", "cat\\ndog\\n")`` finds nothing
|
|
78
|
-
where stdlib finds ``dog``. Three spellings agree, and one of them is usually
|
|
79
|
-
what was meant: ``(?m)[a-z]+$`` (every line's end), ``[a-z]+\\Z`` (the text's
|
|
80
|
-
end, said exactly), or strip the newline first. **This divergence predates the
|
|
81
|
-
irgx backend** — RE2 read ``$`` the same way — it was simply never written down.
|
|
82
|
-
- **POSIX bracket expressions mean different things.** ``[[:alpha:]]`` is a real
|
|
83
|
-
character class to irgx and a nested-set typo to stdlib (which reads it as
|
|
84
|
-
``[`` ``:`` ``a`` ``l`` ``p`` ``h`` and warns). Also inherited from the RE2
|
|
85
|
-
era, and the reason to keep patterns inside the shared grammar.
|
|
86
|
-
- **On the irgx path a compiled pattern's ``.flags`` reflects irgx's own
|
|
87
|
-
bookkeeping** rather than the stdlib bitmask. Match *behavior* is identical;
|
|
88
|
-
only that cosmetic attribute differs from what stdlib would report.
|
|
89
|
-
- **``re.UNICODE`` is a no-op on the irgx path** (irgx is UTF-8-native for
|
|
90
|
-
``str`` input). It is *not* mapped to inline ``(?U)`` — there ``(?U)`` swaps
|
|
91
|
-
greediness, a different meaning entirely.
|
|
92
|
-
|
|
93
|
-
Prior art: the linear-time automaton construction is Thompson's, popularized for
|
|
94
|
-
this use by Russ Cox, "Regular Expression Matching Can Be Simple And Fast"
|
|
95
|
-
(2007, https://swtch.com/~rsc/regexp/regexp1.html). ``rex`` rode Google's RE2
|
|
96
|
-
implementation of it until the engine we publish ourselves — irregex
|
|
97
|
-
(https://github.com/The-Billy-Company/irregex) — grew the ``re``-shaped surface
|
|
98
|
-
this shim needs, at which point paying a third-party dependency for a capability
|
|
99
|
-
we ship stopped making sense.
|
|
1
|
+
r"""Linear regex where Python's semantics survive the native boundary.
|
|
2
|
+
|
|
3
|
+
Swap ``import re`` for ``from unstd import rex`` to keep the module verbs,
|
|
4
|
+
flags, compile errors and ordinary match interface. The optional ``rex`` extra
|
|
5
|
+
provides ``irregex`` (import ``irgx``); a base installation uses stdlib ``re``.
|
|
6
|
+
|
|
7
|
+
The compiler validates with Python first, then reads its parsed operations,
|
|
8
|
+
including scoped flags. Native execution stays available for supported regular
|
|
9
|
+
patterns, bytes patterns, ASCII classes and verbose mode. Unicode word/digit
|
|
10
|
+
classes and boundaries use native execution on ASCII subjects; non-ASCII input
|
|
11
|
+
uses the running interpreter's Unicode rules. Unicode whitespace and cased
|
|
12
|
+
Unicode IGNORECASE expressions use Python, because their native semantics differ
|
|
13
|
+
even on some ASCII input. This is version-independent: no Unicode table is
|
|
14
|
+
pinned to one Python release.
|
|
15
|
+
|
|
16
|
+
Patterns that can match empty text or repeat a nullable body use Python's
|
|
17
|
+
adjacent-match and capture rules. Backreferences, lookaround, unsupported parsed
|
|
18
|
+
operations and the LOCALE/DEBUG flags also fall back. A native operation that
|
|
19
|
+
cannot express a requested match shape uses its typed refusal to recover; a
|
|
20
|
+
lone-surrogate pattern, subject or replacement uses Python because UTF-8 cannot
|
|
21
|
+
encode it. Recovery occurs before a replacement callback starts; callback
|
|
22
|
+
exceptions propagate unchanged and callbacks are never retried.
|
|
23
|
+
|
|
24
|
+
**Only an operation that actually runs on irgx has the linear-time, ReDoS-immune
|
|
25
|
+
guarantee.** Every fallback uses Python's backtracking engine. Installing the
|
|
26
|
+
extra alone does not establish that guarantee, and native eligibility can depend
|
|
27
|
+
on the subject. Keep untrusted patterns inside the supported regular grammar
|
|
28
|
+
and use bytes or faithful ASCII expressions over encodable input when the
|
|
29
|
+
linear guarantee is required. ``(a+)+$`` retains its native safety win; Python
|
|
30
|
+
hangs on its failing tail, so the adverse test checks Python in a subprocess.
|
|
31
|
+
|
|
32
|
+
Compiled objects retain the caller's source and Python flag metadata, accept
|
|
33
|
+
region bounds and round-trip through the module verbs. Native match objects
|
|
34
|
+
supply ``group``/``groups``/``groupdict``/``span``/``expand``/``start``/``end``;
|
|
35
|
+
``.pos`` and ``.endpos`` remain stdlib-only. Callable replacements receive the
|
|
36
|
+
active backend's match. ``findall`` projects an absent group as ``""`` for both
|
|
37
|
+
compiled and module calls; ``groups()`` still reports ``None``. Negative
|
|
38
|
+
replacement/split limits follow Python's unchanged-input rule.
|
|
39
|
+
|
|
40
|
+
``error`` and ``escape`` are stdlib's own objects. Malformed/native-only syntax
|
|
41
|
+
raises Python's error, so ``except rex.error`` remains authoritative. A buffer
|
|
42
|
+
pattern is refused with ``TypeError`` as stdlib refuses it; compiled patterns
|
|
43
|
+
from either backend are accepted, but adding flags to one raises ``ValueError``.
|
|
44
|
+
Compiles, including declines, are cached at 512 entries, oldest out. Without
|
|
45
|
+
that front cache a repeated backreference would pay a native compile and refusal
|
|
46
|
+
every time. ``purge`` clears our cache and both engines' caches. UNICODE never
|
|
47
|
+
becomes inline ``(?U)``: that native spelling changes greediness.
|
|
48
|
+
|
|
49
|
+
Two inherited syntax differences remain deliberate on the native path:
|
|
50
|
+
``$`` means absolute end rather than also the position before a final newline
|
|
51
|
+
(use ``(?m)…$`` for line ends or ``…\Z`` for absolute end), and POSIX bracket
|
|
52
|
+
expressions are real native classes rather than Python's nested-set spelling.
|
|
53
|
+
Keep patterns inside the shared grammar for Python parity. Native match
|
|
54
|
+
``.re.flags`` also reflects native bookkeeping; the facade's ``.flags`` is
|
|
55
|
+
Python's bitmask.
|
|
56
|
+
|
|
57
|
+
Prior art: Thompson's finite automata, explained by Russ Cox at
|
|
58
|
+
https://swtch.com/~rsc/regexp/regexp1.html, and the engine we publish at
|
|
59
|
+
https://github.com/The-Billy-Company/irregex. The previous backend was RE2;
|
|
60
|
+
irregex supplied the existing re-shaped API without that extra dependency.
|
|
100
61
|
"""
|
|
101
62
|
|
|
102
63
|
from __future__ import annotations
|
|
@@ -128,6 +89,8 @@ from re import ( # faithful re-export of the flag + type vocabulary
|
|
|
128
89
|
)
|
|
129
90
|
from typing import TYPE_CHECKING, Any, Final
|
|
130
91
|
|
|
92
|
+
from ._faithful import _LinearPattern, native_profile
|
|
93
|
+
|
|
131
94
|
|
|
132
95
|
if TYPE_CHECKING:
|
|
133
96
|
from collections.abc import Callable, Iterator, Sequence
|
|
@@ -139,7 +102,7 @@ if TYPE_CHECKING:
|
|
|
139
102
|
# irgx's compiled/match types (they deliberately mirror `re.Pattern` /
|
|
140
103
|
# `re.Match`) — named here so the public unions below stay honest without
|
|
141
104
|
# pretending the irgx objects nominally *are* the stdlib classes.
|
|
142
|
-
type _IrgxPattern = _irgx.Pattern
|
|
105
|
+
type _IrgxPattern = _irgx.Pattern | _LinearPattern[Any]
|
|
143
106
|
type _IrgxMatch = _irgx.Match
|
|
144
107
|
# What every module-level verb accepts as `pattern`: stdlib's own
|
|
145
108
|
# `str | Pattern` union, widened by the compiled object this module returns.
|
|
@@ -210,18 +173,12 @@ _cache: dict[tuple[type, str | bytes, int], Pattern[Any] | _IrgxPattern] = {}
|
|
|
210
173
|
|
|
211
174
|
|
|
212
175
|
def _irgx_pattern(pattern: str | bytes, flags: int) -> _IrgxPattern | None:
|
|
213
|
-
"""Compile
|
|
214
|
-
|
|
215
|
-
``None`` (→ stdlib ``re`` fallback) when the backend is absent, `flags`
|
|
216
|
-
carries a bit irgx can't honor, or irgx refuses the pattern — declining it
|
|
217
|
-
as outside the linear grammar (backreferences / lookaround) or rejecting it
|
|
218
|
-
as malformed. Both refusals route to stdlib, which re-raises the malformed
|
|
219
|
-
case as ``re.error``.
|
|
220
|
-
"""
|
|
221
|
-
if not _HAVE_IRGX or flags & ~_HONORED:
|
|
176
|
+
"""Compile with Python validation and a parsed native fidelity profile."""
|
|
177
|
+
if not _HAVE_IRGX or type(pattern) not in (str, bytes) or flags & ~_HONORED:
|
|
222
178
|
return None
|
|
179
|
+
python = _re.compile(pattern, flags)
|
|
223
180
|
try:
|
|
224
|
-
|
|
181
|
+
native = _irgx.compile(
|
|
225
182
|
pattern,
|
|
226
183
|
ignore_case=bool(flags & IGNORECASE),
|
|
227
184
|
multiline=bool(flags & MULTILINE),
|
|
@@ -230,8 +187,14 @@ def _irgx_pattern(pattern: str | bytes, flags: int) -> _IrgxPattern | None:
|
|
|
230
187
|
# A `bytes` pattern is byte-oriented by definition; a `str` one only under ASCII.
|
|
231
188
|
unicode=isinstance(pattern, str) and not flags & ASCII,
|
|
232
189
|
)
|
|
233
|
-
except _irgx.error:
|
|
190
|
+
except (_irgx.error, UnicodeEncodeError):
|
|
234
191
|
return None
|
|
192
|
+
profile = native_profile(pattern, flags)
|
|
193
|
+
return (
|
|
194
|
+
_LinearPattern(native, python, profile, _irgx.error)
|
|
195
|
+
if profile is not None
|
|
196
|
+
else None
|
|
197
|
+
)
|
|
235
198
|
|
|
236
199
|
|
|
237
200
|
# The str/bytes type parameter below is spelled `AnyStr` (typeshed's name for this
|
|
@@ -287,7 +250,7 @@ def compile[AnyStr: (str, bytes)]( # shadows the builtin on purpose: `re.compil
|
|
|
287
250
|
del _cache[next(iter(_cache))]
|
|
288
251
|
_cache[key] = rx
|
|
289
252
|
return rx
|
|
290
|
-
if isinstance(pattern, _re.Pattern) or (
|
|
253
|
+
if isinstance(pattern, _re.Pattern | _LinearPattern) or (
|
|
291
254
|
_HAVE_IRGX and isinstance(pattern, _irgx.Pattern)
|
|
292
255
|
):
|
|
293
256
|
if flags:
|
|
@@ -331,7 +294,7 @@ def findall[AnyStr: (str, bytes)](
|
|
|
331
294
|
spells the same fact ``list[Any]``; this keeps the element types.)
|
|
332
295
|
"""
|
|
333
296
|
rx = compile(pattern, flags)
|
|
334
|
-
if isinstance(rx, _re.Pattern):
|
|
297
|
+
if isinstance(rx, _re.Pattern | _LinearPattern):
|
|
335
298
|
found: list[AnyStr | tuple[AnyStr, ...]] = rx.findall(string)
|
|
336
299
|
return found # stdlib already projects an absent group as ``""``
|
|
337
300
|
rows = rx.findall(string)
|