cli-pydantic 0.1.0__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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.3
2
+ Name: cli-pydantic
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Tom Pollak
6
+ Author-email: Tom Pollak <tomp@graphcore.ai>
7
+ Requires-Dist: pydantic>=2.12.5
8
+ Requires-Dist: pyyaml>=6.0.3
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
File without changes
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "cli-pydantic"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Tom Pollak", email = "tomp@graphcore.ai" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "pydantic>=2.12.5",
12
+ "pyyaml>=6.0.3",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.8.4,<0.9.0"]
17
+ build-backend = "uv_build"
@@ -0,0 +1 @@
1
+ from .lib import parse_unknown_args, deep_merge, model_help
@@ -0,0 +1,101 @@
1
+ from collections import deque
2
+ from typing import get_origin
3
+
4
+ from pydantic import BaseModel
5
+ from pydantic_core import PydanticUndefined
6
+
7
+
8
+ def resolve_field_type(model_cls: type[BaseModel], path: list[str]) -> type | None:
9
+ """Walk a dotted path through nested BaseModels, return the leaf annotation."""
10
+ cls = model_cls
11
+ for p in path:
12
+ if p not in cls.model_fields:
13
+ return None
14
+ ann = cls.model_fields[p].annotation
15
+ if p == path[-1]:
16
+ return ann
17
+ if isinstance(ann, type) and issubclass(ann, BaseModel):
18
+ cls = ann
19
+ else:
20
+ return None
21
+ return None
22
+
23
+
24
+ def parse_unknown_args(tokens: list[str], model_cls: type[BaseModel]) -> dict:
25
+ out = {}
26
+ q = deque(tokens)
27
+
28
+ def has_value() -> bool:
29
+ return bool(q) and not q[0].startswith("--")
30
+
31
+ def route(key: str) -> list[str]:
32
+ parts = key.replace("-", "_").split(".")
33
+ if resolve_field_type(model_cls, parts) is None:
34
+ raise ValueError(f"Unknown option: --{key}")
35
+ return parts
36
+
37
+ def put(parts: list[str], val):
38
+ cur = out
39
+ for p in parts[:-1]:
40
+ cur = cur.setdefault(p, {})
41
+
42
+ k = parts[-1]
43
+ is_list = get_origin(resolve_field_type(model_cls, parts)) is list
44
+
45
+ if is_list:
46
+ vals = val.split(",") if isinstance(val, str) and "," in val else [val]
47
+ cur.setdefault(k, []).extend(vals)
48
+ elif cur.get(k, val) != val:
49
+ raise ValueError(f"Duplicate value for {'.'.join(parts)}")
50
+ else:
51
+ cur[k] = val
52
+
53
+ while q:
54
+ t = q.popleft()
55
+ if not t.startswith("--"):
56
+ raise ValueError(f"Expected --key, got: {t}")
57
+
58
+ s = t[2:]
59
+
60
+ if s.startswith("no-"): # --no-flag
61
+ if "=" in s or has_value():
62
+ raise ValueError(f"--no-* flags can't take a value: {t}")
63
+ key, val = s[3:], False
64
+ elif "=" in s: # --k=v
65
+ key, val = s.split("=", 1)
66
+ else: # --k v / --flag
67
+ key, val = s, (q.popleft() if has_value() else True)
68
+
69
+ put(route(key), val)
70
+
71
+ return out
72
+
73
+
74
+ def deep_merge(base: dict, overrides: dict) -> dict:
75
+ for k, v in overrides.items():
76
+ if isinstance(v, dict) and isinstance(base.get(k), dict):
77
+ deep_merge(base[k], v)
78
+ else:
79
+ base[k] = v
80
+ return base
81
+
82
+
83
+ def model_help(model: type[BaseModel], prefix: str = "") -> list[str]:
84
+ def ty_name(ann) -> str:
85
+ name = getattr(ann, "__name__", None)
86
+ return name if name and "[" not in str(ann) else str(ann)
87
+
88
+ lines = []
89
+ for name, field in model.model_fields.items():
90
+ key = f"{prefix}{name}"
91
+ ann = field.annotation
92
+ if isinstance(ann, type) and issubclass(ann, BaseModel):
93
+ lines.extend(model_help(ann, prefix=f"{key}."))
94
+ else:
95
+ default = (
96
+ "required" if field.default is PydanticUndefined else field.default
97
+ )
98
+ desc = f" {field.description}" if field.description else ""
99
+ lines.append(f" --{key} {ty_name(ann)} (default: {default}){desc}")
100
+ return lines
101
+
File without changes