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
|