docopt2 1.2.0__tar.gz → 1.2.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.
- {docopt2-1.2.0 → docopt2-1.2.1}/PKG-INFO +2 -2
- {docopt2-1.2.0 → docopt2-1.2.1}/README.md +1 -1
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/getting-started.md +1 -2
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/examples.md +2 -2
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/round-trip.md +2 -2
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/index.md +3 -4
- {docopt2-1.2.0 → docopt2-1.2.1}/pyproject.toml +1 -1
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_parser.py +33 -11
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_property_differential.py +26 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_robustness.py +14 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/uv.lock +1 -1
- {docopt2-1.2.0 → docopt2-1.2.1}/.codecov.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.gitattributes +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/dependabot.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/ci.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/codeql.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/codspeed.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/docs.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/fuzz.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/mutation.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/nightly.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/publish.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/scorecard.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.github/workflows/zizmor.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/.gitignore +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/CONTRIBUTING.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/LICENSE +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/NOTICE +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/SECURITY.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/benchmarks/test_perf.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/check.png +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/check.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/coercion.png +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/coercion.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/diagnostic.png +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/diagnostic.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/logo-dark.png +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/logo.png +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/rich-help.png +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/assets/rich-help.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/concepts/design-boundaries.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/check.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/compat.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/completion.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/diagnostics.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/dispatch.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/fmt.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/help.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/stub.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/typed-results.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/guides/usage-dsl.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/check.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/cli.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/compat.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/completion.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/config-templates.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/dispatch.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/docopt.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/examples.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/exceptions.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/fmt.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/grammar.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/overview.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/reference/stub.md +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/docs/stylesheets/extra.css +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/cli.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/completion.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/dispatch.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/layered.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/naval_fate.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/rich_help.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/round_trip.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/examples/typed.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/fuzz/docopt.dict +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/fuzz/fuzz_docopt.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/mkdocs.yml +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/__init__.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/__main__.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_compat.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_completion.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_core.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_diagnostics.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_errors.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_fmt.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_format.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_generate.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_help.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_lint.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_spellcheck.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_stub.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/_typed.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/hypothesis.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/src/docopt2/py.typed +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/_strategies.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/_vendor/docopt_original.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/conftest.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/corpus/awkward_but_legal.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/corpus/layered_config.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/corpus/many_flags.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/corpus/naval_fate.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/corpus/nested_subcommands.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/golden/completion_bash.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/golden/completion_fish.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/golden/completion_nushell.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/golden/completion_powershell.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/golden/completion_zsh.txt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_choices.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_cli.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_coercion_diagnostics.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_compat.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_completion_vs_docopt.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_config.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_config_template.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_core_extra.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_diagnostics.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_dispatch_and_completion.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_divergences.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_docopt_original.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_docs_blocks.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_env.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_examples.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_extended_features.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_fmt.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_format.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_generate.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_getting_started.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_grammar_examples.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_help.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_hypothesis.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_import_cost.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_language_agnostic.docopt +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_lint.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_near_miss.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_package.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_parse_tree.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_property_coercion.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_property_config.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_property_config_template.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_property_extended.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_pydantic.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_readme.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_screenshots.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_source.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_spellcheck.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_stub.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/test_typed_api.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tests/typing_surface/check_typed.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tools/mutation_report.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tools/regolden.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tools/screenshots.py +0 -0
- {docopt2-1.2.0 → docopt2-1.2.1}/tools/shell_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docopt2
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
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
|
|
@@ -53,7 +53,7 @@ Description-Content-Type: text/markdown
|
|
|
53
53
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/pyversions/docopt2" alt="Python"></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/concepts/design-boundaries
|
|
56
|
+
<a href="https://solganis.github.io/docopt2/concepts/design-boundaries/#zero-runtime-dependencies"><img src="https://img.shields.io/badge/runtime%20deps-0-2ea043" alt="zero runtime dependencies, pydantic support optional"></a>
|
|
57
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
58
|
<a href="https://solganis.github.io/docopt2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
59
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>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<a href="https://pypi.org/project/docopt2/"><img src="https://img.shields.io/pypi/pyversions/docopt2" alt="Python"></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/concepts/design-boundaries
|
|
20
|
+
<a href="https://solganis.github.io/docopt2/concepts/design-boundaries/#zero-runtime-dependencies"><img src="https://img.shields.io/badge/runtime%20deps-0-2ea043" alt="zero runtime dependencies, pydantic support optional"></a>
|
|
21
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
22
|
<a href="https://solganis.github.io/docopt2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
23
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>
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
pip install docopt2
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
docopt2 has zero runtime dependencies:
|
|
10
|
-
outside the Python standard library.
|
|
9
|
+
docopt2 has zero runtime dependencies: the core needs nothing outside the Python standard library.
|
|
11
10
|
pydantic support is reflective and optional (`docopt2[pydantic]`).
|
|
12
11
|
|
|
13
12
|
!!! tip "Coming from the original docopt?"
|
|
@@ -66,9 +66,9 @@ checks its happy paths.
|
|
|
66
66
|
```python
|
|
67
67
|
generate_examples(doc, count=4, valid=False, seed=7)
|
|
68
68
|
# [['mine', 'set', 'v1', 'v2', '--unknown'],
|
|
69
|
-
# ['--help', '--unknown'],
|
|
70
69
|
# ['ship', 'new', 'v3', '--unknown'],
|
|
71
|
-
# ['ship', 'new', 'v4', 'v5', '--unknown']
|
|
70
|
+
# ['ship', 'new', 'v4', 'v5', '--unknown'],
|
|
71
|
+
# ['ship', 'new', 'v6', '--unknown']]
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Every one of these raises [`DocoptExit`](../reference/exceptions.md) when passed to `docopt`.
|
|
@@ -41,8 +41,8 @@ typed. The canonical form is fixed and predictable:
|
|
|
41
41
|
|
|
42
42
|
A result that no usage pattern can reproduce raises `ValueError` rather than return a wrong argv. That is a
|
|
43
43
|
hand-built or inconsistent mapping, or a degenerate grammar in which one value is reachable through
|
|
44
|
-
differently-shaped positions (`(
|
|
45
|
-
|
|
44
|
+
differently-shaped positions (`(-a | -b)...`, `[<name>] <path> <name>`) - a shape whose argv is genuinely
|
|
45
|
+
ambiguous, so there is no single canonical form to return.
|
|
46
46
|
|
|
47
47
|
## What it is for
|
|
48
48
|
|
|
@@ -11,10 +11,9 @@ it corrects three of the original's parsing bugs
|
|
|
11
11
|
Diagnostics come with it. Everything else - typed results, linting, stubs, completion, dispatch - is opt-in.
|
|
12
12
|
|
|
13
13
|
!!! note "Zero dependencies"
|
|
14
|
-
The core
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
only when you pass a pydantic model as a schema; the Hypothesis strategy needs `docopt2[hypothesis]`.
|
|
14
|
+
The core runs on the standard library alone - nothing to install, nothing to audit. pydantic and
|
|
15
|
+
Hypothesis are optional extras (`docopt2[pydantic]`, `docopt2[hypothesis]`), used only when you pass a
|
|
16
|
+
pydantic model as a schema or reach for the Hypothesis strategy.
|
|
18
17
|
|
|
19
18
|
## Why docopt2
|
|
20
19
|
|
|
@@ -28,10 +28,13 @@ ErrorType: TypeAlias = type[DocoptExit] | type[DocoptLanguageError]
|
|
|
28
28
|
# (the budget below), because this per-node cap alone still compounds to minutes on a malformed pattern.
|
|
29
29
|
MATCH_LIMIT = 200_000
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
31
|
+
# The global budget bounds docopt()'s whole match with one shared ceiling: an adversarial or malformed
|
|
32
|
+
# pattern, whose `_combine` DFS backtracks exponentially when no complete match exists, rejects in bounded
|
|
33
|
+
# time. It stays a small FIXED number because `Either.matches` yields lazily (greedy-first, materializing its
|
|
34
|
+
# sorted tail only if a caller backtracks past it): an honest match threads a single greedy path - a few
|
|
35
|
+
# hundred descents even for a long `<files>...` argv - so it never approaches the ceiling. Only a search with
|
|
36
|
+
# no complete match, forced to explore its full 2**n fan, reaches it. (An EAGER Either that materialized every
|
|
37
|
+
# outcome to sort them made an honest match's descents grow with argv and false-rejected a real 200-file CLI.)
|
|
35
38
|
_match_budget: contextvars.ContextVar[list[int] | None] = contextvars.ContextVar("docopt2_match_budget", default=None)
|
|
36
39
|
|
|
37
40
|
|
|
@@ -66,9 +69,10 @@ def _spend_budget() -> None:
|
|
|
66
69
|
|
|
67
70
|
Charged per ``_combine`` descent - the exponential engine - so a DFS that dead-ends WITHOUT yielding
|
|
68
71
|
(many optionals before an unmatchable required tail explores 2**n branches, none of them an outcome) is
|
|
69
|
-
bounded too.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
bounded too. A genuine match is cheap because ``Either`` yields greedy-first without materializing its
|
|
73
|
+
fan (see :meth:`Either.matches`), so an honest match threads one greedy path - a few hundred descents even
|
|
74
|
+
over a long ``<files>...`` glob - while an adversarial pattern's 2**n fan-out reaches the cap. A no-op when
|
|
75
|
+
no budget is set.
|
|
72
76
|
"""
|
|
73
77
|
budget = _match_budget.get()
|
|
74
78
|
if budget is not None:
|
|
@@ -484,10 +488,28 @@ class Either(BranchPattern):
|
|
|
484
488
|
"""Exactly one branch must match; the one leaving the fewest leaves wins."""
|
|
485
489
|
|
|
486
490
|
def matches(self, left: list[Pattern], collected: list[Pattern]) -> Iterator[MatchOutcome]:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
# the
|
|
490
|
-
|
|
491
|
+
# Greedy-first, lazily. Each branch yields its own fewest-left outcome first, so the global
|
|
492
|
+
# fewest-left - the greedy result the matcher wants - is the min over the branches' first outcomes.
|
|
493
|
+
# Yield it WITHOUT materializing the rest: a caller that takes only the greedy outcome (the common
|
|
494
|
+
# case, and every complete match) stops here, so an honest match never builds the whole fan. The
|
|
495
|
+
# eager `sorted(list(all))` this replaces made that fan grow with argv and false-rejected long CLIs.
|
|
496
|
+
streams = [child.matches(left, collected) for child in self.children]
|
|
497
|
+
firsts = [next(stream, None) for stream in streams]
|
|
498
|
+
live = [index for index, outcome in enumerate(firsts) if outcome is not None]
|
|
499
|
+
if not live:
|
|
500
|
+
return
|
|
501
|
+
best_index = min(live, key=lambda index: len(cast("MatchOutcome", firsts[index])[0]))
|
|
502
|
+
yield cast("MatchOutcome", firsts[best_index])
|
|
503
|
+
|
|
504
|
+
def _rest() -> Iterator[MatchOutcome]:
|
|
505
|
+
# Every outcome except the one already yielded, in the branch order the eager version listed
|
|
506
|
+
# them, so `[best] + sorted(_rest())` reproduces `sorted(all)` exactly - tie order included.
|
|
507
|
+
for index, (first, stream) in enumerate(zip(firsts, streams, strict=True)):
|
|
508
|
+
if first is not None and index != best_index:
|
|
509
|
+
yield first
|
|
510
|
+
yield from stream
|
|
511
|
+
|
|
512
|
+
yield from sorted(_bounded(_rest()), key=lambda outcome: len(outcome[0]))
|
|
491
513
|
|
|
492
514
|
|
|
493
515
|
def _accumulate_occurrences(node: Pattern, counts: dict[Pattern, int], multiplier: int) -> None:
|
|
@@ -163,6 +163,32 @@ def test_an_ambiguous_alternation_branch_choice_stays_compatible(doc, argv):
|
|
|
163
163
|
_assert_compatible(doc, argv)
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
# A DEEP but honest argv - the kind a CI invocation produces, not an adversary: two options repeated many
|
|
167
|
+
# times plus a long variadic file list. An EAGER `Either` that materialized its whole outcome fan to sort it
|
|
168
|
+
# made an honest match's cost grow with argv, so a real 20-flag, 200-file run reached the match budget and
|
|
169
|
+
# was falsely rejected (1.1.3-1.2.0). `Either` now yields greedy-first lazily, threading one greedy path, so
|
|
170
|
+
# vanilla-accepted argvs stay accepted. Sizes exceed the old fixed budget - the regression's own trigger.
|
|
171
|
+
_DEEP_DOC = """Usage:
|
|
172
|
+
prog run [--verbose] [--jobs=<n>] [--tag=<t>]... [--exclude=<e>]... <suite> <files>...
|
|
173
|
+
prog report (html | json) [--out=<p>]
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _deep_argv(tags: int, excludes: int, files: int) -> list[str]:
|
|
178
|
+
return [
|
|
179
|
+
"run",
|
|
180
|
+
*[f"--tag=t{index}" for index in range(tags)],
|
|
181
|
+
*[f"--exclude=e{index}" for index in range(excludes)],
|
|
182
|
+
"smoke",
|
|
183
|
+
*[f"f{index}" for index in range(files)],
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.parametrize(("tags", "excludes", "files"), [(20, 20, 150), (30, 10, 200), (12, 12, 300)])
|
|
188
|
+
def test_a_deep_honest_argv_is_not_falsely_rejected(tags, excludes, files):
|
|
189
|
+
_assert_compatible(_DEEP_DOC, _deep_argv(tags, excludes, files))
|
|
190
|
+
|
|
191
|
+
|
|
166
192
|
# A PRISTINE oracle: the module above is deliberately patched with docopt2's two fixes, so it can no
|
|
167
193
|
# longer see them. These tests need the original as shipped.
|
|
168
194
|
_pristine_spec = importlib.util.spec_from_file_location("docopt_pristine", _oracle_path)
|
|
@@ -173,3 +173,17 @@ def test_an_ambiguous_pattern_does_not_hang_on_a_long_argv():
|
|
|
173
173
|
with pytest.raises(DocoptExit):
|
|
174
174
|
docopt2.docopt(doc, argv, help=False)
|
|
175
175
|
assert_that(time.perf_counter() - start).described_as("matching a 2**24-ambiguous argv").is_less_than(3.0)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_an_exponential_pattern_with_a_long_argv_still_rejects_in_bounded_time():
|
|
179
|
+
# The subtler cousin: many optional flags before a MISSING required option, then a long file tail. The
|
|
180
|
+
# optionals' 2**n dead-ends fail at the missing option before they reach the files, so the fan is
|
|
181
|
+
# independent of the argv - but the long argv would inflate an argv-scaled budget past that fan and let it
|
|
182
|
+
# run for tens of seconds. The whole match stays on ONE fixed ceiling, so it rejects fast regardless.
|
|
183
|
+
flags = " ".join(f"[-{chr(97 + index)}]" for index in range(24))
|
|
184
|
+
doc = f"usage: prog {flags} --need <files>..."
|
|
185
|
+
argv = [f"-{chr(97 + index)}" for index in range(24)] + [f"f{index}" for index in range(300)]
|
|
186
|
+
start = time.perf_counter()
|
|
187
|
+
with pytest.raises(DocoptExit):
|
|
188
|
+
docopt2.docopt(doc, argv, help=False)
|
|
189
|
+
assert_that(time.perf_counter() - start).described_as("an exponential pattern with a long argv").is_less_than(3.0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|