cli-pydantic 0.1.0__py3-none-any.whl
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.
- cli_pydantic/__init__.py +1 -0
- cli_pydantic/lib.py +101 -0
- cli_pydantic/py.typed +0 -0
- cli_pydantic-0.1.0.dist-info/METADATA +11 -0
- cli_pydantic-0.1.0.dist-info/RECORD +6 -0
- cli_pydantic-0.1.0.dist-info/WHEEL +4 -0
cli_pydantic/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .lib import parse_unknown_args, deep_merge, model_help
|
cli_pydantic/lib.py
ADDED
|
@@ -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
|
+
|
cli_pydantic/py.typed
ADDED
|
File without changes
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
cli_pydantic/__init__.py,sha256=bQv5RB8buC5LQO5DNjDhl4ZudIi9SJFyOuesDCDWVro,60
|
|
2
|
+
cli_pydantic/lib.py,sha256=V9cJVw5ZN9Cx4Y4Zy9WLx-dIdyUt9dEnr7ds-MRe3-8,3168
|
|
3
|
+
cli_pydantic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
cli_pydantic-0.1.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
5
|
+
cli_pydantic-0.1.0.dist-info/METADATA,sha256=Vxj2rtGtWVA76_iHL0kaFtADcn1UhMXL8olXvMiyxGA,281
|
|
6
|
+
cli_pydantic-0.1.0.dist-info/RECORD,,
|