wry 0.1.9.dev2__tar.gz → 0.1.10.dev4__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 (125) hide show
  1. wry-0.1.10.dev4/.github/workflows/check-requirements.yml +32 -0
  2. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/.github/workflows/ci-cd.yml +5 -2
  3. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/.pre-commit-config.yaml +2 -9
  4. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/CHANGELOG.md +28 -1
  5. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/PKG-INFO +61 -12
  6. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/README.md +60 -11
  7. wry-0.1.10.dev4/examples/auto_instantiate_edge_cases.py +165 -0
  8. wry-0.1.10.dev4/examples/auto_instantiate_poc.py +330 -0
  9. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/examples/auto_model_example.py +50 -39
  10. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/examples/simple_cli.py +18 -3
  11. wry-0.1.10.dev4/requirements-dev.txt +28 -0
  12. wry-0.1.10.dev4/scripts/update-requirements.sh +59 -0
  13. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_lambda_parsing.py +6 -6
  14. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_parameter_generation.py +1 -1
  15. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_predicate_handling.py +3 -3
  16. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_lambda_behavior.py +3 -15
  17. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_lambda_error_handling.py +5 -6
  18. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_type_handling.py +2 -2
  19. wry-0.1.10.dev4/tests/unit/test_multiple_option_bug.py +337 -0
  20. wry-0.1.10.dev4/tests/unit/test_variadic_argument_bug.py +154 -0
  21. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/_version.py +3 -3
  22. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/click_integration.py +18 -1
  23. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry.egg-info/PKG-INFO +61 -12
  24. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry.egg-info/SOURCES.txt +7 -0
  25. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/.gitignore +0 -0
  26. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/.markdownlint.json +0 -0
  27. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/LICENSE +0 -0
  28. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/RELEASE_PROCESS.md +0 -0
  29. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/TODO.md +0 -0
  30. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/check.sh +0 -0
  31. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/examples/config.json +0 -0
  32. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/examples/intermediate_example.py +0 -0
  33. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/examples/multi_model_example.py +0 -0
  34. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/examples/source_tracking_example.py +0 -0
  35. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/pyproject.toml +0 -0
  36. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/scripts/README.md +0 -0
  37. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/scripts/extract_release_notes.py +0 -0
  38. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/scripts/test_all_versions.sh +0 -0
  39. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/scripts/test_ci_locally.sh +0 -0
  40. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/scripts/test_with_act.sh +0 -0
  41. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/setup.cfg +0 -0
  42. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/README.md +0 -0
  43. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/__init__.py +0 -0
  44. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/features/__init__.py +0 -0
  45. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/features/test_auto_model.py +0 -0
  46. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/features/test_multi_model.py +0 -0
  47. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/features/test_source_precedence.py +0 -0
  48. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/integration/__init__.py +0 -0
  49. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/integration/test_click_edge_cases.py +0 -0
  50. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/integration/test_click_integration.py +0 -0
  51. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/integration/test_click_integration_extended.py +0 -0
  52. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/integration/test_context_handling.py +0 -0
  53. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/__init__.py +0 -0
  54. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/auto_model/__init__.py +0 -0
  55. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/auto_model/test_auto_model_annotation_inference.py +0 -0
  56. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/auto_model/test_auto_model_field_processing.py +0 -0
  57. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/auto_model/test_field_annotation_handling.py +0 -0
  58. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/auto_model/test_field_annotations.py +0 -0
  59. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/auto_model/test_type_inference.py +0 -0
  60. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/README_TESTING_STRATEGY.md +0 -0
  61. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/__init__.py +0 -0
  62. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_bool_flag_handling.py +0 -0
  63. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_config_building.py +0 -0
  64. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_constraint_formatting.py +0 -0
  65. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_decorator_edge_cases.py +0 -0
  66. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_interval_constraints.py +0 -0
  67. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_click_length_constraints.py +0 -0
  68. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_closure_extraction_errors.py +0 -0
  69. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_closure_handling.py +0 -0
  70. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_constraint_behavior.py +0 -0
  71. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_constraint_edge_cases.py +0 -0
  72. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_env_vars_option.py +0 -0
  73. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_json_config_loading.py +0 -0
  74. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_predicate_source_errors.py +0 -0
  75. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/click/test_strict_mode_errors.py +0 -0
  76. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/__init__.py +0 -0
  77. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_accessors.py +0 -0
  78. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_advanced_features.py +0 -0
  79. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_core.py +0 -0
  80. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_edge_cases.py +0 -0
  81. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_env_utils.py +0 -0
  82. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_field_constraint_extraction.py +0 -0
  83. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_field_utils.py +0 -0
  84. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_field_utils_edge_cases.py +0 -0
  85. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_sources.py +0 -0
  86. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/core/test_type_checking_blocks.py +0 -0
  87. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/__init__.py +0 -0
  88. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_accessor_caching.py +0 -0
  89. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_extract_edge_cases.py +0 -0
  90. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_click_context_handling.py +0 -0
  91. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_data_extraction.py +0 -0
  92. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_default_handling.py +0 -0
  93. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_environment_integration.py +0 -0
  94. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_extract_subset_edge_cases.py +0 -0
  95. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_extraction_methods.py +0 -0
  96. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_field_errors.py +0 -0
  97. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_model_object_extraction.py +0 -0
  98. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_non_dict_object_extraction.py +0 -0
  99. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/model/test_object_attribute_extraction.py +0 -0
  100. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/multi_model/__init__.py +0 -0
  101. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/multi_model/test_multi_model.py +0 -0
  102. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/multi_model/test_type_checking.py +0 -0
  103. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_auto_model_field_processing.py +0 -0
  104. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_comprehensive_imports.py +0 -0
  105. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_generate_click_classmethod.py +0 -0
  106. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_init.py +0 -0
  107. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_init_edge_cases.py +0 -0
  108. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_init_version_edge_cases.py +0 -0
  109. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_model_extraction_methods.py +0 -0
  110. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_type_checking_imports.py +0 -0
  111. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_version_fallback.py +0 -0
  112. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/tests/unit/test_version_parsing.py +0 -0
  113. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/__init__.py +0 -0
  114. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/auto_model.py +0 -0
  115. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/core/__init__.py +0 -0
  116. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/core/accessors.py +0 -0
  117. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/core/env_utils.py +0 -0
  118. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/core/field_utils.py +0 -0
  119. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/core/model.py +0 -0
  120. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/core/sources.py +0 -0
  121. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/multi_model.py +0 -0
  122. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry/py.typed +0 -0
  123. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry.egg-info/dependency_links.txt +0 -0
  124. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry.egg-info/requires.txt +0 -0
  125. {wry-0.1.9.dev2 → wry-0.1.10.dev4}/wry.egg-info/top_level.txt +0 -0
