pydantic-parse 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.
- pydantic_parse-0.1.0/LICENSE +21 -0
- pydantic_parse-0.1.0/PKG-INFO +72 -0
- pydantic_parse-0.1.0/README.md +56 -0
- pydantic_parse-0.1.0/pyproject.toml +33 -0
- pydantic_parse-0.1.0/src/pydantic_parse/__init__.py +2 -0
- pydantic_parse-0.1.0/src/pydantic_parse/__main__.py +51 -0
- pydantic_parse-0.1.0/src/pydantic_parse/__version__.py +2 -0
- pydantic_parse-0.1.0/src/pydantic_parse/argparse/argument_parser.py +46 -0
- pydantic_parse-0.1.0/src/pydantic_parse/argparse_model/field.py +105 -0
- pydantic_parse-0.1.0/src/pydantic_parse/argparse_model/internal_attr.py +17 -0
- pydantic_parse-0.1.0/src/pydantic_parse/argparse_model/meta.py +78 -0
- pydantic_parse-0.1.0/src/pydantic_parse/argparse_model/model.py +72 -0
- pydantic_parse-0.1.0/src/pydantic_parse/exceptions.py +2 -0
- pydantic_parse-0.1.0/src/pydantic_parse/foo.py +5 -0
- pydantic_parse-0.1.0/src/pydantic_parse/logger.py +154 -0
- pydantic_parse-0.1.0/src/pydantic_parse/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mariia Redchuk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydantic-parse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pydantic adaptor for argparse
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: sagitta42
|
|
7
|
+
Author-email: mariia.redchuk@gmail.com
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: pydantic (>=2.13.5,<3.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pydantic-parse
|
|
17
|
+
|
|
18
|
+
Pydantic adaptor for argparse
|
|
19
|
+
|
|
20
|
+
Example
|
|
21
|
+
```python
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
|
|
25
|
+
class TestChoices(enum.StrEnum):
|
|
26
|
+
alice = "Alice"
|
|
27
|
+
bob = "Bob"
|
|
28
|
+
|
|
29
|
+
class TestModel(ArgModel):
|
|
30
|
+
name: TestChoices = ArgField(description="Name")
|
|
31
|
+
some_value: Optional[str] = ArgField(
|
|
32
|
+
description="value",
|
|
33
|
+
optional=True,
|
|
34
|
+
default=None,
|
|
35
|
+
flag=True
|
|
36
|
+
)
|
|
37
|
+
flag: bool = ArgField(description="flag", default=False, flag=True)
|
|
38
|
+
|
|
39
|
+
parser = PydanticArgParser()
|
|
40
|
+
parser.add_arguments_from_model(TestModel)
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
CLI
|
|
45
|
+
```bash
|
|
46
|
+
my-package Alice --some-value 42
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Results in `vars(args)`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
{
|
|
53
|
+
'name': <TestChoices.alice: 'Alice'>,
|
|
54
|
+
'some_value': '42',
|
|
55
|
+
'flag': False
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or of course
|
|
60
|
+
```python
|
|
61
|
+
model = TestModel(**vars(args))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
gives
|
|
65
|
+
```python
|
|
66
|
+
TestModel
|
|
67
|
+
name=<TestChoices.alice: 'Alice'> some_value='42' flag=False
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
-----
|
|
71
|
+
*Made with [poetiq](https://pypi.org/project/poetiq)*
|
|
72
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# pydantic-parse
|
|
2
|
+
|
|
3
|
+
Pydantic adaptor for argparse
|
|
4
|
+
|
|
5
|
+
Example
|
|
6
|
+
```python
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
|
|
10
|
+
class TestChoices(enum.StrEnum):
|
|
11
|
+
alice = "Alice"
|
|
12
|
+
bob = "Bob"
|
|
13
|
+
|
|
14
|
+
class TestModel(ArgModel):
|
|
15
|
+
name: TestChoices = ArgField(description="Name")
|
|
16
|
+
some_value: Optional[str] = ArgField(
|
|
17
|
+
description="value",
|
|
18
|
+
optional=True,
|
|
19
|
+
default=None,
|
|
20
|
+
flag=True
|
|
21
|
+
)
|
|
22
|
+
flag: bool = ArgField(description="flag", default=False, flag=True)
|
|
23
|
+
|
|
24
|
+
parser = PydanticArgParser()
|
|
25
|
+
parser.add_arguments_from_model(TestModel)
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
CLI
|
|
30
|
+
```bash
|
|
31
|
+
my-package Alice --some-value 42
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Results in `vars(args)`:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
{
|
|
38
|
+
'name': <TestChoices.alice: 'Alice'>,
|
|
39
|
+
'some_value': '42',
|
|
40
|
+
'flag': False
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or of course
|
|
45
|
+
```python
|
|
46
|
+
model = TestModel(**vars(args))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
gives
|
|
50
|
+
```python
|
|
51
|
+
TestModel
|
|
52
|
+
name=<TestChoices.alice: 'Alice'> some_value='42' flag=False
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
-----
|
|
56
|
+
*Made with [poetiq](https://pypi.org/project/poetiq)*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pydantic-parse"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pydantic adaptor for argparse"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "sagitta42",email = "mariia.redchuk@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pydantic (>=2.13.5,<3.0.0)"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
pydantic-parse = "pydantic_parse.__main__:main"
|
|
16
|
+
|
|
17
|
+
[tool.poetry]
|
|
18
|
+
packages = [{include = "pydantic_parse", from = "src"}]
|
|
19
|
+
|
|
20
|
+
[tool.poetiq]
|
|
21
|
+
type = "package"
|
|
22
|
+
settings = false
|
|
23
|
+
progressbar = false
|
|
24
|
+
my_base_model = false
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
28
|
+
build-backend = "poetry.core.masonry.api"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest (>=9.1.1,<10.0.0)"
|
|
33
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import enum
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic_parse.argparse.argument_parser import PydanticArgParser
|
|
7
|
+
from pydantic_parse.argparse_model.field import ArgField
|
|
8
|
+
from pydantic_parse.argparse_model.model import ArgModel
|
|
9
|
+
from pydantic_parse.foo import is_answer
|
|
10
|
+
from pydantic_parse.logger import logg
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
|
|
15
|
+
class TestChoices(enum.StrEnum):
|
|
16
|
+
alice = "Alice"
|
|
17
|
+
bob = "Bob"
|
|
18
|
+
|
|
19
|
+
class TestModel(ArgModel):
|
|
20
|
+
name: TestChoices = ArgField(description="Name")
|
|
21
|
+
some_value: Optional[str] = ArgField(
|
|
22
|
+
description="value",
|
|
23
|
+
optional=True,
|
|
24
|
+
default=None,
|
|
25
|
+
flag=True
|
|
26
|
+
)
|
|
27
|
+
flag: bool = ArgField(description="flag", default=False, flag=True)
|
|
28
|
+
|
|
29
|
+
parser = PydanticArgParser()
|
|
30
|
+
|
|
31
|
+
parser.add_arguments_from_model(TestModel)
|
|
32
|
+
|
|
33
|
+
if len(sys.argv) == 1:
|
|
34
|
+
parser.print_help()
|
|
35
|
+
sys.exit(0)
|
|
36
|
+
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
# subparsers = parser.add_subparsers(dest="command")
|
|
40
|
+
# foo_subparser = subparsers.add_parser("foo", help="foo functionalities")
|
|
41
|
+
# foo_subparser.add_argument("answer", type=int, help="Answer to check")
|
|
42
|
+
|
|
43
|
+
# output = is_answer(args.answer)
|
|
44
|
+
logg.info(vars(args))
|
|
45
|
+
|
|
46
|
+
model = TestModel(**vars(args))
|
|
47
|
+
logg.info(model)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
4
|
+
from pydantic_parse.argparse_model.field import ArgFieldInfo
|
|
5
|
+
from pydantic_parse.argparse_model.model import ArgModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PydanticArgParser(argparse.ArgumentParser):
|
|
9
|
+
def add_arguments_from_model(self, model: Type[ArgModel]):
|
|
10
|
+
for arg_name, arg_info in model.arg_fields().items():
|
|
11
|
+
self.add_argument_from_field(arg_name, arg_info)
|
|
12
|
+
|
|
13
|
+
def add_argument_from_field(
|
|
14
|
+
self, name: str, arg_info: ArgFieldInfo, **kwargs
|
|
15
|
+
) -> argparse.Action:
|
|
16
|
+
arg_name = name.replace("_", "-")
|
|
17
|
+
if arg_info.flag:
|
|
18
|
+
arg_name = f"--{arg_name}"
|
|
19
|
+
|
|
20
|
+
if arg_info.flag and arg_info.arg_type is bool:
|
|
21
|
+
return self.add_argument(
|
|
22
|
+
arg_name,
|
|
23
|
+
action="store_true",
|
|
24
|
+
default=False if arg_info.is_required() else arg_info.default,
|
|
25
|
+
help=arg_info.description,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# FIXME: currently for bool
|
|
29
|
+
# if default was set but is not optional,
|
|
30
|
+
# ends up giving None in default but allows to be not given
|
|
31
|
+
# in theory, no non-optional flags --> unify, currently quickfix
|
|
32
|
+
return super().add_argument(
|
|
33
|
+
arg_name,
|
|
34
|
+
type=arg_info.arg_type,
|
|
35
|
+
choices=arg_info.choices,
|
|
36
|
+
default=arg_info.default if arg_info.optional else None,
|
|
37
|
+
nargs=(
|
|
38
|
+
"?"
|
|
39
|
+
if (arg_info.flag and arg_info.informative)
|
|
40
|
+
or (not arg_info.flag and arg_info.optional)
|
|
41
|
+
else None
|
|
42
|
+
),
|
|
43
|
+
const=arg_info.const if arg_info.flag and arg_info.informative else None,
|
|
44
|
+
help=arg_info.description,
|
|
45
|
+
**kwargs,
|
|
46
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Any, Union, get_args, get_origin
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pydantic.fields import FieldInfo
|
|
6
|
+
from pydantic_core import PydanticUndefined
|
|
7
|
+
|
|
8
|
+
# from pydantic.fields import _FieldInfoInputs, _FieldInfoAsDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ArgFieldInfo(FieldInfo): # type: ignore[misc]
|
|
12
|
+
__slots__ = ("flag", "optional", "informative", "const")
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, flag: bool, optional: bool, informative: bool, const: Any, **kwargs: Any
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(**kwargs)
|
|
18
|
+
|
|
19
|
+
self.flag: bool = flag
|
|
20
|
+
self.optional: bool = optional
|
|
21
|
+
self.informative: bool = informative
|
|
22
|
+
self.const: Any = const
|
|
23
|
+
|
|
24
|
+
def as_dict(self) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Serialize argument properties as dict.
|
|
27
|
+
|
|
28
|
+
Get FieldInfo serialization and extract annotation and attributes.
|
|
29
|
+
Add custom argument field info slots.
|
|
30
|
+
Ignore pydantic undefined properties.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
ret = dict(self.asdict())
|
|
34
|
+
ret.pop("metadata")
|
|
35
|
+
|
|
36
|
+
attr: dict[str, Any] = ret.pop("attributes")
|
|
37
|
+
slots = {name: getattr(self, name) for name in self.__class__.__slots__}
|
|
38
|
+
full_attr = attr | slots
|
|
39
|
+
defined_attr = {
|
|
40
|
+
name: value
|
|
41
|
+
for name, value in full_attr.items()
|
|
42
|
+
if value is not PydanticUndefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ret |= defined_attr
|
|
46
|
+
|
|
47
|
+
return ret
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def choices(self) -> list | None:
|
|
51
|
+
assert self.arg_type is not None
|
|
52
|
+
if issubclass(self.arg_type, enum.Enum):
|
|
53
|
+
ret = [item.value for item in self.arg_type]
|
|
54
|
+
return ret
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_field_info(
|
|
59
|
+
cls,
|
|
60
|
+
field_info: FieldInfo,
|
|
61
|
+
*,
|
|
62
|
+
flag: bool,
|
|
63
|
+
optional: bool,
|
|
64
|
+
informative: bool,
|
|
65
|
+
const: Any,
|
|
66
|
+
) -> "ArgFieldInfo":
|
|
67
|
+
new = cls.__new__(cls)
|
|
68
|
+
for slot in FieldInfo.__slots__:
|
|
69
|
+
setattr(new, slot, getattr(field_info, slot))
|
|
70
|
+
new.flag = flag
|
|
71
|
+
new.optional = optional
|
|
72
|
+
new.informative = informative
|
|
73
|
+
new.const = const
|
|
74
|
+
return new
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def arg_type(self) -> type:
|
|
78
|
+
"""
|
|
79
|
+
Get argument type from annotation.
|
|
80
|
+
|
|
81
|
+
Extract real type from type union to cover Optional[type] case.
|
|
82
|
+
"""
|
|
83
|
+
# TODO: validator
|
|
84
|
+
assert self.annotation is not None
|
|
85
|
+
if get_origin(self.annotation) is Union:
|
|
86
|
+
types = get_args(self.annotation)
|
|
87
|
+
real_types = [tp for tp in types if not tp is type(None)]
|
|
88
|
+
# TODO: validator
|
|
89
|
+
assert len(real_types) == 1
|
|
90
|
+
return real_types[0]
|
|
91
|
+
return self.annotation
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ArgField(
|
|
95
|
+
*,
|
|
96
|
+
flag: bool = False,
|
|
97
|
+
optional: bool = False,
|
|
98
|
+
informative: bool = False,
|
|
99
|
+
const: Any = None,
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> Any:
|
|
102
|
+
field_info = Field(**kwargs)
|
|
103
|
+
return ArgFieldInfo.from_field_info(
|
|
104
|
+
field_info, flag=flag, optional=optional, informative=informative, const=const
|
|
105
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InternalAttr(enum.StrEnum):
|
|
5
|
+
subparser = "subparser__"
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def values(cls) -> list[str]:
|
|
9
|
+
return [c.value for c in cls]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AttrDescription(enum.StrEnum):
|
|
13
|
+
subparser = "Subparser"
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_attr(cls, attr: InternalAttr) -> str:
|
|
17
|
+
return cls[attr.name].value
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from typing import Any, Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from pydantic_parse.argparse_model.field import ArgField, ArgFieldInfo
|
|
6
|
+
from pydantic_parse.argparse_model.internal_attr import AttrDescription, InternalAttr
|
|
7
|
+
from pydantic_parse.exceptions import PydanticParseTypeError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def nested_merge(first: dict, second: dict) -> dict:
|
|
11
|
+
for key, b_val in second.items():
|
|
12
|
+
if key in first and isinstance(first[key], dict) and isinstance(b_val, dict):
|
|
13
|
+
nested_merge(first[key], b_val)
|
|
14
|
+
else:
|
|
15
|
+
first[key] = b_val
|
|
16
|
+
return first
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArgModelMeta(type(BaseModel)):
|
|
20
|
+
"""
|
|
21
|
+
Metaclass for ArgModel creation.
|
|
22
|
+
|
|
23
|
+
Takes care of fields hidden to user.
|
|
24
|
+
Requires model fields to be defined via ArgField rather than standard Field.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __new__(
|
|
28
|
+
mcs,
|
|
29
|
+
name: str,
|
|
30
|
+
bases: tuple[Type, ...],
|
|
31
|
+
namespace: dict[str, Any],
|
|
32
|
+
/,
|
|
33
|
+
# subparser: str | None = None,
|
|
34
|
+
**kwds: Any,
|
|
35
|
+
):
|
|
36
|
+
namespace.setdefault("__annotations__", {})
|
|
37
|
+
|
|
38
|
+
# mcs._add_field_namespace_info(namespace, InternalAttr.subparser, subparser)
|
|
39
|
+
|
|
40
|
+
cls = super().__new__(mcs, name, bases, namespace, **kwds)
|
|
41
|
+
|
|
42
|
+
for field_name, field_info in getattr(cls, "__pydantic_fields__", {}).items():
|
|
43
|
+
if field_name in InternalAttr.values():
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if not isinstance(field_info, ArgFieldInfo):
|
|
47
|
+
raise PydanticParseTypeError(
|
|
48
|
+
f"{cls.__name__}.{field_name} must be declared with {ArgField.__name__}(...), "
|
|
49
|
+
f"not Field() or a bare default (got {type(field_info).__name__})"
|
|
50
|
+
)
|
|
51
|
+
return cls
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def _add_field_namespace_info(
|
|
55
|
+
mcs, namespace: dict, field_name: InternalAttr, parameter: Any
|
|
56
|
+
):
|
|
57
|
+
nested_merge(namespace, mcs._get_field_namespace_info(field_name, parameter))
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def _get_field_namespace_info(
|
|
61
|
+
mcs, field_name: InternalAttr, parameter: Any
|
|
62
|
+
) -> dict:
|
|
63
|
+
"""
|
|
64
|
+
Create information to add to namespace to create field.
|
|
65
|
+
|
|
66
|
+
parameter: parameter to be added - defines annotation and default value.
|
|
67
|
+
name: field name
|
|
68
|
+
|
|
69
|
+
The default value is crucial to set fixed argument model parameters once during
|
|
70
|
+
child class definition.
|
|
71
|
+
"""
|
|
72
|
+
ret = {
|
|
73
|
+
"__annotations__": {field_name: type(parameter)},
|
|
74
|
+
field_name: Field(
|
|
75
|
+
default=parameter, description=AttrDescription.from_attr(field_name)
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
return ret
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Any, Self, Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, model_validator
|
|
4
|
+
|
|
5
|
+
from pydantic_parse.argparse_model.field import ArgFieldInfo
|
|
6
|
+
from pydantic_parse.argparse_model.internal_attr import InternalAttr
|
|
7
|
+
from pydantic_parse.argparse_model.meta import ArgModelMeta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ArgModel(BaseModel, metaclass=ArgModelMeta):
|
|
11
|
+
"""
|
|
12
|
+
Argument model.
|
|
13
|
+
|
|
14
|
+
Defines a set of arguments, their type and description.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def arg_dump(self, **kwargs) -> dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Argument dump.
|
|
20
|
+
|
|
21
|
+
Model dump of arguments present in ArgModel instance.
|
|
22
|
+
Exclude internal attributes (non-arguments)
|
|
23
|
+
|
|
24
|
+
Arguments can be further excluded via exclude={name: True} in **kwargs.
|
|
25
|
+
Internal arguments however are always excluded.
|
|
26
|
+
User standard model_dump()
|
|
27
|
+
"""
|
|
28
|
+
exclude_args = {key: True for key in InternalAttr}
|
|
29
|
+
|
|
30
|
+
if not "exclude" in kwargs:
|
|
31
|
+
kwargs["exclude"] = {}
|
|
32
|
+
|
|
33
|
+
kwargs["exclude"] |= exclude_args
|
|
34
|
+
|
|
35
|
+
return self.model_dump(**kwargs)
|
|
36
|
+
|
|
37
|
+
# @classmethod
|
|
38
|
+
# def subparser(cls) -> str:
|
|
39
|
+
# """
|
|
40
|
+
# Subparser.
|
|
41
|
+
|
|
42
|
+
# Default value of internal field is set at child class definition.
|
|
43
|
+
# """
|
|
44
|
+
# ret = cls.model_fields[InternalAttr.subparser].default
|
|
45
|
+
# return ret
|
|
46
|
+
|
|
47
|
+
# TODO: property like model_fields
|
|
48
|
+
@classmethod
|
|
49
|
+
def arg_fields(cls) -> dict[str, ArgFieldInfo]:
|
|
50
|
+
"""
|
|
51
|
+
Model info that represent arguments.
|
|
52
|
+
|
|
53
|
+
Non-argument (internal) attribuges are skipped.
|
|
54
|
+
"""
|
|
55
|
+
ret = {
|
|
56
|
+
field_name: field_info
|
|
57
|
+
for field_name, field_info in cls.model_fields.items()
|
|
58
|
+
if not field_name in InternalAttr
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ret
|
|
62
|
+
|
|
63
|
+
# @model_validator(mode="before")
|
|
64
|
+
# def check_hidden(self) -> Self:
|
|
65
|
+
# # TODO: check that hidden fields have not been given in input; raise error that they are reserved
|
|
66
|
+
# # raise ValueError("Provide subparser in your ArgModel class definition!")
|
|
67
|
+
# return self
|
|
68
|
+
|
|
69
|
+
@model_validator(mode="before")
|
|
70
|
+
def check_field_info(self) -> Self:
|
|
71
|
+
# TODO: mode-before model validator that all fields have a description and annotation
|
|
72
|
+
return self
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from dotenv import dotenv_values
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AnsiStyle(enum.StrEnum):
|
|
10
|
+
normal = "0"
|
|
11
|
+
bold = "1"
|
|
12
|
+
start = "\033["
|
|
13
|
+
end = "\033[0m"
|
|
14
|
+
fg8bit = "38;5"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnsiColor(enum.StrEnum):
|
|
18
|
+
green = "32"
|
|
19
|
+
grey = "90"
|
|
20
|
+
red = "31"
|
|
21
|
+
yellow = "33"
|
|
22
|
+
white = "37"
|
|
23
|
+
lila = "91"
|
|
24
|
+
|
|
25
|
+
def apply(self, message: Any, bold: bool = False) -> str:
|
|
26
|
+
"""
|
|
27
|
+
To be used with color based.
|
|
28
|
+
|
|
29
|
+
Bold not implemented for 8 bit colors.
|
|
30
|
+
"""
|
|
31
|
+
if self.is_8bit:
|
|
32
|
+
style = AnsiStyle.fg8bit
|
|
33
|
+
else:
|
|
34
|
+
style = AnsiStyle.bold if bold else AnsiStyle.normal
|
|
35
|
+
|
|
36
|
+
ret = f"{AnsiStyle.start}{style};{self}m{message}{AnsiStyle.end}"
|
|
37
|
+
return ret
|
|
38
|
+
|
|
39
|
+
def bold(self, message: Any) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Shortcut for bold colored text
|
|
42
|
+
"""
|
|
43
|
+
ret = self.apply(message, bold=True)
|
|
44
|
+
return ret
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def is_8bit(self) -> bool:
|
|
48
|
+
return self == AnsiColor.lila
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LevelFormatter(logging.Formatter):
|
|
52
|
+
def __init__(self, formats, default_fmt=None, datefmt=None):
|
|
53
|
+
super().__init__(datefmt=datefmt)
|
|
54
|
+
self.formats = {
|
|
55
|
+
level: logging.Formatter(fmt, datefmt=datefmt)
|
|
56
|
+
for level, fmt in formats.items()
|
|
57
|
+
}
|
|
58
|
+
self.default_formatter = logging.Formatter(
|
|
59
|
+
default_fmt or "%(message)s", datefmt=datefmt
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def format(self, record):
|
|
63
|
+
formatter = self.formats.get(record.levelno, self.default_formatter)
|
|
64
|
+
return formatter.format(record)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Logger:
|
|
68
|
+
def __init__(self, log_level=logging.INFO):
|
|
69
|
+
self.log_level = log_level
|
|
70
|
+
|
|
71
|
+
self._logger = logging.getLogger(__name__)
|
|
72
|
+
|
|
73
|
+
full_format = "%(asctime)s [%(levelname)s] %(classname)s.%(funcName)s:%(lineno)d - %(message)s"
|
|
74
|
+
short_format = "[%(levelname)s] %(classname)s - %(message)s"
|
|
75
|
+
no_format = ""
|
|
76
|
+
level_format = full_format if log_level == logging.DEBUG else no_format
|
|
77
|
+
formatter = LevelFormatter(
|
|
78
|
+
{
|
|
79
|
+
logging.DEBUG: full_format,
|
|
80
|
+
logging.ERROR: level_format,
|
|
81
|
+
logging.INFO: level_format,
|
|
82
|
+
logging.WARNING: level_format,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
handler = logging.StreamHandler()
|
|
87
|
+
handler.setFormatter(formatter)
|
|
88
|
+
handler.setLevel(log_level)
|
|
89
|
+
self._logger.addHandler(handler)
|
|
90
|
+
|
|
91
|
+
self._logger.setLevel(log_level)
|
|
92
|
+
self._logger.propagate = False
|
|
93
|
+
|
|
94
|
+
def info(self, message: Any, header: bool = False, poetiq: bool = False):
|
|
95
|
+
if poetiq:
|
|
96
|
+
color = AnsiColor.lila
|
|
97
|
+
elif header:
|
|
98
|
+
color = AnsiColor.green
|
|
99
|
+
else:
|
|
100
|
+
color = AnsiColor.white
|
|
101
|
+
|
|
102
|
+
return self._log(logging.INFO, color.apply(message, header))
|
|
103
|
+
|
|
104
|
+
def error(self, message: Any):
|
|
105
|
+
return self._log(logging.ERROR, AnsiColor.red.apply(message))
|
|
106
|
+
|
|
107
|
+
def warning(self, message: Any, important: bool = False):
|
|
108
|
+
if important:
|
|
109
|
+
message = f"! WARNING ! {message}"
|
|
110
|
+
return self._log(logging.WARNING, AnsiColor.yellow.apply(message, important))
|
|
111
|
+
|
|
112
|
+
def debug(self, message: Any):
|
|
113
|
+
return self._log(logging.DEBUG, AnsiColor.grey.apply(message))
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_debug(self) -> bool:
|
|
117
|
+
return self.log_level == logging.DEBUG
|
|
118
|
+
|
|
119
|
+
def _log(self, level, message: Any):
|
|
120
|
+
"""
|
|
121
|
+
Common log interface for info/error/warning/debug.
|
|
122
|
+
|
|
123
|
+
Auto-detect class name of caller.
|
|
124
|
+
Account for stack level to display correct funcName and lineno.
|
|
125
|
+
Skip 2 stack levels including this method,
|
|
126
|
+
the info/error/warning/debug method calling it,
|
|
127
|
+
"""
|
|
128
|
+
self._logger.log(
|
|
129
|
+
level,
|
|
130
|
+
message,
|
|
131
|
+
extra={"classname": self._get_caller_class_name()},
|
|
132
|
+
stacklevel=3,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _get_caller_class_name(self):
|
|
136
|
+
"""
|
|
137
|
+
Determine class name of where logger is called from.
|
|
138
|
+
|
|
139
|
+
Obtain frame index 3 corresponding to actual caller
|
|
140
|
+
(0 = this method, 1 = _log internal method, 2 = info/warning/error/debug logger call).
|
|
141
|
+
Return class or module name of that frame.
|
|
142
|
+
"""
|
|
143
|
+
frame = inspect.stack()[3].frame
|
|
144
|
+
if "self" in frame.f_locals:
|
|
145
|
+
return type(frame.f_locals["self"]).__name__
|
|
146
|
+
elif "cls" in frame.f_locals:
|
|
147
|
+
return frame.f_locals["cls"].__name__
|
|
148
|
+
return frame.f_globals.get("__name__", "-")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# NOTE: dotenv_values() in some cases yielded empty .env for unexplained reason
|
|
152
|
+
env_config = dotenv_values(Path.cwd() / ".env")
|
|
153
|
+
is_debug = env_config.get("DEBUG_PYDANTIC_PARSE", "").lower() in ("true", "1")
|
|
154
|
+
logg = Logger(log_level=logging.DEBUG if is_debug else logging.INFO)
|
|
File without changes
|