cmodel 0.0.1__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.
- cmodel-0.0.1/LICENSE.txt +20 -0
- cmodel-0.0.1/PKG-INFO +42 -0
- cmodel-0.0.1/README.md +7 -0
- cmodel-0.0.1/pyproject.toml +188 -0
- cmodel-0.0.1/python/cmodel/__init__.py +11 -0
- cmodel-0.0.1/python/cmodel/base.py +206 -0
- cmodel-0.0.1/python/cmodel/py.typed +1 -0
- cmodel-0.0.1/python/cmodel/types.py +94 -0
cmodel-0.0.1/LICENSE.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Ryan Morshead <ryan.morshead@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify,
|
|
8
|
+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to the following
|
|
10
|
+
conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
13
|
+
substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
16
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
17
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
20
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
cmodel-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cmodel
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Model C structs with Pydantic
|
|
5
|
+
Keywords:
|
|
6
|
+
Author: Ryan Morshead
|
|
7
|
+
Author-email: Ryan Morshead <ryan.morshead@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) Ryan Morshead <ryan.morshead@gmail.com>
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
13
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
14
|
+
without restriction, including without limitation the rights to use, copy, modify,
|
|
15
|
+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
16
|
+
permit persons to whom the Software is furnished to do so, subject to the following
|
|
17
|
+
conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
20
|
+
substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
23
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
24
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
25
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
26
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
27
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
28
|
+
Classifier: Development Status :: 4 - Beta
|
|
29
|
+
Classifier: Programming Language :: Python
|
|
30
|
+
Requires-Dist: pydantic>=2.12.5
|
|
31
|
+
Requires-Dist: pydantic-walk-core-schema>=1.0.0
|
|
32
|
+
Requires-Python: >=3.12, <4
|
|
33
|
+
Project-URL: Source, https://github.com/rmorshea/cmodel
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# CModel
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/cmodel)
|
|
39
|
+
[](https://pypi.org/project/cmodel)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
42
|
+
Model C structs with Pydantic
|
cmodel-0.0.1/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# CModel
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/cmodel)
|
|
4
|
+
[](https://pypi.org/project/cmodel)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Model C structs with Pydantic
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.10.3,<0.11.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cmodel"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Model C structs with Pydantic"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<4"
|
|
11
|
+
license = { file = "LICENSE.txt" }
|
|
12
|
+
keywords = []
|
|
13
|
+
authors = [{ name = "Ryan Morshead", email = "ryan.morshead@gmail.com" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Programming Language :: Python",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"pydantic>=2.12.5",
|
|
20
|
+
"pydantic-walk-core-schema>=1.0.0",
|
|
21
|
+
]
|
|
22
|
+
[project.urls]
|
|
23
|
+
Source = "https://github.com/rmorshea/cmodel"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
{ include-group = "util" },
|
|
28
|
+
{ include-group = "docs" },
|
|
29
|
+
{ include-group = "lint" },
|
|
30
|
+
{ include-group = "test" },
|
|
31
|
+
]
|
|
32
|
+
util = [
|
|
33
|
+
"click==8.1.7",
|
|
34
|
+
"ipykernel==6.29.5",
|
|
35
|
+
"copier==9.4.1",
|
|
36
|
+
]
|
|
37
|
+
docs = [
|
|
38
|
+
{ include-group = "test" },
|
|
39
|
+
"mkdocs-gen-files==0.5.0",
|
|
40
|
+
"mkdocs-literate-nav==0.6.1",
|
|
41
|
+
"mkdocs-material==9.5.39",
|
|
42
|
+
"mkdocs-open-in-new-tab==1.0.5",
|
|
43
|
+
"mkdocs==1.6.1",
|
|
44
|
+
"mkdocstrings-python==1.16.12",
|
|
45
|
+
]
|
|
46
|
+
lint = [
|
|
47
|
+
{ include-group = "test" },
|
|
48
|
+
"mdformat-admon @ git+https://github.com/rmorshea/mdformat-admon.git@0e513d7a2c265faf74441938ccbd1010660609f4",
|
|
49
|
+
"mdformat-mkdocs==4.1.0",
|
|
50
|
+
"mdformat-pyproject==0.0.1",
|
|
51
|
+
"mdformat-tables==1.0.0",
|
|
52
|
+
"mdformat==0.7.22",
|
|
53
|
+
"pyright==1.1.389",
|
|
54
|
+
"ruff==0.7.3",
|
|
55
|
+
"yamlfix==1.17.0",
|
|
56
|
+
"doccmd==2024.11.14",
|
|
57
|
+
"deptry==0.24.0",
|
|
58
|
+
]
|
|
59
|
+
test = [
|
|
60
|
+
"coverage[toml]==7.6.1",
|
|
61
|
+
"diff-cover==9.2.0",
|
|
62
|
+
"pycobertura==3.3.2",
|
|
63
|
+
"pytest-asyncio==0.24.0",
|
|
64
|
+
"pytest-examples==0.0.13",
|
|
65
|
+
"pytest==8.3.3",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
asyncio_mode = "auto"
|
|
70
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
71
|
+
python_files = ["test_*.py", "*_test.py", "test/*.py", "tests/**/*.py", "test.py"]
|
|
72
|
+
filterwarnings = ["error"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff]
|
|
75
|
+
line-length = 100
|
|
76
|
+
|
|
77
|
+
[tool.ruff.format]
|
|
78
|
+
docstring-code-format = true
|
|
79
|
+
quote-style = "double"
|
|
80
|
+
indent-style = "space"
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint]
|
|
83
|
+
preview = true
|
|
84
|
+
select = ["ALL"]
|
|
85
|
+
ignore = [
|
|
86
|
+
"A005", # Module shadowing built-in
|
|
87
|
+
"ANN", # Let pyright handle annotations
|
|
88
|
+
"ANN401", # Allow Any type hints
|
|
89
|
+
"ARG005", # Unused lambda argument
|
|
90
|
+
"B027", # Allow non-abstract empty methods in abstract base classes
|
|
91
|
+
"B039", # Mutable default for contextvars
|
|
92
|
+
"C901", # Ignore complexity
|
|
93
|
+
"COM812", # Trailing comma
|
|
94
|
+
"CPY001", # Copyright at top of file
|
|
95
|
+
"D100", # Docstring for module
|
|
96
|
+
"D104", # Ignore missing docstring for __init__.py
|
|
97
|
+
"D105", # Docstring for magic method
|
|
98
|
+
"D107", # Docstring for __init__ method
|
|
99
|
+
"D203", # One blank line before class
|
|
100
|
+
"D213", # Multi-line docstring summary second line
|
|
101
|
+
"D407", # Docstring dashes under section names
|
|
102
|
+
"D413", # Docstring blank line after last section
|
|
103
|
+
"DOC201", # Return type documentation
|
|
104
|
+
"DOC402", # Yield type documentation
|
|
105
|
+
"DOC501", # Ignore raises missing from docstring
|
|
106
|
+
"ERA001", # Commented out code
|
|
107
|
+
"FBT003", # Allow boolean positional values in function calls, like `dict.get(... True)`
|
|
108
|
+
"PL", # PyLint
|
|
109
|
+
"PYI", # Stub files
|
|
110
|
+
"RET503", # Explicit return
|
|
111
|
+
"RET505", # Unnecessary return statement after return
|
|
112
|
+
"S105", # Ignore checks for possible passwords
|
|
113
|
+
"SIM117", # Use a single `with` statement
|
|
114
|
+
"ISC001", # implicitly concatenated strings on a single line
|
|
115
|
+
]
|
|
116
|
+
unfixable = [
|
|
117
|
+
"COM819", # Trailing comma
|
|
118
|
+
]
|
|
119
|
+
fixable = ["ALL"]
|
|
120
|
+
extend-safe-fixes = ["TCH"]
|
|
121
|
+
[tool.ruff.lint.isort]
|
|
122
|
+
known-first-party = ["cmodel"]
|
|
123
|
+
force-single-line = true
|
|
124
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
125
|
+
ban-relative-imports = "all"
|
|
126
|
+
[tool.ruff.lint.per-file-ignores]
|
|
127
|
+
"{*_test.py,conftest.py,**/test/*.py,test.py}" = [
|
|
128
|
+
"ARG001", # Unused argument (pytest fixtures)
|
|
129
|
+
"PLC2701", # Private imports
|
|
130
|
+
"RUF029", # Async functions without await
|
|
131
|
+
"S101", # Assert statements
|
|
132
|
+
"D", # Docstrings
|
|
133
|
+
"ANN", # Type annotations
|
|
134
|
+
]
|
|
135
|
+
"**.ipynb" = [
|
|
136
|
+
"T201", # Print statements
|
|
137
|
+
]
|
|
138
|
+
"docs/**" = [
|
|
139
|
+
"INP001", # Implicit namespace package
|
|
140
|
+
"D", # Docstrings
|
|
141
|
+
]
|
|
142
|
+
"doccmd_*.py" = [
|
|
143
|
+
"ANN", # Type annotations
|
|
144
|
+
"B018", # Useless expression
|
|
145
|
+
"FA102", # Unsafe __futures__ annotations usage
|
|
146
|
+
"RUF029", # No await in async function
|
|
147
|
+
"S101", # Assert statements
|
|
148
|
+
"S106", # Possible passwords
|
|
149
|
+
"SIM115", # Use context manager for opening files
|
|
150
|
+
"T201", # Print
|
|
151
|
+
"TCH002", # Move third-party import into a type-checking block
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
[tool.yamlfix]
|
|
155
|
+
line_length = 100
|
|
156
|
+
|
|
157
|
+
[tool.coverage.run]
|
|
158
|
+
source_pkgs = ["cmodel", "tests"]
|
|
159
|
+
branch = true
|
|
160
|
+
omit = []
|
|
161
|
+
|
|
162
|
+
[tool.coverage.paths]
|
|
163
|
+
cmodel = ["python"]
|
|
164
|
+
tests = ["tests"]
|
|
165
|
+
|
|
166
|
+
[tool.coverage.report]
|
|
167
|
+
exclude_lines = [
|
|
168
|
+
"# nocov",
|
|
169
|
+
"@overload",
|
|
170
|
+
"if TYPE_CHECKING:",
|
|
171
|
+
"raise AssertionError",
|
|
172
|
+
"raise NotImplementedError",
|
|
173
|
+
'if __name__ == .__main__.:',
|
|
174
|
+
'\.\.\.\n($|\s*#.*)',
|
|
175
|
+
]
|
|
176
|
+
show_missing = true
|
|
177
|
+
skip_covered = true
|
|
178
|
+
sort = "Name"
|
|
179
|
+
|
|
180
|
+
[tool.diff_cover]
|
|
181
|
+
compare_branch = "origin/main"
|
|
182
|
+
fail_under = 100
|
|
183
|
+
include_untracked = true
|
|
184
|
+
|
|
185
|
+
[tool.uv.build-backend]
|
|
186
|
+
module-name = "cmodel"
|
|
187
|
+
module-root = "python"
|
|
188
|
+
source-exclude = ["test", "tests", "conftest.py", "*_test.py", "test_*.py"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
|
|
4
|
+
from cmodel import types as types
|
|
5
|
+
from cmodel.base import CFmt as CFmt
|
|
6
|
+
from cmodel.base import CModel as CModel
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version(__name__)
|
|
10
|
+
except PackageNotFoundError: # nocov
|
|
11
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from struct import calcsize
|
|
5
|
+
from struct import pack
|
|
6
|
+
from struct import unpack_from
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
from typing import Literal
|
|
10
|
+
from typing import Self
|
|
11
|
+
from typing import TypedDict
|
|
12
|
+
from typing import overload
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
from pydantic import GetCoreSchemaHandler
|
|
16
|
+
from pydantic import SerializationInfo
|
|
17
|
+
from pydantic import ValidationInfo
|
|
18
|
+
from pydantic import model_validator
|
|
19
|
+
from pydantic_core import core_schema as cs
|
|
20
|
+
from pydantic_walk_core_schema import walk_core_schema
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CModel(BaseModel):
|
|
24
|
+
"""Base class for models that can be packed/unpacked to/from C binary data."""
|
|
25
|
+
|
|
26
|
+
c_field_formats: ClassVar[tuple[str, ...]] = ()
|
|
27
|
+
"""The struct format strings for each field used to pack/unpack the C binary data."""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def __get_pydantic_core_schema__(
|
|
31
|
+
cls,
|
|
32
|
+
source: Any,
|
|
33
|
+
handler: GetCoreSchemaHandler,
|
|
34
|
+
) -> cs.CoreSchema:
|
|
35
|
+
try:
|
|
36
|
+
CModel # type:ignore[reportUnusedExpression] # noqa: B018
|
|
37
|
+
except NameError:
|
|
38
|
+
# we're defining the schema for this class - just return it
|
|
39
|
+
return handler(source)
|
|
40
|
+
else:
|
|
41
|
+
# we're defining the schema for a subclass
|
|
42
|
+
adapter = _ModelSchemaAdapter(handler)
|
|
43
|
+
schema = adapter.adapt(handler(source))
|
|
44
|
+
cls.c_format = tuple(adapter.format)
|
|
45
|
+
return schema
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def c_unpack(cls, buffer: BytesIO) -> Self:
|
|
49
|
+
"""Read a C binary data buffer as a packed struct and return an instance of the model."""
|
|
50
|
+
ctx: _Context = {"io": buffer}
|
|
51
|
+
return cls.model_validate(_USE_BUFFER, context={_CONTEXT_KEY: ctx})
|
|
52
|
+
|
|
53
|
+
def c_pack(self, buffer: BytesIO) -> None:
|
|
54
|
+
"""Write the model instance to a C binary data buffer as a packed struct."""
|
|
55
|
+
ctx: _Context = {"io": buffer}
|
|
56
|
+
self.model_dump(context={_CONTEXT_KEY: ctx})
|
|
57
|
+
|
|
58
|
+
@model_validator(mode="before")
|
|
59
|
+
@classmethod
|
|
60
|
+
def _validate_model(cls, value: Any) -> Any:
|
|
61
|
+
if value is _USE_BUFFER:
|
|
62
|
+
# Indicate that we're unpacking by ensuring each field is present and gets "validated".
|
|
63
|
+
# The validator for each field will then read from the buffer.
|
|
64
|
+
return dict.fromkeys(cls.model_fields, _USE_BUFFER)
|
|
65
|
+
else:
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class CFmt[T]:
|
|
71
|
+
"""Metadata for a C field, used in the Annotated type of each field in a CModel."""
|
|
72
|
+
|
|
73
|
+
fmt: str
|
|
74
|
+
validate: Callable[[tuple[Any, ...]], T] = lambda x: x # pyright: ignore[reportAssignmentType]
|
|
75
|
+
dump: Callable[[T], tuple[Any, ...]] = lambda x: x # pyright: ignore[reportAssignmentType]
|
|
76
|
+
|
|
77
|
+
def __get_pydantic_core_schema__(
|
|
78
|
+
self,
|
|
79
|
+
source: Any,
|
|
80
|
+
handler: GetCoreSchemaHandler,
|
|
81
|
+
) -> cs.CoreSchema:
|
|
82
|
+
fmt = self.fmt
|
|
83
|
+
size = calcsize(fmt)
|
|
84
|
+
validate = self.validate
|
|
85
|
+
dump = self.dump
|
|
86
|
+
|
|
87
|
+
def validator(value: Any, info: ValidationInfo) -> Any:
|
|
88
|
+
if value is _USE_BUFFER:
|
|
89
|
+
ctx = _get_context(info, required=True)
|
|
90
|
+
io = ctx["io"]
|
|
91
|
+
value = unpack_from(fmt, io.getbuffer(), io.tell())
|
|
92
|
+
io.seek(size, 1)
|
|
93
|
+
return validate(value)
|
|
94
|
+
else:
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
def serializer(value: Any, info: SerializationInfo) -> Any:
|
|
98
|
+
if ctx := _get_context(info):
|
|
99
|
+
ctx["io"].write(pack(fmt, *dump(value)))
|
|
100
|
+
else:
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
return cs.with_info_before_validator_function(
|
|
104
|
+
validator,
|
|
105
|
+
schema=handler(source),
|
|
106
|
+
metadata={_METADATA_KEY: self},
|
|
107
|
+
serialization=cs.plain_serializer_function_ser_schema(serializer, info_arg=True),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_USE_BUFFER = object()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
type _Recurse = Callable[[cs.CoreSchema, _Recurse], cs.CoreSchema]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _ModelSchemaAdapter:
|
|
118
|
+
_VISIT_TYPES: ClassVar[set[cs.CoreSchemaType]] = {
|
|
119
|
+
"tuple",
|
|
120
|
+
}
|
|
121
|
+
_ALLOWED_TYPES: ClassVar[set[cs.CoreSchemaType]] = {
|
|
122
|
+
"model",
|
|
123
|
+
"model-fields",
|
|
124
|
+
"function-before",
|
|
125
|
+
"default",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def __init__(self, handler: GetCoreSchemaHandler) -> None:
|
|
129
|
+
self.handler = handler
|
|
130
|
+
self.format: list[str] = []
|
|
131
|
+
self.visitors: dict[str, _Recurse] = {}
|
|
132
|
+
for schema_type in self._VISIT_TYPES:
|
|
133
|
+
method_name = f"visit_{schema_type.replace('-', '_')}"
|
|
134
|
+
self.visitors[schema_type] = getattr(self, method_name)
|
|
135
|
+
|
|
136
|
+
def adapt(self, schema: cs.CoreSchema) -> cs.CoreSchema:
|
|
137
|
+
return walk_core_schema(schema, self.visit)
|
|
138
|
+
|
|
139
|
+
def visit(self, schema: cs.CoreSchema, recurse: _Recurse) -> cs.CoreSchema:
|
|
140
|
+
if _get_metadata(schema):
|
|
141
|
+
return schema
|
|
142
|
+
schema_type = schema["type"]
|
|
143
|
+
visit_fn = self.visitors.get(schema_type)
|
|
144
|
+
if visit_fn is not None:
|
|
145
|
+
return visit_fn(schema, recurse)
|
|
146
|
+
elif schema_type in self._ALLOWED_TYPES:
|
|
147
|
+
return recurse(schema, self.visit)
|
|
148
|
+
else:
|
|
149
|
+
msg = f"Unsupported schema type {schema['type']!r} in CModel"
|
|
150
|
+
raise TypeError(msg)
|
|
151
|
+
|
|
152
|
+
def visit_tuple(self, schema: cs.TupleSchema, recurse: _Recurse) -> cs.CoreSchema:
|
|
153
|
+
if schema.get("variadic_item_index") is not None:
|
|
154
|
+
msg = "CModel does not support variadic tuples"
|
|
155
|
+
raise ValueError(msg)
|
|
156
|
+
|
|
157
|
+
size = len(schema["items_schema"])
|
|
158
|
+
use_buffer = (_USE_BUFFER,) * size
|
|
159
|
+
|
|
160
|
+
def before_validator(value: Any) -> Any:
|
|
161
|
+
if value is _USE_BUFFER:
|
|
162
|
+
return use_buffer
|
|
163
|
+
else:
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
return cs.no_info_before_validator_function(
|
|
167
|
+
before_validator,
|
|
168
|
+
schema=recurse(schema, self.visit),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def visit_model(self, schema: cs.ModelSchema, recurse: _Recurse) -> cs.CoreSchema:
|
|
172
|
+
return recurse(schema, self.visit)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _Context(TypedDict):
|
|
176
|
+
io: BytesIO
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@overload
|
|
180
|
+
def _get_context(
|
|
181
|
+
info: ValidationInfo | SerializationInfo, *, required: Literal[True]
|
|
182
|
+
) -> _Context: ...
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@overload
|
|
186
|
+
def _get_context(
|
|
187
|
+
info: ValidationInfo | SerializationInfo, *, required: bool = ...
|
|
188
|
+
) -> _Context | None: ...
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _get_context(
|
|
192
|
+
info: ValidationInfo | SerializationInfo, *, required: bool = False
|
|
193
|
+
) -> _Context | None:
|
|
194
|
+
ctx = info.context.get(_CONTEXT_KEY) if isinstance(info.context, dict) else None
|
|
195
|
+
if ctx is None and required:
|
|
196
|
+
msg = "Context is required for CModel packing/unpacking"
|
|
197
|
+
raise ValueError(msg)
|
|
198
|
+
return ctx
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _get_metadata(schema: cs.CoreSchema) -> CFmt:
|
|
202
|
+
return schema.get("metadata", {}).get(_METADATA_KEY, {})
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_CONTEXT_KEY = "cmodel"
|
|
206
|
+
_METADATA_KEY = "cmodel"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PEP-561
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Annotated as An
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from cmodel.base import CFmt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _make_one_or_many[T](_: type[T], fmt: str) -> Callable[[int], CFmt[T]]:
|
|
10
|
+
return lambda count: (
|
|
11
|
+
CFmt[T](fmt, operator.itemgetter(0), lambda x: (x,))
|
|
12
|
+
if count == 1
|
|
13
|
+
else CFmt[T](fmt=f"{count}{fmt}") # pyright: ignore[reportArgumentType]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
c_signed_char = _make_one_or_many(int, "b")
|
|
18
|
+
"""CFormat for one or more signed chars. For count=1 the value is returned as an int."""
|
|
19
|
+
SignedChar = An[int, c_signed_char(1)]
|
|
20
|
+
"""C format for a single signed char."""
|
|
21
|
+
|
|
22
|
+
c_unsigned_char = _make_one_or_many(int, "B")
|
|
23
|
+
"""CFormat for one or more unsigned chars. For count=1 the value is returned as an int."""
|
|
24
|
+
UnsignedChar = An[int, c_unsigned_char(1)]
|
|
25
|
+
"""C format for a single unsigned char."""
|
|
26
|
+
|
|
27
|
+
c_bool = _make_one_or_many(bool, "?")
|
|
28
|
+
"""CFormat for one or more bools. For count=1 the value is returned as a bool."""
|
|
29
|
+
Bool = An[bool, c_bool(1)]
|
|
30
|
+
"""C format for a single bool."""
|
|
31
|
+
|
|
32
|
+
c_short = _make_one_or_many(int, "h")
|
|
33
|
+
"""CFormat for one or more shorts. For count=1 the value is returned as an int."""
|
|
34
|
+
Short = An[int, c_short(1)]
|
|
35
|
+
"""C format for a single short."""
|
|
36
|
+
|
|
37
|
+
c_unsigned_short = _make_one_or_many(int, "H")
|
|
38
|
+
"""CFormat for one or more unsigned shorts. For count=1 the value is returned as an int."""
|
|
39
|
+
UnsignedShort = An[int, c_unsigned_short(1)]
|
|
40
|
+
"""C format for a single unsigned short."""
|
|
41
|
+
|
|
42
|
+
c_int = _make_one_or_many(int, "i")
|
|
43
|
+
"""CFormat for one or more ints. For count=1 the value is returned as an int."""
|
|
44
|
+
Int = An[int, c_int(1)]
|
|
45
|
+
"""C format for a single int."""
|
|
46
|
+
|
|
47
|
+
c_unsigned_int = _make_one_or_many(int, "I")
|
|
48
|
+
"""CFormat for one or more unsigned ints. For count=1 the value is returned as an int."""
|
|
49
|
+
UnsignedInt = An[int, c_unsigned_int(1)]
|
|
50
|
+
"""C format for a single unsigned int."""
|
|
51
|
+
|
|
52
|
+
c_long = _make_one_or_many(int, "l")
|
|
53
|
+
"""CFormat for one or more longs. For count=1 the value is returned as an int."""
|
|
54
|
+
Long = An[int, c_long(1)]
|
|
55
|
+
"""C format for a single long."""
|
|
56
|
+
|
|
57
|
+
c_unsigned_long = _make_one_or_many(int, "L")
|
|
58
|
+
"""CFormat for one or more unsigned longs. For count=1 the value is returned as an int."""
|
|
59
|
+
UnsignedLong = An[int, c_unsigned_long(1)]
|
|
60
|
+
"""C format for a single unsigned long."""
|
|
61
|
+
|
|
62
|
+
c_float = _make_one_or_many(float, "f")
|
|
63
|
+
"""CFormat for one or more floats. For count=1 the value is returned as a float."""
|
|
64
|
+
Float = An[float, c_float(1)]
|
|
65
|
+
"""C format for a single float."""
|
|
66
|
+
|
|
67
|
+
c_double = _make_one_or_many(float, "d")
|
|
68
|
+
"""CFormat for one or more doubles. For count=1 the value is returned as a float."""
|
|
69
|
+
Double = An[float, c_double(1)]
|
|
70
|
+
"""C format for a single double."""
|
|
71
|
+
|
|
72
|
+
c_complex_float = _make_one_or_many(complex, "F")
|
|
73
|
+
"""CFormat for one or more complex floats. For count=1 the value is returned as a complex."""
|
|
74
|
+
ComplexFloat = An[complex, c_complex_float(1)]
|
|
75
|
+
"""C format for a single complex float."""
|
|
76
|
+
|
|
77
|
+
c_complex_double = _make_one_or_many(complex, "D")
|
|
78
|
+
"""CFormat for one or more complex doubles. For count=1 the value is returned as a complex."""
|
|
79
|
+
ComplexDouble = An[complex, c_complex_double(1)]
|
|
80
|
+
"""C format for a single complex double."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def c_uuid() -> CFmt[UUID]:
|
|
84
|
+
"""CFormat for a UUID, represented as a 16-byte array."""
|
|
85
|
+
return CFmt(fmt="16s", validate=lambda x: UUID(bytes=x[0]), dump=lambda x: (x.bytes,))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
Uuid = An[UUID, c_uuid()]
|
|
89
|
+
"""C format for a single UUID."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def c_char(count: int) -> CFmt:
|
|
93
|
+
"""CFormat for a char array of the given count."""
|
|
94
|
+
return CFmt(fmt=f"{count}s", validate=operator.itemgetter(0), dump=lambda x: (x,))
|