wry 0.2.1.dev1__tar.gz → 0.2.2.dev1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wry
3
- Version: 0.2.1.dev1
3
+ Version: 0.2.2.dev1
4
4
  Summary: Why Repeat Yourself? - Define your CLI once with Pydantic models
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wry"
3
- version = "0.2.1.dev1" # Placeholder - actual version comes from git tags via poetry-dynamic-versioning
3
+ version = "0.2.2.dev1" # Placeholder - actual version comes from git tags via poetry-dynamic-versioning
4
4
  description = "Why Repeat Yourself? - Define your CLI once with Pydantic models"
5
5
  authors = ["Tyler House <26489166+tahouse@users.noreply.github.com>"]
6
6
  readme = "README.md"
@@ -89,6 +89,7 @@ from .multi_model import ( # noqa: E402
89
89
  # Convenience aliases
90
90
  AutoOption = AutoClickParameter.OPTION
91
91
  AutoArgument = AutoClickParameter.ARGUMENT
92
+ AutoExclude = AutoClickParameter.EXCLUDE
92
93
 
93
94
  # Re-export all public APIs
94
95
  __all__ = [
@@ -1,8 +1,8 @@
1
1
  # file generated by poetry-dynamic-versioning
2
2
  from typing import TYPE_CHECKING
3
3
 
4
- __version__: str = "0.2.1.dev1" # This will be replaced during build
5
- __version_tuple__: tuple[int, ...] = (0, 2, 1, "dev1") # This will be replaced during build
4
+ __version__: str = "0.2.2.dev1" # This will be replaced during build
5
+ __version_tuple__: tuple[int, ...] = (0, 2, 2, "dev1") # This will be replaced during build
6
6
  __commit_id__: str | None = None # This will be replaced during build
7
7
 
8
8
  # For compatibility with setuptools-scm test expectations
@@ -336,6 +336,7 @@ def generate_click_parameters(
336
336
  """
337
337
  arguments: list[ClickParameterDecorator[Any]] = [] # Arguments must come first
338
338
  options: list[ClickParameterDecorator[Any]] = [] # Options come after arguments
339
+ argument_docs: list[tuple[str, str]] = [] # Track (arg_name, description) for docstring injection
339
340
  type_hints = get_type_hints(model_class, include_extras=True)
340
341
 
341
342
  for field_name, field_info in model_class.model_fields.items():
@@ -379,14 +380,22 @@ def generate_click_parameters(
379
380
  # Auto-generate Click option from Field info
380
381
  option_name = f"--{field_name.replace('_', '-')}"
381
382
 
383
+ # Check if field is required first (needed to decide on default handling)
384
+ is_required = field_info.is_required() or field_type == AutoClickParameter.REQUIRED_OPTION
385
+
382
386
  click_kwargs: dict[str, Any] = {
383
- "default": (field_info.default if field_info.default is not PydanticUndefined else None),
384
387
  "help": field_info.description or f"{field_name.replace('_', ' ').title()}",
385
388
  "show_default": True,
386
389
  }
387
390
 
388
- # Check if field is required
389
- is_required = field_info.is_required() or field_type == AutoClickParameter.REQUIRED_OPTION
391
+ # Only set default if field has one, or if field is not required
392
+ # For required fields without a default, we must NOT set default=None
393
+ # or Click will think there IS a default and won't enforce the requirement
394
+ if field_info.default is not PydanticUndefined:
395
+ click_kwargs["default"] = field_info.default
396
+ elif not is_required:
397
+ # Optional field without explicit default gets None
398
+ click_kwargs["default"] = None
390
399
 
391
400
  # Determine Click type from annotation
392
401
  base_type = get_args(annotation)[0]
@@ -502,6 +511,10 @@ def generate_click_parameters(
502
511
  # Note: required=False to allow --config to replace arg
503
512
  arguments.append(click.argument(argument_name, **click_kwargs, required=False))
504
513
 
514
+ # Track argument description for docstring injection
515
+ if field_info.description:
516
+ argument_docs.append((argument_name.upper(), field_info.description))
517
+
505
518
  elif click_parameter:
506
519
  # Determine if it's an argument or option
507
520
  if hasattr(click_parameter, "__name__") and "argument" in str(click_parameter):
@@ -573,6 +586,21 @@ def generate_click_parameters(
573
586
  if config_and_env_options:
574
587
  func._has_config_option = True # type: ignore
575
588
 
589
+ # Inject argument descriptions into docstring BEFORE applying decorators
590
+ if argument_docs:
591
+ original_doc = func.__doc__ or ""
592
+ # Build argument documentation section
593
+ # Use \b to prevent Click from rewrapping, and format like Options section
594
+ arg_doc_lines = ["\n\n\b"]
595
+ arg_doc_lines.append("\n\b\bArguments:")
596
+ for arg_name, description in argument_docs:
597
+ # Match Click's Options formatting: 2 space indent, left-aligned
598
+ arg_doc_lines.append(f"\n\b\b {arg_name.ljust(18)} {description}")
599
+ arg_doc_section = "".join(arg_doc_lines)
600
+
601
+ # Append to existing docstring
602
+ func.__doc__ = original_doc.rstrip() + arg_doc_section
603
+
576
604
  # Apply arguments first, then options (Click requirement)
577
605
  all_decorators = arguments + final_options
578
606
  for dec in reversed(all_decorators):
@@ -720,23 +748,15 @@ def eager_json_config(ctx: click.Context, param: click.Parameter, value: Any) ->
720
748
  with open(value) as f:
721
749
  json_data = json.load(f)
722
750
 
723
- # Pre-fill missing required parameters from JSON
724
- # This prevents Click from throwing MissingParameter errors
725
- for json_key, json_value in json_data.items():
726
- param_key = json_key.replace("-", "_") # Handle kebab-case
727
- if param_key not in ctx.params:
728
- ctx.params[param_key] = json_value
729
-
730
- # Store for later merging
751
+ # Store JSON data for later merging in from_click_context
731
752
  ctx.ensure_object(dict)["json_data"] = json_data
732
753
 
733
- # Also modify Click argument requirements if they exist
734
- # This allows JSON to satisfy required arguments
754
+ # Mark parameters from JSON as not required
755
+ # This allows JSON to satisfy required arguments without modifying defaults
735
756
  if hasattr(ctx, "command") and ctx.command:
736
757
  for p in ctx.command.params:
737
758
  if (isinstance(p, click.Argument) or isinstance(p, click.Option)) and p.name in json_data:
738
759
  p.required = False
739
- p.default = json_data[p.name]
740
760
 
741
761
  return value
742
762
 
@@ -66,7 +66,7 @@ class WryModel(BaseModel):
66
66
  if "_value_sources" not in data:
67
67
  # Only initialize sources if not already provided
68
68
  self._value_sources = {}
69
- for field_name in self.model_fields:
69
+ for field_name in self.__class__.model_fields:
70
70
  # Mark non-default values as programmatic
71
71
  if field_name in data:
72
72
  self._value_sources[field_name] = ValueSource.CLI
@@ -526,6 +526,7 @@ class WryModel(BaseModel):
526
526
  # Only override if it's actually from CLI
527
527
  if "COMMANDLINE" in source_str:
528
528
  config_data[field_name] = TrackedValue(value, ValueSource.CLI)
529
+ continue
529
530
  # Skip if it's DEFAULT or ENVIRONMENT - already handled above
530
531
  continue
531
532
  except (AttributeError, RuntimeError):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes