wry 0.5.1.dev5__tar.gz → 0.5.2__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.5.1.dev5 → wry-0.5.2}/CHANGELOG.md +48 -0
- {wry-0.5.1.dev5/wry.egg-info → wry-0.5.2}/PKG-INFO +7 -1
- {wry-0.5.1.dev5 → wry-0.5.2}/README.md +6 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_inheritance.py +2 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_multi_model.py +5 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_click_integration_extended.py +1 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_context_handling.py +3 -0
- wry-0.5.2/tests/unit/auto_model/test_optional_list_comma_separated.py +388 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_explicit_argument_help_injection.py +2 -2
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_argument_help_injection.py +6 -6
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_multiple_option_bug.py +2 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/_version.py +3 -3
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/click_integration.py +53 -17
- {wry-0.5.1.dev5 → wry-0.5.2/wry.egg-info}/PKG-INFO +7 -1
- {wry-0.5.1.dev5 → wry-0.5.2}/wry.egg-info/SOURCES.txt +1 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/.cursorrules +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/.github/workflows/ci-cd.yml +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/.gitignore +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/.markdownlint.json +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/.pre-commit-config.yaml +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/AI_KNOWLEDGE_BASE.md +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/CONTRIBUTING.md +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/LICENSE +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/RELEASE_PROCESS.md +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/TODO.md +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/check.sh +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/examples/autowrymodel_comprehensive.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/examples/config.json +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/examples/multimodel_comprehensive.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/examples/sample_config.json +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/examples/wrymodel_comprehensive.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/pyproject.toml +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/scripts/README.md +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/scripts/extract_release_notes.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/scripts/test_all_versions.sh +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/scripts/test_ci_locally.sh +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/scripts/test_with_act.sh +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/scripts/update-dependencies.sh +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/setup.cfg +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/README.md +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_auto_model.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_source_precedence.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_click_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_click_integration.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_annotation_inference.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_field_processing.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_list_fields.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_comma_separated_lists.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_field_annotation_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_field_annotations.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_type_inference.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/README_TESTING_STRATEGY.md +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_argument_types.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_bool_flag_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_config_building.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_constraint_formatting.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_decorator_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_interval_constraints.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_lambda_parsing.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_length_constraints.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_parameter_generation.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_predicate_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_closure_extraction_errors.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_closure_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_constraint_behavior.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_constraint_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_env_vars_option.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_field_alias_with_click_options.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_format_constraint_text.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_json_config_loading.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_lambda_behavior.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_lambda_error_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_predicate_source_errors.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_strict_mode_errors.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_type_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_accessors.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_advanced_features.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_core.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_env_utils.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_field_constraint_extraction.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_field_utils.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_field_utils_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_sources.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_type_checking_blocks.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_accessor_caching.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_extract_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_extract_subset_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_click_context_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_data_extraction.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_default_handling.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_environment_integration.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_extract_subset_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_extraction_methods.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_field_errors.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_object_extraction.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_non_dict_object_extraction.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_object_attribute_extraction.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/multi_model/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/multi_model/test_multi_model.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/multi_model/test_type_checking.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_auto_model_field_processing.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_comprehensive_imports.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_exclude_enum.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_generate_click_classmethod.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_help_system.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_init.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_init_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_init_version_edge_cases.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_model_extraction_methods.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_type_checking_imports.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_variadic_argument_bug.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_version_fallback.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_version_parsing.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/auto_model.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/comma_separated.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/__init__.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/accessors.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/env_utils.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/field_utils.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/model.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/sources.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/help_system.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/multi_model.py +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry/py.typed +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry.egg-info/dependency_links.txt +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry.egg-info/requires.txt +0 -0
- {wry-0.5.1.dev5 → wry-0.5.2}/wry.egg-info/top_level.txt +0 -0
|
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.2] - 2025-10-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Optional list fields with comma-separated parsing** 🐛
|
|
15
|
+
**THE BIG ONE:** Fixed critical bug where `list[str] | None` and `Optional[list[str]]` fields didn't work with comma-separated parsing
|
|
16
|
+
- **Root cause**: Nested `Annotated` types created by `AutoWryModel` (e.g., `Annotated[Optional[Annotated[list[str], CommaSeparated]], AutoOption]`) weren't being properly unwrapped
|
|
17
|
+
- **Fix 1**: Extract metadata from inner `Annotated` types within `Optional`/`Union` wrappers
|
|
18
|
+
- **Fix 2**: Unwrap `Annotated` types after unwrapping `Optional` to get to the base list type
|
|
19
|
+
- Works with both model-wide `comma_separated_lists` ClassVar and per-field `CommaSeparated` annotation
|
|
20
|
+
- Works with `list[str] | None`, `Optional[list[T]]`, and all numeric types (int, float)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Comprehensive tests for optional comma-separated lists** ✅
|
|
25
|
+
Added 13 test cases covering all use cases and edge cases:
|
|
26
|
+
- MVP bug: `list[str] | None` with model-wide `comma_separated_lists`
|
|
27
|
+
- Per-field `Annotated[list[T], CommaSeparated] | None`
|
|
28
|
+
- Old-style `Optional[list[T]]` syntax
|
|
29
|
+
- Multiple types: str, int, float
|
|
30
|
+
- Required vs optional lists
|
|
31
|
+
- Source tracking verification
|
|
32
|
+
- Default values: `None`, `[]`, `default_factory`
|
|
33
|
+
- Space handling in comma-separated input
|
|
34
|
+
- Double-nested `Annotated` types (the actual bug pattern)
|
|
35
|
+
- **Total test count: 507 tests (all passing)** ✨
|
|
36
|
+
|
|
37
|
+
## [0.5.1] - 2025-10-14
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **Comma-separated lists with `default_factory`** 🐛
|
|
42
|
+
- Fixed bug where fields with `default_factory=list` would receive `None` instead of empty list when not provided
|
|
43
|
+
- Added proper handling in `click_integration.py` to call `default_factory()` for default values
|
|
44
|
+
- All comma-separated list tests now pass without validation errors
|
|
45
|
+
|
|
10
46
|
### Added
|
|
11
47
|
|
|
12
48
|
- **Development guidelines** 📚
|
|
@@ -17,6 +53,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
53
|
|
|
18
54
|
### Changed
|
|
19
55
|
|
|
56
|
+
- **Test quality improvements** ✨
|
|
57
|
+
- Fixed Pydantic shadow warnings by renaming `source` field to `source_path`/`source_file` in test cases
|
|
58
|
+
- Added `@pytest.mark.filterwarnings` to suppress intentional warnings in tests
|
|
59
|
+
- Tests that validate warning behavior no longer pollute test output
|
|
60
|
+
- All 494 tests pass cleanly with zero warnings
|
|
61
|
+
|
|
62
|
+
- **Test quality improvements** ✨
|
|
63
|
+
- Fixed Pydantic shadow warnings by renaming `source` field to `source_path`/`source_file` in test cases
|
|
64
|
+
- Added `@pytest.mark.filterwarnings` to suppress intentional warnings in tests
|
|
65
|
+
- Tests that validate warning behavior no longer pollute test output
|
|
66
|
+
- All 494 tests pass cleanly with zero warnings
|
|
67
|
+
|
|
20
68
|
- **Documentation cross-references** 🔗
|
|
21
69
|
- README.md: Added Contributing section with links to CONTRIBUTING.md, .cursorrules, AI_KNOWLEDGE_BASE.md
|
|
22
70
|
- AI_KNOWLEDGE_BASE.md: Updated header with related documentation links
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wry
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -1071,6 +1071,7 @@ mypy wry
|
|
|
1071
1071
|
wry has comprehensive documentation for different audiences:
|
|
1072
1072
|
|
|
1073
1073
|
### For Users
|
|
1074
|
+
|
|
1074
1075
|
- 📘 **`README.md`** (this file) - Getting started, features, usage examples
|
|
1075
1076
|
- 📁 **`examples/`** - Working code examples
|
|
1076
1077
|
- `examples/autowrymodel_comprehensive.py` - Complete AutoWryModel features
|
|
@@ -1078,12 +1079,14 @@ wry has comprehensive documentation for different audiences:
|
|
|
1078
1079
|
- `examples/multimodel_comprehensive.py` - Multi-model usage
|
|
1079
1080
|
|
|
1080
1081
|
### For Contributors
|
|
1082
|
+
|
|
1081
1083
|
- 📖 **`CONTRIBUTING.md`** - Complete contributor guide with code patterns and checklists
|
|
1082
1084
|
- 🤖 **`.cursorrules`** - AI assistant quick reference (references CONTRIBUTING.md)
|
|
1083
1085
|
- 🚀 **`RELEASE_PROCESS.md`** - How to create releases and manage versions
|
|
1084
1086
|
- 📋 **`TODO.md`** - Current tasks, planned features, and work in progress
|
|
1085
1087
|
|
|
1086
1088
|
### Technical Reference
|
|
1089
|
+
|
|
1087
1090
|
- 📚 **`AI_KNOWLEDGE_BASE.md`** - Complete technical reference for AI/LLMs (also useful for humans)
|
|
1088
1091
|
- 📝 **`CHANGELOG.md`** - Version history and all changes
|
|
1089
1092
|
- 🧪 **`tests/README.md`** - Test organization and structure
|
|
@@ -1092,12 +1095,14 @@ wry has comprehensive documentation for different audiences:
|
|
|
1092
1095
|
### Quick Start Navigation
|
|
1093
1096
|
|
|
1094
1097
|
**I'm a user, I want to...**
|
|
1098
|
+
|
|
1095
1099
|
- Get started → README.md "Quick Start" section
|
|
1096
1100
|
- See examples → `examples/` directory
|
|
1097
1101
|
- Understand features → README.md "Features" section
|
|
1098
1102
|
- Track config sources → README.md "Value Source Tracking" section
|
|
1099
1103
|
|
|
1100
1104
|
**I'm a contributor, I want to...**
|
|
1105
|
+
|
|
1101
1106
|
- Set up development → CONTRIBUTING.md "Development Setup" section
|
|
1102
1107
|
- Add a feature → CONTRIBUTING.md "Adding New Features" section
|
|
1103
1108
|
- Run tests → CONTRIBUTING.md "Testing" section
|
|
@@ -1105,6 +1110,7 @@ wry has comprehensive documentation for different audiences:
|
|
|
1105
1110
|
- Check current tasks → TODO.md
|
|
1106
1111
|
|
|
1107
1112
|
**I'm an AI assistant, I want to...**
|
|
1113
|
+
|
|
1108
1114
|
- Quick reference → `.cursorrules`
|
|
1109
1115
|
- Technical details → `AI_KNOWLEDGE_BASE.md`
|
|
1110
1116
|
- Code patterns → `CONTRIBUTING.md`
|
|
@@ -1027,6 +1027,7 @@ mypy wry
|
|
|
1027
1027
|
wry has comprehensive documentation for different audiences:
|
|
1028
1028
|
|
|
1029
1029
|
### For Users
|
|
1030
|
+
|
|
1030
1031
|
- 📘 **`README.md`** (this file) - Getting started, features, usage examples
|
|
1031
1032
|
- 📁 **`examples/`** - Working code examples
|
|
1032
1033
|
- `examples/autowrymodel_comprehensive.py` - Complete AutoWryModel features
|
|
@@ -1034,12 +1035,14 @@ wry has comprehensive documentation for different audiences:
|
|
|
1034
1035
|
- `examples/multimodel_comprehensive.py` - Multi-model usage
|
|
1035
1036
|
|
|
1036
1037
|
### For Contributors
|
|
1038
|
+
|
|
1037
1039
|
- 📖 **`CONTRIBUTING.md`** - Complete contributor guide with code patterns and checklists
|
|
1038
1040
|
- 🤖 **`.cursorrules`** - AI assistant quick reference (references CONTRIBUTING.md)
|
|
1039
1041
|
- 🚀 **`RELEASE_PROCESS.md`** - How to create releases and manage versions
|
|
1040
1042
|
- 📋 **`TODO.md`** - Current tasks, planned features, and work in progress
|
|
1041
1043
|
|
|
1042
1044
|
### Technical Reference
|
|
1045
|
+
|
|
1043
1046
|
- 📚 **`AI_KNOWLEDGE_BASE.md`** - Complete technical reference for AI/LLMs (also useful for humans)
|
|
1044
1047
|
- 📝 **`CHANGELOG.md`** - Version history and all changes
|
|
1045
1048
|
- 🧪 **`tests/README.md`** - Test organization and structure
|
|
@@ -1048,12 +1051,14 @@ wry has comprehensive documentation for different audiences:
|
|
|
1048
1051
|
### Quick Start Navigation
|
|
1049
1052
|
|
|
1050
1053
|
**I'm a user, I want to...**
|
|
1054
|
+
|
|
1051
1055
|
- Get started → README.md "Quick Start" section
|
|
1052
1056
|
- See examples → `examples/` directory
|
|
1053
1057
|
- Understand features → README.md "Features" section
|
|
1054
1058
|
- Track config sources → README.md "Value Source Tracking" section
|
|
1055
1059
|
|
|
1056
1060
|
**I'm a contributor, I want to...**
|
|
1061
|
+
|
|
1057
1062
|
- Set up development → CONTRIBUTING.md "Development Setup" section
|
|
1058
1063
|
- Add a feature → CONTRIBUTING.md "Adding New Features" section
|
|
1059
1064
|
- Run tests → CONTRIBUTING.md "Testing" section
|
|
@@ -1061,6 +1066,7 @@ wry has comprehensive documentation for different audiences:
|
|
|
1061
1066
|
- Check current tasks → TODO.md
|
|
1062
1067
|
|
|
1063
1068
|
**I'm an AI assistant, I want to...**
|
|
1069
|
+
|
|
1064
1070
|
- Quick reference → `.cursorrules`
|
|
1065
1071
|
- Technical details → `AI_KNOWLEDGE_BASE.md`
|
|
1066
1072
|
- Code patterns → `CONTRIBUTING.md`
|
|
@@ -483,6 +483,8 @@ class TestInheritanceEdgeCases:
|
|
|
483
483
|
assert len(ChildConfig.model_fields) == 2
|
|
484
484
|
|
|
485
485
|
|
|
486
|
+
@pytest.mark.filterwarnings("ignore:Function.*already decorated:UserWarning")
|
|
487
|
+
@pytest.mark.filterwarnings("ignore:The parameter.*is used more than once:UserWarning")
|
|
486
488
|
class TestMultiModelInheritance:
|
|
487
489
|
"""Test inheritance scenarios with multi-model commands."""
|
|
488
490
|
|
|
@@ -44,6 +44,7 @@ class CacheArgs(WryModel):
|
|
|
44
44
|
class TestSplitKwargs:
|
|
45
45
|
"""Test the split_kwargs_by_model function."""
|
|
46
46
|
|
|
47
|
+
@pytest.mark.filterwarnings("ignore:Unused kwargs:UserWarning")
|
|
47
48
|
def test_split_kwargs_basic(self):
|
|
48
49
|
"""Test basic kwarg splitting between models."""
|
|
49
50
|
kwargs = {"host": "example.com", "port": 3306, "debug": True, "workers": 8, "unknown": "ignored"}
|
|
@@ -133,6 +134,8 @@ class TestCreateModels:
|
|
|
133
134
|
create_models(ctx, kwargs, DatabaseArgs)
|
|
134
135
|
|
|
135
136
|
|
|
137
|
+
@pytest.mark.filterwarnings("ignore:Function.*already decorated:UserWarning")
|
|
138
|
+
@pytest.mark.filterwarnings("ignore:The parameter.*is used more than once:UserWarning")
|
|
136
139
|
class TestMultiModelDecorator:
|
|
137
140
|
"""Test the multi_model decorator."""
|
|
138
141
|
|
|
@@ -279,6 +282,8 @@ class TestSingletonOption:
|
|
|
279
282
|
# They should be different objects
|
|
280
283
|
assert option1 is not option2
|
|
281
284
|
|
|
285
|
+
@pytest.mark.filterwarnings("ignore:Function.*already decorated:UserWarning")
|
|
286
|
+
@pytest.mark.filterwarnings("ignore:The parameter.*is used more than once:UserWarning")
|
|
282
287
|
def test_singleton_option_in_model(self):
|
|
283
288
|
"""Test using singleton option in a model."""
|
|
284
289
|
from wry.multi_model import _SINGLETON_OPTIONS
|
|
@@ -457,6 +457,7 @@ class TestAddConfigOption:
|
|
|
457
457
|
class TestExplicitClickDecorators:
|
|
458
458
|
"""Test handling of explicit Click decorators in annotations."""
|
|
459
459
|
|
|
460
|
+
@pytest.mark.filterwarnings("ignore:The parameter.*is used more than once:UserWarning")
|
|
460
461
|
def test_explicit_option_decorator(self):
|
|
461
462
|
"""Test field with explicit click.option in annotation."""
|
|
462
463
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from typing import Annotated, Any
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
|
+
import pytest
|
|
6
7
|
from click.testing import CliRunner
|
|
7
8
|
from pydantic import Field
|
|
8
9
|
|
|
@@ -78,6 +79,8 @@ class TestContextHandling:
|
|
|
78
79
|
assert result.exit_code == 0
|
|
79
80
|
assert "Name: direct" in result.output
|
|
80
81
|
|
|
82
|
+
@pytest.mark.filterwarnings("ignore:Function.*already decorated:UserWarning")
|
|
83
|
+
@pytest.mark.filterwarnings("ignore:The parameter.*is used more than once:UserWarning")
|
|
81
84
|
def test_multiple_decorators_requires_care(self):
|
|
82
85
|
"""Test that multiple decorators need explicit pass_context control."""
|
|
83
86
|
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Tests for comma-separated lists with Optional/Union types."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, ClassVar
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click.testing import CliRunner
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from wry import AutoWryModel, CommaSeparated
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestOptionalListCommaSeparated:
|
|
13
|
+
"""Test comma-separated parsing with Optional list types."""
|
|
14
|
+
|
|
15
|
+
def test_mvp_bug_optional_list_model_wide_comma_separated(self):
|
|
16
|
+
"""Test the exact MVP bug: list[str] | None with model-wide comma_separated_lists.
|
|
17
|
+
|
|
18
|
+
This was the original bug report where Optional list fields with comma_separated_lists
|
|
19
|
+
enabled at the model level would fail with validation errors.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
class Config(AutoWryModel):
|
|
23
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
24
|
+
items: list[str] | None = Field(default=None, description="List of items")
|
|
25
|
+
|
|
26
|
+
@click.command()
|
|
27
|
+
@Config.generate_click_parameters()
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
30
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
31
|
+
click.echo(f"Raw kwargs: {kwargs}")
|
|
32
|
+
click.echo(f"Parsed config: {config.items}")
|
|
33
|
+
|
|
34
|
+
runner = CliRunner()
|
|
35
|
+
|
|
36
|
+
# This was the failing case - passing comma-separated values
|
|
37
|
+
result = runner.invoke(cmd, ["--items", "x,y,z"])
|
|
38
|
+
assert result.exit_code == 0, f"Failed: {result.output}"
|
|
39
|
+
assert "Parsed config: ['x', 'y', 'z']" in result.output
|
|
40
|
+
|
|
41
|
+
# Also test with no value
|
|
42
|
+
result = runner.invoke(cmd, [])
|
|
43
|
+
assert result.exit_code == 0
|
|
44
|
+
assert "Parsed config: None" in result.output
|
|
45
|
+
|
|
46
|
+
def test_optional_list_with_comma_separated_model_wide(self):
|
|
47
|
+
"""Test that Optional[list[T]] works with model-wide comma_separated_lists."""
|
|
48
|
+
|
|
49
|
+
class Config(AutoWryModel):
|
|
50
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
51
|
+
items: list[str] | None = Field(default=None, description="List of items")
|
|
52
|
+
|
|
53
|
+
@click.command()
|
|
54
|
+
@Config.generate_click_parameters()
|
|
55
|
+
@click.pass_context
|
|
56
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
57
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
58
|
+
click.echo(f"Items: {config.items}")
|
|
59
|
+
|
|
60
|
+
runner = CliRunner()
|
|
61
|
+
|
|
62
|
+
# Test with comma-separated values
|
|
63
|
+
result = runner.invoke(cmd, ["--items", "a,b,c"])
|
|
64
|
+
assert result.exit_code == 0, f"Failed: {result.output}"
|
|
65
|
+
assert "Items: ['a', 'b', 'c']" in result.output
|
|
66
|
+
|
|
67
|
+
# Test with single value
|
|
68
|
+
result = runner.invoke(cmd, ["--items", "single"])
|
|
69
|
+
assert result.exit_code == 0
|
|
70
|
+
assert "Items: ['single']" in result.output
|
|
71
|
+
|
|
72
|
+
# Test with no value (should be None)
|
|
73
|
+
result = runner.invoke(cmd, [])
|
|
74
|
+
assert result.exit_code == 0
|
|
75
|
+
assert "Items: None" in result.output
|
|
76
|
+
|
|
77
|
+
def test_optional_list_with_per_field_comma_separated(self):
|
|
78
|
+
"""Test that Optional[list[T]] works with per-field CommaSeparated annotation."""
|
|
79
|
+
|
|
80
|
+
class Config(AutoWryModel):
|
|
81
|
+
items: Annotated[list[str], CommaSeparated] | None = Field(default=None, description="List of items")
|
|
82
|
+
|
|
83
|
+
@click.command()
|
|
84
|
+
@Config.generate_click_parameters()
|
|
85
|
+
@click.pass_context
|
|
86
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
87
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
88
|
+
click.echo(f"Items: {config.items}")
|
|
89
|
+
|
|
90
|
+
runner = CliRunner()
|
|
91
|
+
|
|
92
|
+
# Test with comma-separated values
|
|
93
|
+
result = runner.invoke(cmd, ["--items", "x,y,z"])
|
|
94
|
+
assert result.exit_code == 0
|
|
95
|
+
assert "Items: ['x', 'y', 'z']" in result.output
|
|
96
|
+
|
|
97
|
+
# Test with no value (should be None)
|
|
98
|
+
result = runner.invoke(cmd, [])
|
|
99
|
+
assert result.exit_code == 0
|
|
100
|
+
assert "Items: None" in result.output
|
|
101
|
+
|
|
102
|
+
def test_optional_list_int_with_comma_separated(self):
|
|
103
|
+
"""Test that Optional[list[int]] works with comma-separated parsing."""
|
|
104
|
+
|
|
105
|
+
class Config(AutoWryModel):
|
|
106
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
107
|
+
ports: list[int] | None = Field(default=None, description="Port numbers")
|
|
108
|
+
|
|
109
|
+
@click.command()
|
|
110
|
+
@Config.generate_click_parameters()
|
|
111
|
+
@click.pass_context
|
|
112
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
113
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
114
|
+
click.echo(f"Ports: {config.ports}")
|
|
115
|
+
if config.ports:
|
|
116
|
+
click.echo(f"Types: {[type(p).__name__ for p in config.ports]}")
|
|
117
|
+
|
|
118
|
+
runner = CliRunner()
|
|
119
|
+
|
|
120
|
+
# Test with comma-separated integers
|
|
121
|
+
result = runner.invoke(cmd, ["--ports", "80,443,8080"])
|
|
122
|
+
assert result.exit_code == 0
|
|
123
|
+
assert "Ports: [80, 443, 8080]" in result.output
|
|
124
|
+
assert "Types: ['int', 'int', 'int']" in result.output
|
|
125
|
+
|
|
126
|
+
# Test with no value (should be None)
|
|
127
|
+
result = runner.invoke(cmd, [])
|
|
128
|
+
assert result.exit_code == 0
|
|
129
|
+
assert "Ports: None" in result.output
|
|
130
|
+
|
|
131
|
+
def test_optional_list_with_default_empty_list(self):
|
|
132
|
+
"""Test Optional list with default=[] instead of default=None."""
|
|
133
|
+
|
|
134
|
+
class Config(AutoWryModel):
|
|
135
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
136
|
+
tags: list[str] | None = Field(default=[], description="Tags")
|
|
137
|
+
|
|
138
|
+
@click.command()
|
|
139
|
+
@Config.generate_click_parameters()
|
|
140
|
+
@click.pass_context
|
|
141
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
142
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
143
|
+
click.echo(f"Tags: {config.tags}")
|
|
144
|
+
|
|
145
|
+
runner = CliRunner()
|
|
146
|
+
|
|
147
|
+
# Test with values
|
|
148
|
+
result = runner.invoke(cmd, ["--tags", "a,b"])
|
|
149
|
+
assert result.exit_code == 0
|
|
150
|
+
assert "Tags: ['a', 'b']" in result.output
|
|
151
|
+
|
|
152
|
+
# Test with no value (should be empty list)
|
|
153
|
+
result = runner.invoke(cmd, [])
|
|
154
|
+
assert result.exit_code == 0
|
|
155
|
+
assert "Tags: []" in result.output
|
|
156
|
+
|
|
157
|
+
def test_optional_list_with_default_factory(self):
|
|
158
|
+
"""Test Optional list with default_factory."""
|
|
159
|
+
|
|
160
|
+
class Config(AutoWryModel):
|
|
161
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
162
|
+
items: list[str] = Field(default_factory=list, description="Items")
|
|
163
|
+
|
|
164
|
+
@click.command()
|
|
165
|
+
@Config.generate_click_parameters()
|
|
166
|
+
@click.pass_context
|
|
167
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
168
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
169
|
+
click.echo(f"Items: {config.items}")
|
|
170
|
+
|
|
171
|
+
runner = CliRunner()
|
|
172
|
+
|
|
173
|
+
# Test with values
|
|
174
|
+
result = runner.invoke(cmd, ["--items", "a,b,c"])
|
|
175
|
+
assert result.exit_code == 0
|
|
176
|
+
assert "Items: ['a', 'b', 'c']" in result.output
|
|
177
|
+
|
|
178
|
+
# Test with no value (should be empty list from factory)
|
|
179
|
+
result = runner.invoke(cmd, [])
|
|
180
|
+
assert result.exit_code == 0
|
|
181
|
+
assert "Items: []" in result.output
|
|
182
|
+
|
|
183
|
+
def test_optional_list_old_syntax(self):
|
|
184
|
+
"""Test Optional[list[T]] using old Optional[] syntax instead of | None."""
|
|
185
|
+
|
|
186
|
+
class Config(AutoWryModel):
|
|
187
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
188
|
+
items: list[str] | None = Field(default=None, description="Items")
|
|
189
|
+
|
|
190
|
+
@click.command()
|
|
191
|
+
@Config.generate_click_parameters()
|
|
192
|
+
@click.pass_context
|
|
193
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
194
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
195
|
+
click.echo(f"Items: {config.items}")
|
|
196
|
+
|
|
197
|
+
runner = CliRunner()
|
|
198
|
+
|
|
199
|
+
# Test with values
|
|
200
|
+
result = runner.invoke(cmd, ["--items", "x,y"])
|
|
201
|
+
assert result.exit_code == 0
|
|
202
|
+
assert "Items: ['x', 'y']" in result.output
|
|
203
|
+
|
|
204
|
+
# Test with no value
|
|
205
|
+
result = runner.invoke(cmd, [])
|
|
206
|
+
assert result.exit_code == 0
|
|
207
|
+
assert "Items: None" in result.output
|
|
208
|
+
|
|
209
|
+
def test_optional_list_float(self):
|
|
210
|
+
"""Test Optional[list[float]] with comma-separated parsing."""
|
|
211
|
+
|
|
212
|
+
class Config(AutoWryModel):
|
|
213
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
214
|
+
values: list[float] | None = Field(default=None, description="Float values")
|
|
215
|
+
|
|
216
|
+
@click.command()
|
|
217
|
+
@Config.generate_click_parameters()
|
|
218
|
+
@click.pass_context
|
|
219
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
220
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
221
|
+
click.echo(f"Values: {config.values}")
|
|
222
|
+
if config.values:
|
|
223
|
+
click.echo(f"Types: {[type(v).__name__ for v in config.values]}")
|
|
224
|
+
|
|
225
|
+
runner = CliRunner()
|
|
226
|
+
|
|
227
|
+
# Test with comma-separated floats
|
|
228
|
+
result = runner.invoke(cmd, ["--values", "1.5,2.7,3.14"])
|
|
229
|
+
assert result.exit_code == 0
|
|
230
|
+
assert "Values: [1.5, 2.7, 3.14]" in result.output
|
|
231
|
+
assert "Types: ['float', 'float', 'float']" in result.output
|
|
232
|
+
|
|
233
|
+
# Test with no value
|
|
234
|
+
result = runner.invoke(cmd, [])
|
|
235
|
+
assert result.exit_code == 0
|
|
236
|
+
assert "Values: None" in result.output
|
|
237
|
+
|
|
238
|
+
def test_mixed_optional_and_required_lists(self):
|
|
239
|
+
"""Test model with both optional and required list fields."""
|
|
240
|
+
|
|
241
|
+
class Config(AutoWryModel):
|
|
242
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
243
|
+
required_items: list[str] = Field(description="Required items")
|
|
244
|
+
optional_items: list[str] | None = Field(default=None, description="Optional items")
|
|
245
|
+
|
|
246
|
+
@click.command()
|
|
247
|
+
@Config.generate_click_parameters()
|
|
248
|
+
@click.pass_context
|
|
249
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
250
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
251
|
+
click.echo(f"Required: {config.required_items}")
|
|
252
|
+
click.echo(f"Optional: {config.optional_items}")
|
|
253
|
+
|
|
254
|
+
runner = CliRunner()
|
|
255
|
+
|
|
256
|
+
# Test with both fields
|
|
257
|
+
result = runner.invoke(cmd, ["--required-items", "a,b", "--optional-items", "x,y"])
|
|
258
|
+
assert result.exit_code == 0
|
|
259
|
+
assert "Required: ['a', 'b']" in result.output
|
|
260
|
+
assert "Optional: ['x', 'y']" in result.output
|
|
261
|
+
|
|
262
|
+
# Test with only required field
|
|
263
|
+
result = runner.invoke(cmd, ["--required-items", "a"])
|
|
264
|
+
assert result.exit_code == 0
|
|
265
|
+
assert "Required: ['a']" in result.output
|
|
266
|
+
assert "Optional: None" in result.output
|
|
267
|
+
|
|
268
|
+
# Test missing required field should fail
|
|
269
|
+
result = runner.invoke(cmd, [])
|
|
270
|
+
assert result.exit_code != 0
|
|
271
|
+
|
|
272
|
+
def test_optional_list_source_tracking(self):
|
|
273
|
+
"""Test that source tracking works correctly with optional comma-separated lists."""
|
|
274
|
+
|
|
275
|
+
class Config(AutoWryModel):
|
|
276
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
277
|
+
items: list[str] | None = Field(default=None, description="Items")
|
|
278
|
+
|
|
279
|
+
@click.command()
|
|
280
|
+
@Config.generate_click_parameters()
|
|
281
|
+
@click.pass_context
|
|
282
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
283
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
284
|
+
click.echo(f"Items: {config.items}")
|
|
285
|
+
click.echo(f"Source: {config.get_value_source('items').value}")
|
|
286
|
+
|
|
287
|
+
runner = CliRunner()
|
|
288
|
+
|
|
289
|
+
# Test CLI source
|
|
290
|
+
result = runner.invoke(cmd, ["--items", "a,b,c"])
|
|
291
|
+
assert result.exit_code == 0
|
|
292
|
+
assert "Items: ['a', 'b', 'c']" in result.output
|
|
293
|
+
assert "Source: cli" in result.output
|
|
294
|
+
|
|
295
|
+
# Test DEFAULT source when not provided
|
|
296
|
+
result = runner.invoke(cmd, [])
|
|
297
|
+
assert result.exit_code == 0
|
|
298
|
+
assert "Items: None" in result.output
|
|
299
|
+
assert "Source: default" in result.output
|
|
300
|
+
|
|
301
|
+
def test_optional_annotated_list_non_optional(self):
|
|
302
|
+
"""Test non-optional Annotated list with CommaSeparated (for contrast)."""
|
|
303
|
+
|
|
304
|
+
class Config(AutoWryModel):
|
|
305
|
+
items: Annotated[list[str], CommaSeparated] = Field(description="Required items")
|
|
306
|
+
|
|
307
|
+
@click.command()
|
|
308
|
+
@Config.generate_click_parameters()
|
|
309
|
+
@click.pass_context
|
|
310
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
311
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
312
|
+
click.echo(f"Items: {config.items}")
|
|
313
|
+
|
|
314
|
+
runner = CliRunner()
|
|
315
|
+
|
|
316
|
+
# Test with values
|
|
317
|
+
result = runner.invoke(cmd, ["--items", "a,b,c"])
|
|
318
|
+
assert result.exit_code == 0
|
|
319
|
+
assert "Items: ['a', 'b', 'c']" in result.output
|
|
320
|
+
|
|
321
|
+
# Test without values should fail (required)
|
|
322
|
+
result = runner.invoke(cmd, [])
|
|
323
|
+
assert result.exit_code != 0
|
|
324
|
+
|
|
325
|
+
def test_optional_list_with_spaces(self):
|
|
326
|
+
"""Test comma-separated parsing handles spaces correctly."""
|
|
327
|
+
|
|
328
|
+
class Config(AutoWryModel):
|
|
329
|
+
comma_separated_lists: ClassVar[bool] = True
|
|
330
|
+
items: list[str] | None = Field(default=None, description="Items")
|
|
331
|
+
|
|
332
|
+
@click.command()
|
|
333
|
+
@Config.generate_click_parameters()
|
|
334
|
+
@click.pass_context
|
|
335
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
336
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
337
|
+
click.echo(f"Items: {config.items}")
|
|
338
|
+
|
|
339
|
+
runner = CliRunner()
|
|
340
|
+
|
|
341
|
+
# Test with spaces after commas (should be stripped)
|
|
342
|
+
result = runner.invoke(cmd, ["--items", "a, b, c"])
|
|
343
|
+
assert result.exit_code == 0
|
|
344
|
+
assert "Items: ['a', 'b', 'c']" in result.output
|
|
345
|
+
|
|
346
|
+
# Test single item with spaces
|
|
347
|
+
result = runner.invoke(cmd, ["--items", " item "])
|
|
348
|
+
assert result.exit_code == 0
|
|
349
|
+
assert "Items: ['item']" in result.output
|
|
350
|
+
|
|
351
|
+
def test_double_nested_optional_annotated(self):
|
|
352
|
+
"""Test the specific pattern that was buggy.
|
|
353
|
+
|
|
354
|
+
Tests: Annotated[Optional[Annotated[list, CommaSeparated]], AutoOption]
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
class Config(AutoWryModel):
|
|
358
|
+
# This creates: Annotated[Optional[Annotated[list[str], CommaSeparated]], AutoOption]
|
|
359
|
+
items: Annotated[list[str], CommaSeparated] | None = Field(default=None, description="Items")
|
|
360
|
+
ports: Annotated[list[int], CommaSeparated] | None = Field(default=None, description="Ports")
|
|
361
|
+
|
|
362
|
+
@click.command()
|
|
363
|
+
@Config.generate_click_parameters()
|
|
364
|
+
@click.pass_context
|
|
365
|
+
def cmd(ctx: click.Context, **kwargs: Any):
|
|
366
|
+
config = Config.from_click_context(ctx, **kwargs)
|
|
367
|
+
click.echo(f"Items: {config.items}")
|
|
368
|
+
click.echo(f"Ports: {config.ports}")
|
|
369
|
+
|
|
370
|
+
runner = CliRunner()
|
|
371
|
+
|
|
372
|
+
# Test both fields
|
|
373
|
+
result = runner.invoke(cmd, ["--items", "a,b,c", "--ports", "80,443"])
|
|
374
|
+
assert result.exit_code == 0
|
|
375
|
+
assert "Items: ['a', 'b', 'c']" in result.output
|
|
376
|
+
assert "Ports: [80, 443]" in result.output
|
|
377
|
+
|
|
378
|
+
# Test neither field
|
|
379
|
+
result = runner.invoke(cmd, [])
|
|
380
|
+
assert result.exit_code == 0
|
|
381
|
+
assert "Items: None" in result.output
|
|
382
|
+
assert "Ports: None" in result.output
|
|
383
|
+
|
|
384
|
+
# Test only one field
|
|
385
|
+
result = runner.invoke(cmd, ["--items", "x"])
|
|
386
|
+
assert result.exit_code == 0
|
|
387
|
+
assert "Items: ['x']" in result.output
|
|
388
|
+
assert "Ports: None" in result.output
|
|
@@ -57,7 +57,7 @@ class TestExplicitArgumentHelpInjection:
|
|
|
57
57
|
|
|
58
58
|
class Config(WryModel):
|
|
59
59
|
# Auto-generated argument with Field description
|
|
60
|
-
|
|
60
|
+
source_file: Annotated[str, AutoArgument] = Field(description="Source file")
|
|
61
61
|
# Explicit click.argument with help
|
|
62
62
|
dest: Annotated[str, click.argument("destination", help="Destination file")] = Field()
|
|
63
63
|
|
|
@@ -70,7 +70,7 @@ class TestExplicitArgumentHelpInjection:
|
|
|
70
70
|
# Both should be in the docstring
|
|
71
71
|
assert cmd.__doc__ is not None
|
|
72
72
|
assert "Arguments:" in cmd.__doc__
|
|
73
|
-
assert "
|
|
73
|
+
assert "SOURCE_FILE" in cmd.__doc__
|
|
74
74
|
assert "Source file" in cmd.__doc__
|
|
75
75
|
assert "DESTINATION" in cmd.__doc__
|
|
76
76
|
assert "Destination file" in cmd.__doc__
|
|
@@ -16,7 +16,7 @@ class TestArgumentHelpInjection:
|
|
|
16
16
|
"""Test that AutoWryModel injects argument help into docstring."""
|
|
17
17
|
|
|
18
18
|
class Config(AutoWryModel):
|
|
19
|
-
|
|
19
|
+
source_path: Annotated[str, AutoClickParameter.ARGUMENT] = Field(description="Source file path")
|
|
20
20
|
dest: Annotated[str, AutoClickParameter.ARGUMENT] = Field(description="Destination file path")
|
|
21
21
|
verbose: bool = False
|
|
22
22
|
|
|
@@ -25,14 +25,14 @@ class TestArgumentHelpInjection:
|
|
|
25
25
|
def copy(**kwargs: Any):
|
|
26
26
|
"""Copy files from source to destination."""
|
|
27
27
|
config = Config(**kwargs)
|
|
28
|
-
click.echo(f"{config.
|
|
28
|
+
click.echo(f"{config.source_path} -> {config.dest}")
|
|
29
29
|
|
|
30
30
|
runner = CliRunner()
|
|
31
31
|
result = runner.invoke(copy, ["--help"])
|
|
32
32
|
|
|
33
33
|
assert result.exit_code == 0
|
|
34
34
|
assert "Arguments:" in result.output
|
|
35
|
-
assert "
|
|
35
|
+
assert "SOURCE_PATH" in result.output
|
|
36
36
|
assert "Source file path" in result.output
|
|
37
37
|
assert "DEST" in result.output
|
|
38
38
|
assert "Destination file path" in result.output
|
|
@@ -116,7 +116,7 @@ class TestArgumentHelpInjection:
|
|
|
116
116
|
|
|
117
117
|
class Config(AutoWryModel):
|
|
118
118
|
# Argument with description
|
|
119
|
-
|
|
119
|
+
source_path: Annotated[str, AutoClickParameter.ARGUMENT] = Field(description="Source path")
|
|
120
120
|
# Argument without description - should not appear in docstring
|
|
121
121
|
dest: Annotated[str, AutoClickParameter.ARGUMENT] = Field(default="out.txt")
|
|
122
122
|
|
|
@@ -125,14 +125,14 @@ class TestArgumentHelpInjection:
|
|
|
125
125
|
def copy(**kwargs: Any):
|
|
126
126
|
"""Copy file."""
|
|
127
127
|
config = Config(**kwargs)
|
|
128
|
-
click.echo(f"{config.
|
|
128
|
+
click.echo(f"{config.source_path} -> {config.dest}")
|
|
129
129
|
|
|
130
130
|
runner = CliRunner()
|
|
131
131
|
result = runner.invoke(copy, ["--help"])
|
|
132
132
|
|
|
133
133
|
assert result.exit_code == 0
|
|
134
134
|
assert "Arguments:" in result.output
|
|
135
|
-
assert "
|
|
135
|
+
assert "SOURCE_PATH" in result.output
|
|
136
136
|
assert "Source path" in result.output
|
|
137
137
|
# DEST should not appear in Arguments section since it has no description
|
|
138
138
|
# (it will appear in usage line though)
|