apcore-cli 0.10.2__tar.gz → 0.10.4__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 (101) hide show
  1. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/CHANGELOG.md +16 -0
  2. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/PKG-INFO +4 -4
  3. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/README.md +1 -1
  4. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/pyproject.toml +3 -3
  5. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/ref_resolver.py +24 -0
  6. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/schema_parser.py +35 -0
  7. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_ref_resolver.py +23 -0
  8. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_schema_parser.py +32 -0
  9. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.github/CODEOWNERS +0 -0
  10. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.github/copilot-ignore +0 -0
  11. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.github/workflows/ci.yml +0 -0
  12. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.gitignore +0 -0
  13. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.gitmessage +0 -0
  14. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.pre-commit-config.yaml +0 -0
  15. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/CLAUDE.md +0 -0
  16. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/LICENSE +0 -0
  17. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/Makefile +0 -0
  18. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/README.md +0 -0
  19. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/math/add.py +0 -0
  20. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/math/multiply.py +0 -0
  21. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/sysutil/disk.py +0 -0
  22. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/sysutil/env.py +0 -0
  23. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/sysutil/info.py +0 -0
  24. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/text/reverse.py +0 -0
  25. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/text/upper.py +0 -0
  26. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/text/wordcount.py +0 -0
  27. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/run_examples.sh +0 -0
  28. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/approval-gate.md +0 -0
  29. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/config-resolver.md +0 -0
  30. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/core-dispatcher.md +0 -0
  31. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/discovery.md +0 -0
  32. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/exposure-filtering.md +0 -0
  33. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/grouped-commands.md +0 -0
  34. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/output-formatter.md +0 -0
  35. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/overview.md +0 -0
  36. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/schema-parser.md +0 -0
  37. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/security-manager.md +0 -0
  38. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/shell-integration.md +0 -0
  39. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/state.json +0 -0
  40. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/__init__.py +0 -0
  41. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/__main__.py +0 -0
  42. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/_sandbox_runner.py +0 -0
  43. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/approval.py +0 -0
  44. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/builtin_group.py +0 -0
  45. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/cli.py +0 -0
  46. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/config.py +0 -0
  47. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/discovery.py +0 -0
  48. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/display_helpers.py +0 -0
  49. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/exit_codes.py +0 -0
  50. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/exposure.py +0 -0
  51. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/factory.py +0 -0
  52. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/init_cmd.py +0 -0
  53. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/output.py +0 -0
  54. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/__init__.py +0 -0
  55. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/audit.py +0 -0
  56. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/auth.py +0 -0
  57. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/config_encryptor.py +0 -0
  58. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/sandbox.py +0 -0
  59. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/shell.py +0 -0
  60. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/strategy.py +0 -0
  61. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/system_cmd.py +0 -0
  62. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/system_usage.py +0 -0
  63. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/validate.py +0 -0
  64. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/__init__.py +0 -0
  65. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conformance/__init__.py +0 -0
  66. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conformance/test_apcli_visibility.py +0 -0
  67. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conformance/test_snake_case_kwargs.py +0 -0
  68. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conftest.py +0 -0
  69. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/shell_test_utils.py +0 -0
  70. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_apcli_integration.py +0 -0
  71. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_approval.py +0 -0
  72. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_bugfixes.py +0 -0
  73. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_builtin_group.py +0 -0
  74. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_cli.py +0 -0
  75. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_config.py +0 -0
  76. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_discovery.py +0 -0
  77. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_discovery_fe13.py +0 -0
  78. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_display_helpers.py +0 -0
  79. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_e2e.py +0 -0
  80. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_exit_codes.py +0 -0
  81. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_exposure.py +0 -0
  82. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_factory_fe13.py +0 -0
  83. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_init_cmd.py +0 -0
  84. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_integration.py +0 -0
  85. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_list_command_filters.py +0 -0
  86. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_output.py +0 -0
  87. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_output_format_exec.py +0 -0
  88. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_output_format_markdown_skill.py +0 -0
  89. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_public_api.py +0 -0
  90. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_sandbox_runner.py +0 -0
  91. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/__init__.py +0 -0
  92. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_audit.py +0 -0
  93. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_auth.py +0 -0
  94. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_config_encryptor.py +0 -0
  95. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_sandbox.py +0 -0
  96. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_shell.py +0 -0
  97. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_strategy.py +0 -0
  98. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_system_cmd.py +0 -0
  99. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_system_usage.py +0 -0
  100. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_toolkit_integration.py +0 -0
  101. {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_validate.py +0 -0
@@ -5,6 +5,22 @@ All notable changes to apcore-cli (Python SDK) will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.10.4] - 2026-07-14
9
+
10
+ Patch release. Bumps the required `apcore` floor to `0.26.0` to align the ecosystem on the 0.26.0 governance layer (Execution Policy, governance events, no-handler fail-loud — additive, no breaking changes). No code or API changes; all 798 tests pass (5 xfailed) unmodified against apcore 0.26.0.
11
+
12
+ ## [0.10.3] - 2026-07-07
13
+
14
+ Patch release: fixes Pydantic v2 `Optional[...]` field type mapping and bumps the required `apcore-toolkit` floor. All 798 tests pass (5 xfailed).
15
+
16
+ ### Fixed
17
+
18
+ - **`Optional[X]` fields mapped to the wrong Click type.** Pydantic v2 emits `Optional[X]` as `{"anyOf": [<X>, {"type": "null"}]}` rather than `{"type": "X"}`, so every optional CLI option fell through to `click.STRING` with a spurious "no type specified" warning — `Optional[bool]` lost its flag, `Optional[int]` lost `INT`, and the `*_file` convention stopped applying. `_map_type` (`schema_parser.py`) and the `anyOf`/`oneOf` branch of `_resolve_node` (`ref_resolver.py`) now recover the dominant non-null type from `anyOf`. JSON Schema list-form types (`{"type": ["string", "null"]}`) are likewise reduced to their first non-null string, so they no longer risk an unhashable-key `TypeError` in the type lookup. Regression tests added in `tests/test_schema_parser.py` and `tests/test_ref_resolver.py`.
19
+
20
+ ### Changed
21
+
22
+ - Bumped the required `apcore-toolkit` floor to `>= 0.10.0` (which centralizes `RegistryWriter` field mapping and adds the shared annotation-preservation conformance verifier — additive, no breaking changes).
23
+
8
24
  ## [0.10.2] - 2026-06-24
