dataclass-args 1.2.0__tar.gz → 1.2.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 (29) hide show
  1. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/PKG-INFO +1 -1
  2. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args/__init__.py +1 -1
  3. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args/builder.py +34 -7
  4. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args/utils.py +5 -5
  5. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args.egg-info/PKG-INFO +1 -1
  6. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args.egg-info/SOURCES.txt +2 -0
  7. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/pyproject.toml +1 -2
  8. dataclass_args-1.2.2/tests/test_boolean_base_configs.py +326 -0
  9. dataclass_args-1.2.2/tests/test_description.py +243 -0
  10. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/LICENSE +0 -0
  11. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/README.md +0 -0
  12. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args/annotations.py +0 -0
  13. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args/exceptions.py +0 -0
  14. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args/file_loading.py +0 -0
  15. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args.egg-info/dependency_links.txt +0 -0
  16. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args.egg-info/requires.txt +0 -0
  17. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/dataclass_args.egg-info/top_level.txt +0 -0
  18. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/setup.cfg +0 -0
  19. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_annotations.py +0 -0
  20. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_basic.py +0 -0
  21. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_boolean_flags.py +0 -0
  22. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_builder_advanced.py +0 -0
  23. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_cli_choices.py +0 -0
  24. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_cli_short.py +0 -0
  25. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_combine_annotations.py +0 -0
  26. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_config_merging_simple.py +0 -0
  27. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_file_loading.py +0 -0
  28. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_positional.py +0 -0
  29. {dataclass_args-1.2.0 → dataclass_args-1.2.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataclass-args
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Zero-boilerplate CLI generation from Python dataclasses with advanced type support and file loading
5
5
  Author-email: Martin Bartlett <martin.j.bartlett@gmail.com>
6
6
  License: MIT
@@ -65,7 +65,7 @@ from .exceptions import ConfigBuilderError, ConfigurationError, FileLoadingError
65
65
  from .file_loading import is_file_loadable_value, load_file_content
66
66
  from .utils import load_structured_file
67
67
 
68
- __version__ = "1.2.0"
68
+ __version__ = "1.2.2"
69
69
 
70
70
  __all__ = [
71
71
  # Main API
@@ -55,12 +55,15 @@ class GenericConfigBuilder:
55
55
  def __init__(
56
56
  self,
57
57
  config_class: Type,
58
+ description: Optional[str] = None,
58
59
  ):
59
60
  """
60
61
  Initialize builder for a specific dataclass type.
61
62
 
62
63
  Args:
63
64
  config_class: Dataclass type to build configurations for
65
+ description: Optional description for ArgumentParser help text.
66
+ If not provided, uses "Build {ClassName} from CLI"
64
67
 
65
68
  Raises:
66
69
  ConfigBuilderError: If config_class is not a dataclass
@@ -71,6 +74,7 @@ class GenericConfigBuilder:
71
74
  )
72
75
 
73
76
  self.config_class = config_class
77
+ self.description = description
74
78
  self._config_fields = self._analyze_config_fields()
75
79
 
76
80
  def _should_include_field(
@@ -387,14 +391,12 @@ class GenericConfigBuilder:
387
391
  # Get default value
388
392
  default_value = info.get("default", False)
389
393
 
390
- # Set parser default to the field's default value
391
- parser.set_defaults(**{dest_name: default_value})
392
-
393
394
  # Add positive form (--flag or -f)
394
395
  parser.add_argument(
395
396
  *positive_args,
396
397
  action="store_true",
397
398
  dest=dest_name,
399
+ default=argparse.SUPPRESS,
398
400
  help=f"{help_text} (default: {default_value})",
399
401
  )
400
402
 
@@ -404,6 +406,7 @@ class GenericConfigBuilder:
404
406
  negative_name,
405
407
  action="store_false",
406
408
  dest=dest_name,
409
+ default=argparse.SUPPRESS,
407
410
  help=f"Disable {help_text}",
408
411
  )
409
412
 
@@ -722,6 +725,7 @@ def build_config_from_cli(
722
725
  args: Optional[List[str]] = None,
723
726
  base_config_name: str = "config",
724
727
  base_configs: Optional[BaseConfigInput] = None,
728
+ description: Optional[str] = None,
725
729
  ) -> Any:
726
730
  """
727
731
  Build dataclass instance from CLI arguments with optional base configs.
@@ -739,6 +743,8 @@ def build_config_from_cli(
739
743
  - str: Path to a single config file
740
744
  - dict: A single configuration dictionary
741
745
  - List[Union[str, dict]]: Multiple configs applied in order
746
+ description: Optional description for ArgumentParser help text.
747
+ If not provided, uses "Build {ClassName} from CLI"
742
748
 
743
749
  Returns:
744
750
  Instance of config_class built from merged configurations
@@ -750,6 +756,12 @@ def build_config_from_cli(
750
756
  # Single dict
751
757
  config = build_config_from_cli(MyConfig, base_configs={'debug': True})
752
758
 
759
+ # With custom description
760
+ config = build_config_from_cli(
761
+ MyConfig,
762
+ description="Configure the application server"
763
+ )
764
+
753
765
  # Mixed list
754
766
  config = build_config_from_cli(
755
767
  MyConfig,
@@ -764,10 +776,14 @@ def build_config_from_cli(
764
776
  if args is None:
765
777
  args = sys.argv[1:]
766
778
 
767
- builder = GenericConfigBuilder(config_class)
768
- parser = argparse.ArgumentParser(
769
- description=f"Build {config_class.__name__} from CLI"
779
+ builder = GenericConfigBuilder(config_class, description=description)
780
+
781
+ desc = (
782
+ builder.description
783
+ if builder.description is not None
784
+ else f"Build {config_class.__name__} from CLI"
770
785
  )
786
+ parser = argparse.ArgumentParser(description=desc)
771
787
  builder.add_arguments(parser, base_config_name)
772
788
 
773
789
  parsed_args = parser.parse_args(args)
@@ -778,6 +794,7 @@ def build_config(
778
794
  config_class: Type,
779
795
  args: Optional[List[str]] = None,
780
796
  base_configs: Optional[BaseConfigInput] = None,
797
+ description: Optional[str] = None,
781
798
  ) -> Any:
782
799
  """
783
800
  Simplified convenience function to build dataclass from CLI arguments.
@@ -794,6 +811,8 @@ def build_config(
794
811
  - str: Path to a single config file
795
812
  - dict: A single configuration dictionary
796
813
  - List[Union[str, dict]]: Multiple configs applied in order
814
+ description: Optional description for ArgumentParser help text.
815
+ If not provided, uses "Build {ClassName} from CLI"
797
816
 
798
817
  Returns:
799
818
  Instance of config_class built from merged configurations
@@ -805,6 +824,12 @@ def build_config(
805
824
  # With base config file
806
825
  config = build_config(Config, base_configs='defaults.yaml')
807
826
 
827
+ # With custom description
828
+ config = build_config(
829
+ Config,
830
+ description="My application configuration tool"
831
+ )
832
+
808
833
  # With mixed sources
809
834
  config = build_config(
810
835
  Config,
@@ -815,4 +840,6 @@ def build_config(
815
840
  ]
816
841
  )
817
842
  """
818
- return build_config_from_cli(config_class, args, base_configs=base_configs)
843
+ return build_config_from_cli(
844
+ config_class, args, base_configs=base_configs, description=description
845
+ )
@@ -3,6 +3,7 @@ Utility functions for dataclass CLI configuration.
3
3
  """
4
4
 
5
5
  import json
6
+ import sys
6
7
  from pathlib import Path
7
8
  from typing import Any, Dict, Union
8
9
 
@@ -13,15 +14,14 @@ try:
13
14
  except ImportError:
14
15
  HAS_YAML = False
15
16
 
16
- try:
17
- # Python 3.11+
17
+ # Handle TOML imports for different Python versions
18
+ if sys.version_info >= (3, 11):
18
19
  import tomllib
19
20
 
20
21
  HAS_TOML = True
21
- except ImportError:
22
+ else:
22
23
  try:
23
- # Python < 3.11
24
- import tomli as tomllib
24
+ import tomli as tomllib # type: ignore[no-redef]
25
25
 
26
26
  HAS_TOML = True
27
27
  except ImportError:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataclass-args
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Zero-boilerplate CLI generation from Python dataclasses with advanced type support and file loading
5
5
  Author-email: Martin Bartlett <martin.j.bartlett@gmail.com>
6
6
  License: MIT
@@ -14,12 +14,14 @@ dataclass_args.egg-info/requires.txt
14
14
  dataclass_args.egg-info/top_level.txt
15
15
  tests/test_annotations.py
16
16
  tests/test_basic.py
17
+ tests/test_boolean_base_configs.py
17
18
  tests/test_boolean_flags.py
18
19
  tests/test_builder_advanced.py
19
20
  tests/test_cli_choices.py
20
21
  tests/test_cli_short.py
21
22
  tests/test_combine_annotations.py
22
23
  tests/test_config_merging_simple.py
24
+ tests/test_description.py
23
25
  tests/test_file_loading.py
24
26
  tests/test_positional.py
25
27
  tests/test_utils.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dataclass-args"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "Zero-boilerplate CLI generation from Python dataclasses with advanced type support and file loading"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -153,7 +153,6 @@ line_length = 88
153
153
  known_first_party = ["dataclass_args"]
154
154
 
155
155
  [tool.mypy]
156
- python_version = "3.9"
157
156
  warn_return_any = false
158
157
  warn_unused_configs = true
159
158
  disallow_untyped_defs = false
@@ -0,0 +1,326 @@
1
+ """
2
+ Tests for boolean field handling with base_configs.
3
+
4
+ This module tests that boolean fields from base_configs are preserved correctly
5
+ when not overridden by CLI flags.
6
+ """
7
+
8
+ import tempfile
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ from dataclass_args import build_config
15
+
16
+
17
+ @dataclass
18
+ class BoolConfig:
19
+ """Test config with various boolean fields."""
20
+
21
+ debug: bool = False
22
+ verbose: bool = True
23
+ enabled: bool = False
24
+ disabled: bool = True
25
+
26
+
27
+ class TestBooleanFromDict:
28
+ """Test boolean values from base_configs dict."""
29
+
30
+ def test_bool_true_overrides_false_default(self):
31
+ """Boolean True from dict overrides False default."""
32
+ config = build_config(BoolConfig, args=[], base_configs={"debug": True})
33
+ assert config.debug is True
34
+
35
+ def test_bool_false_overrides_true_default(self):
36
+ """Boolean False from dict overrides True default."""
37
+ config = build_config(BoolConfig, args=[], base_configs={"verbose": False})
38
+ assert config.verbose is False
39
+
40
+ def test_multiple_bool_overrides(self):
41
+ """Multiple boolean overrides from dict."""
42
+ config = build_config(
43
+ BoolConfig,
44
+ args=[],
45
+ base_configs={
46
+ "debug": True,
47
+ "verbose": False,
48
+ "enabled": True,
49
+ "disabled": False,
50
+ },
51
+ )
52
+ assert config.debug is True
53
+ assert config.verbose is False
54
+ assert config.enabled is True
55
+ assert config.disabled is False
56
+
57
+ def test_partial_bool_overrides(self):
58
+ """Some fields from dict, others use defaults."""
59
+ config = build_config(
60
+ BoolConfig, args=[], base_configs={"debug": True, "enabled": True}
61
+ )
62
+ assert config.debug is True # from dict
63
+ assert config.verbose is True # default
64
+ assert config.enabled is True # from dict
65
+ assert config.disabled is True # default
66
+
67
+
68
+ class TestBooleanFromFile:
69
+ """Test boolean values from base_configs file."""
70
+
71
+ def test_bool_from_json_file(self):
72
+ """Boolean values from JSON file."""
73
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
74
+ f.write('{"debug": true, "verbose": false}')
75
+ config_file = f.name
76
+
77
+ try:
78
+ config = build_config(BoolConfig, args=[], base_configs=config_file)
79
+ assert config.debug is True
80
+ assert config.verbose is False
81
+ finally:
82
+ Path(config_file).unlink()
83
+
84
+ def test_bool_from_yaml_file(self):
85
+ """Boolean values from YAML file."""
86
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
87
+ f.write("debug: true\nverbose: false\n")
88
+ config_file = f.name
89
+
90
+ try:
91
+ config = build_config(BoolConfig, args=[], base_configs=config_file)
92
+ assert config.debug is True
93
+ assert config.verbose is False
94
+ finally:
95
+ Path(config_file).unlink()
96
+
97
+
98
+ class TestBooleanCLIOverride:
99
+ """Test that CLI arguments override base_configs."""
100
+
101
+ def test_cli_flag_overrides_dict(self):
102
+ """CLI --flag overrides dict value."""
103
+ config = build_config(
104
+ BoolConfig, args=["--debug"], base_configs={"debug": False}
105
+ )
106
+ assert config.debug is True
107
+
108
+ def test_cli_no_flag_overrides_dict(self):
109
+ """CLI --no-flag overrides dict value."""
110
+ config = build_config(
111
+ BoolConfig, args=["--no-verbose"], base_configs={"verbose": True}
112
+ )
113
+ assert config.verbose is False
114
+
115
+ def test_multiple_cli_overrides(self):
116
+ """Multiple CLI flags override dict values."""
117
+ config = build_config(
118
+ BoolConfig,
119
+ args=["--debug", "--no-verbose", "--enabled"],
120
+ base_configs={"debug": False, "verbose": True, "enabled": False},
121
+ )
122
+ assert config.debug is True
123
+ assert config.verbose is False
124
+ assert config.enabled is True
125
+
126
+
127
+ class TestBooleanMixed:
128
+ """Test mixed scenarios: some CLI, some base_configs, some defaults."""
129
+
130
+ def test_cli_partial_dict_rest(self):
131
+ """Some from CLI, some from dict, some defaults."""
132
+ config = build_config(
133
+ BoolConfig,
134
+ args=["--debug"], # Only debug from CLI
135
+ base_configs={
136
+ "verbose": False,
137
+ "enabled": True,
138
+ }, # verbose and enabled from dict
139
+ )
140
+ assert config.debug is True # from CLI
141
+ assert config.verbose is False # from dict
142
+ assert config.enabled is True # from dict
143
+ assert config.disabled is True # default
144
+
145
+ def test_dict_without_cli_preserves_values(self):
146
+ """Dict values preserved when no CLI flag for that field."""
147
+ config = build_config(
148
+ BoolConfig,
149
+ args=["--enabled"], # Only set enabled via CLI
150
+ base_configs={
151
+ "debug": True, # Should be preserved
152
+ "verbose": False, # Should be preserved
153
+ },
154
+ )
155
+ assert config.debug is True # from dict (no CLI override)
156
+ assert config.verbose is False # from dict (no CLI override)
157
+ assert config.enabled is True # from CLI
158
+ assert config.disabled is True # default
159
+
160
+ def test_cli_overrides_one_dict_preserves_others(self):
161
+ """CLI overrides one field, dict values preserved for others."""
162
+ config = build_config(
163
+ BoolConfig,
164
+ args=["--no-verbose"], # Override only verbose
165
+ base_configs={
166
+ "debug": True,
167
+ "verbose": True, # Will be overridden
168
+ "enabled": True,
169
+ },
170
+ )
171
+ assert config.debug is True # from dict
172
+ assert config.verbose is False # from CLI (overrides dict)
173
+ assert config.enabled is True # from dict
174
+
175
+
176
+ class TestBooleanDefaultBehavior:
177
+ """Test that defaults work correctly without base_configs or CLI."""
178
+
179
+ def test_no_args_no_config_uses_defaults(self):
180
+ """No args, no config uses dataclass defaults."""
181
+ config = build_config(BoolConfig, args=[])
182
+ assert config.debug is False
183
+ assert config.verbose is True
184
+ assert config.enabled is False
185
+ assert config.disabled is True
186
+
187
+ def test_empty_dict_uses_defaults(self):
188
+ """Empty dict uses dataclass defaults."""
189
+ config = build_config(BoolConfig, args=[], base_configs={})
190
+ assert config.debug is False
191
+ assert config.verbose is True
192
+
193
+
194
+ class TestBooleanConfigFilePlusCLI:
195
+ """Test --config file argument combined with CLI flags."""
196
+
197
+ def test_config_file_plus_cli_override(self):
198
+ """--config file combined with CLI override."""
199
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
200
+ f.write('{"debug": true, "verbose": false}')
201
+ config_file = f.name
202
+
203
+ try:
204
+ config = build_config(
205
+ BoolConfig,
206
+ args=["--config", config_file, "--debug"], # CLI overrides
207
+ )
208
+ # CLI explicitly set debug=True (same as file, but from CLI)
209
+ assert config.debug is True
210
+ # File set verbose=False
211
+ assert config.verbose is False
212
+ finally:
213
+ Path(config_file).unlink()
214
+
215
+ def test_base_configs_plus_config_file_plus_cli(self):
216
+ """Hierarchy: base_configs < --config file < CLI."""
217
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
218
+ f.write('{"verbose": false}')
219
+ config_file = f.name
220
+
221
+ try:
222
+ config = build_config(
223
+ BoolConfig,
224
+ args=["--config", config_file, "--enabled"],
225
+ base_configs={"debug": True}, # Lowest priority
226
+ )
227
+ assert config.debug is True # from base_configs
228
+ assert config.verbose is False # from --config file
229
+ assert config.enabled is True # from CLI
230
+ finally:
231
+ Path(config_file).unlink()
232
+
233
+
234
+ class TestBooleanEdgeCases:
235
+ """Test edge cases for boolean handling."""
236
+
237
+ def test_bool_explicitly_set_to_default_via_cli(self):
238
+ """CLI flag explicitly sets value to match default."""
239
+ # Default is debug=False, explicitly set --no-debug
240
+ config = build_config(
241
+ BoolConfig,
242
+ args=["--no-debug"], # Explicitly False
243
+ base_configs={"debug": True}, # Try to override
244
+ )
245
+ # CLI should win even though it matches default
246
+ assert config.debug is False
247
+
248
+ def test_bool_both_positive_and_negative_flags(self):
249
+ """Last flag wins if both positive and negative specified."""
250
+ config = build_config(
251
+ BoolConfig,
252
+ args=["--debug", "--no-debug"], # Conflicting flags
253
+ )
254
+ # argparse behavior: last one wins
255
+ assert config.debug is False
256
+
257
+ def test_all_false_values(self):
258
+ """All booleans set to False."""
259
+ config = build_config(
260
+ BoolConfig,
261
+ args=[],
262
+ base_configs={
263
+ "debug": False,
264
+ "verbose": False,
265
+ "enabled": False,
266
+ "disabled": False,
267
+ },
268
+ )
269
+ assert config.debug is False
270
+ assert config.verbose is False
271
+ assert config.enabled is False
272
+ assert config.disabled is False
273
+
274
+ def test_all_true_values(self):
275
+ """All booleans set to True."""
276
+ config = build_config(
277
+ BoolConfig,
278
+ args=[],
279
+ base_configs={
280
+ "debug": True,
281
+ "verbose": True,
282
+ "enabled": True,
283
+ "disabled": True,
284
+ },
285
+ )
286
+ assert config.debug is True
287
+ assert config.verbose is True
288
+ assert config.enabled is True
289
+ assert config.disabled is True
290
+
291
+
292
+ class TestBooleanMultipleBasConfigs:
293
+ """Test boolean handling with multiple base_configs."""
294
+
295
+ def test_list_of_dicts_later_overrides_earlier(self):
296
+ """Later dict in list overrides earlier dict."""
297
+ config = build_config(
298
+ BoolConfig,
299
+ args=[],
300
+ base_configs=[
301
+ {"debug": True, "verbose": False},
302
+ {"debug": False}, # Overrides debug only
303
+ ],
304
+ )
305
+ assert config.debug is False # from second dict
306
+ assert config.verbose is False # from first dict
307
+
308
+ def test_file_and_dict_combination(self):
309
+ """Mix files and dicts in base_configs list."""
310
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
311
+ f.write('{"debug": true}')
312
+ config_file = f.name
313
+
314
+ try:
315
+ config = build_config(
316
+ BoolConfig,
317
+ args=[],
318
+ base_configs=[
319
+ config_file, # File first
320
+ {"verbose": False}, # Dict second
321
+ ],
322
+ )
323
+ assert config.debug is True # from file
324
+ assert config.verbose is False # from dict
325
+ finally:
326
+ Path(config_file).unlink()
@@ -0,0 +1,243 @@
1
+ """Tests for custom description parameter."""
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from io import StringIO
6
+ from typing import List, Optional
7
+
8
+ import pytest
9
+
10
+ from dataclass_args import GenericConfigBuilder, build_config, build_config_from_cli
11
+
12
+
13
+ @dataclass
14
+ class SimpleConfig:
15
+ """Simple config for testing."""
16
+
17
+ name: str = "default"
18
+ count: int = 10
19
+
20
+
21
+ class TestCustomDescription:
22
+ """Test custom description parameter functionality."""
23
+
24
+ def test_builder_with_custom_description(self):
25
+ """Test GenericConfigBuilder with custom description."""
26
+ custom_desc = "My custom application configuration"
27
+ builder = GenericConfigBuilder(SimpleConfig, description=custom_desc)
28
+
29
+ assert builder.description == custom_desc
30
+ assert builder.config_class == SimpleConfig
31
+
32
+ def test_builder_with_no_description(self):
33
+ """Test GenericConfigBuilder without description defaults to None."""
34
+ builder = GenericConfigBuilder(SimpleConfig)
35
+
36
+ assert builder.description is None
37
+ assert builder.config_class == SimpleConfig
38
+
39
+ def test_build_config_with_custom_description(self, capsys):
40
+ """Test build_config with custom description in help."""
41
+ custom_desc = "Configure the application server"
42
+
43
+ with pytest.raises(SystemExit) as exc_info:
44
+ build_config(SimpleConfig, args=["--help"], description=custom_desc)
45
+
46
+ assert exc_info.value.code == 0
47
+ captured = capsys.readouterr()
48
+ assert custom_desc in captured.out
49
+
50
+ def test_build_config_with_default_description(self, capsys):
51
+ """Test build_config without description uses default format."""
52
+ with pytest.raises(SystemExit) as exc_info:
53
+ build_config(SimpleConfig, args=["--help"])
54
+
55
+ assert exc_info.value.code == 0
56
+ captured = capsys.readouterr()
57
+ assert "Build SimpleConfig from CLI" in captured.out
58
+
59
+ def test_build_config_from_cli_with_custom_description(self, capsys):
60
+ """Test build_config_from_cli with custom description."""
61
+ custom_desc = "Server configuration utility"
62
+
63
+ with pytest.raises(SystemExit) as exc_info:
64
+ build_config_from_cli(
65
+ SimpleConfig, args=["--help"], description=custom_desc
66
+ )
67
+
68
+ assert exc_info.value.code == 0
69
+ captured = capsys.readouterr()
70
+ assert custom_desc in captured.out
71
+
72
+ def test_build_config_from_cli_with_default_description(self, capsys):
73
+ """Test build_config_from_cli without description uses default."""
74
+ with pytest.raises(SystemExit) as exc_info:
75
+ build_config_from_cli(SimpleConfig, args=["--help"])
76
+
77
+ assert exc_info.value.code == 0
78
+ captured = capsys.readouterr()
79
+ assert "Build SimpleConfig from CLI" in captured.out
80
+
81
+ def test_custom_description_does_not_affect_functionality(self):
82
+ """Test that custom description doesn't change parsing behavior."""
83
+ # Without description
84
+ config1 = build_config(SimpleConfig, args=["--name", "test1", "--count", "100"])
85
+
86
+ # With description
87
+ config2 = build_config(
88
+ SimpleConfig,
89
+ args=["--name", "test1", "--count", "100"],
90
+ description="Custom description",
91
+ )
92
+
93
+ assert config1.name == config2.name == "test1"
94
+ assert config1.count == config2.count == 100
95
+
96
+ def test_backward_compatibility_direct_builder(self):
97
+ """Test that existing code using GenericConfigBuilder still works."""
98
+ import argparse
99
+
100
+ # Old pattern: GenericConfigBuilder without description
101
+ builder = GenericConfigBuilder(SimpleConfig)
102
+ parser = argparse.ArgumentParser(description="Custom parser")
103
+ builder.add_arguments(parser)
104
+
105
+ args = parser.parse_args(["--name", "legacy", "--count", "42"])
106
+ config = builder.build_config(args)
107
+
108
+ assert config.name == "legacy"
109
+ assert config.count == 42
110
+
111
+ def test_backward_compatibility_convenience_functions(self):
112
+ """Test that existing convenience function calls still work."""
113
+ # Old pattern: build_config without description
114
+ config = build_config(SimpleConfig, args=["--name", "oldstyle"])
115
+
116
+ assert config.name == "oldstyle"
117
+ assert config.count == 10 # default
118
+
119
+ def test_multiline_description(self, capsys):
120
+ """Test that multiline descriptions work correctly."""
121
+ multiline_desc = """Configure the application server.
122
+
123
+ This tool allows you to set various server parameters
124
+ including name, port, and other settings."""
125
+
126
+ with pytest.raises(SystemExit) as exc_info:
127
+ build_config(SimpleConfig, args=["--help"], description=multiline_desc)
128
+
129
+ assert exc_info.value.code == 0
130
+ captured = capsys.readouterr()
131
+ # argparse may format this, but key parts should be present
132
+ assert "Configure the application server" in captured.out
133
+
134
+ def test_empty_string_description(self, capsys):
135
+ """Test that empty string description is handled."""
136
+ with pytest.raises(SystemExit) as exc_info:
137
+ build_config(SimpleConfig, args=["--help"], description="")
138
+
139
+ assert exc_info.value.code == 0
140
+ captured = capsys.readouterr()
141
+ # Empty description should be used (not default)
142
+ assert "Build SimpleConfig from CLI" not in captured.out
143
+
144
+ def test_description_with_special_characters(self, capsys):
145
+ """Test description with special characters."""
146
+ special_desc = "Config tool: <options> [required] {advanced}"
147
+
148
+ with pytest.raises(SystemExit) as exc_info:
149
+ build_config(SimpleConfig, args=["--help"], description=special_desc)
150
+
151
+ assert exc_info.value.code == 0
152
+ captured = capsys.readouterr()
153
+ assert "Config tool:" in captured.out
154
+
155
+
156
+ class TestDescriptionWithComplexConfig:
157
+ """Test description parameter with more complex configurations."""
158
+
159
+ @dataclass
160
+ class ComplexConfig:
161
+ """Complex config with various field types."""
162
+
163
+ host: str = "localhost"
164
+ port: int = 8080
165
+ debug: bool = False
166
+ tags: Optional[List[str]] = None
167
+
168
+ def __post_init__(self):
169
+ if self.tags is None:
170
+ self.tags = []
171
+
172
+ def test_complex_config_with_description(self, capsys):
173
+ """Test complex config with custom description."""
174
+ desc = "Advanced server configuration with multiple options"
175
+
176
+ with pytest.raises(SystemExit) as exc_info:
177
+ build_config(self.ComplexConfig, args=["--help"], description=desc)
178
+
179
+ assert exc_info.value.code == 0
180
+ captured = capsys.readouterr()
181
+ assert desc in captured.out
182
+ # Should still show all the arguments
183
+ assert "--host" in captured.out
184
+ assert "--port" in captured.out
185
+ assert "--debug" in captured.out
186
+
187
+ def test_complex_config_parsing_with_description(self):
188
+ """Test that complex config parsing works with description."""
189
+ config = build_config(
190
+ self.ComplexConfig,
191
+ args=["--host", "0.0.0.0", "--port", "9000", "--debug"],
192
+ description="Test configuration",
193
+ )
194
+
195
+ assert config.host == "0.0.0.0"
196
+ assert config.port == 9000
197
+ assert config.debug is True
198
+
199
+
200
+ class TestDescriptionEdgeCases:
201
+ """Test edge cases and error conditions."""
202
+
203
+ def test_none_description_explicitly(self):
204
+ """Test explicitly passing None as description."""
205
+ builder = GenericConfigBuilder(SimpleConfig, description=None)
206
+ assert builder.description is None
207
+
208
+ def test_description_type_validation(self):
209
+ """Test that non-string descriptions work (argparse coerces to str)."""
210
+ # argparse will convert to string, so this should work
211
+ builder = GenericConfigBuilder(SimpleConfig, description=123)
212
+ assert builder.description == 123
213
+
214
+ # When used in ArgumentParser, argparse will handle conversion
215
+ import argparse
216
+
217
+ desc = builder.description or f"Build {builder.config_class.__name__} from CLI"
218
+ parser = argparse.ArgumentParser(description=str(desc))
219
+ assert "123" in parser.format_help()
220
+
221
+ def test_very_long_description(self, capsys):
222
+ """Test with a very long description."""
223
+ long_desc = "A " + "very " * 100 + "long description."
224
+
225
+ with pytest.raises(SystemExit) as exc_info:
226
+ build_config(SimpleConfig, args=["--help"], description=long_desc)
227
+
228
+ assert exc_info.value.code == 0
229
+ captured = capsys.readouterr()
230
+ # argparse will format it, but it should appear
231
+ assert "very" in captured.out
232
+
233
+ def test_description_with_unicode(self, capsys):
234
+ """Test description with unicode characters."""
235
+ unicode_desc = "配置工具 - Configuration Tool 🚀"
236
+
237
+ with pytest.raises(SystemExit) as exc_info:
238
+ build_config(SimpleConfig, args=["--help"], description=unicode_desc)
239
+
240
+ assert exc_info.value.code == 0
241
+ captured = capsys.readouterr()
242
+ # Check for parts of the unicode string
243
+ assert "Configuration Tool" in captured.out
File without changes
File without changes
File without changes