wry 0.1.10.dev4__tar.gz → 0.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.
Files changed (127) hide show
  1. {wry-0.1.10.dev4 → wry-0.2.0}/PKG-INFO +61 -46
  2. {wry-0.1.10.dev4 → wry-0.2.0}/README.md +46 -17
  3. wry-0.2.0/pyproject.toml +152 -0
  4. {wry-0.1.10.dev4 → wry-0.2.0}/wry/__init__.py +5 -22
  5. wry-0.2.0/wry/_version.py +14 -0
  6. {wry-0.1.10.dev4 → wry-0.2.0}/wry/auto_model.py +56 -49
  7. {wry-0.1.10.dev4 → wry-0.2.0}/wry/click_integration.py +12 -2
  8. wry-0.1.10.dev4/.github/workflows/check-requirements.yml +0 -32
  9. wry-0.1.10.dev4/.github/workflows/ci-cd.yml +0 -227
  10. wry-0.1.10.dev4/.gitignore +0 -142
  11. wry-0.1.10.dev4/.markdownlint.json +0 -3
  12. wry-0.1.10.dev4/.pre-commit-config.yaml +0 -52
  13. wry-0.1.10.dev4/CHANGELOG.md +0 -184
  14. wry-0.1.10.dev4/RELEASE_PROCESS.md +0 -80
  15. wry-0.1.10.dev4/TODO.md +0 -104
  16. wry-0.1.10.dev4/check.sh +0 -3
  17. wry-0.1.10.dev4/examples/auto_instantiate_edge_cases.py +0 -165
  18. wry-0.1.10.dev4/examples/auto_instantiate_poc.py +0 -330
  19. wry-0.1.10.dev4/examples/auto_model_example.py +0 -187
  20. wry-0.1.10.dev4/examples/config.json +0 -6
  21. wry-0.1.10.dev4/examples/intermediate_example.py +0 -89
  22. wry-0.1.10.dev4/examples/multi_model_example.py +0 -171
  23. wry-0.1.10.dev4/examples/simple_cli.py +0 -55
  24. wry-0.1.10.dev4/examples/source_tracking_example.py +0 -102
  25. wry-0.1.10.dev4/pyproject.toml +0 -140
  26. wry-0.1.10.dev4/requirements-dev.txt +0 -28
  27. wry-0.1.10.dev4/scripts/README.md +0 -23
  28. wry-0.1.10.dev4/scripts/extract_release_notes.py +0 -112
  29. wry-0.1.10.dev4/scripts/test_all_versions.sh +0 -86
  30. wry-0.1.10.dev4/scripts/test_ci_locally.sh +0 -71
  31. wry-0.1.10.dev4/scripts/test_with_act.sh +0 -36
  32. wry-0.1.10.dev4/scripts/update-requirements.sh +0 -59
  33. wry-0.1.10.dev4/setup.cfg +0 -4
  34. wry-0.1.10.dev4/tests/README.md +0 -65
  35. wry-0.1.10.dev4/tests/__init__.py +0 -1
  36. wry-0.1.10.dev4/tests/features/__init__.py +0 -1
  37. wry-0.1.10.dev4/tests/features/test_auto_model.py +0 -253
  38. wry-0.1.10.dev4/tests/features/test_multi_model.py +0 -304
  39. wry-0.1.10.dev4/tests/features/test_source_precedence.py +0 -302
  40. wry-0.1.10.dev4/tests/integration/__init__.py +0 -1
  41. wry-0.1.10.dev4/tests/integration/test_click_edge_cases.py +0 -171
  42. wry-0.1.10.dev4/tests/integration/test_click_integration.py +0 -217
  43. wry-0.1.10.dev4/tests/integration/test_click_integration_extended.py +0 -483
  44. wry-0.1.10.dev4/tests/integration/test_context_handling.py +0 -124
  45. wry-0.1.10.dev4/tests/unit/__init__.py +0 -1
  46. wry-0.1.10.dev4/tests/unit/auto_model/__init__.py +0 -0
  47. wry-0.1.10.dev4/tests/unit/auto_model/test_auto_model_annotation_inference.py +0 -47
  48. wry-0.1.10.dev4/tests/unit/auto_model/test_auto_model_field_processing.py +0 -64
  49. wry-0.1.10.dev4/tests/unit/auto_model/test_field_annotation_handling.py +0 -64
  50. wry-0.1.10.dev4/tests/unit/auto_model/test_field_annotations.py +0 -172
  51. wry-0.1.10.dev4/tests/unit/auto_model/test_type_inference.py +0 -70
  52. wry-0.1.10.dev4/tests/unit/click/README_TESTING_STRATEGY.md +0 -50
  53. wry-0.1.10.dev4/tests/unit/click/__init__.py +0 -0
  54. wry-0.1.10.dev4/tests/unit/click/test_bool_flag_handling.py +0 -58
  55. wry-0.1.10.dev4/tests/unit/click/test_click_config_building.py +0 -51
  56. wry-0.1.10.dev4/tests/unit/click/test_click_constraint_formatting.py +0 -90
  57. wry-0.1.10.dev4/tests/unit/click/test_click_decorator_edge_cases.py +0 -81
  58. wry-0.1.10.dev4/tests/unit/click/test_click_interval_constraints.py +0 -45
  59. wry-0.1.10.dev4/tests/unit/click/test_click_lambda_parsing.py +0 -56
  60. wry-0.1.10.dev4/tests/unit/click/test_click_length_constraints.py +0 -47
  61. wry-0.1.10.dev4/tests/unit/click/test_click_parameter_generation.py +0 -108
  62. wry-0.1.10.dev4/tests/unit/click/test_click_predicate_handling.py +0 -69
  63. wry-0.1.10.dev4/tests/unit/click/test_closure_extraction_errors.py +0 -94
  64. wry-0.1.10.dev4/tests/unit/click/test_closure_handling.py +0 -82
  65. wry-0.1.10.dev4/tests/unit/click/test_constraint_behavior.py +0 -113
  66. wry-0.1.10.dev4/tests/unit/click/test_constraint_edge_cases.py +0 -84
  67. wry-0.1.10.dev4/tests/unit/click/test_env_vars_option.py +0 -70
  68. wry-0.1.10.dev4/tests/unit/click/test_json_config_loading.py +0 -102
  69. wry-0.1.10.dev4/tests/unit/click/test_lambda_behavior.py +0 -85
  70. wry-0.1.10.dev4/tests/unit/click/test_lambda_error_handling.py +0 -62
  71. wry-0.1.10.dev4/tests/unit/click/test_predicate_source_errors.py +0 -46
  72. wry-0.1.10.dev4/tests/unit/click/test_strict_mode_errors.py +0 -81
  73. wry-0.1.10.dev4/tests/unit/click/test_type_handling.py +0 -87
  74. wry-0.1.10.dev4/tests/unit/core/__init__.py +0 -0
  75. wry-0.1.10.dev4/tests/unit/core/test_accessors.py +0 -69
  76. wry-0.1.10.dev4/tests/unit/core/test_advanced_features.py +0 -518
  77. wry-0.1.10.dev4/tests/unit/core/test_core.py +0 -176
  78. wry-0.1.10.dev4/tests/unit/core/test_edge_cases.py +0 -291
  79. wry-0.1.10.dev4/tests/unit/core/test_env_utils.py +0 -73
  80. wry-0.1.10.dev4/tests/unit/core/test_field_constraint_extraction.py +0 -65
  81. wry-0.1.10.dev4/tests/unit/core/test_field_utils.py +0 -107
  82. wry-0.1.10.dev4/tests/unit/core/test_field_utils_edge_cases.py +0 -66
  83. wry-0.1.10.dev4/tests/unit/core/test_sources.py +0 -32
  84. wry-0.1.10.dev4/tests/unit/core/test_type_checking_blocks.py +0 -56
  85. wry-0.1.10.dev4/tests/unit/model/__init__.py +0 -0
  86. wry-0.1.10.dev4/tests/unit/model/test_accessor_caching.py +0 -78
  87. wry-0.1.10.dev4/tests/unit/model/test_extract_edge_cases.py +0 -118
  88. wry-0.1.10.dev4/tests/unit/model/test_model_click_context_handling.py +0 -123
  89. wry-0.1.10.dev4/tests/unit/model/test_model_data_extraction.py +0 -96
  90. wry-0.1.10.dev4/tests/unit/model/test_model_default_handling.py +0 -74
  91. wry-0.1.10.dev4/tests/unit/model/test_model_environment_integration.py +0 -66
  92. wry-0.1.10.dev4/tests/unit/model/test_model_extract_subset_edge_cases.py +0 -116
  93. wry-0.1.10.dev4/tests/unit/model/test_model_extraction_methods.py +0 -88
  94. wry-0.1.10.dev4/tests/unit/model/test_model_field_errors.py +0 -54
  95. wry-0.1.10.dev4/tests/unit/model/test_model_object_extraction.py +0 -102
  96. wry-0.1.10.dev4/tests/unit/model/test_non_dict_object_extraction.py +0 -84
  97. wry-0.1.10.dev4/tests/unit/model/test_object_attribute_extraction.py +0 -110
  98. wry-0.1.10.dev4/tests/unit/multi_model/__init__.py +0 -0
  99. wry-0.1.10.dev4/tests/unit/multi_model/test_multi_model.py +0 -48
  100. wry-0.1.10.dev4/tests/unit/multi_model/test_type_checking.py +0 -24
  101. wry-0.1.10.dev4/tests/unit/test_auto_model_field_processing.py +0 -64
  102. wry-0.1.10.dev4/tests/unit/test_comprehensive_imports.py +0 -71
  103. wry-0.1.10.dev4/tests/unit/test_generate_click_classmethod.py +0 -47
  104. wry-0.1.10.dev4/tests/unit/test_init.py +0 -90
  105. wry-0.1.10.dev4/tests/unit/test_init_edge_cases.py +0 -123
  106. wry-0.1.10.dev4/tests/unit/test_init_version_edge_cases.py +0 -64
  107. wry-0.1.10.dev4/tests/unit/test_model_extraction_methods.py +0 -88
  108. wry-0.1.10.dev4/tests/unit/test_multiple_option_bug.py +0 -337
  109. wry-0.1.10.dev4/tests/unit/test_type_checking_imports.py +0 -49
  110. wry-0.1.10.dev4/tests/unit/test_variadic_argument_bug.py +0 -154
  111. wry-0.1.10.dev4/tests/unit/test_version_fallback.py +0 -60
  112. wry-0.1.10.dev4/tests/unit/test_version_parsing.py +0 -62
  113. wry-0.1.10.dev4/wry/_version.py +0 -34
  114. wry-0.1.10.dev4/wry.egg-info/PKG-INFO +0 -697
  115. wry-0.1.10.dev4/wry.egg-info/SOURCES.txt +0 -123
  116. wry-0.1.10.dev4/wry.egg-info/dependency_links.txt +0 -1
  117. wry-0.1.10.dev4/wry.egg-info/requires.txt +0 -22
  118. wry-0.1.10.dev4/wry.egg-info/top_level.txt +0 -1
  119. {wry-0.1.10.dev4 → wry-0.2.0}/LICENSE +0 -0
  120. {wry-0.1.10.dev4 → wry-0.2.0}/wry/core/__init__.py +0 -0
  121. {wry-0.1.10.dev4 → wry-0.2.0}/wry/core/accessors.py +0 -0
  122. {wry-0.1.10.dev4 → wry-0.2.0}/wry/core/env_utils.py +0 -0
  123. {wry-0.1.10.dev4 → wry-0.2.0}/wry/core/field_utils.py +0 -0
  124. {wry-0.1.10.dev4 → wry-0.2.0}/wry/core/model.py +0 -0
  125. {wry-0.1.10.dev4 → wry-0.2.0}/wry/core/sources.py +0 -0
  126. {wry-0.1.10.dev4 → wry-0.2.0}/wry/multi_model.py +0 -0
  127. {wry-0.1.10.dev4 → wry-0.2.0}/wry/py.typed +0 -0
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wry
3
- Version: 0.1.10.dev4
3
+ Version: 0.2.0
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
- Project-URL: Homepage, https://github.com/tahouse/wry
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-Python: >=3.10
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 in development mode
382
- pip install -e ".[dev,test]"
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
- pip install -e ".[dev,test]" --upgrade
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
- To ensure consistency between local development and CI environments, we use pinned dependencies:
520
+ We use Poetry for dependency management to ensure consistent environments:
532
521
 