9
25
 
10
26
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apcore-cli
3
- Version: 0.10.2
3
+ Version: 0.10.4
4
4
  Summary: Terminal adapter for apcore — execute AI-Perceivable modules from the command line
5
5
  Project-URL: Homepage, https://aiperceivable.com
6
6
  Project-URL: Repository, https://github.com/aiperceivable/apcore-cli-python
@@ -21,8 +21,8 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Requires-Python: >=3.11
24
- Requires-Dist: apcore-toolkit>=0.9.1
25
- Requires-Dist: apcore>=0.25.0
24
+ Requires-Dist: apcore-toolkit>=0.10.0
25
+ Requires-Dist: apcore>=0.26.0
26
26
  Requires-Dist: click>=8.1
27
27
  Requires-Dist: cryptography>=41.0
28
28
  Requires-Dist: jsonschema>=4.20
@@ -89,7 +89,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
89
89
  pip install apcore-cli
90
90
  ```
91
91
 
92
- Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.7.0` (now a **required** runtime dependency as of v0.9.0 — previously optional; see the [tech-design ADR-09](https://github.com/aiperceivable/apcore-cli/blob/main/docs/tech-design.md) for the byte-equivalent toolkit-delegated tier rationale). The `[toolkit]` extras group is retained as a no-op for backward compat:
92
+ Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.10.0` (now a **required** runtime dependency as of v0.9.0 — previously optional; see the [tech-design ADR-09](https://github.com/aiperceivable/apcore-cli/blob/main/docs/tech-design.md) for the byte-equivalent toolkit-delegated tier rationale). The `[toolkit]` extras group is retained as a no-op for backward compat:
93
93
 
94
94
  ```bash
95
95
  pip install "apcore-cli[toolkit]" # equivalent to plain `pip install apcore-cli`
@@ -48,7 +48,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
48
48
  pip install apcore-cli
49
49
  ```
50
50
 
51
- Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.7.0` (now a **required** runtime dependency as of v0.9.0 — previously optional; see the [tech-design ADR-09](https://github.com/aiperceivable/apcore-cli/blob/main/docs/tech-design.md) for the byte-equivalent toolkit-delegated tier rationale). The `[toolkit]` extras group is retained as a no-op for backward compat:
51
+ Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.10.0` (now a **required** runtime dependency as of v0.9.0 — previously optional; see the [tech-design ADR-09](https://github.com/aiperceivable/apcore-cli/blob/main/docs/tech-design.md) for the byte-equivalent toolkit-delegated tier rationale). The `[toolkit]` extras group is retained as a no-op for backward compat:
52
52
 
53
53
  ```bash
54
54
  pip install "apcore-cli[toolkit]" # equivalent to plain `pip install apcore-cli`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apcore-cli"
7
- version = "0.10.2"
7
+ version = "0.10.4"
8
8
  description = "Terminal adapter for apcore — execute AI-Perceivable modules from the command line"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -26,8 +26,8 @@ classifiers = [
26
26
  "Environment :: Console",
27
27
  ]
28
28
  dependencies = [
29
- "apcore>=0.25.0",
30
- "apcore-toolkit>=0.9.1",
29
+ "apcore>=0.26.0",
30
+ "apcore-toolkit>=0.10.0",
31
31
  "click>=8.1",
32
32
  "jsonschema>=4.20",
33
33
  "rich>=13.0",
@@ -117,8 +117,10 @@ def _resolve_node(
117
117
  merged["properties"].update(node["properties"])
118
118
  sibling_required: list[str] = list(node["required"]) if isinstance(node.get("required"), list) else []
119
119
  all_required_sets: list[set[str]] = []
120
+ resolved_branches: list[dict] = []
120
121
  for sub_schema in node[keyword]:
121
122
  resolved = _resolve_node(sub_schema, defs, visited, depth + 1, max_depth, module_id)
123
+ resolved_branches.append(resolved)
122
124
  if "properties" in resolved:
123
125
  merged["properties"].update(resolved["properties"])
124
126
  if "required" in resolved:
@@ -136,6 +138,28 @@ def _resolve_node(
136
138
  for k, v in node.items():
137
139
  if k not in (keyword, "properties", "required") and k not in merged:
138
140
  merged[k] = v
141
+ # Pydantic v2 emits Optional[X] as {"anyOf": [<X-schema>, {"type": "null"}]}.
142
+ # The branch merge above copies properties but never the scalar `type` from
143
+ # branches, so Optional[str]/Optional[int]/etc. lose their type entirely.
144
+ # Recover it: if the merged result has no `type`, pick the type of the first
145
+ # non-null branch (ignoring {"type": "null"} sentinels).
146
+ if "type" not in merged:
147
+ for branch in resolved_branches:
148
+ branch_type = branch.get("type")
149
+ if isinstance(branch_type, str) and branch_type != "null":
150
+ merged["type"] = branch_type
151
+ break
152
+ if isinstance(branch_type, list):
153
+ # JSON Schema list-form type (e.g. ["string", "null"]):
154
+ # recover the first non-null string so downstream never
155
+ # sees an unhashable list type.
156
+ recovered = next(
157
+ (t for t in branch_type if isinstance(t, str) and t != "null"),
158
+ None,
159
+ )
160
+ if recovered is not None:
161
+ merged["type"] = recovered
162
+ break
139
163
  return merged
140
164
 
141
165
  # Recursively process every dict-valued child of this node.
@@ -34,10 +34,45 @@ RESERVED_PROPERTY_NAMES: frozenset[str] = frozenset(
34
34
  )
35
35
 
36
36
 
37
+ def _resolve_type_from_any_of(prop_schema: dict) -> str | None:
38
+ """Extract the dominant non-null type from an anyOf schema.
39
+
40
+ Pydantic v2 emits Optional[X] as {"anyOf": [<X-schema>, {"type": "null"}]}.
41
+ We pick the first non-null branch and return its type string so the caller
42
+ can avoid a "no type" warning for what is effectively a typed optional field.
43
+ $ref entries (nested models) are treated as "object".
44
+ """
45
+ branches = [b for b in prop_schema.get("anyOf", []) if b.get("type") != "null"]
46
+ if not branches:
47
+ return None
48
+ first = branches[0]
49
+ branch_type = first.get("type")
50
+ if isinstance(branch_type, str):
51
+ return branch_type
52
+ if isinstance(branch_type, list):
53
+ # JSON Schema list-form type (e.g. ["string", "null"]): first non-null string.
54
+ return next((t for t in branch_type if isinstance(t, str) and t != "null"), None)
55
+ if "$ref" in first: # nested model
56
+ return "object"
57
+ return None
58
+
59
+
37
60
  def _map_type(prop_name: str, prop_schema: dict) -> Any:
38
61
  """Map JSON Schema type to Click parameter type."""
39
62
  schema_type = prop_schema.get("type")
40
63
 
64
+ # JSON Schema allows a list-form type (e.g. ["string", "null"]). Reduce it to
65
+ # the first non-null string so it maps cleanly and never reaches the
66
+ # (hashable-key) type_map lookup below as an unhashable list.
67
+ if isinstance(schema_type, list):
68
+ schema_type = next((t for t in schema_type if isinstance(t, str) and t != "null"), None)
69
+
70
+ # Pydantic v2 generates Optional[X] as {"anyOf": [<X-schema>, {"type": "null"}]}
71
+ # rather than {"type": "X"}. Resolve the dominant branch so we don't fall
72
+ # through to the "no type specified" warning for every optional field.
73
+ if schema_type is None and "anyOf" in prop_schema:
74
+ schema_type = _resolve_type_from_any_of(prop_schema)
75
+
41
76
  # Check file convention
42
77
  if schema_type == "string" and (prop_name.endswith("_file") or prop_schema.get("x-cli-file") is True):
43
78
  return click.Path(exists=True)
@@ -415,3 +415,26 @@ class TestRefResolverExceptions:
415
415
  cur = cur["properties"]["inner"]
416
416
  depth_seen += 1
417
417
  assert depth_seen == 50
418
+
419
+
420
+ class TestOptionalTypeRecovery:
421
+ """Optional[X] (Pydantic v2 anyOf-null) must recover the scalar `type` so the
422
+ schema parser does not fall through to a "no type" warning."""
423
+
424
+ def test_optional_scalar_recovers_type(self):
425
+ result = resolve_refs(
426
+ {
427
+ "type": "object",
428
+ "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}]}},
429
+ }
430
+ )
431
+ assert result["properties"]["name"]["type"] == "string"
432
+
433
+ def test_optional_list_form_type_recovers_string(self):
434
+ result = resolve_refs(
435
+ {
436
+ "type": "object",
437
+ "properties": {"name": {"anyOf": [{"type": ["string", "null"]}, {"type": "null"}]}},
438
+ }
439
+ )
440
+ assert result["properties"]["name"]["type"] == "string"
@@ -332,3 +332,35 @@ class TestNoFlagCollision:
332
332
  }
333
333
  result = schema_to_click_options(schema)
334
334
  assert len(result) == 2
335
+
336
+
337
+ class TestMapTypeOptional:
338
+ """Pydantic v2 Optional[X] (anyOf-null) and JSON Schema list-form types."""
339
+
340
+ def test_optional_string(self):
341
+ assert _map_type("name", {"anyOf": [{"type": "string"}, {"type": "null"}]}) is click.STRING
342
+
343
+ def test_optional_integer(self):
344
+ assert _map_type("count", {"anyOf": [{"type": "integer"}, {"type": "null"}]}) is click.INT
345
+
346
+ def test_optional_boolean(self):
347
+ assert _map_type("flag", {"anyOf": [{"type": "boolean"}, {"type": "null"}]}) is _BOOLEAN_FLAG
348
+
349
+ def test_optional_model_ref_maps_to_string(self):
350
+ # Optional[Model] -> anyOf[$ref, null]; $ref treated as object -> STRING
351
+ assert _map_type("cfg", {"anyOf": [{"$ref": "#/$defs/M"}, {"type": "null"}]}) is click.STRING
352
+
353
+ def test_optional_string_file_convention(self):
354
+ # The *_file convention must still apply through an Optional[str].
355
+ result = _map_type("data_file", {"anyOf": [{"type": "string"}, {"type": "null"}]})
356
+ assert isinstance(result, click.Path)
357
+
358
+ def test_list_form_type_does_not_raise(self):
359
+ # JSON Schema {"type": ["string", "null"]} must reduce to STRING, not TypeError.
360
+ assert _map_type("name", {"type": ["string", "null"]}) is click.STRING
361
+
362
+ def test_anyof_only_null_falls_back_to_string(self, caplog):
363
+ with caplog.at_level(logging.WARNING, logger="apcore_cli.schema_parser"):
364
+ result = _map_type("field", {"anyOf": [{"type": "null"}]})
365
+ assert result is click.STRING
366
+ assert "No type specified" in caplog.text
File without changes
File without changes
File without changes
File without changes
File without changes