@@ -0,0 +1,32 @@
1
+ name: Check Requirements
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - 'pyproject.toml'
7
+ - 'requirements-dev.txt'
8
+ - '.github/workflows/check-requirements.yml'
9
+
10
+ jobs:
11
+ check-requirements:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: '3.12'
20
+
21
+ - name: Check requirements consistency
22
+ run: |
23
+ echo "Checking that requirements-dev.txt matches pyproject.toml constraints..."
24
+ python -m pip install --upgrade pip
25
+
26
+ # Install from requirements file
27
+ pip install -r requirements-dev.txt
28
+
29
+ # Check if package can be installed with these deps
30
+ pip install --no-deps -e .
31
+
32
+ echo "✅ Requirements file is valid and consistent!"
@@ -35,14 +35,17 @@ jobs:
35
35
  uses: actions/cache@v4
36
36
  with:
37
37
  path: ~/.cache/pip
38
- key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
38
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml', 'requirements-dev.txt') }}
39
39
  restore-keys: |
40
40
  ${{ runner.os }}-pip-
41
41
 
42
42
  - name: Install dependencies
43
43
  run: |
44
44
  python -m pip install --upgrade pip
45
- pip install -e ".[dev,test]"
45
+ # Install exact versions from requirements file first
46
+ pip install -r requirements-dev.txt
47
+ # Then install the package in editable mode (without deps to avoid conflicts)
48
+ pip install --no-deps -e .
46
49
 
47
50
  - name: Install pre-commit hooks
48
51
  run: |
@@ -1,6 +1,6 @@
1
1
  repos:
