Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/python-perf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Python Performance Tests

on:
workflow_dispatch:
pull_request:
push:
branches: [ 'main', 'next' ]

env:
PYTHON_VERSION_DEFAULT: "3.14"

permissions:
contents: read

jobs:
perf-full:
name: Performance Full
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
# see https://github.com/actions/checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Python Environment
# see https://github.com/actions/setup-python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.PYTHON_VERSION_DEFAULT }}
architecture: 'x64'
- name: Install env dependencies
run: python -m pip install --upgrade pip
- name: Install project
run: pip install --user -e ".[testing]"
- name: Run full perf suite
env:
RFC3987_PERF_MODE: full
RFC3987_PERF_RESULTS_JSON: perf-results/full-results.json
run: |
mkdir -p perf-results
python -m pytest -m perf -s
- name: Upload full perf results
if: always()
# see https://github.com/actions/upload-artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: perf-full-results
path: perf-results/
2 changes: 1 addition & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Install project
run: python -m pip install --user -e ".[testing]"
- name: Run Tests
run: python -m pytest tests
run: python -m pytest -m "not perf" tests
publish:
name: Publish
needs:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@ jobs:
- name: Install project
run: pip install --user -e ".[testing]"
- name: Run Tests
run: python -m pytest tests
run: python -m pytest -m "not perf" tests
17 changes: 16 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,22 @@ Pull requests are welcome! Please keep the following in mind:
Tests are written with [pytest](https://docs.pytest.org/).

```sh
python -m pytest tests
python -m pytest -m "not perf" tests
```

Performance benchmarks live in `tests/test_perf.py` and are marked with `perf`.

```sh
python -m pytest -m perf tests/test_perf.py -s
```

The benchmark suite is deterministic and can export JSON for later comparison.

```sh
RFC3987_PERF_MODE=full \
RFC3987_PERF_RESULTS_JSON=perf-results/current.json \
RFC3987_PERF_BASELINE_JSON=perf-results/baseline.json \
python -m pytest -m perf tests/test_perf.py -s
```

## Running Static Analysis
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ testing = [

[tool.pytest.ini_options]
pythonpath = ["src"]
markers = [
"perf: performance and resource-reporting benchmarks",
]

[tool.hatch.build.targets.sdist]
exclude = [
Expand Down
75 changes: 70 additions & 5 deletions tests/_data/invalid_syntax.json
Original file line number Diff line number Diff line change
Expand Up @@ -1747,11 +1747,76 @@
"reason": "Caret in fragment"
}
],
"isegment_nz_nc": [],
"ipath_noscheme": [],
"ipath": [],
"irelative_part": [],
"irelative_ref": [],
"isegment_nz_nc": [
{
"value": "",
"reason": "Empty string — must be at least one character (non-zero-length)"
},
{
"value": "a:b",
"reason": "Colon is not allowed in isegment_nz_nc"
},
{
"value": ":start",
"reason": "Colon at start is not allowed in isegment_nz_nc"
},
{
"value": "seg ment",
"reason": "Unencoded space is not allowed"
},
{
"value": "path\nline",
"reason": "Newline character is not allowed"
}
],
"ipath_noscheme": [
{
"value": "scheme:path",
"reason": "Colon in first segment makes it look like a scheme, which is invalid for ipath_noscheme"
},
{
"value": "path with spaces",
"reason": "Unencoded space is not allowed"
},
{
"value": "path\nto",
"reason": "Newline character is not allowed"
}
],
"ipath": [
{
"value": "path with spaces",
"reason": "Unencoded space is not allowed in any path form"
},
{
"value": "path\nto",
"reason": "Newline character is not allowed"
},
{
"value": "path%GGinvalid",
"reason": "Invalid percent-encoding — G is not a hex digit"
}
],
"irelative_part": [
{
"value": "path with spaces",
"reason": "Unencoded space is not allowed"
},
{
"value": "//[unclosed",
"reason": "Unclosed IP-literal bracket is invalid"
}
],
"irelative_ref": [
{
"value": "path with spaces?q=1",
"reason": "Unencoded space is not allowed"
},
{
"value": "//[unclosed/path",
"reason": "Unclosed IP-literal bracket is invalid"
}
],
"iri": [
{
"value": "http://example.com/%ZZ",
Expand Down
77 changes: 76 additions & 1 deletion tests/_data/valid_syntax.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,37 @@
{
"value": "https://exämple.example.org/pageセクション1",
"expect_lark": true,
"reason": ""
"reason": "Unicode path segment — no fragment allowed in absolute_iri (contrast with iri entry that uses page#セクション1)"
},
{
"value": "urn:isbn:0451450523",
"expect_lark": true,
"reason": "URN with ISBN namespace"
},
{
"value": "ldap://[2001:db8::7]/c=GB?objectClass?one",
"expect_lark": true,
"reason": "LDAP IRI with IPv6 host"
},
{
"value": "coap://example.com:5683/sensor/über",
"expect_lark": true,
"reason": "CoAP IRI with non-default port and Unicode path"
},
{
"value": "http://例え.jp/path?クエリ=値",
"expect_lark": true,
"reason": "IRI with Japanese host and query"
},
{
"value": "git+https://github.com/user/repo",
"expect_lark": true,
"reason": "Git+HTTPS scheme with compound scheme"
},
{
"value": "data:text/plain;base64,SGVsbG8h",
"expect_lark": true,
"reason": "Data URI with base64-encoded content"
}
],
"gen_delims": [],
Expand Down Expand Up @@ -81,6 +111,26 @@
"value": "https://exämple.example.org/page#セクション1",
"expect_lark": true,
"reason": ""
},
{
"value": "urn:example:a123,z456",
"expect_lark": true,
"reason": "URN with alphanumeric local part"
},
{
"value": "http://例え.jp/path?クエリ=値#fragment",
"expect_lark": true,
"reason": "IRI with Japanese host, query, and fragment"
},
{
"value": "ldap://[2001:db8::7]/c=GB?objectClass?one",
"expect_lark": true,
"reason": "LDAP IRI with IPv6 host"
},
{
"value": "coap://example.com:5683/sensor/über",
"expect_lark": true,
"reason": "CoAP IRI with non-default port and Unicode path"
}
],
"iri_reference": [
Expand Down Expand Up @@ -158,6 +208,31 @@
"value": "//[2041:0:140F::875B:131B]/foo",
"expect_lark": true,
"reason": "`IPv6address` case: [ *4( h16 \":\" ) h16 ] \"::\" ls32"
},
{
"value": "?query=only#fragment",
"expect_lark": true,
"reason": "Query-only relative reference with fragment"
},
{
"value": "//user@example.com/docs",
"expect_lark": true,
"reason": "Network-path reference with userinfo"
},
{
"value": "path/to/über.html",
"expect_lark": true,
"reason": "Relative path with Unicode segment"
},
{
"value": "",
"expect_lark": true,
"reason": "Empty string is a valid IRI reference"
},
{
"value": "#",
"expect_lark": true,
"reason": "Empty fragment reference"
}
],
"reserved": [],
Expand Down
Loading