docopt2 1.1.2__tar.gz → 1.2.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.
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/workflows/ci.yml +10 -2
- docopt2-1.2.0/.github/workflows/codspeed.yml +40 -0
- docopt2-1.2.0/.github/workflows/fuzz.yml +50 -0
- docopt2-1.2.0/.github/workflows/nightly.yml +57 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/PKG-INFO +10 -13
- {docopt2-1.1.2 → docopt2-1.2.0}/README.md +8 -11
- docopt2-1.2.0/benchmarks/test_perf.py +53 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/concepts/design-boundaries.md +1 -1
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/completion.md +3 -2
- docopt2-1.2.0/fuzz/docopt.dict +31 -0
- docopt2-1.2.0/fuzz/fuzz_docopt.py +81 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/pyproject.toml +7 -2
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/__main__.py +3 -1
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_compat.py +30 -7
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_completion.py +82 -25
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_core.py +76 -29
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_fmt.py +3 -1
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_generate.py +17 -4
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_help.py +33 -10
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_parser.py +73 -4
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_typed.py +43 -7
- docopt2-1.2.0/tests/corpus/awkward_but_legal.txt +18 -0
- docopt2-1.2.0/tests/corpus/layered_config.txt +14 -0
- docopt2-1.2.0/tests/corpus/many_flags.txt +35 -0
- docopt2-1.2.0/tests/corpus/naval_fate.txt +16 -0
- docopt2-1.2.0/tests/corpus/nested_subcommands.txt +25 -0
- docopt2-1.2.0/tests/golden/completion_nushell.txt +12 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_choices.py +46 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_cli.py +11 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_compat.py +16 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_config_template.py +27 -2
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_core_extra.py +22 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_diagnostics.py +17 -3
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_dispatch_and_completion.py +108 -1
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_env.py +16 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_extended_features.py +12 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_fmt.py +12 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_help.py +40 -2
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_property_differential.py +43 -2
- docopt2-1.2.0/tests/test_robustness.py +175 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_typed_api.py +17 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tools/regolden.py +1 -1
- {docopt2-1.1.2 → docopt2-1.2.0}/tools/shell_check.py +21 -6
- {docopt2-1.1.2 → docopt2-1.2.0}/uv.lock +42 -1
- docopt2-1.1.2/.github/workflows/nightly.yml +0 -32
- {docopt2-1.1.2 → docopt2-1.2.0}/.codecov.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.gitattributes +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/dependabot.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/workflows/codeql.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/workflows/docs.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/workflows/mutation.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/workflows/publish.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/workflows/scorecard.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.github/workflows/zizmor.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/.gitignore +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/CONTRIBUTING.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/LICENSE +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/NOTICE +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/SECURITY.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/check.png +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/check.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/coercion.png +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/coercion.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/diagnostic.png +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/diagnostic.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/logo-dark.png +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/logo.png +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/rich-help.png +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/assets/rich-help.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/getting-started.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/check.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/compat.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/diagnostics.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/dispatch.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/examples.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/fmt.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/help.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/round-trip.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/stub.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/typed-results.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/guides/usage-dsl.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/index.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/check.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/cli.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/compat.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/completion.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/config-templates.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/dispatch.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/docopt.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/examples.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/exceptions.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/fmt.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/grammar.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/overview.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/reference/stub.md +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/docs/stylesheets/extra.css +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/cli.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/completion.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/dispatch.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/layered.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/naval_fate.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/rich_help.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/round_trip.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/examples/typed.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/mkdocs.yml +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/__init__.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_diagnostics.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_errors.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_format.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_lint.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_spellcheck.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/_stub.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/hypothesis.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/src/docopt2/py.typed +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/_strategies.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/_vendor/docopt_original.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/conftest.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/golden/completion_bash.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/golden/completion_fish.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/golden/completion_powershell.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/golden/completion_zsh.txt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_coercion_diagnostics.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_completion_vs_docopt.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_config.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_divergences.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_docopt_original.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_docs_blocks.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_examples.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_format.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_generate.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_getting_started.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_grammar_examples.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_hypothesis.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_import_cost.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_language_agnostic.docopt +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_lint.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_near_miss.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_package.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_parse_tree.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_property_coercion.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_property_config.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_property_config_template.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_property_extended.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_pydantic.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_readme.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_screenshots.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_source.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_spellcheck.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/test_stub.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tests/typing_surface/check_typed.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tools/mutation_report.py +0 -0
- {docopt2-1.1.2 → docopt2-1.2.0}/tools/screenshots.py +0 -0
|
@@ -86,10 +86,10 @@ jobs:
|
|
|
86
86
|
run: uv sync --group typecheck --locked
|
|
87
87
|
|
|
88
88
|
- name: Ruff check
|
|
89
|
-
run: uv run ruff check src tests tools
|
|
89
|
+
run: uv run ruff check src tests tools fuzz benchmarks
|
|
90
90
|
|
|
91
91
|
- name: Ruff format check
|
|
92
|
-
run: uv run ruff format --check src tests tools
|
|
92
|
+
run: uv run ruff format --check src tests tools fuzz benchmarks
|
|
93
93
|
|
|
94
94
|
- name: Type check (ty)
|
|
95
95
|
run: uv run ty check
|
|
@@ -128,6 +128,14 @@ jobs:
|
|
|
128
128
|
if: runner.os == 'Linux'
|
|
129
129
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends zsh fish
|
|
130
130
|
|
|
131
|
+
- name: Install nushell
|
|
132
|
+
# Not in the Ubuntu repos; pinned to the version the completer targets (`commandline complete`
|
|
133
|
+
# landed in 0.114.0). Drop the release tarball on PATH.
|
|
134
|
+
if: runner.os == 'Linux'
|
|
135
|
+
run: |
|
|
136
|
+
curl -sSfL https://github.com/nushell/nushell/releases/download/0.114.1/nu-0.114.1-x86_64-unknown-linux-gnu.tar.gz | tar xz -C "$HOME"
|
|
137
|
+
echo "$HOME/nu-0.114.1-x86_64-unknown-linux-gnu" >> "$GITHUB_PATH"
|
|
138
|
+
|
|
131
139
|
- name: Complete in a real terminal
|
|
132
140
|
run: uv run --locked python tools/shell_check.py
|
|
133
141
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CodSpeed
|
|
2
|
+
|
|
3
|
+
# Continuous performance gate: run the micro-benchmarks (benchmarks/test_perf.py) under CodSpeed's CPU
|
|
4
|
+
# simulation, which measures a deterministic instruction count rather than wall time, so a runner's noise
|
|
5
|
+
# never flakes it. CodSpeed compares each commit to the base and reports a pass/fail on the PR. A public
|
|
6
|
+
# repo uploads over OIDC (id-token), so no token secret is needed - only the CodSpeed GitHub App installed.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
pull_request:
|
|
12
|
+
branches: [main]
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
benchmarks:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
30
|
+
with:
|
|
31
|
+
persist-credentials: false
|
|
32
|
+
|
|
33
|
+
- name: Install uv
|
|
34
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
35
|
+
|
|
36
|
+
- name: Run benchmarks under CodSpeed
|
|
37
|
+
uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5
|
|
38
|
+
with:
|
|
39
|
+
mode: simulation
|
|
40
|
+
run: uv run --locked --group benchmark pytest benchmarks/ --codspeed --no-cov
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Fuzz
|
|
2
|
+
|
|
3
|
+
# Coverage-guided fuzzing of the public API with atheris, off the PR path. The invariant is the one
|
|
4
|
+
# tests/test_robustness.py states and Hypothesis checks on grammar-shaped input: no entry point may raise
|
|
5
|
+
# anything but DocoptExit / DocoptLanguageError for any input. The Nightly job explores well-formed usage
|
|
6
|
+
# with Hypothesis; this reaches the malformed byte shapes it cannot, under coverage feedback, and a
|
|
7
|
+
# per-input -timeout turns a runaway (a pathological slowdown or an outright hang) into a finding. Unlike
|
|
8
|
+
# Mutation (report-only), a crash or timeout here is a real bug: the job fails and uploads the reproducer,
|
|
9
|
+
# which becomes a committed regression test.
|
|
10
|
+
#
|
|
11
|
+
# Linux only: atheris ships no Windows wheel. Pinned to Python 3.12, which has a prebuilt manylinux wheel.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
schedule:
|
|
15
|
+
- cron: "41 4 * * 1"
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
concurrency:
|
|
22
|
+
group: ${{ github.workflow }}
|
|
23
|
+
cancel-in-progress: true
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
fuzz:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 20
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
31
|
+
with:
|
|
32
|
+
persist-credentials: false
|
|
33
|
+
|
|
34
|
+
- name: Install uv
|
|
35
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
36
|
+
|
|
37
|
+
- name: Fuzz the public API for ten minutes
|
|
38
|
+
run: |
|
|
39
|
+
mkdir -p fuzz-findings
|
|
40
|
+
uv run --locked --no-dev --python 3.12 --with atheris \
|
|
41
|
+
python fuzz/fuzz_docopt.py \
|
|
42
|
+
-max_total_time=600 -timeout=30 -rss_limit_mb=4096 -artifact_prefix=fuzz-findings/
|
|
43
|
+
|
|
44
|
+
- name: Upload any crash or timeout reproducer
|
|
45
|
+
if: failure()
|
|
46
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
47
|
+
with:
|
|
48
|
+
name: fuzz-findings
|
|
49
|
+
path: fuzz-findings/
|
|
50
|
+
if-no-files-found: ignore
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Nightly
|
|
2
|
+
|
|
3
|
+
# The gate runs Hypothesis derandomised at 300 examples, deliberately: a PR check that flakes is a check
|
|
4
|
+
# people learn to ignore. The cost is that every run sees the same inputs, so a rare violation can sit
|
|
5
|
+
# there forever. This explores 10,000 randomised examples per property, off the critical path, where a
|
|
6
|
+
# failure is a finding rather than a blocked merge.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: "23 3 * * *"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.workflow }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
explore:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
25
|
+
with:
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
30
|
+
|
|
31
|
+
- name: Property tests, wide and randomised
|
|
32
|
+
run: uv run --locked pytest --hypothesis-profile=explore -q --no-cov
|
|
33
|
+
|
|
34
|
+
# A red nightly is a real property violation, not noise (unlike the Mutation survivors), yet it is off
|
|
35
|
+
# the merge path, so nothing surfaces it - a failure sat unseen for days. On failure open, or bump, a
|
|
36
|
+
# single tracking issue so it is visible without anyone watching the schedule.
|
|
37
|
+
notify:
|
|
38
|
+
needs: explore
|
|
39
|
+
if: failure()
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
permissions:
|
|
42
|
+
issues: write
|
|
43
|
+
steps:
|
|
44
|
+
- name: Open or bump the tracking issue
|
|
45
|
+
env:
|
|
46
|
+
GH_TOKEN: ${{ github.token }}
|
|
47
|
+
REPO: ${{ github.repository }}
|
|
48
|
+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
49
|
+
run: |
|
|
50
|
+
title="Nightly wide-hypothesis run is failing"
|
|
51
|
+
number=$(gh issue list --repo "$REPO" --state open --search "$title in:title" --json number --jq '.[0].number // empty')
|
|
52
|
+
if [ -n "$number" ]; then
|
|
53
|
+
gh issue comment "$number" --repo "$REPO" --body "Still failing: $RUN_URL"
|
|
54
|
+
else
|
|
55
|
+
gh issue create --repo "$REPO" --title "$title" \
|
|
56
|
+
--body "The scheduled wide Hypothesis run (10k examples) failed - it catches divergences the 300-example gate misses. Run: $RUN_URL"
|
|
57
|
+
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docopt2
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Typed successor to docopt: the usage message is the parser spec.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/docopt2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/docopt2
|
|
@@ -11,7 +11,7 @@ License-Expression: MIT
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
License-File: NOTICE
|
|
13
13
|
Keywords: argument-parser,cli,command-line,docopt,posix,typed,usage
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Environment :: Console
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
@@ -47,20 +47,16 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
</p>
|
|
48
48
|
|
|
49
49
|
<p align="center">
|
|
50
|
-
<a href="https://github.com/Solganis/docopt2/actions/workflows/ci.yml"><img src="https://
|
|
50
|
+
<a href="https://github.com/Solganis/docopt2/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Solganis/docopt2/ci.yml?branch=main&label=CI" alt="CI"></a>
|
|
51
|
+
<a href="https://codecov.io/gh/Solganis/docopt2"><img src="https://codecov.io/gh/Solganis/docopt2/graph/badge.svg" alt="Coverage"></a>
|
|
51
52
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/v/docopt2" alt="PyPI version"></a>
|
|
52
53
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/pyversions/docopt2" alt="Python"></a>
|
|
53
|
-
<a href="https://codecov.io/gh/Solganis/docopt2"><img src="https://codecov.io/gh/Solganis/docopt2/graph/badge.svg" alt="Coverage"></a>
|
|
54
54
|
<a href="https://pepy.tech/projects/docopt2"><img src="https://static.pepy.tech/badge/docopt2/month" alt="Downloads"></a>
|
|
55
55
|
<br>
|
|
56
|
-
<a href="https://solganis.github.io/docopt2/"><img src="https://img.shields.io/badge/
|
|
57
|
-
<a href="https://
|
|
58
|
-
<a href="https://github.
|
|
59
|
-
<a href="https://github.com/
|
|
60
|
-
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/docopt2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/docopt2/badge" alt="OpenSSF Scorecard"></a>
|
|
61
|
-
<br>
|
|
62
|
-
<img src="https://img.shields.io/badge/runtime%20deps-0-2ea043" alt="zero runtime dependencies, pydantic support optional">
|
|
63
|
-
<img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="the typed API is checked by ty, mypy --strict, and pyright">
|
|
56
|
+
<a href="https://solganis.github.io/docopt2/concepts/design-boundaries/"><img src="https://img.shields.io/badge/runtime%20deps-0-2ea043" alt="zero runtime dependencies, pydantic support optional"></a>
|
|
57
|
+
<a href="https://solganis.github.io/docopt2/guides/typed-results/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="the typed API is checked by ty, mypy --strict, and pyright"></a>
|
|
58
|
+
<a href="https://solganis.github.io/docopt2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
59
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/docopt2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/docopt2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
64
60
|
</p>
|
|
65
61
|
|
|
66
62
|
---
|
|
@@ -95,7 +91,7 @@ Route each subcommand to its own handler - no <code>if args[...]</code> ladder.
|
|
|
95
91
|
</td>
|
|
96
92
|
<td valign="top">
|
|
97
93
|
<a href="#shell-completion"><b>Shell completion</b></a><br>
|
|
98
|
-
Tab-completion that knows your grammar: bash, zsh, fish, PowerShell.
|
|
94
|
+
Tab-completion that knows your grammar: bash, zsh, fish, PowerShell, nushell.
|
|
99
95
|
</td>
|
|
100
96
|
</tr>
|
|
101
97
|
<tr>
|
|
@@ -274,6 +270,7 @@ app.run() # parse argv, call the matched command's handler
|
|
|
274
270
|
<a href="https://www.zsh.org/"><img src="https://img.shields.io/badge/zsh-F15A24?logo=zsh&logoColor=white" alt="zsh"></a>
|
|
275
271
|
<a href="https://fishshell.com/"><img src="https://img.shields.io/badge/fish-34C534?logo=fishshell&logoColor=white" alt="fish"></a>
|
|
276
272
|
<a href="https://learn.microsoft.com/powershell/"><img src="https://img.shields.io/badge/PowerShell-5391FE" alt="PowerShell"></a>
|
|
273
|
+
<a href="https://www.nushell.sh/"><img src="https://img.shields.io/badge/nushell-4E9A06?logo=nushell&logoColor=white" alt="nushell"></a>
|
|
277
274
|
</p>
|
|
278
275
|
|
|
279
276
|
Generate the completion script for your shell; Tab then narrows to exactly what is valid at the cursor -<br>
|
|
@@ -11,20 +11,16 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
<a href="https://github.com/Solganis/docopt2/actions/workflows/ci.yml"><img src="https://
|
|
14
|
+
<a href="https://github.com/Solganis/docopt2/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Solganis/docopt2/ci.yml?branch=main&label=CI" alt="CI"></a>
|
|
15
|
+
<a href="https://codecov.io/gh/Solganis/docopt2"><img src="https://codecov.io/gh/Solganis/docopt2/graph/badge.svg" alt="Coverage"></a>
|
|
15
16
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/v/docopt2" alt="PyPI version"></a>
|
|
16
17
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/pyversions/docopt2" alt="Python"></a>
|
|
17
|
-
<a href="https://codecov.io/gh/Solganis/docopt2"><img src="https://codecov.io/gh/Solganis/docopt2/graph/badge.svg" alt="Coverage"></a>
|
|
18
18
|
<a href="https://pepy.tech/projects/docopt2"><img src="https://static.pepy.tech/badge/docopt2/month" alt="Downloads"></a>
|
|
19
19
|
<br>
|
|
20
|
-
<a href="https://solganis.github.io/docopt2/"><img src="https://img.shields.io/badge/
|
|
21
|
-
<a href="https://
|
|
22
|
-
<a href="https://github.
|
|
23
|
-
<a href="https://github.com/
|
|
24
|
-
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/docopt2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/docopt2/badge" alt="OpenSSF Scorecard"></a>
|
|
25
|
-
<br>
|
|
26
|
-
<img src="https://img.shields.io/badge/runtime%20deps-0-2ea043" alt="zero runtime dependencies, pydantic support optional">
|
|
27
|
-
<img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="the typed API is checked by ty, mypy --strict, and pyright">
|
|
20
|
+
<a href="https://solganis.github.io/docopt2/concepts/design-boundaries/"><img src="https://img.shields.io/badge/runtime%20deps-0-2ea043" alt="zero runtime dependencies, pydantic support optional"></a>
|
|
21
|
+
<a href="https://solganis.github.io/docopt2/guides/typed-results/"><img src="https://img.shields.io/badge/type--checked-ty%20%7C%20mypy%20%7C%20pyright-2ea043" alt="the typed API is checked by ty, mypy --strict, and pyright"></a>
|
|
22
|
+
<a href="https://solganis.github.io/docopt2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
23
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/docopt2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/docopt2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
28
24
|
</p>
|
|
29
25
|
|
|
30
26
|
---
|
|
@@ -59,7 +55,7 @@ Route each subcommand to its own handler - no <code>if args[...]</code> ladder.
|
|
|
59
55
|
</td>
|
|
60
56
|
<td valign="top">
|
|
61
57
|
<a href="#shell-completion"><b>Shell completion</b></a><br>
|
|
62
|
-
Tab-completion that knows your grammar: bash, zsh, fish, PowerShell.
|
|
58
|
+
Tab-completion that knows your grammar: bash, zsh, fish, PowerShell, nushell.
|
|
63
59
|
</td>
|
|
64
60
|
</tr>
|
|
65
61
|
<tr>
|
|
@@ -238,6 +234,7 @@ app.run() # parse argv, call the matched command's handler
|
|
|
238
234
|
<a href="https://www.zsh.org/"><img src="https://img.shields.io/badge/zsh-F15A24?logo=zsh&logoColor=white" alt="zsh"></a>
|
|
239
235
|
<a href="https://fishshell.com/"><img src="https://img.shields.io/badge/fish-34C534?logo=fishshell&logoColor=white" alt="fish"></a>
|
|
240
236
|
<a href="https://learn.microsoft.com/powershell/"><img src="https://img.shields.io/badge/PowerShell-5391FE" alt="PowerShell"></a>
|
|
237
|
+
<a href="https://www.nushell.sh/"><img src="https://img.shields.io/badge/nushell-4E9A06?logo=nushell&logoColor=white" alt="nushell"></a>
|
|
241
238
|
</p>
|
|
242
239
|
|
|
243
240
|
Generate the completion script for your shell; Tab then narrows to exactly what is valid at the cursor -<br>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# CodSpeed regression gate: micro-benchmarks of docopt2's own hot paths, measured commit-over-commit under
|
|
2
|
+
# CPU simulation. Run manually with `uv run --group benchmark pytest benchmarks/ --codspeed --no-cov`; the
|
|
3
|
+
# CodSpeed CI job runs the same. Not collected by the default suite (testpaths=tests), so it never gates on
|
|
4
|
+
# the machine's noise - only CodSpeed's simulated instruction count does.
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
|
|
9
|
+
from docopt2 import complete, docopt
|
|
10
|
+
|
|
11
|
+
# A git/naval-shaped CLI: subcommands, a variadic, a default, and two mutex groups.
|
|
12
|
+
NAVAL = """Usage:
|
|
13
|
+
naval ship new <name>...
|
|
14
|
+
naval ship <name> move <x> <y> [--speed=<kn>]
|
|
15
|
+
naval mine (set | remove) <x> <y> [--moored | --drifting]
|
|
16
|
+
naval --help
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--speed=<kn> Speed in knots [default: 10].
|
|
20
|
+
--moored Moored (anchored) mine.
|
|
21
|
+
--drifting Drifting mine.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Eight optionals on one line: exercises the _combine search that the match budget bounds - a guard against
|
|
25
|
+
# a regression in the matcher's cost.
|
|
26
|
+
MANY_OPTIONALS = "usage: prog [-a] [-b] [-c] [-d] [-e] [-f] [-g] [-h] <src> <dst>"
|
|
27
|
+
|
|
28
|
+
TYPED_DOC = "Usage: app <host> <port> [--verbose] [--retries=<n>]"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass
|
|
32
|
+
class Config:
|
|
33
|
+
host: str
|
|
34
|
+
port: int
|
|
35
|
+
verbose: bool
|
|
36
|
+
retries: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_single_shot_parse_and_match(benchmark):
|
|
40
|
+
# The realistic per-invocation cost: parse the usage and match one argv (docopt2 reparses every call).
|
|
41
|
+
benchmark(lambda: docopt(NAVAL, ["ship", "Titanic", "move", "1", "2", "--speed=20"], help=False))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_match_many_optionals(benchmark):
|
|
45
|
+
benchmark(lambda: docopt(MANY_OPTIONALS, ["-a", "-c", "-e", "in.txt", "out.txt"], help=False))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_typed_binding_and_coercion(benchmark):
|
|
49
|
+
benchmark(lambda: docopt(TYPED_DOC, ["localhost", "8080", "--verbose", "--retries=3"], schema=Config, help=False))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_completion_latency(benchmark):
|
|
53
|
+
benchmark(lambda: complete(NAVAL, ["ship"]))
|
|
@@ -118,7 +118,7 @@ the surrounding program or a dedicated library, layered on top of the parsed res
|
|
|
118
118
|
|
|
119
119
|
The typed surface is plain typing with no runtime cost. The [`Cli`](../reference/cli.md) base class is
|
|
120
120
|
the only decorator-shaped sugar, and it is deliberately a base class rather than a method-injecting
|
|
121
|
-
decorator so the result keeps real static types
|
|
121
|
+
decorator, so the result keeps real static types a type checker can see instead of degrading to `Any`.
|
|
122
122
|
|
|
123
123
|
Typing is a layer over the drop-in core, not a rewrite of it. The same `docopt()` call runs the same
|
|
124
124
|
parse; passing `schema=` adds one final bind step that coerces the string values to the field types you
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Shell completion
|
|
2
2
|
|
|
3
3
|
`generate_completion` emits a context-aware completion script for the CLI described by your usage, for
|
|
4
|
-
bash, zsh, fish,
|
|
4
|
+
bash, zsh, fish, PowerShell, and nushell. The script carries no grammar of
|
|
5
5
|
its own: it delegates every Tab back to your program, whose [`docopt`](../reference/docopt.md) call
|
|
6
6
|
resolves the candidates from the usage. This guide covers generating and installing that script, and
|
|
7
7
|
computing the candidates directly with `complete()`.
|
|
@@ -15,7 +15,7 @@ script = generate_completion(doc, prog="naval", shell="bash")
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
The signature is `generate_completion(doc, prog, shell="bash") -> str`. `shell` is one of `"bash"`,
|
|
18
|
-
`"zsh"`, `"fish"`, or `"
|
|
18
|
+
`"zsh"`, `"fish"`, `"powershell"`, or `"nushell"`; any other value raises `ValueError`, as does a `prog` that is not a
|
|
19
19
|
plain command name (letters, digits, `.`, `_`, `-`). A malformed `doc` fails here, loudly, rather than
|
|
20
20
|
silently at Tab time.
|
|
21
21
|
|
|
@@ -67,6 +67,7 @@ have the program print it, then load it with the shell's standard mechanism:
|
|
|
67
67
|
| zsh | `"zsh"` | `source` it from `~/.zshrc`, or save it as `_naval` on your `$fpath`. |
|
|
68
68
|
| fish | `"fish"` | Save it as `~/.config/fish/completions/naval.fish`. |
|
|
69
69
|
| PowerShell | `"powershell"` | Dot-source it from your `$PROFILE`. |
|
|
70
|
+
| nushell | `"nushell"` | Save it under a name that is **not** `naval.nu` (a module cannot export a command of its own name), then `use naval-completions.nu *` from your config, or drop it in a `$nu.vendor-autoload-dirs` directory. Needs nushell 0.108+. |
|
|
70
71
|
|
|
71
72
|
!!! note
|
|
72
73
|
Each Tab is a full launch of your program, so its startup cost (imports and so on) is the completion
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# libFuzzer dictionary of docopt usage-grammar tokens. Without it the fuzzer almost never synthesises the
|
|
2
|
+
# word "usage:" from random bytes, so it bounces off the section detector; with it, libFuzzer splices these
|
|
3
|
+
# in during mutation and reaches the pattern matcher, coercion and the Options parser. "\x00" is the harness
|
|
4
|
+
# separator between the usage message and the argv tokens (see fuzz_docopt.py).
|
|
5
|
+
"usage:"
|
|
6
|
+
"Usage:"
|
|
7
|
+
"options:"
|
|
8
|
+
"Options:"
|
|
9
|
+
"arguments:"
|
|
10
|
+
"Arguments:"
|
|
11
|
+
"[options]"
|
|
12
|
+
"[default: 1]"
|
|
13
|
+
"[env: VAR]"
|
|
14
|
+
"[config: k.v]"
|
|
15
|
+
"--help"
|
|
16
|
+
"--version"
|
|
17
|
+
"-h"
|
|
18
|
+
"--"
|
|
19
|
+
"..."
|
|
20
|
+
" | "
|
|
21
|
+
"["
|
|
22
|
+
"]"
|
|
23
|
+
"("
|
|
24
|
+
")"
|
|
25
|
+
"<arg>"
|
|
26
|
+
"ARG"
|
|
27
|
+
"=<val>"
|
|
28
|
+
"prog "
|
|
29
|
+
"\x0a"
|
|
30
|
+
"\x0a "
|
|
31
|
+
"\x00"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Coverage-guided fuzzing of the public API with atheris (Linux/macOS only; there is no Windows wheel).
|
|
2
|
+
# The invariant is the one tests/test_robustness.py states and Hypothesis checks on grammar-shaped input:
|
|
3
|
+
# no entry point may raise anything but its own DocoptExit / DocoptLanguageError for ANY input. Hypothesis
|
|
4
|
+
# only ever builds well-formed usage; atheris reaches the malformed shapes it cannot - unbalanced brackets,
|
|
5
|
+
# NUL bytes, an option line run into its description - from arbitrary bytes under coverage feedback, and the
|
|
6
|
+
# libFuzzer -timeout turns a runaway (a pathological slowdown or an outright hang) into a reported finding.
|
|
7
|
+
#
|
|
8
|
+
# atheris has no Windows wheel; run on Linux/macOS. The Fuzz workflow is the canonical invocation (corpus
|
|
9
|
+
# seeds + dict + flags); replay a saved reproducer with: python fuzz/fuzz_docopt.py <reproducer-file>
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import atheris
|
|
16
|
+
|
|
17
|
+
with atheris.instrument_imports():
|
|
18
|
+
from docopt2 import (
|
|
19
|
+
DocoptExit,
|
|
20
|
+
DocoptLanguageError,
|
|
21
|
+
check,
|
|
22
|
+
complete,
|
|
23
|
+
docopt,
|
|
24
|
+
format_usage,
|
|
25
|
+
generate_completion,
|
|
26
|
+
generate_config_template,
|
|
27
|
+
generate_examples,
|
|
28
|
+
generate_stub,
|
|
29
|
+
parse_tree,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# DocoptExit (a SystemExit subclass, and the --help path) and DocoptLanguageError are the only errors the
|
|
33
|
+
# public API may raise. Anything else escaping is the finding. Mirrors _OWN_ERRORS in test_robustness.py so
|
|
34
|
+
# the fuzzer and the property suite can never disagree on what "correct" means.
|
|
35
|
+
_OWN_ERRORS = (DocoptExit, DocoptLanguageError, SystemExit)
|
|
36
|
+
|
|
37
|
+
# Every public entry point, driven with help and completion off so a fuzzed `--help` cannot exit the
|
|
38
|
+
# process mid-run. One input exercises all of them. check_compat is deliberately absent: it runs docopt()
|
|
39
|
+
# once per generated sample against argvs IT builds from the (fuzzer-maximised) grammar, so it manufactures
|
|
40
|
+
# a slow-but-bounded run that is a property of the sample count, not a defect - its own logic is
|
|
41
|
+
# deterministic and unit-tested. The runtime surface it shares (docopt, generate_examples) is fuzzed here.
|
|
42
|
+
_ENTRY_POINTS = (
|
|
43
|
+
lambda doc, argv: docopt(doc, argv, help=False, complete=False),
|
|
44
|
+
lambda doc, argv: check(doc),
|
|
45
|
+
lambda doc, argv: format_usage(doc),
|
|
46
|
+
lambda doc, argv: generate_stub(doc),
|
|
47
|
+
lambda doc, argv: generate_examples(doc, count=2, seed=0),
|
|
48
|
+
lambda doc, argv: generate_config_template(doc),
|
|
49
|
+
lambda doc, argv: generate_completion(doc, "prog"),
|
|
50
|
+
lambda doc, argv: complete(doc, argv),
|
|
51
|
+
lambda doc, argv: parse_tree(doc),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_one_input(data: bytes) -> None:
|
|
56
|
+
"""Split the bytes on NUL into a usage message and argv tokens, then drive every entry point.
|
|
57
|
+
|
|
58
|
+
A plain split (rather than atheris.FuzzedDataProvider) keeps the encoding transparent: a seed file that
|
|
59
|
+
is a real usage message decodes back to itself, so the corpus in fuzz/corpus and the docopt.dict tokens
|
|
60
|
+
actually steer the fuzzer into the parser instead of leaving it to guess the word ``usage:`` from noise.
|
|
61
|
+
"""
|
|
62
|
+
usage_bytes, *argv_bytes = data.split(b"\x00")
|
|
63
|
+
doc = usage_bytes.decode("utf-8", "replace")
|
|
64
|
+
# Cap the token count: the matcher is linear per step but each step scans the argv, so an ambiguous
|
|
65
|
+
# pattern against a many-hundred-token argv is bounded-but-slow. A real invocation is short (a long
|
|
66
|
+
# `<files>...` glob is the OneOrMore path, which stays linear), so 32 tokens covers the shapes worth
|
|
67
|
+
# fuzzing without letting the harness manufacture a slow run out of an unrealistic argv.
|
|
68
|
+
argv = [token.decode("utf-8", "replace") for token in argv_bytes[:32]]
|
|
69
|
+
for entry in _ENTRY_POINTS:
|
|
70
|
+
with contextlib.suppress(*_OWN_ERRORS):
|
|
71
|
+
entry(doc, argv)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> None:
|
|
75
|
+
"""libFuzzer entry: flags (-max_total_time, -timeout, -artifact_prefix, ...) come in on sys.argv."""
|
|
76
|
+
atheris.Setup(sys.argv, test_one_input)
|
|
77
|
+
atheris.Fuzz()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docopt2"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Typed successor to docopt: the usage message is the parser spec."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
]
|
|
12
12
|
keywords = ["docopt", "cli", "command-line", "argument-parser", "usage", "posix", "typed"]
|
|
13
13
|
classifiers = [
|
|
14
|
-
"Development Status ::
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
15
|
"Environment :: Console",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Operating System :: OS Independent",
|
|
@@ -96,6 +96,11 @@ typecheck = [
|
|
|
96
96
|
mutation = [
|
|
97
97
|
"mutmut>=3.3.1",
|
|
98
98
|
]
|
|
99
|
+
# Performance regression gate. Own group: pytest-codspeed is only needed by the CodSpeed CI job and a
|
|
100
|
+
# manual `pytest benchmarks/ --codspeed`, not by the suite every contributor runs.
|
|
101
|
+
benchmark = [
|
|
102
|
+
"pytest-codspeed>=5",
|
|
103
|
+
]
|
|
99
104
|
|
|
100
105
|
[tool.mutmut]
|
|
101
106
|
source_paths = ["src/docopt2/"]
|
|
@@ -59,7 +59,9 @@ def _read_usage(source: str) -> str:
|
|
|
59
59
|
return sys.stdin.read()
|
|
60
60
|
path = Path(source)
|
|
61
61
|
try:
|
|
62
|
-
|
|
62
|
+
# utf-8-sig, not utf-8: a leading BOM (Windows editors add one) is stripped, exactly as Python
|
|
63
|
+
# does for a source file it runs - otherwise `ast.parse` chokes on the U+FEFF a .py starts with.
|
|
64
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
63
65
|
except UnicodeDecodeError as error:
|
|
64
66
|
raise _CliError(f"{source}: not a UTF-8 text file") from error
|
|
65
67
|
if path.suffix != ".py":
|
|
@@ -34,35 +34,58 @@ def check_compat(old_doc: str, new_doc: str, *, samples: int = 300) -> list[str]
|
|
|
34
34
|
"""Report backward-incompatible changes from ``old_doc`` to ``new_doc``, most reliable part first.
|
|
35
35
|
|
|
36
36
|
Every entry is a *definite* break - an invocation the old usage accepts that the new one rejects: a
|
|
37
|
-
removed option
|
|
38
|
-
|
|
37
|
+
removed option, a removed command, or a concrete argument vector the new grammar no longer accepts
|
|
38
|
+
(found by sampling the old grammar's accepted set and replaying it against the new).
|
|
39
39
|
|
|
40
40
|
An empty list means **no break was found**, not a proof of compatibility: the accepted set is infinite
|
|
41
41
|
and only ``samples`` invocations are checked, so read it like a passing test ("no breakage detected"),
|
|
42
42
|
never as a guarantee. It never claims "compatible" - it only surfaces breaks it can prove.
|
|
43
43
|
"""
|
|
44
|
+
old_examples = generate_examples(old_doc, count=samples, seed=0)
|
|
44
45
|
removed_options = _spellings(old_doc) - _spellings(new_doc)
|
|
45
|
-
|
|
46
|
+
# A command literal absent from new is only a break if it actually rejects something. `(add|rm) <p>`
|
|
47
|
+
# generalized to `<cmd> <p>` drops the `add`/`rm` literals, yet new accepts every old invocation (they
|
|
48
|
+
# match `<cmd>`). Report a command only when a sampled old invocation that used it is rejected by new.
|
|
49
|
+
gone = _usable(old_doc, Command) - _usable(new_doc, Command)
|
|
50
|
+
removed_commands = {name for name in gone if _command_removal_breaks(new_doc, name, old_examples)}
|
|
46
51
|
breaks = [f"option `{name}` removed" for name in sorted(removed_options)]
|
|
47
52
|
breaks += [f"command `{name}` removed" for name in sorted(removed_commands)]
|
|
48
53
|
removed = removed_options | removed_commands
|
|
49
54
|
seen_shapes: set[tuple[str, ...]] = set()
|
|
50
55
|
examples: list[str] = []
|
|
51
|
-
for argv in
|
|
56
|
+
for argv in old_examples:
|
|
52
57
|
shape = tuple(re.sub(r"v\d+", "<val>", token) for token in argv) # collapse placeholder values
|
|
53
58
|
if shape in seen_shapes or _explained(argv, removed): # one example per shape; skip structural repeats
|
|
54
59
|
seen_shapes.add(shape)
|
|
55
60
|
continue
|
|
56
61
|
seen_shapes.add(shape)
|
|
57
|
-
|
|
58
|
-
docopt(new_doc, argv, help=False, complete=False)
|
|
59
|
-
except (DocoptExit, DocoptLanguageError):
|
|
62
|
+
if _rejects(new_doc, argv):
|
|
60
63
|
examples.append(f"`{' '.join(argv)}` no longer accepted")
|
|
61
64
|
if len(examples) >= _MAX_EXAMPLES:
|
|
62
65
|
break
|
|
63
66
|
return breaks + examples
|
|
64
67
|
|
|
65
68
|
|
|
69
|
+
def _rejects(doc: str, argv: list[str]) -> bool:
|
|
70
|
+
"""Whether ``doc`` refuses ``argv`` (the shape a break takes: old accepted it, new does not)."""
|
|
71
|
+
try:
|
|
72
|
+
docopt(doc, argv, help=False, complete=False)
|
|
73
|
+
except (DocoptExit, DocoptLanguageError):
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _command_removal_breaks(new_doc: str, name: str, old_examples: list[list[str]]) -> bool:
|
|
79
|
+
"""Whether dropping the command literal ``name`` rejects some invocation the old grammar accepted.
|
|
80
|
+
|
|
81
|
+
False when new absorbed the command into a positional (it still accepts those invocations); True when
|
|
82
|
+
the command is genuinely gone. ``generate_examples`` emits positional values as ``v1``, ``v2``... and
|
|
83
|
+
a command only by its name, so a command literal appears in a sampled argv iff it matched as that
|
|
84
|
+
command - a plain ``name in argv`` is enough. Bounded by the sampled examples, like the rest.
|
|
85
|
+
"""
|
|
86
|
+
return any(name in argv and _rejects(new_doc, argv) for argv in old_examples)
|
|
87
|
+
|
|
88
|
+
|
|
66
89
|
def _explained(argv: list[str], removed: set[str]) -> bool:
|
|
67
90
|
"""Whether the argv uses a removed option or command - so a named structural break already covers it."""
|
|
68
91
|
return any(token.split("=", 1)[0] in removed or token in removed for token in argv)
|