533
522
  ```bash
534
- # Install development dependencies with exact versions
535
- pip install -r requirements-dev.txt
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
- # Install the package in editable mode
538
- pip install -e .
529
+ # Activate the virtual environment
530
+ poetry shell
539
531
  ```
540
532
 
541
- **Important**: Always use `requirements-dev.txt` for development to ensure your local environment matches CI/CD.
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
- python -m venv venv
571
- source venv/bin/activate # On Windows: venv\Scripts\activate
572
- pip install -e ".[dev,test]"
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 in development mode
336
- pip install -e ".[dev,test]"
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
- pip install -e ".[dev,test]" --upgrade
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
- To ensure consistency between local development and CI environments, we use pinned dependencies:
489
+ We use Poetry for dependency management to ensure consistent environments:
486
490
 
487
491
  ```bash
488
- # Install development dependencies with exact versions
489
- pip install -r requirements-dev.txt
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
- # Install the package in editable mode
492
- pip install -e .
498
+ # Activate the virtual environment
499
+ poetry shell
493
500
  ```
494
501
 
495
- **Important**: Always use `requirements-dev.txt` for development to ensure your local environment matches CI/CD.
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
- python -m venv venv
525
- source venv/bin/activate # On Windows: venv\Scripts\activate
526
- pip install -e ".[dev,test]"
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.0" # 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, metadata=[commit]) }}
56
+ {%- else -%}
57
+ {{ serialize_pep440(bump_version(base), stage, revision, dev=distance, metadata=[commit]) }}
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 setuptools-scm from git tags
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/editable installs
59
- try:
60
- # Try to get version dynamically for editable installs
61
- from setuptools_scm import get_version
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.0" # This will be replaced during build
5
+ __version_tuple__: tuple[int, ...] = (0, 2, 0) # 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
- # Get field names from the class
50
- for attr_name in dir(cls):
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
- # Check if annotation exists
59
- if attr_name in cls.__annotations__:
60
- annotation = cls.__annotations__[attr_name]
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 Annotated, Any, TypeAlias, cast, get_args, get_origin, get_type_hints
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
- if get_origin(annotation) is not Annotated:
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('_', '-')}"