wry 0.1.10.dev4__tar.gz → 0.2.1.dev1__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.
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/PKG-INFO +61 -46
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/README.md +46 -17
- wry-0.2.1.dev1/pyproject.toml +152 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/__init__.py +5 -22
- wry-0.2.1.dev1/wry/_version.py +14 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/auto_model.py +56 -49
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/click_integration.py +12 -2
- wry-0.1.10.dev4/.github/workflows/check-requirements.yml +0 -32
- wry-0.1.10.dev4/.github/workflows/ci-cd.yml +0 -227
- wry-0.1.10.dev4/.gitignore +0 -142
- wry-0.1.10.dev4/.markdownlint.json +0 -3
- wry-0.1.10.dev4/.pre-commit-config.yaml +0 -52
- wry-0.1.10.dev4/CHANGELOG.md +0 -184
- wry-0.1.10.dev4/RELEASE_PROCESS.md +0 -80
- wry-0.1.10.dev4/TODO.md +0 -104
- wry-0.1.10.dev4/check.sh +0 -3
- wry-0.1.10.dev4/examples/auto_instantiate_edge_cases.py +0 -165
- wry-0.1.10.dev4/examples/auto_instantiate_poc.py +0 -330
- wry-0.1.10.dev4/examples/auto_model_example.py +0 -187
- wry-0.1.10.dev4/examples/config.json +0 -6
- wry-0.1.10.dev4/examples/intermediate_example.py +0 -89
- wry-0.1.10.dev4/examples/multi_model_example.py +0 -171
- wry-0.1.10.dev4/examples/simple_cli.py +0 -55
- wry-0.1.10.dev4/examples/source_tracking_example.py +0 -102
- wry-0.1.10.dev4/pyproject.toml +0 -140
- wry-0.1.10.dev4/requirements-dev.txt +0 -28
- wry-0.1.10.dev4/scripts/README.md +0 -23
- wry-0.1.10.dev4/scripts/extract_release_notes.py +0 -112
- wry-0.1.10.dev4/scripts/test_all_versions.sh +0 -86
- wry-0.1.10.dev4/scripts/test_ci_locally.sh +0 -71
- wry-0.1.10.dev4/scripts/test_with_act.sh +0 -36
- wry-0.1.10.dev4/scripts/update-requirements.sh +0 -59
- wry-0.1.10.dev4/setup.cfg +0 -4
- wry-0.1.10.dev4/tests/README.md +0 -65
- wry-0.1.10.dev4/tests/__init__.py +0 -1
- wry-0.1.10.dev4/tests/features/__init__.py +0 -1
- wry-0.1.10.dev4/tests/features/test_auto_model.py +0 -253
- wry-0.1.10.dev4/tests/features/test_multi_model.py +0 -304
- wry-0.1.10.dev4/tests/features/test_source_precedence.py +0 -302
- wry-0.1.10.dev4/tests/integration/__init__.py +0 -1
- wry-0.1.10.dev4/tests/integration/test_click_edge_cases.py +0 -171
- wry-0.1.10.dev4/tests/integration/test_click_integration.py +0 -217
- wry-0.1.10.dev4/tests/integration/test_click_integration_extended.py +0 -483
- wry-0.1.10.dev4/tests/integration/test_context_handling.py +0 -124
- wry-0.1.10.dev4/tests/unit/__init__.py +0 -1
- wry-0.1.10.dev4/tests/unit/auto_model/__init__.py +0 -0
- wry-0.1.10.dev4/tests/unit/auto_model/test_auto_model_annotation_inference.py +0 -47
- wry-0.1.10.dev4/tests/unit/auto_model/test_auto_model_field_processing.py +0 -64
- wry-0.1.10.dev4/tests/unit/auto_model/test_field_annotation_handling.py +0 -64
- wry-0.1.10.dev4/tests/unit/auto_model/test_field_annotations.py +0 -172
- wry-0.1.10.dev4/tests/unit/auto_model/test_type_inference.py +0 -70
- wry-0.1.10.dev4/tests/unit/click/README_TESTING_STRATEGY.md +0 -50
- wry-0.1.10.dev4/tests/unit/click/__init__.py +0 -0
- wry-0.1.10.dev4/tests/unit/click/test_bool_flag_handling.py +0 -58
- wry-0.1.10.dev4/tests/unit/click/test_click_config_building.py +0 -51
- wry-0.1.10.dev4/tests/unit/click/test_click_constraint_formatting.py +0 -90
- wry-0.1.10.dev4/tests/unit/click/test_click_decorator_edge_cases.py +0 -81
- wry-0.1.10.dev4/tests/unit/click/test_click_interval_constraints.py +0 -45
- wry-0.1.10.dev4/tests/unit/click/test_click_lambda_parsing.py +0 -56
- wry-0.1.10.dev4/tests/unit/click/test_click_length_constraints.py +0 -47
- wry-0.1.10.dev4/tests/unit/click/test_click_parameter_generation.py +0 -108
- wry-0.1.10.dev4/tests/unit/click/test_click_predicate_handling.py +0 -69
- wry-0.1.10.dev4/tests/unit/click/test_closure_extraction_errors.py +0 -94
- wry-0.1.10.dev4/tests/unit/click/test_closure_handling.py +0 -82
- wry-0.1.10.dev4/tests/unit/click/test_constraint_behavior.py +0 -113
- wry-0.1.10.dev4/tests/unit/click/test_constraint_edge_cases.py +0 -84
- wry-0.1.10.dev4/tests/unit/click/test_env_vars_option.py +0 -70
- wry-0.1.10.dev4/tests/unit/click/test_json_config_loading.py +0 -102
- wry-0.1.10.dev4/tests/unit/click/test_lambda_behavior.py +0 -85
- wry-0.1.10.dev4/tests/unit/click/test_lambda_error_handling.py +0 -62
- wry-0.1.10.dev4/tests/unit/click/test_predicate_source_errors.py +0 -46
- wry-0.1.10.dev4/tests/unit/click/test_strict_mode_errors.py +0 -81
- wry-0.1.10.dev4/tests/unit/click/test_type_handling.py +0 -87
- wry-0.1.10.dev4/tests/unit/core/__init__.py +0 -0
- wry-0.1.10.dev4/tests/unit/core/test_accessors.py +0 -69
- wry-0.1.10.dev4/tests/unit/core/test_advanced_features.py +0 -518
- wry-0.1.10.dev4/tests/unit/core/test_core.py +0 -176
- wry-0.1.10.dev4/tests/unit/core/test_edge_cases.py +0 -291
- wry-0.1.10.dev4/tests/unit/core/test_env_utils.py +0 -73
- wry-0.1.10.dev4/tests/unit/core/test_field_constraint_extraction.py +0 -65
- wry-0.1.10.dev4/tests/unit/core/test_field_utils.py +0 -107
- wry-0.1.10.dev4/tests/unit/core/test_field_utils_edge_cases.py +0 -66
- wry-0.1.10.dev4/tests/unit/core/test_sources.py +0 -32
- wry-0.1.10.dev4/tests/unit/core/test_type_checking_blocks.py +0 -56
- wry-0.1.10.dev4/tests/unit/model/__init__.py +0 -0
- wry-0.1.10.dev4/tests/unit/model/test_accessor_caching.py +0 -78
- wry-0.1.10.dev4/tests/unit/model/test_extract_edge_cases.py +0 -118
- wry-0.1.10.dev4/tests/unit/model/test_model_click_context_handling.py +0 -123
- wry-0.1.10.dev4/tests/unit/model/test_model_data_extraction.py +0 -96
- wry-0.1.10.dev4/tests/unit/model/test_model_default_handling.py +0 -74
- wry-0.1.10.dev4/tests/unit/model/test_model_environment_integration.py +0 -66
- wry-0.1.10.dev4/tests/unit/model/test_model_extract_subset_edge_cases.py +0 -116
- wry-0.1.10.dev4/tests/unit/model/test_model_extraction_methods.py +0 -88
- wry-0.1.10.dev4/tests/unit/model/test_model_field_errors.py +0 -54
- wry-0.1.10.dev4/tests/unit/model/test_model_object_extraction.py +0 -102
- wry-0.1.10.dev4/tests/unit/model/test_non_dict_object_extraction.py +0 -84
- wry-0.1.10.dev4/tests/unit/model/test_object_attribute_extraction.py +0 -110
- wry-0.1.10.dev4/tests/unit/multi_model/__init__.py +0 -0
- wry-0.1.10.dev4/tests/unit/multi_model/test_multi_model.py +0 -48
- wry-0.1.10.dev4/tests/unit/multi_model/test_type_checking.py +0 -24
- wry-0.1.10.dev4/tests/unit/test_auto_model_field_processing.py +0 -64
- wry-0.1.10.dev4/tests/unit/test_comprehensive_imports.py +0 -71
- wry-0.1.10.dev4/tests/unit/test_generate_click_classmethod.py +0 -47
- wry-0.1.10.dev4/tests/unit/test_init.py +0 -90
- wry-0.1.10.dev4/tests/unit/test_init_edge_cases.py +0 -123
- wry-0.1.10.dev4/tests/unit/test_init_version_edge_cases.py +0 -64
- wry-0.1.10.dev4/tests/unit/test_model_extraction_methods.py +0 -88
- wry-0.1.10.dev4/tests/unit/test_multiple_option_bug.py +0 -337
- wry-0.1.10.dev4/tests/unit/test_type_checking_imports.py +0 -49
- wry-0.1.10.dev4/tests/unit/test_variadic_argument_bug.py +0 -154
- wry-0.1.10.dev4/tests/unit/test_version_fallback.py +0 -60
- wry-0.1.10.dev4/tests/unit/test_version_parsing.py +0 -62
- wry-0.1.10.dev4/wry/_version.py +0 -34
- wry-0.1.10.dev4/wry.egg-info/PKG-INFO +0 -697
- wry-0.1.10.dev4/wry.egg-info/SOURCES.txt +0 -123
- wry-0.1.10.dev4/wry.egg-info/dependency_links.txt +0 -1
- wry-0.1.10.dev4/wry.egg-info/requires.txt +0 -22
- wry-0.1.10.dev4/wry.egg-info/top_level.txt +0 -1
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/LICENSE +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/core/__init__.py +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/core/accessors.py +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/core/env_utils.py +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/core/field_utils.py +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/core/model.py +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/core/sources.py +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/multi_model.py +0 -0
- {wry-0.1.10.dev4 → wry-0.2.1.dev1}/wry/py.typed +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wry
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.2.1.dev1
|
|
4
4
|
Summary: Why Repeat Yourself? - Define your CLI once with Pydantic models
|
|
5
|
-
Author-email: Tyler House <26489166+tahouse@users.noreply.github.com>
|
|
6
5
|
License: MIT
|
|
7
|
-
|
|
8
|
-
Project-URL: Repository, https://github.com/tahouse/wry
|
|
9
|
-
Project-URL: Issues, https://github.com/tahouse/wry/issues
|
|
10
|
-
Project-URL: Documentation, https://github.com/tahouse/wry#readme
|
|
6
|
+
License-File: LICENSE
|
|
11
7
|
Keywords: cli,pydantic,click,wry,dry,configuration,type-safe
|
|
8
|
+
Author: Tyler House
|
|
9
|
+
Author-email: 26489166+tahouse@users.noreply.github.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
12
11
|
Classifier: Development Status :: 1 - Planning
|
|
13
12
|
Classifier: Intended Audience :: Developers
|
|
14
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -16,33 +15,19 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
21
|
Classifier: Topic :: System :: System Shells
|
|
21
22
|
Classifier: Topic :: Utilities
|
|
22
|
-
Requires-
|
|
23
|
+
Requires-Dist: annotated-types (>=0.6.0,<0.7.0)
|
|
24
|
+
Requires-Dist: click (>=8.0,<9.0)
|
|
25
|
+
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
26
|
+
Requires-Dist: pydantic-core (>=2.23.4,<3.0.0)
|
|
27
|
+
Project-URL: Documentation, https://github.com/tahouse/wry#readme
|
|
28
|
+
Project-URL: Homepage, https://github.com/tahouse/wry
|
|
29
|
+
Project-URL: Repository, https://github.com/tahouse/wry
|
|
23
30
|
Description-Content-Type: text/markdown
|
|
24
|
-
License-File: LICENSE
|
|
25
|
-
Requires-Dist: click>=8.0
|
|
26
|
-
Requires-Dist: pydantic>=2.9.2
|
|
27
|
-
Requires-Dist: annotated-types>=0.5.0
|
|
28
|
-
Requires-Dist: pydantic-core>=2.23.4
|
|
29
|
-
Provides-Extra: dev
|
|
30
|
-
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
31
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
-
Requires-Dist: pytest-xdist; extra == "dev"
|
|
33
|
-
Requires-Dist: ruff; extra == "dev"
|
|
34
|
-
Requires-Dist: mypy>=1.17.1; extra == "dev"
|
|
35
|
-
Requires-Dist: build; extra == "dev"
|
|
36
|
-
Requires-Dist: twine; extra == "dev"
|
|
37
|
-
Requires-Dist: setuptools-scm>=8.0; extra == "dev"
|
|
38
|
-
Requires-Dist: pre-commit; extra == "dev"
|
|
39
|
-
Requires-Dist: safety; extra == "dev"
|
|
40
|
-
Requires-Dist: bandit[toml]; extra == "dev"
|
|
41
|
-
Provides-Extra: test
|
|
42
|
-
Requires-Dist: pytest>=6.0; extra == "test"
|
|
43
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
44
|
-
Requires-Dist: pytest-xdist; extra == "test"
|
|
45
|
-
Dynamic: license-file
|
|
46
31
|
|
|
47
32
|
# wry - Why Repeat Yourself? CLI
|
|
48
33
|
|
|
@@ -378,11 +363,14 @@ cd wry
|
|
|
378
363
|
python -m venv venv
|
|
379
364
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
380
365
|
|
|
381
|
-
# Install
|
|
382
|
-
pip install
|
|
366
|
+
# Install Poetry (if not already installed)
|
|
367
|
+
pip install poetry
|
|
368
|
+
|
|
369
|
+
# Install all dependencies in development mode
|
|
370
|
+
poetry install
|
|
383
371
|
|
|
384
372
|
# Install pre-commit hooks
|
|
385
|
-
pre-commit install
|
|
373
|
+
poetry run pre-commit install
|
|
386
374
|
```
|
|
387
375
|
|
|
388
376
|
### Running Tests
|
|
@@ -443,7 +431,8 @@ To ensure consistent behavior between local development and CI:
|
|
|
443
431
|
Install the exact versions used in CI:
|
|
444
432
|
|
|
445
433
|
```bash
|
|
446
|
-
|
|
434
|
+
poetry update
|
|
435
|
+
poetry install
|
|
447
436
|
```
|
|
448
437
|
|
|
449
438
|
### Coverage Requirements
|
|
@@ -528,17 +517,43 @@ We welcome contributions! Please follow these guidelines to ensure a smooth proc
|
|
|
528
517
|
|
|
529
518
|
### Development Setup
|
|
530
519
|
|
|
531
|
-
|
|
520
|
+
We use Poetry for dependency management to ensure consistent environments:
|
|
532
521
|
|
|
533
522
|
```bash
|
|
534
|
-
# Install
|
|
535
|
-
|
|
523
|
+
# Install Poetry (if not already installed)
|
|
524
|
+
curl -sSL https://install.python-poetry.org | python3 -
|
|
525
|
+
|
|
526
|
+
# Install dependencies (including dev dependencies)
|
|
527
|
+
poetry install
|
|
536
528
|
|
|
537
|
-
#
|
|
538
|
-
|
|
529
|
+
# Activate the virtual environment
|
|
530
|
+
poetry shell
|
|
539
531
|
```
|
|
540
532
|
|
|
541
|
-
**Important**:
|
|
533
|
+
**Important**: Poetry automatically creates an isolated virtual environment and uses `poetry.lock` to ensure everyone has the exact same dependencies.
|
|
534
|
+
|
|
535
|
+
### Common Poetry Commands
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
# Install dependencies from lock file
|
|
539
|
+
poetry install
|
|
540
|
+
|
|
541
|
+
# Update dependencies within version constraints
|
|
542
|
+
poetry update
|
|
543
|
+
|
|
544
|
+
# Add a new dependency
|
|
545
|
+
poetry add package-name
|
|
546
|
+
|
|
547
|
+
# Add a development dependency
|
|
548
|
+
poetry add --group dev package-name
|
|
549
|
+
|
|
550
|
+
# Show installed packages
|
|
551
|
+
poetry show
|
|
552
|
+
|
|
553
|
+
# Run commands in the Poetry environment
|
|
554
|
+
poetry run python script.py
|
|
555
|
+
poetry run pytest
|
|
556
|
+
```
|
|
542
557
|
|
|
543
558
|
### Getting Started
|
|
544
559
|
|
|
@@ -567,10 +582,9 @@ pip install -e .
|
|
|
567
582
|
1. **Set up development environment**:
|
|
568
583
|
|
|
569
584
|
```bash
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
pre-commit install
|
|
585
|
+
poetry install # Creates venv and installs all dependencies
|
|
586
|
+
poetry shell # Activate the virtual environment
|
|
587
|
+
poetry run pre-commit install # Set up git hooks
|
|
574
588
|
```
|
|
575
589
|
|
|
576
590
|
2. **Make your changes**:
|
|
@@ -583,13 +597,13 @@ pip install -e .
|
|
|
583
597
|
|
|
584
598
|
```bash
|
|
585
599
|
# Run tests
|
|
586
|
-
pytest
|
|
600
|
+
poetry run pytest
|
|
587
601
|
|
|
588
602
|
# Check coverage (must be 100%)
|
|
589
|
-
pytest --cov=wry --cov-report=term-missing
|
|
603
|
+
poetry run pytest --cov=wry --cov-report=term-missing
|
|
590
604
|
|
|
591
605
|
# Run linting
|
|
592
|
-
pre-commit run --all-files
|
|
606
|
+
poetry run pre-commit run --all-files
|
|
593
607
|
```
|
|
594
608
|
|
|
595
609
|
4. **Commit your changes**:
|
|
@@ -695,3 +709,4 @@ If you're interested in this feature, please provide feedback!
|
|
|
695
709
|
- Built on top of [Click](https://click.palletsprojects.com/) and [Pydantic](https://pydantic-docs.helpmanual.io/)
|
|
696
710
|
- Inspired by the DRY (Don't Repeat Yourself) principle
|
|
697
711
|
We'd also like to acknowledgme `pydanclick`, which uses a similar clean syntax (no kwargs to command functions). The code for this feature will be independently written given that `wry` supports source tracking, constraint help text creation, instantiation from config files, and several other features not supported by `pydanclick`.
|
|
712
|
+
|
|
@@ -332,11 +332,14 @@ cd wry
|
|
|
332
332
|
python -m venv venv
|
|
333
333
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
334
334
|
|
|
335
|
-
# Install
|
|
336
|
-
pip install
|
|
335
|
+
# Install Poetry (if not already installed)
|
|
336
|
+
pip install poetry
|
|
337
|
+
|
|
338
|
+
# Install all dependencies in development mode
|
|
339
|
+
poetry install
|
|
337
340
|
|
|
338
341
|
# Install pre-commit hooks
|
|
339
|
-
pre-commit install
|
|
342
|
+
poetry run pre-commit install
|
|
340
343
|
```
|
|
341
344
|
|
|
342
345
|
### Running Tests
|
|
@@ -397,7 +400,8 @@ To ensure consistent behavior between local development and CI:
|
|
|
397
400
|
Install the exact versions used in CI:
|
|
398
401
|
|
|
399
402
|
```bash
|
|
400
|
-
|
|
403
|
+
poetry update
|
|
404
|
+
poetry install
|
|
401
405
|
```
|
|
402
406
|
|
|
403
407
|
### Coverage Requirements
|
|
@@ -482,17 +486,43 @@ We welcome contributions! Please follow these guidelines to ensure a smooth proc
|
|
|
482
486
|
|
|
483
487
|
### Development Setup
|
|
484
488
|
|
|
485
|
-
|
|
489
|
+
We use Poetry for dependency management to ensure consistent environments:
|
|
486
490
|
|
|
487
491
|
```bash
|
|
488
|
-
# Install
|
|
489
|
-
|
|
492
|
+
# Install Poetry (if not already installed)
|
|
493
|
+
curl -sSL https://install.python-poetry.org | python3 -
|
|
494
|
+
|
|
495
|
+
# Install dependencies (including dev dependencies)
|
|
496
|
+
poetry install
|
|
490
497
|
|
|
491
|
-
#
|
|
492
|
-
|
|
498
|
+
# Activate the virtual environment
|
|
499
|
+
poetry shell
|
|
493
500
|
```
|
|
494
501
|
|
|
495
|
-
**Important**:
|
|
502
|
+
**Important**: Poetry automatically creates an isolated virtual environment and uses `poetry.lock` to ensure everyone has the exact same dependencies.
|
|
503
|
+
|
|
504
|
+
### Common Poetry Commands
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
# Install dependencies from lock file
|
|
508
|
+
poetry install
|
|
509
|
+
|
|
510
|
+
# Update dependencies within version constraints
|
|
511
|
+
poetry update
|
|
512
|
+
|
|
513
|
+
# Add a new dependency
|
|
514
|
+
poetry add package-name
|
|
515
|
+
|
|
516
|
+
# Add a development dependency
|
|
517
|
+
poetry add --group dev package-name
|
|
518
|
+
|
|
519
|
+
# Show installed packages
|
|
520
|
+
poetry show
|
|
521
|
+
|
|
522
|
+
# Run commands in the Poetry environment
|
|
523
|
+
poetry run python script.py
|
|
524
|
+
poetry run pytest
|
|
525
|
+
```
|
|
496
526
|
|
|
497
527
|
### Getting Started
|
|
498
528
|
|
|
@@ -521,10 +551,9 @@ pip install -e .
|
|
|
521
551
|
1. **Set up development environment**:
|
|
522
552
|
|
|
523
553
|
```bash
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
pre-commit install
|
|
554
|
+
poetry install # Creates venv and installs all dependencies
|
|
555
|
+
poetry shell # Activate the virtual environment
|
|
556
|
+
poetry run pre-commit install # Set up git hooks
|
|
528
557
|
```
|
|
529
558
|
|
|
530
559
|
2. **Make your changes**:
|
|
@@ -537,13 +566,13 @@ pip install -e .
|
|
|
537
566
|
|
|
538
567
|
```bash
|
|
539
568
|
# Run tests
|
|
540
|
-
pytest
|
|
569
|
+
poetry run pytest
|
|
541
570
|
|
|
542
571
|
# Check coverage (must be 100%)
|
|
543
|
-
pytest --cov=wry --cov-report=term-missing
|
|
572
|
+
poetry run pytest --cov=wry --cov-report=term-missing
|
|
544
573
|
|
|
545
574
|
# Run linting
|
|
546
|
-
pre-commit run --all-files
|
|
575
|
+
poetry run pre-commit run --all-files
|
|
547
576
|
```
|
|
548
577
|
|
|
549
578
|
4. **Commit your changes**:
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "wry"
|
|
3
|
+
version = "0.2.1.dev1" # Placeholder - actual version comes from git tags via poetry-dynamic-versioning
|
|
4
|
+
description = "Why Repeat Yourself? - Define your CLI once with Pydantic models"
|
|
5
|
+
authors = ["Tyler House <26489166+tahouse@users.noreply.github.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
homepage = "https://github.com/tahouse/wry"
|
|
9
|
+
repository = "https://github.com/tahouse/wry"
|
|
10
|
+
documentation = "https://github.com/tahouse/wry#readme"
|
|
11
|
+
keywords = ["cli", "pydantic", "click", "wry", "dry", "configuration", "type-safe"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 1 - Planning",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Topic :: System :: System Shells",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
]
|
|
24
|
+
packages = [{include = "wry"}]
|
|
25
|
+
|
|
26
|
+
[tool.poetry.dependencies]
|
|
27
|
+
python = "^3.10"
|
|
28
|
+
click = "^8.0"
|
|
29
|
+
pydantic = "^2.9.2"
|
|
30
|
+
annotated-types = "^0.6.0"
|
|
31
|
+
pydantic-core = "^2.23.4"
|
|
32
|
+
|
|
33
|
+
[tool.poetry.group.dev.dependencies]
|
|
34
|
+
pytest = "^8.4.2"
|
|
35
|
+
pytest-cov = "^7.0.0"
|
|
36
|
+
pytest-xdist = "^3.8.0"
|
|
37
|
+
coverage = {extras = ["toml"], version = "^7.10.7"}
|
|
38
|
+
ruff = "^0.13.2"
|
|
39
|
+
mypy = "^1.18.2"
|
|
40
|
+
build = "^1.2.1"
|
|
41
|
+
twine = "^6.0.0"
|
|
42
|
+
pre-commit = "^3.8.0"
|
|
43
|
+
safety = "^3.2.3"
|
|
44
|
+
bandit = {extras = ["toml"], version = "^1.7.5"}
|
|
45
|
+
|
|
46
|
+
[tool.poetry-dynamic-versioning]
|
|
47
|
+
enable = false
|
|
48
|
+
vcs = "git"
|
|
49
|
+
style = "pep440"
|
|
50
|
+
pattern = "^v(?P<base>\\d+\\.\\d+\\.\\d+)"
|
|
51
|
+
format-jinja = """
|
|
52
|
+
{%- if distance == 0 -%}
|
|
53
|
+
{{ serialize_pep440(base, stage, revision) }}
|
|
54
|
+
{%- elif revision is not none -%}
|
|
55
|
+
{{ serialize_pep440(base, stage, revision + 1, dev=distance) }}
|
|
56
|
+
{%- else -%}
|
|
57
|
+
{{ serialize_pep440(bump_version(base), stage, revision, dev=distance) }}
|
|
58
|
+
{%- endif -%}
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
[tool.poetry-dynamic-versioning.substitution]
|
|
62
|
+
files = ["wry/_version.py"]
|
|
63
|
+
|
|
64
|
+
[build-system]
|
|
65
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
66
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
67
|
+
|
|
68
|
+
# Keep existing tool configurations
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
target-version = "py310"
|
|
71
|
+
line-length = 120
|
|
72
|
+
extend-exclude = [
|
|
73
|
+
"wry/_version.py", # Auto-generated by setuptools-scm
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = [
|
|
78
|
+
"E", # pycodestyle errors
|
|
79
|
+
"W", # pycodestyle warnings
|
|
80
|
+
"F", # pyflakes
|
|
81
|
+
"I", # isort
|
|
82
|
+
"B", # flake8-bugbear
|
|
83
|
+
"C4", # flake8-comprehensions
|
|
84
|
+
"UP", # pyupgrade
|
|
85
|
+
]
|
|
86
|
+
ignore = []
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.per-file-ignores]
|
|
89
|
+
# Allow lambdas and assert False in test files
|
|
90
|
+
"tests/**/*.py" = ["E731", "B011"]
|
|
91
|
+
# Allow unused variables in test files
|
|
92
|
+
"tests/**/test_*.py" = ["F841"]
|
|
93
|
+
|
|
94
|
+
[tool.mypy]
|
|
95
|
+
python_version = "3.10"
|
|
96
|
+
warn_return_any = true
|
|
97
|
+
warn_unused_configs = true
|
|
98
|
+
disallow_untyped_defs = true
|
|
99
|
+
disallow_incomplete_defs = true
|
|
100
|
+
check_untyped_defs = true
|
|
101
|
+
disallow_untyped_decorators = true
|
|
102
|
+
no_implicit_optional = true
|
|
103
|
+
warn_redundant_casts = true
|
|
104
|
+
warn_unused_ignores = true
|
|
105
|
+
warn_no_return = true
|
|
106
|
+
warn_unreachable = true
|
|
107
|
+
strict_optional = true
|
|
108
|
+
strict_equality = true
|
|
109
|
+
extra_checks = true
|
|
110
|
+
|
|
111
|
+
# Per-module options
|
|
112
|
+
[[tool.mypy.overrides]]
|
|
113
|
+
module = "tests.*"
|
|
114
|
+
disallow_untyped_defs = false
|
|
115
|
+
disallow_untyped_decorators = false
|
|
116
|
+
|
|
117
|
+
[tool.pytest.ini_options]
|
|
118
|
+
testpaths = ["tests"]
|
|
119
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
120
|
+
python_classes = ["Test*"]
|
|
121
|
+
python_functions = ["test_*"]
|
|
122
|
+
addopts = "-ra -q --strict-markers --cov=wry --cov-report=term-missing --cov-report=html --cov-report=xml"
|
|
123
|
+
markers = [
|
|
124
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
125
|
+
"integration: marks tests as integration tests",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
[tool.coverage.run]
|
|
129
|
+
branch = true
|
|
130
|
+
source = ["wry"]
|
|
131
|
+
omit = [
|
|
132
|
+
"tests/*",
|
|
133
|
+
"wry/_version.py",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[tool.coverage.report]
|
|
137
|
+
exclude_lines = [
|
|
138
|
+
"pragma: no cover",
|
|
139
|
+
"def __repr__",
|
|
140
|
+
"if self.debug:",
|
|
141
|
+
"if TYPE_CHECKING:",
|
|
142
|
+
"raise AssertionError",
|
|
143
|
+
"raise NotImplementedError",
|
|
144
|
+
"if __name__ == .__main__.:",
|
|
145
|
+
"class .*\\bProtocol\\):",
|
|
146
|
+
"@(abc\\.)?abstractmethod",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
[tool.bandit]
|
|
150
|
+
targets = ["wry"]
|
|
151
|
+
exclude_dirs = ["tests", "examples"]
|
|
152
|
+
skips = ["B101"] # assert_used - needed for type checking
|
|
@@ -46,7 +46,7 @@ Example:
|
|
|
46
46
|
Coming soon - this package is under active development.
|
|
47
47
|
"""
|
|
48
48
|
|
|
49
|
-
# Version is managed by
|
|
49
|
+
# Version is managed by poetry-dynamic-versioning from git tags
|
|
50
50
|
try:
|
|
51
51
|
from ._version import __commit_id__, __version__
|
|
52
52
|
|
|
@@ -55,27 +55,10 @@ try:
|
|
|
55
55
|
if __commit_id__:
|
|
56
56
|
__version_full__ += f"+{__commit_id__}"
|
|
57
57
|
except ImportError:
|
|
58
|
-
# Fallback for development
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
__version__ = get_version(root="../..", relative_to=__file__)
|
|
64
|
-
# Extract commit hash from version if present
|
|
65
|
-
if "+" in __version__ and "g" in __version__:
|
|
66
|
-
# Version like "0.0.2+g1234567" or "0.0.2.dev1+g1234567"
|
|
67
|
-
__commit_id__ = __version__.split("+")[-1].split(".")[0]
|
|
68
|
-
__version_full__ = __version__
|
|
69
|
-
else:
|
|
70
|
-
__commit_id__ = None
|
|
71
|
-
__version_full__ = __version__
|
|
72
|
-
# Clean version for consistency
|
|
73
|
-
__version__ = __version__.split("+")[0]
|
|
74
|
-
except Exception:
|
|
75
|
-
# Ultimate fallback
|
|
76
|
-
__version__ = "0.0.1-dev"
|
|
77
|
-
__version_full__ = __version__
|
|
78
|
-
__commit_id__ = None
|
|
58
|
+
# Fallback for development when _version.py is not generated
|
|
59
|
+
__version__ = "0.0.0"
|
|
60
|
+
__version_full__ = __version__
|
|
61
|
+
__commit_id__ = None
|
|
79
62
|
|
|
80
63
|
__author__ = "Tyler House"
|
|
81
64
|
__email__ = "26489166+tahouse@users.noreply.github.com"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# file generated by poetry-dynamic-versioning
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
__version__: str = "0.2.1.dev1" # This will be replaced during build
|
|
5
|
+
__version_tuple__: tuple[int, ...] = (0, 2, 1, "dev1") # This will be replaced during build
|
|
6
|
+
__commit_id__: str | None = None # This will be replaced during build
|
|
7
|
+
|
|
8
|
+
# For compatibility with setuptools-scm test expectations
|
|
9
|
+
VERSION_TUPLE = __version_tuple__
|
|
10
|
+
COMMIT_ID = __commit_id__
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
# Version will be injected by poetry-dynamic-versioning
|
|
14
|
+
pass
|
|
@@ -42,66 +42,73 @@ class AutoWryModel(WryModel):
|
|
|
42
42
|
"""Automatically add AutoOption to all unannotated fields."""
|
|
43
43
|
super().__init_subclass__(**kwargs)
|
|
44
44
|
|
|
45
|
+
# Skip if we've already processed this class
|
|
46
|
+
if hasattr(cls, "_autowrymodel_processed"):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Mark this class as processed
|
|
50
|
+
cls._autowrymodel_processed = True # type: ignore[attr-defined]
|
|
51
|
+
|
|
45
52
|
# Process annotations to add AutoOption where needed
|
|
46
53
|
if not hasattr(cls, "__annotations__"):
|
|
47
54
|
cls.__annotations__ = {}
|
|
48
55
|
|
|
49
|
-
#
|
|
50
|
-
for attr_name in
|
|
56
|
+
# Process all annotations to add AutoOption where needed
|
|
57
|
+
for attr_name, annotation in cls.__annotations__.copy().items():
|
|
51
58
|
if attr_name.startswith("_"):
|
|
52
59
|
continue
|
|
53
60
|
|
|
61
|
+
# Check if it's already Annotated
|
|
62
|
+
origin = get_origin(annotation)
|
|
63
|
+
# Compare using string representation to handle module reload scenarios
|
|
64
|
+
if origin is not None and str(origin) == "<class 'typing.Annotated'>":
|
|
65
|
+
# Check if it has any Click-related metadata
|
|
66
|
+
metadata = get_args(annotation)[1:]
|
|
67
|
+
has_click_metadata = any(
|
|
68
|
+
# Check for AutoClickParameter enums
|
|
69
|
+
isinstance(m, AutoClickParameter)
|
|
70
|
+
or
|
|
71
|
+
# Check for Click decorators
|
|
72
|
+
(hasattr(m, "__module__") and "click" in str(m.__module__))
|
|
73
|
+
for m in metadata
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if has_click_metadata:
|
|
77
|
+
# Already has Click configuration, skip
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Add AutoOption to existing annotation
|
|
81
|
+
base_type = get_args(annotation)[0]
|
|
82
|
+
# For Python 3.10 compatibility, we need to reconstruct manually
|
|
83
|
+
# Create a new annotation with AutoOption prepended to existing metadata
|
|
84
|
+
if not metadata:
|
|
85
|
+
cls.__annotations__[attr_name] = Annotated[base_type, AutoClickParameter.OPTION]
|
|
86
|
+
elif len(metadata) == 1:
|
|
87
|
+
cls.__annotations__[attr_name] = Annotated[base_type, AutoClickParameter.OPTION, metadata[0]]
|
|
88
|
+
elif len(metadata) == 2:
|
|
89
|
+
cls.__annotations__[attr_name] = Annotated[
|
|
90
|
+
base_type, AutoClickParameter.OPTION, metadata[0], metadata[1]
|
|
91
|
+
]
|
|
92
|
+
else:
|
|
93
|
+
# For more metadata, we skip adding AutoOption to avoid complexity
|
|
94
|
+
# This is a rare case and the field will still work
|
|
95
|
+
pass
|
|
96
|
+
else:
|
|
97
|
+
# Not annotated, add AutoOption
|
|
98
|
+
cls.__annotations__[attr_name] = Annotated[annotation, AutoClickParameter.OPTION]
|
|
99
|
+
|
|
100
|
+
# Also process fields that are defined with Field() but not in annotations
|
|
101
|
+
for attr_name in dir(cls):
|
|
102
|
+
if attr_name.startswith("_") or attr_name in cls.__annotations__:
|
|
103
|
+
continue
|
|
104
|
+
|
|
54
105
|
attr_value = getattr(cls, attr_name)
|
|
55
106
|
|
|
56
107
|
# Check if it's a field
|
|
57
108
|
if isinstance(attr_value, FieldInfo):
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# Check if it's already Annotated
|
|
63
|
-
origin = get_origin(annotation)
|
|
64
|
-
if origin is not None and str(origin) == str(Annotated):
|
|
65
|
-
# Check if it has any Click-related metadata
|
|
66
|
-
metadata = get_args(annotation)[1:]
|
|
67
|
-
has_click_metadata = any(
|
|
68
|
-
# Check for AutoClickParameter enums
|
|
69
|
-
isinstance(m, AutoClickParameter)
|
|
70
|
-
or
|
|
71
|
-
# Check for Click decorators
|
|
72
|
-
(hasattr(m, "__module__") and "click" in str(m.__module__))
|
|
73
|
-
for m in metadata
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
if has_click_metadata:
|
|
77
|
-
# Already has Click configuration, skip
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
# Add AutoOption to existing annotation
|
|
81
|
-
base_type = get_args(annotation)[0]
|
|
82
|
-
# For Python 3.10 compatibility, we need to reconstruct manually
|
|
83
|
-
# Create a new annotation with AutoOption prepended to existing metadata
|
|
84
|
-
if not metadata:
|
|
85
|
-
cls.__annotations__[attr_name] = Annotated[base_type, AutoClickParameter.OPTION]
|
|
86
|
-
elif len(metadata) == 1:
|
|
87
|
-
cls.__annotations__[attr_name] = Annotated[
|
|
88
|
-
base_type, AutoClickParameter.OPTION, metadata[0]
|
|
89
|
-
]
|
|
90
|
-
elif len(metadata) == 2:
|
|
91
|
-
cls.__annotations__[attr_name] = Annotated[
|
|
92
|
-
base_type, AutoClickParameter.OPTION, metadata[0], metadata[1]
|
|
93
|
-
]
|
|
94
|
-
else:
|
|
95
|
-
# For more metadata, we skip adding AutoOption to avoid complexity
|
|
96
|
-
# This is a rare case and the field will still work
|
|
97
|
-
pass
|
|
98
|
-
else:
|
|
99
|
-
# Not annotated, add AutoOption
|
|
100
|
-
cls.__annotations__[attr_name] = Annotated[annotation, AutoClickParameter.OPTION]
|
|
101
|
-
else:
|
|
102
|
-
# No annotation, infer type from field
|
|
103
|
-
field_type = attr_value.annotation or Any
|
|
104
|
-
cls.__annotations__[attr_name] = Annotated[field_type, AutoClickParameter.OPTION]
|
|
109
|
+
# No annotation, infer type from field
|
|
110
|
+
field_type = attr_value.annotation or Any
|
|
111
|
+
cls.__annotations__[attr_name] = Annotated[field_type, AutoClickParameter.OPTION]
|
|
105
112
|
|
|
106
113
|
|
|
107
114
|
# Convenience function for creating auto models dynamically
|
|
@@ -13,7 +13,7 @@ import inspect
|
|
|
13
13
|
import types
|
|
14
14
|
from collections.abc import Callable, Mapping, Sequence
|
|
15
15
|
from enum import Enum, auto
|
|
16
|
-
from typing import
|
|
16
|
+
from typing import Any, TypeAlias, cast, get_args, get_origin, get_type_hints
|
|
17
17
|
|
|
18
18
|
import click
|
|
19
19
|
from annotated_types import (
|
|
@@ -43,6 +43,7 @@ class AutoClickParameter(Enum):
|
|
|
43
43
|
OPTION = auto()
|
|
44
44
|
REQUIRED_OPTION = auto()
|
|
45
45
|
ARGUMENT = auto()
|
|
46
|
+
EXCLUDE = auto()
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
ClickParameterDecorator: TypeAlias = Callable[[FC], FC]
|
|
@@ -341,7 +342,9 @@ def generate_click_parameters(
|
|
|
341
342
|
annotation = type_hints.get(field_name)
|
|
342
343
|
|
|
343
344
|
# Skip fields without annotations
|
|
344
|
-
|
|
345
|
+
origin = get_origin(annotation)
|
|
346
|
+
# Compare using string representation to handle module reload scenarios
|
|
347
|
+
if origin is None or str(origin) != "<class 'typing.Annotated'>":
|
|
345
348
|
continue
|
|
346
349
|
|
|
347
350
|
# Get metadata from annotation
|
|
@@ -357,6 +360,9 @@ def generate_click_parameters(
|
|
|
357
360
|
field_type = AutoClickParameter.REQUIRED_OPTION
|
|
358
361
|
elif item == AutoClickParameter.ARGUMENT:
|
|
359
362
|
field_type = AutoClickParameter.ARGUMENT
|
|
363
|
+
elif item == AutoClickParameter.EXCLUDE:
|
|
364
|
+
field_type = AutoClickParameter.EXCLUDE
|
|
365
|
+
break # Skip this field entirely
|
|
360
366
|
elif (
|
|
361
367
|
hasattr(item, "__module__")
|
|
362
368
|
and "click" in str(item.__module__)
|
|
@@ -365,6 +371,10 @@ def generate_click_parameters(
|
|
|
365
371
|
click_parameter = item
|
|
366
372
|
break
|
|
367
373
|
|
|
374
|
+
# Skip excluded fields
|
|
375
|
+
if field_type == AutoClickParameter.EXCLUDE:
|
|
376
|
+
continue
|
|
377
|
+
|
|
368
378
|
if field_type == AutoClickParameter.OPTION or field_type == AutoClickParameter.REQUIRED_OPTION:
|
|
369
379
|
# Auto-generate Click option from Field info
|
|
370
380
|
option_name = f"--{field_name.replace('_', '-')}"
|