2
2
  - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v4.4.0
3
+ rev: v6.0.0
4
4
  hooks:
5
5
  - id: trailing-whitespace
6
6
  - id: end-of-file-fixer
@@ -25,7 +25,7 @@ repos:
25
25
  entry: pytest
26
26
  language: system
27
27
  types: [python]
28
- args: [--cov=wry, --cov-branch, --cov-report=xml, --cov-report=term-missing, --cov-fail-under=90]
28
+ args: [tests/unit/, --cov=wry, --cov-branch, --cov-report=xml, --cov-report=term-missing, --cov-fail-under=85]
29
29
  pass_filenames: false
30
30
  always_run: true
31
31
 
@@ -41,13 +41,6 @@ repos:
41
41
  - annotated-types>=0.6.0
42
42
  - setuptools-scm>=8.0
43
43
 
44
- # Temporarily disabled - bandit has dependency issues in pre-commit
45
- # - repo: https://github.com/PyCQA/bandit
46
- # rev: 1.7.5
47
- # hooks:
48
- # - id: bandit
49
- # args: [-c, pyproject.toml]
50
-
51
44
  exclude: |
52
45
  (?x)(
53
46
  # Exclude auto-generated files
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.9] - 2025-09-29
11
+
12
+ ### Added
13
+
14
+ - **Automatic multiple option support for list types**
15
+ - `list[str]` and `tuple[str, ...]` fields now automatically generate Click options with `multiple=True`
16
+ - Supports both `AutoWryModel` and `WryModel` with proper type conversion
17
+ - Handles edge cases: empty lists, single values, and multiple values
18
+ - Works with different data types: `list[int]`, `list[str]`, etc.
19
+ - Maintains type safety: Click tuples are correctly converted to Python lists
20
+
21
+ ### Fixed
22
+
23
+ - **Variadic argument bug resolution**
24
+ - Fixed issue where variadic Click arguments (`nargs=-1`) were incorrectly converted to strings
25
+ - Variadic arguments now preserve their tuple type when used with `@generate_click_parameters`
26
+ - Resolves duplicate parameter warnings and validation errors
27
+
28
+ ### Testing
29
+
30
+ - **Comprehensive test coverage for multiple options**
31
+ - Added `tests/unit/test_multiple_option_bug.py` with 6 test cases
32
+ - Added `tests/unit/test_variadic_argument_bug.py` with 3 test cases
33
+ - Tests cover AutoWryModel, WryModel, edge cases, type validation, and different data types
34
+ - All tests pass with proper type checking and validation
35
+
10
36
  ## [0.1.8] - 2025-09-10
11
37
 
12
38
  ### Fixed
@@ -144,7 +170,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
144
170
 
145
171
  - Initial version as `drycli` package (before rename to `wry`)
146
172
 
147
- [Unreleased]: https://github.com/tahouse/wry/compare/v0.1.8...HEAD
173
+ [Unreleased]: https://github.com/tahouse/wry/compare/v0.1.9...HEAD
174
+ [0.1.9]: https://github.com/tahouse/wry/compare/v0.1.8...v0.1.9
148
175
  [0.1.8]: https://github.com/tahouse/wry/compare/v0.1.7...v0.1.8
149
176
  [0.1.7]: https://github.com/tahouse/wry/compare/v0.1.6...v0.1.7
150
177
  [0.1.6]: https://github.com/tahouse/wry/compare/v0.1.5...v0.1.6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wry
3
- Version: 0.1.9.dev2
3
+ Version: 0.1.10.dev4
4
4
  Summary: Why Repeat Yourself? - Define your CLI once with Pydantic models
5
5
  Author-email: Tyler House <26489166+tahouse@users.noreply.github.com>
6
6
  License: MIT
