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.
Files changed (139) hide show
  1. {wry-0.5.1.dev5 → wry-0.5.2}/CHANGELOG.md +48 -0
  2. {wry-0.5.1.dev5/wry.egg-info → wry-0.5.2}/PKG-INFO +7 -1
  3. {wry-0.5.1.dev5 → wry-0.5.2}/README.md +6 -0
  4. {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_inheritance.py +2 -0
  5. {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_multi_model.py +5 -0
  6. {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_click_integration_extended.py +1 -0
  7. {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_context_handling.py +3 -0
  8. wry-0.5.2/tests/unit/auto_model/test_optional_list_comma_separated.py +388 -0
  9. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_explicit_argument_help_injection.py +2 -2
  10. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_argument_help_injection.py +6 -6
  11. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_multiple_option_bug.py +2 -0
  12. {wry-0.5.1.dev5 → wry-0.5.2}/wry/_version.py +3 -3
  13. {wry-0.5.1.dev5 → wry-0.5.2}/wry/click_integration.py +53 -17
  14. {wry-0.5.1.dev5 → wry-0.5.2/wry.egg-info}/PKG-INFO +7 -1
  15. {wry-0.5.1.dev5 → wry-0.5.2}/wry.egg-info/SOURCES.txt +1 -0
  16. {wry-0.5.1.dev5 → wry-0.5.2}/.cursorrules +0 -0
  17. {wry-0.5.1.dev5 → wry-0.5.2}/.github/workflows/ci-cd.yml +0 -0
  18. {wry-0.5.1.dev5 → wry-0.5.2}/.gitignore +0 -0
  19. {wry-0.5.1.dev5 → wry-0.5.2}/.markdownlint.json +0 -0
  20. {wry-0.5.1.dev5 → wry-0.5.2}/.pre-commit-config.yaml +0 -0
  21. {wry-0.5.1.dev5 → wry-0.5.2}/AI_KNOWLEDGE_BASE.md +0 -0
  22. {wry-0.5.1.dev5 → wry-0.5.2}/CONTRIBUTING.md +0 -0
  23. {wry-0.5.1.dev5 → wry-0.5.2}/LICENSE +0 -0
  24. {wry-0.5.1.dev5 → wry-0.5.2}/RELEASE_PROCESS.md +0 -0
  25. {wry-0.5.1.dev5 → wry-0.5.2}/TODO.md +0 -0
  26. {wry-0.5.1.dev5 → wry-0.5.2}/check.sh +0 -0
  27. {wry-0.5.1.dev5 → wry-0.5.2}/examples/autowrymodel_comprehensive.py +0 -0
  28. {wry-0.5.1.dev5 → wry-0.5.2}/examples/config.json +0 -0
  29. {wry-0.5.1.dev5 → wry-0.5.2}/examples/multimodel_comprehensive.py +0 -0
  30. {wry-0.5.1.dev5 → wry-0.5.2}/examples/sample_config.json +0 -0
  31. {wry-0.5.1.dev5 → wry-0.5.2}/examples/wrymodel_comprehensive.py +0 -0
  32. {wry-0.5.1.dev5 → wry-0.5.2}/pyproject.toml +0 -0
  33. {wry-0.5.1.dev5 → wry-0.5.2}/scripts/README.md +0 -0
  34. {wry-0.5.1.dev5 → wry-0.5.2}/scripts/extract_release_notes.py +0 -0
  35. {wry-0.5.1.dev5 → wry-0.5.2}/scripts/test_all_versions.sh +0 -0
  36. {wry-0.5.1.dev5 → wry-0.5.2}/scripts/test_ci_locally.sh +0 -0
  37. {wry-0.5.1.dev5 → wry-0.5.2}/scripts/test_with_act.sh +0 -0
  38. {wry-0.5.1.dev5 → wry-0.5.2}/scripts/update-dependencies.sh +0 -0
  39. {wry-0.5.1.dev5 → wry-0.5.2}/setup.cfg +0 -0
  40. {wry-0.5.1.dev5 → wry-0.5.2}/tests/README.md +0 -0
  41. {wry-0.5.1.dev5 → wry-0.5.2}/tests/__init__.py +0 -0
  42. {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/__init__.py +0 -0
  43. {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_auto_model.py +0 -0
  44. {wry-0.5.1.dev5 → wry-0.5.2}/tests/features/test_source_precedence.py +0 -0
  45. {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/__init__.py +0 -0
  46. {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_click_edge_cases.py +0 -0
  47. {wry-0.5.1.dev5 → wry-0.5.2}/tests/integration/test_click_integration.py +0 -0
  48. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/__init__.py +0 -0
  49. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/__init__.py +0 -0
  50. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_annotation_inference.py +0 -0
  51. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_edge_cases.py +0 -0
  52. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_field_processing.py +0 -0
  53. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_auto_model_list_fields.py +0 -0
  54. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_comma_separated_lists.py +0 -0
  55. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_field_annotation_handling.py +0 -0
  56. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_field_annotations.py +0 -0
  57. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/auto_model/test_type_inference.py +0 -0
  58. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/README_TESTING_STRATEGY.md +0 -0
  59. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/__init__.py +0 -0
  60. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_argument_types.py +0 -0
  61. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_bool_flag_handling.py +0 -0
  62. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_config_building.py +0 -0
  63. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_constraint_formatting.py +0 -0
  64. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_decorator_edge_cases.py +0 -0
  65. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_interval_constraints.py +0 -0
  66. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_lambda_parsing.py +0 -0
  67. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_length_constraints.py +0 -0
  68. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_parameter_generation.py +0 -0
  69. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_click_predicate_handling.py +0 -0
  70. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_closure_extraction_errors.py +0 -0
  71. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_closure_handling.py +0 -0
  72. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_constraint_behavior.py +0 -0
  73. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_constraint_edge_cases.py +0 -0
  74. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_env_vars_option.py +0 -0
  75. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_field_alias_with_click_options.py +0 -0
  76. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_format_constraint_text.py +0 -0
  77. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_json_config_loading.py +0 -0
  78. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_lambda_behavior.py +0 -0
  79. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_lambda_error_handling.py +0 -0
  80. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_predicate_source_errors.py +0 -0
  81. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_strict_mode_errors.py +0 -0
  82. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/click/test_type_handling.py +0 -0
  83. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/__init__.py +0 -0
  84. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_accessors.py +0 -0
  85. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_advanced_features.py +0 -0
  86. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_core.py +0 -0
  87. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_edge_cases.py +0 -0
  88. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_env_utils.py +0 -0
  89. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_field_constraint_extraction.py +0 -0
  90. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_field_utils.py +0 -0
  91. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_field_utils_edge_cases.py +0 -0
  92. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_sources.py +0 -0
  93. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/core/test_type_checking_blocks.py +0 -0
  94. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/__init__.py +0 -0
  95. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_accessor_caching.py +0 -0
  96. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_extract_edge_cases.py +0 -0
  97. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_extract_subset_edge_cases.py +0 -0
  98. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_click_context_handling.py +0 -0
  99. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_data_extraction.py +0 -0
  100. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_default_handling.py +0 -0
  101. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_edge_cases.py +0 -0
  102. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_environment_integration.py +0 -0
  103. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_extract_subset_edge_cases.py +0 -0
  104. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_extraction_methods.py +0 -0
  105. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_field_errors.py +0 -0
  106. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_model_object_extraction.py +0 -0
  107. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_non_dict_object_extraction.py +0 -0
  108. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/model/test_object_attribute_extraction.py +0 -0
  109. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/multi_model/__init__.py +0 -0
  110. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/multi_model/test_multi_model.py +0 -0
  111. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/multi_model/test_type_checking.py +0 -0
  112. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_auto_model_field_processing.py +0 -0
  113. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_comprehensive_imports.py +0 -0
  114. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_exclude_enum.py +0 -0
  115. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_generate_click_classmethod.py +0 -0
  116. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_help_system.py +0 -0
  117. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_init.py +0 -0
  118. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_init_edge_cases.py +0 -0
  119. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_init_version_edge_cases.py +0 -0
  120. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_model_extraction_methods.py +0 -0
  121. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_type_checking_imports.py +0 -0
  122. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_variadic_argument_bug.py +0 -0
  123. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_version_fallback.py +0 -0
  124. {wry-0.5.1.dev5 → wry-0.5.2}/tests/unit/test_version_parsing.py +0 -0
  125. {wry-0.5.1.dev5 → wry-0.5.2}/wry/__init__.py +0 -0
  126. {wry-0.5.1.dev5 → wry-0.5.2}/wry/auto_model.py +0 -0
  127. {wry-0.5.1.dev5 → wry-0.5.2}/wry/comma_separated.py +0 -0
  128. {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/__init__.py +0 -0
  129. {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/accessors.py +0 -0
  130. {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/env_utils.py +0 -0
  131. {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/field_utils.py +0 -0
  132. {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/model.py +0 -0
  133. {wry-0.5.1.dev5 → wry-0.5.2}/wry/core/sources.py +0 -0
  134. {wry-0.5.1.dev5 → wry-0.5.2}/wry/help_system.py +0 -0
  135. {wry-0.5.1.dev5 → wry-0.5.2}/wry/multi_model.py +0 -0
  136. {wry-0.5.1.dev5 → wry-0.5.2}/wry/py.typed +0 -0
  137. {wry-0.5.1.dev5 → wry-0.5.2}/wry.egg-info/dependency_links.txt +0 -0
  138. {wry-0.5.1.dev5 → wry-0.5.2}/wry.egg-info/requires.txt +0 -0
  139. {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.1.dev5
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
- source: Annotated[str, AutoArgument] = Field(description="Source file")
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 "SOURCE" in cmd.__doc__
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
- source: Annotated[str, AutoClickParameter.ARGUMENT] = Field(description="Source file path")
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.source} -> {config.dest}")
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 "SOURCE" in result.output
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
- source: Annotated[str, AutoClickParameter.ARGUMENT] = Field(description="Source path")
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.source} -> {config.dest}")
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 "SOURCE" in result.output
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)