@@ -81,7 +81,7 @@ The simplest way to use wry is with `AutoWryModel`, which automatically generate
81
81
  ```python
82
82
  import click
83
83
  from pydantic import Field
84
- from wry import AutoWryModel, generate_click_parameters
84
+ from wry import AutoWryModel
85
85
 
86
86
  class AppArgs(AutoWryModel):
87
87
  """Configuration for my app."""
@@ -91,9 +91,10 @@ class AppArgs(AutoWryModel):
91
91
  verbose: bool = Field(default=False, description="Verbose output")
92
92
 
93
93
  @click.command()
94
- @generate_click_parameters(AppArgs)
94
+ @AppArgs.generate_click_parameters()
95
95
  def main(**kwargs):
96
96
  """My simple CLI application."""
97
+ # Create the model instance from kwargs
97
98
  config = AppArgs(**kwargs)
98
99
  click.echo(f"Hello {config.name}, you are {config.age} years old!")
99
100
 
@@ -101,6 +102,8 @@ if __name__ == "__main__":
101
102
  main()
102
103
  ```
103
104
 
105
+ **Note**: Currently, `generate_click_parameters()` passes individual parameters as kwargs to your function. You need to instantiate the model yourself. See the [Future Features](#future-features) section for a potential cleaner API.
106
+
104
107
  Run it:
105
108
 
106
109
  ```bash
@@ -122,7 +125,7 @@ wry can track where each configuration value came from. You have two options:
122
125
 
123
126
  ```python
124
127
  @click.command()
125
- @generate_click_parameters(AppArgs)
128
+ @AppArgs.generate_click_parameters()
126
129
  def main(**kwargs):
127
130
  # Simple instantiation - no source tracking
128
131
  config = AppArgs(**kwargs)
@@ -133,7 +136,7 @@ def main(**kwargs):
133
136
 
134
137
  ```python
135
138
  @click.command()
136
- @generate_click_parameters(AppArgs)
139
+ @AppArgs.generate_click_parameters()
137
140
  @click.pass_context
138
141
  def main(ctx, **kwargs):
139
142
  # Full source tracking with context
@@ -210,7 +213,7 @@ Use multiple Pydantic models in a single command:
210
213
  ```python
211
214
  from typing import Annotated
212
215
  import click
213
- from wry import WryModel, AutoOption, generate_click_parameters, multi_model
216
+ from wry import WryModel, AutoOption, multi_model, create_models
214
217
 
215
218
  class ServerConfig(WryModel):
216
219
  host: Annotated[str, AutoOption] = "localhost"
@@ -222,7 +225,13 @@ class DatabaseArgs(WryModel):
222
225
 
223
226
  @click.command()
224
227
  @multi_model(ServerConfig, DatabaseConfig)
225
- def serve(server: ServerConfig, database: DatabaseConfig):
228
+ @click.pass_context
229
+ def serve(ctx, **kwargs):
230
+ # Create model instances
231
+ configs = create_models(ctx, kwargs, ServerConfig, DatabaseConfig)
232
+ server = configs[ServerConfig]
233
+ database = configs[DatabaseConfig]
234
+
226
235
  print(f"Starting server at {server.host}:{server.port}")
227
236
  print(f"Database: {database.db_url} (pool size: {database.pool_size})")
228
237
  ```
@@ -233,7 +242,7 @@ Automatically generate options for all fields:
233
242
 
234
243
  ```python
235
244
  import click
236
- from wry import AutoWryModel, generate_click_parameters
245
+ from wry import AutoWryModel
237
246
  from pydantic import Field
238
247
 
239
248
  class QuickConfig(AutoWryModel):
@@ -244,7 +253,7 @@ class QuickConfig(AutoWryModel):
244
253
  email: str = Field(description="Your email")
245
254
 
246
255
  @click.command()
247
- @generate_click_parameters(QuickConfig)
256
+ @QuickConfig.generate_click_parameters()
248
257
  def quickstart(config: QuickConfig):
249
258
  print(f"Hello {config.name}!")
250
259
  ```
@@ -307,8 +316,8 @@ By default, `generate_click_parameters` runs in strict mode to prevent common mi
307
316
 
308
317
  ```python
309
318
  @click.command()
310
- @generate_click_parameters(Config) # strict=True by default
311
- @generate_click_parameters(Config) # ERROR: Duplicate decorator detected!
319
+ @Config.generate_click_parameters() # strict=True by default
320
+ @Config.generate_click_parameters() # ERROR: Duplicate decorator detected!
312
321
  def main(**kwargs):
313
322
  pass
314
323
  ```
@@ -316,7 +325,7 @@ def main(**kwargs):
316
325
  To allow multiple decorators (not recommended):
317
326
 
318
327
  ```python
319
- @generate_click_parameters(Config, strict=False)
328
+ @Config.generate_click_parameters(strict=False)
320
329
  ```
321
330
 
322
331
  ### Manual Field Control
@@ -517,6 +526,20 @@ The wry codebase is organized into focused modules:
517
526
 
518
527
  We welcome contributions! Please follow these guidelines to ensure a smooth process.
519
528
 
529
+ ### Development Setup
530
+
531
+ To ensure consistency between local development and CI environments, we use pinned dependencies:
532
+
533
+ ```bash
534
+ # Install development dependencies with exact versions
535
+ pip install -r requirements-dev.txt
536
+
537
+ # Install the package in editable mode
538
+ pip install -e .
539
+ ```
540
+
541
+ **Important**: Always use `requirements-dev.txt` for development to ensure your local environment matches CI/CD.
542
+
520
543
  ### Getting Started
521
544
 
522
545
  1. **Fork the repository** on GitHub
@@ -642,7 +665,33 @@ We welcome contributions! Please follow these guidelines to ensure a smooth proc
642
665
 
643
666
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
644
667
 
668
+ ## Future Features
669
+
670
+ ### Automatic Model Instantiation (Proof of Concept)
671
+
672
+ We're exploring a cleaner API that would automatically instantiate models and pass them to your function. See `examples/auto_instantiate_poc.py` for a working proof of concept:
673
+
674
+ ```python
675
+ # Potential future syntax
676
+ @click.command()
677
+ @AppConfig.click_command() # or @auto_instantiate(AppConfig)
678
+ def main(config: AppConfig):
679
+ """The decorator would handle instantiation automatically."""
680
+ click.echo(f"Hello {config.name}!")
681
+ # Source tracking would work automatically too!
682
+ ```
683
+
684
+ This would:
685
+
686
+ - Automatically handle `@click.pass_context` when needed for source tracking
687
+ - Instantiate the model and pass it with the correct parameter name
688
+ - Support multiple models in a single command
689
+ - Make the API more intuitive and similar to other libraries
690
+
691
+ If you're interested in this feature, please provide feedback!
692
+
645
693
  ## Acknowledgments
646
694
 
647
695
  - Built on top of [Click](https://click.palletsprojects.com/) and [Pydantic](https://pydantic-docs.helpmanual.io/)
648
696
  - Inspired by the DRY (Don't Repeat Yourself) principle
697
+ 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`.
@@ -35,7 +35,7 @@ The simplest way to use wry is with `AutoWryModel`, which automatically generate
35
35
  ```python
36
36
  import click
37
37
  from pydantic import Field
38
- from wry import AutoWryModel, generate_click_parameters
38
+ from wry import AutoWryModel
39
39
 
40
40
  class AppArgs(AutoWryModel):
41
41
  """Configuration for my app."""
@@ -45,9 +45,10 @@ class AppArgs(AutoWryModel):
45
45
  verbose: bool = Field(default=False, description="Verbose output")
46
46
 
47
47
  @click.command()
48
- @generate_click_parameters(AppArgs)
48
+ @AppArgs.generate_click_parameters()
49
49
  def main(**kwargs):
50
50
  """My simple CLI application."""
51
+ # Create the model instance from kwargs
51
52
  config = AppArgs(**kwargs)
52
53
  click.echo(f"Hello {config.name}, you are {config.age} years old!")
53
54
 
@@ -55,6 +56,8 @@ if __name__ == "__main__":
55
56
  main()
56
57
  ```
57
58
 
59
+ **Note**: Currently, `generate_click_parameters()` passes individual parameters as kwargs to your function. You need to instantiate the model yourself. See the [Future Features](#future-features) section for a potential cleaner API.
60
+
58
61
  Run it:
59
62
 
60
63
  ```bash
@@ -76,7 +79,7 @@ wry can track where each configuration value came from. You have two options:
76
79
 
77
80
  ```python
78
81
  @click.command()
79
- @generate_click_parameters(AppArgs)
82
+ @AppArgs.generate_click_parameters()
80
83
  def main(**kwargs):
81
84
  # Simple instantiation - no source tracking
82
85
  config = AppArgs(**kwargs)
@@ -87,7 +90,7 @@ def main(**kwargs):
87
90
 
88
91
  ```python
89
92
  @click.command()
90
- @generate_click_parameters(AppArgs)
93
+ @AppArgs.generate_click_parameters()
91
94
  @click.pass_context
92
95
  def main(ctx, **kwargs):
93
96
  # Full source tracking with context
@@ -164,7 +167,7 @@ Use multiple Pydantic models in a single command:
164
167
  ```python
165
168
  from typing import Annotated
166
169
  import click
167
- from wry import WryModel, AutoOption, generate_click_parameters, multi_model
170
+ from wry import WryModel, AutoOption, multi_model, create_models
168
171
 
169
172
  class ServerConfig(WryModel):
170
173
  host: Annotated[str, AutoOption] = "localhost"
@@ -176,7 +179,13 @@ class DatabaseArgs(WryModel):
176
179
 
177
180
  @click.command()
178
181
  @multi_model(ServerConfig, DatabaseConfig)
179
- def serve(server: ServerConfig, database: DatabaseConfig):
182
+ @click.pass_context
183
+ def serve(ctx, **kwargs):
184
+ # Create model instances
185
+ configs = create_models(ctx, kwargs, ServerConfig, DatabaseConfig)
186
+ server = configs[ServerConfig]
187
+ database = configs[DatabaseConfig]
188
+
180
189
  print(f"Starting server at {server.host}:{server.port}")
181
190
  print(f"Database: {database.db_url} (pool size: {database.pool_size})")
182
191
  ```
@@ -187,7 +196,7 @@ Automatically generate options for all fields:
187
196
 
188
197
  ```python
189
198
  import click
190
- from wry import AutoWryModel, generate_click_parameters
199
+ from wry import AutoWryModel
191
200
  from pydantic import Field
192
201
 
193
202
  class QuickConfig(AutoWryModel):
@@ -198,7 +207,7 @@ class QuickConfig(AutoWryModel):
198
207
  email: str = Field(description="Your email")
199
208
 
200
209
  @click.command()
201
- @generate_click_parameters(QuickConfig)
210
+ @QuickConfig.generate_click_parameters()
202
211
  def quickstart(config: QuickConfig):
203
212
  print(f"Hello {config.name}!")
204
213
  ```
@@ -261,8 +270,8 @@ By default, `generate_click_parameters` runs in strict mode to prevent common mi
261
270
 
262
271
  ```python
263
272
  @click.command()
264
- @generate_click_parameters(Config) # strict=True by default
265
- @generate_click_parameters(Config) # ERROR: Duplicate decorator detected!
273
+ @Config.generate_click_parameters() # strict=True by default
274
+ @Config.generate_click_parameters() # ERROR: Duplicate decorator detected!
266
275
  def main(**kwargs):
267
276
  pass
268
277
  ```
@@ -270,7 +279,7 @@ def main(**kwargs):
270
279
  To allow multiple decorators (not recommended):
271
280
 
272
281
  ```python
273
- @generate_click_parameters(Config, strict=False)
282
+ @Config.generate_click_parameters(strict=False)
274
283
  ```
275
284
 
276
285
  ### Manual Field Control
@@ -471,6 +480,20 @@ The wry codebase is organized into focused modules:
471
480
 
472
481
  We welcome contributions! Please follow these guidelines to ensure a smooth process.
473
482
 
483
+ ### Development Setup
484
+
485
+ To ensure consistency between local development and CI environments, we use pinned dependencies:
486
+
487
+ ```bash
488
+ # Install development dependencies with exact versions
489
+ pip install -r requirements-dev.txt
490
+
491
+ # Install the package in editable mode
492
+ pip install -e .
493
+ ```
494
+
495
+ **Important**: Always use `requirements-dev.txt` for development to ensure your local environment matches CI/CD.
496
+
474
497
  ### Getting Started
475
498
 
476
499
  1. **Fork the repository** on GitHub
@@ -596,7 +619,33 @@ We welcome contributions! Please follow these guidelines to ensure a smooth proc
596
619
 
597
620
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
598
621
 
622
+ ## Future Features
623
+
624
+ ### Automatic Model Instantiation (Proof of Concept)
625
+
626
+ We're exploring a cleaner API that would automatically instantiate models and pass them to your function. See `examples/auto_instantiate_poc.py` for a working proof of concept:
627
+
628
+ ```python
629
+ # Potential future syntax
630
+ @click.command()
631
+ @AppConfig.click_command() # or @auto_instantiate(AppConfig)
632
+ def main(config: AppConfig):
633
+ """The decorator would handle instantiation automatically."""
634
+ click.echo(f"Hello {config.name}!")
635
+ # Source tracking would work automatically too!
636
+ ```
637
+
638
+ This would:
639
+
640
+ - Automatically handle `@click.pass_context` when needed for source tracking
641
+ - Instantiate the model and pass it with the correct parameter name
642
+ - Support multiple models in a single command
643
+ - Make the API more intuitive and similar to other libraries
644
+
645
+ If you're interested in this feature, please provide feedback!
646
+
599
647
  ## Acknowledgments
600
648
 
601
649
  - Built on top of [Click](https://click.palletsprojects.com/) and [Pydantic](https://pydantic-docs.helpmanual.io/)
602
650
  - Inspired by the DRY (Don't Repeat Yourself) principle
651
+ 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`.
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """Edge cases for automatic model instantiation - mixing models with raw Click options."""
3
+
4
+ import click
5
+ from pydantic import Field
6
+
7
+ from examples.auto_instantiate_poc import auto_instantiate, multi_auto_instantiate
8
+ from wry import AutoWryModel
9
+
10
+
11
+ class ServerConfig(AutoWryModel):
12
+ """Server configuration."""
13
+
14
+ host: str = Field(default="localhost", description="Server host")
15
+ port: int = Field(default=8080, description="Server port")
16
+
17
+
18
+ class DatabaseConfig(AutoWryModel):
19
+ """Database configuration."""
20
+
21
+ url: str = Field(default="sqlite:///app.db", description="Database URL")
22
+ pool_size: int = Field(default=5, description="Connection pool size")
23
+
24
+
25
+ # Example 1: Single model + additional Click options
26
+ @click.command()
27
+ @click.option("--dry-run", is_flag=True, help="Don't actually start the server")
28
+ @click.option("--config-file", type=click.Path(), help="External config file")
29
+ @auto_instantiate(ServerConfig)
30
+ def mixed_single(server: ServerConfig, dry_run: bool, config_file: str):
31
+ """Mix model with raw Click options."""
32
+ if config_file:
33
+ click.echo(f"Loading additional config from: {config_file}")
34
+
35
+ if dry_run:
36
+ click.echo("DRY RUN MODE - not starting server")
37
+ else:
38
+ click.echo(f"Starting server at {server.host}:{server.port}")
39
+
40
+
41
+ # Example 2: Multiple models + additional Click options
42
+ @click.command()
43
+ @click.option("--environment", type=click.Choice(["dev", "staging", "prod"]), default="dev")
44
+ @click.option("--force", is_flag=True, help="Force deployment")
45
+ @multi_auto_instantiate(ServerConfig, DatabaseConfig)
46
+ def mixed_multi(server: ServerConfig, db: DatabaseConfig, environment: str, force: bool):
47
+ """Mix multiple models with raw Click options."""
48
+ click.echo(f"Deploying to {environment} environment")
49
+
50
+ if force:
51
+ click.echo("FORCE mode enabled")
52
+
53
+ click.echo(f"Server: {server.host}:{server.port}")
54
+ click.echo(f"Database: {db.url}")
55
+
56
+
57
+ # Example 3: What happens with name conflicts?
58
+ class ConfigWithExtraOptions(AutoWryModel):
59
+ """Model that might conflict with raw options."""
60
+
61
+ name: str = Field(description="Application name")
62
+ version: str = Field(default="1.0.0", description="Version")
63
+ # Note: 'force' would conflict with the Click option below
64
+
65
+
66
+ @click.command()
67
+ @click.option("--force", is_flag=True, help="Force update") # Raw Click option
68
+ @click.option("--verbose", "-v", count=True, help="Verbosity level") # Another raw option
69
+ @auto_instantiate(ConfigWithExtraOptions)
70
+ def potential_conflict(config: ConfigWithExtraOptions, force: bool, verbose: int):
71
+ """Test potential naming conflicts."""
72
+ click.echo(f"App: {config.name} v{config.version}")
73
+ click.echo(f"Force: {force}")
74
+ click.echo(f"Verbosity: {verbose}")
75
+
76
+
77
+ # Example 4: Complex scenario - models + Click args + options
78
+ @click.command()
79
+ @click.argument("action", type=click.Choice(["start", "stop", "restart"]))
80
+ @click.option("--timeout", type=int, default=30, help="Operation timeout")
81
+ @multi_auto_instantiate(ServerConfig, DatabaseConfig)
82
+ def complex_command(action: str, server: ServerConfig, db: DatabaseConfig, timeout: int):
83
+ """Complex mix of arguments, options, and models."""
84
+ click.echo(f"Action: {action} (timeout: {timeout}s)")
85
+ click.echo(f"Server: {server.host}:{server.port}")
86
+ click.echo(f"Database: {db.url}")
87
+
88
+
89
+ # Example 5: How would parameter naming work?
90
+ def demonstrate_parameter_resolution():
91
+ """Show how parameters are resolved in different scenarios."""
92
+
93
+ scenarios = """
94
+ Current POC behavior:
95
+
96
+ 1. The decorator inspects function signature for type hints
97
+ 2. Parameters with model types get the instantiated models
98
+ 3. Other parameters get their values from Click kwargs
99
+
100
+ Example function signature:
101
+ def cmd(server: ServerConfig, db: DatabaseConfig, force: bool, verbose: int)
102
+
103
+ The decorator:
104
+ - Finds 'server: ServerConfig' → injects ServerConfig instance
105
+ - Finds 'db: DatabaseConfig' → injects DatabaseConfig instance
106
+ - 'force' and 'verbose' → passed through from Click kwargs
107
+
108
+ Potential issues:
109
+
110
+ 1. Name conflicts: If a model has a field 'force' and there's also a
111
+ Click option '--force', they would conflict in kwargs
112
+
113
+ 2. Parameter order: Currently relies on **kwargs which doesn't preserve
114
+ order, but function parameters have order
115
+
116
+ 3. Missing parameters: What if the function expects a parameter that
117
+ isn't in kwargs or models?
118
+ """
119
+
120
+ return scenarios
121
+
122
+
123
+ # Test the examples
124
+ if __name__ == "__main__":
125
+ import sys
126
+
127
+ from click.testing import CliRunner
128
+
129
+ runner = CliRunner()
130
+
131
+ if len(sys.argv) > 1:
132
+ # Allow running specific commands
133
+ import importlib.util
134
+
135
+ spec = importlib.util.spec_from_file_location("module", __file__)
136
+ module = importlib.util.module_from_spec(spec)
137
+ spec.loader.exec_module(module)
138
+
139
+ cmd_name = sys.argv[1]
140
+ cmd = getattr(module, cmd_name, None)
141
+ if cmd and hasattr(cmd, "callback"):
142
+ cmd(sys.argv[2:])
143
+ else:
144
+ # Run all tests
145
+ print("=== Single Model + Raw Options ===")
146
+ result = runner.invoke(mixed_single, ["--host", "0.0.0.0", "--dry-run", "--config-file", "app.yaml"])
147
+ print(result.output)
148
+
149
+ print("\n=== Multiple Models + Raw Options ===")
150
+ result = runner.invoke(
151
+ mixed_multi,
152
+ ["--environment", "prod", "--force", "--host", "api.example.com", "--url", "postgres://prod-db/app"],
153
+ )
154
+ print(result.output)
155
+
156
+ print("\n=== Potential Conflicts ===")
157
+ result = runner.invoke(potential_conflict, ["--name", "MyApp", "--force", "-vvv"])
158
+ print(result.output)
159
+
160
+ print("\n=== Complex Scenario ===")
161
+ result = runner.invoke(complex_command, ["restart", "--timeout", "60", "--port", "9000", "--pool-size", "20"])
162
+ print(result.output)
163
+
164
+ print("\n=== Parameter Resolution Logic ===")
165
+ print(demonstrate_parameter_resolution())