python lsp
This commit is contained in:
+4
-1
@@ -2,4 +2,7 @@ node_modules
|
||||
out
|
||||
dist
|
||||
*.vsix
|
||||
target
|
||||
target
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
Vendored
+49
-12
@@ -11,10 +11,7 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}out/client/**/*.js",
|
||||
"${workspaceRoot}out/server/**/*.js"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/client/**/*.js"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"preLaunchTask": {
|
||||
"type": "npm",
|
||||
@@ -22,16 +19,56 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Extension Tests",
|
||||
"name": "Python Attach",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}",
|
||||
"justMyCode": false,
|
||||
"presentation": {
|
||||
"hidden": false,
|
||||
"group": "",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug Extension (hidden)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionTestsPath=${workspaceFolder}/out/test"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
|
||||
"preLaunchTask": "npm: watch"
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/client/**/*.js"],
|
||||
"env": {
|
||||
"USE_DEBUGPY": "True"
|
||||
},
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
"group": "",
|
||||
"order": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Python debug server (hidden)",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"listen": { "host": "localhost", "port": 5678 },
|
||||
"justMyCode": true,
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
"group": "",
|
||||
"order": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Debug Extension and Python",
|
||||
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
|
||||
"stopAll": true,
|
||||
"preLaunchTask": "npm: watch",
|
||||
"presentation": {
|
||||
"hidden": false,
|
||||
"group": "",
|
||||
"order": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ruff.configuration": {
|
||||
"lint": {
|
||||
"extend-ignore": ["F821", "E402"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const folderName = path.basename(__dirname)
|
||||
export const EXTENSION_ROOT_DIR =
|
||||
folderName === "common"
|
||||
? path.dirname(path.dirname(path.dirname(__dirname)))
|
||||
: path.dirname(path.dirname(__dirname))
|
||||
: path.dirname(__dirname)
|
||||
export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, "server")
|
||||
export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, "src", `lsp_server.py`)
|
||||
export const DEBUG_SERVER_SCRIPT_PATH = path.join(
|
||||
|
||||
+3
-3
@@ -5,15 +5,15 @@ const watch = process.argv.includes("--watch")
|
||||
|
||||
async function main() {
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: ["client/src/extension.ts", "server/src/server.ts"],
|
||||
entryPoints: ["client/src/extension.ts"],
|
||||
bundle: true,
|
||||
format: "cjs",
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
outdir: "out",
|
||||
// outfile: "client/out/extension.js",
|
||||
// outdir: "out",
|
||||
outfile: "./dist/extension.js",
|
||||
external: ["vscode"],
|
||||
logLevel: "silent",
|
||||
plugins: [
|
||||
|
||||
+5
-1
@@ -4,11 +4,15 @@
|
||||
"description": "",
|
||||
"version": "0.3.0",
|
||||
"publisher": "Christoph",
|
||||
"serverInfo": {
|
||||
"name": "NX Postprocessor Support",
|
||||
"module": "nx-post-support"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.cbsk-tech.de/Christoph/nx_post_support.git"
|
||||
},
|
||||
"main": "./out/client/src/extension.js",
|
||||
"main": "./dist/extension.js",
|
||||
"keywords": [
|
||||
"cdl",
|
||||
"NX CDL",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.11
|
||||
@@ -0,0 +1,104 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Classes Without Boilerplate
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
from typing import Callable, Literal, Protocol
|
||||
|
||||
from . import converters, exceptions, filters, setters, validators
|
||||
from ._cmp import cmp_using
|
||||
from ._config import get_run_validators, set_run_validators
|
||||
from ._funcs import asdict, assoc, astuple, has, resolve_types
|
||||
from ._make import (
|
||||
NOTHING,
|
||||
Attribute,
|
||||
Converter,
|
||||
Factory,
|
||||
_Nothing,
|
||||
attrib,
|
||||
attrs,
|
||||
evolve,
|
||||
fields,
|
||||
fields_dict,
|
||||
make_class,
|
||||
validate,
|
||||
)
|
||||
from ._next_gen import define, field, frozen, mutable
|
||||
from ._version_info import VersionInfo
|
||||
|
||||
|
||||
s = attributes = attrs
|
||||
ib = attr = attrib
|
||||
dataclass = partial(attrs, auto_attribs=True) # happy Easter ;)
|
||||
|
||||
|
||||
class AttrsInstance(Protocol):
|
||||
pass
|
||||
|
||||
|
||||
NothingType = Literal[_Nothing.NOTHING]
|
||||
|
||||
__all__ = [
|
||||
"NOTHING",
|
||||
"Attribute",
|
||||
"AttrsInstance",
|
||||
"Converter",
|
||||
"Factory",
|
||||
"NothingType",
|
||||
"asdict",
|
||||
"assoc",
|
||||
"astuple",
|
||||
"attr",
|
||||
"attrib",
|
||||
"attributes",
|
||||
"attrs",
|
||||
"cmp_using",
|
||||
"converters",
|
||||
"define",
|
||||
"evolve",
|
||||
"exceptions",
|
||||
"field",
|
||||
"fields",
|
||||
"fields_dict",
|
||||
"filters",
|
||||
"frozen",
|
||||
"get_run_validators",
|
||||
"has",
|
||||
"ib",
|
||||
"make_class",
|
||||
"mutable",
|
||||
"resolve_types",
|
||||
"s",
|
||||
"set_run_validators",
|
||||
"setters",
|
||||
"validate",
|
||||
"validators",
|
||||
]
|
||||
|
||||
|
||||
def _make_getattr(mod_name: str) -> Callable:
|
||||
"""
|
||||
Create a metadata proxy for packaging information that uses *mod_name* in
|
||||
its warnings and errors.
|
||||
"""
|
||||
|
||||
def __getattr__(name: str) -> str:
|
||||
if name not in ("__version__", "__version_info__"):
|
||||
msg = f"module {mod_name} has no attribute {name}"
|
||||
raise AttributeError(msg)
|
||||
|
||||
from importlib.metadata import metadata
|
||||
|
||||
meta = metadata("attrs")
|
||||
|
||||
if name == "__version_info__":
|
||||
return VersionInfo._from_version_string(meta["version"])
|
||||
|
||||
return meta["version"]
|
||||
|
||||
return __getattr__
|
||||
|
||||
|
||||
__getattr__ = _make_getattr(__name__)
|
||||
@@ -0,0 +1,389 @@
|
||||
import enum
|
||||
import sys
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Literal,
|
||||
Mapping,
|
||||
Protocol,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
|
||||
# `import X as X` is required to make these public
|
||||
from . import converters as converters
|
||||
from . import exceptions as exceptions
|
||||
from . import filters as filters
|
||||
from . import setters as setters
|
||||
from . import validators as validators
|
||||
from ._cmp import cmp_using as cmp_using
|
||||
from ._typing_compat import AttrsInstance_
|
||||
from ._version_info import VersionInfo
|
||||
from attrs import (
|
||||
define as define,
|
||||
field as field,
|
||||
mutable as mutable,
|
||||
frozen as frozen,
|
||||
_EqOrderType,
|
||||
_ValidatorType,
|
||||
_ConverterType,
|
||||
_ReprArgType,
|
||||
_OnSetAttrType,
|
||||
_OnSetAttrArgType,
|
||||
_FieldTransformer,
|
||||
_ValidatorArgType,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeGuard, TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeGuard, TypeAlias
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import dataclass_transform
|
||||
else:
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
__version__: str
|
||||
__version_info__: VersionInfo
|
||||
__title__: str
|
||||
__description__: str
|
||||
__url__: str
|
||||
__uri__: str
|
||||
__author__: str
|
||||
__email__: str
|
||||
__license__: str
|
||||
__copyright__: str
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_C = TypeVar("_C", bound=type)
|
||||
|
||||
_FilterType = Callable[["Attribute[_T]", _T], bool]
|
||||
|
||||
# We subclass this here to keep the protocol's qualified name clean.
|
||||
class AttrsInstance(AttrsInstance_, Protocol):
|
||||
pass
|
||||
|
||||
_A = TypeVar("_A", bound=type[AttrsInstance])
|
||||
|
||||
class _Nothing(enum.Enum):
|
||||
NOTHING = enum.auto()
|
||||
|
||||
NOTHING = _Nothing.NOTHING
|
||||
NothingType: TypeAlias = Literal[_Nothing.NOTHING]
|
||||
|
||||
# NOTE: Factory lies about its return type to make this possible:
|
||||
# `x: List[int] # = Factory(list)`
|
||||
# Work around mypy issue #4554 in the common case by using an overload.
|
||||
|
||||
@overload
|
||||
def Factory(factory: Callable[[], _T]) -> _T: ...
|
||||
@overload
|
||||
def Factory(
|
||||
factory: Callable[[Any], _T],
|
||||
takes_self: Literal[True],
|
||||
) -> _T: ...
|
||||
@overload
|
||||
def Factory(
|
||||
factory: Callable[[], _T],
|
||||
takes_self: Literal[False],
|
||||
) -> _T: ...
|
||||
|
||||
In = TypeVar("In")
|
||||
Out = TypeVar("Out")
|
||||
|
||||
class Converter(Generic[In, Out]):
|
||||
@overload
|
||||
def __init__(self, converter: Callable[[In], Out]) -> None: ...
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
converter: Callable[[In, AttrsInstance, Attribute], Out],
|
||||
*,
|
||||
takes_self: Literal[True],
|
||||
takes_field: Literal[True],
|
||||
) -> None: ...
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
converter: Callable[[In, Attribute], Out],
|
||||
*,
|
||||
takes_field: Literal[True],
|
||||
) -> None: ...
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
converter: Callable[[In, AttrsInstance], Out],
|
||||
*,
|
||||
takes_self: Literal[True],
|
||||
) -> None: ...
|
||||
|
||||
class Attribute(Generic[_T]):
|
||||
name: str
|
||||
default: _T | None
|
||||
validator: _ValidatorType[_T] | None
|
||||
repr: _ReprArgType
|
||||
cmp: _EqOrderType
|
||||
eq: _EqOrderType
|
||||
order: _EqOrderType
|
||||
hash: bool | None
|
||||
init: bool
|
||||
converter: Converter | None
|
||||
metadata: dict[Any, Any]
|
||||
type: type[_T] | None
|
||||
kw_only: bool
|
||||
on_setattr: _OnSetAttrType
|
||||
alias: str | None
|
||||
|
||||
def evolve(self, **changes: Any) -> "Attribute[Any]": ...
|
||||
|
||||
# NOTE: We had several choices for the annotation to use for type arg:
|
||||
# 1) Type[_T]
|
||||
# - Pros: Handles simple cases correctly
|
||||
# - Cons: Might produce less informative errors in the case of conflicting
|
||||
# TypeVars e.g. `attr.ib(default='bad', type=int)`
|
||||
# 2) Callable[..., _T]
|
||||
# - Pros: Better error messages than #1 for conflicting TypeVars
|
||||
# - Cons: Terrible error messages for validator checks.
|
||||
# e.g. attr.ib(type=int, validator=validate_str)
|
||||
# -> error: Cannot infer function type argument
|
||||
# 3) type (and do all of the work in the mypy plugin)
|
||||
# - Pros: Simple here, and we could customize the plugin with our own errors.
|
||||
# - Cons: Would need to write mypy plugin code to handle all the cases.
|
||||
# We chose option #1.
|
||||
|
||||
# `attr` lies about its return type to make the following possible:
|
||||
# attr() -> Any
|
||||
# attr(8) -> int
|
||||
# attr(validator=<some callable>) -> Whatever the callable expects.
|
||||
# This makes this type of assignments possible:
|
||||
# x: int = attr(8)
|
||||
#
|
||||
# This form catches explicit None or no default but with no other arguments
|
||||
# returns Any.
|
||||
@overload
|
||||
def attrib(
|
||||
default: None = ...,
|
||||
validator: None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: _EqOrderType | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
type: None = ...,
|
||||
converter: None = ...,
|
||||
factory: None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
) -> Any: ...
|
||||
|
||||
# This form catches an explicit None or no default and infers the type from the
|
||||
# other arguments.
|
||||
@overload
|
||||
def attrib(
|
||||
default: None = ...,
|
||||
validator: _ValidatorArgType[_T] | None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: _EqOrderType | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
type: type[_T] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
) -> _T: ...
|
||||
|
||||
# This form catches an explicit default argument.
|
||||
@overload
|
||||
def attrib(
|
||||
default: _T,
|
||||
validator: _ValidatorArgType[_T] | None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: _EqOrderType | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
type: type[_T] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
) -> _T: ...
|
||||
|
||||
# This form covers type=non-Type: e.g. forward references (str), Any
|
||||
@overload
|
||||
def attrib(
|
||||
default: _T | None = ...,
|
||||
validator: _ValidatorArgType[_T] | None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: _EqOrderType | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
type: object = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
) -> Any: ...
|
||||
@overload
|
||||
@dataclass_transform(order_default=True, field_specifiers=(attrib, field))
|
||||
def attrs(
|
||||
maybe_cls: _C,
|
||||
these: dict[str, Any] | None = ...,
|
||||
repr_ns: str | None = ...,
|
||||
repr: bool = ...,
|
||||
cmp: _EqOrderType | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
frozen: bool = ...,
|
||||
weakref_slot: bool = ...,
|
||||
str: bool = ...,
|
||||
auto_attribs: bool = ...,
|
||||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
auto_detect: bool = ...,
|
||||
collect_by_mro: bool = ...,
|
||||
getstate_setstate: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
match_args: bool = ...,
|
||||
unsafe_hash: bool | None = ...,
|
||||
) -> _C: ...
|
||||
@overload
|
||||
@dataclass_transform(order_default=True, field_specifiers=(attrib, field))
|
||||
def attrs(
|
||||
maybe_cls: None = ...,
|
||||
these: dict[str, Any] | None = ...,
|
||||
repr_ns: str | None = ...,
|
||||
repr: bool = ...,
|
||||
cmp: _EqOrderType | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
frozen: bool = ...,
|
||||
weakref_slot: bool = ...,
|
||||
str: bool = ...,
|
||||
auto_attribs: bool = ...,
|
||||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
auto_detect: bool = ...,
|
||||
collect_by_mro: bool = ...,
|
||||
getstate_setstate: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
match_args: bool = ...,
|
||||
unsafe_hash: bool | None = ...,
|
||||
) -> Callable[[_C], _C]: ...
|
||||
def fields(cls: type[AttrsInstance]) -> Any: ...
|
||||
def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
|
||||
def validate(inst: AttrsInstance) -> None: ...
|
||||
def resolve_types(
|
||||
cls: _A,
|
||||
globalns: dict[str, Any] | None = ...,
|
||||
localns: dict[str, Any] | None = ...,
|
||||
attribs: list[Attribute[Any]] | None = ...,
|
||||
include_extras: bool = ...,
|
||||
) -> _A: ...
|
||||
|
||||
# TODO: add support for returning a proper attrs class from the mypy plugin
|
||||
# we use Any instead of _CountingAttr so that e.g. `make_class('Foo',
|
||||
# [attr.ib()])` is valid
|
||||
def make_class(
|
||||
name: str,
|
||||
attrs: list[str] | tuple[str, ...] | dict[str, Any],
|
||||
bases: tuple[type, ...] = ...,
|
||||
class_body: dict[str, Any] | None = ...,
|
||||
repr_ns: str | None = ...,
|
||||
repr: bool = ...,
|
||||
cmp: _EqOrderType | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
frozen: bool = ...,
|
||||
weakref_slot: bool = ...,
|
||||
str: bool = ...,
|
||||
auto_attribs: bool = ...,
|
||||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
collect_by_mro: bool = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
) -> type: ...
|
||||
|
||||
# _funcs --
|
||||
|
||||
# TODO: add support for returning TypedDict from the mypy plugin
|
||||
# FIXME: asdict/astuple do not honor their factory args. Waiting on one of
|
||||
# these:
|
||||
# https://github.com/python/mypy/issues/4236
|
||||
# https://github.com/python/typing/issues/253
|
||||
# XXX: remember to fix attrs.asdict/astuple too!
|
||||
def asdict(
|
||||
inst: AttrsInstance,
|
||||
recurse: bool = ...,
|
||||
filter: _FilterType[Any] | None = ...,
|
||||
dict_factory: type[Mapping[Any, Any]] = ...,
|
||||
retain_collection_types: bool = ...,
|
||||
value_serializer: Callable[[type, Attribute[Any], Any], Any] | None = ...,
|
||||
tuple_keys: bool | None = ...,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
# TODO: add support for returning NamedTuple from the mypy plugin
|
||||
def astuple(
|
||||
inst: AttrsInstance,
|
||||
recurse: bool = ...,
|
||||
filter: _FilterType[Any] | None = ...,
|
||||
tuple_factory: type[Sequence[Any]] = ...,
|
||||
retain_collection_types: bool = ...,
|
||||
) -> tuple[Any, ...]: ...
|
||||
def has(cls: type) -> TypeGuard[type[AttrsInstance]]: ...
|
||||
def assoc(inst: _T, **changes: Any) -> _T: ...
|
||||
def evolve(inst: _T, **changes: Any) -> _T: ...
|
||||
|
||||
# _config --
|
||||
|
||||
def set_run_validators(run: bool) -> None: ...
|
||||
def get_run_validators() -> bool: ...
|
||||
|
||||
# aliases --
|
||||
|
||||
s = attributes = attrs
|
||||
ib = attr = attrib
|
||||
dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;)
|
||||
@@ -0,0 +1,160 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
import functools
|
||||
import types
|
||||
|
||||
from ._make import __ne__
|
||||
|
||||
|
||||
_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
|
||||
|
||||
|
||||
def cmp_using(
|
||||
eq=None,
|
||||
lt=None,
|
||||
le=None,
|
||||
gt=None,
|
||||
ge=None,
|
||||
require_same_type=True,
|
||||
class_name="Comparable",
|
||||
):
|
||||
"""
|
||||
Create a class that can be passed into `attrs.field`'s ``eq``, ``order``,
|
||||
and ``cmp`` arguments to customize field comparison.
|
||||
|
||||
The resulting class will have a full set of ordering methods if at least
|
||||
one of ``{lt, le, gt, ge}`` and ``eq`` are provided.
|
||||
|
||||
Args:
|
||||
eq (typing.Callable | None):
|
||||
Callable used to evaluate equality of two objects.
|
||||
|
||||
lt (typing.Callable | None):
|
||||
Callable used to evaluate whether one object is less than another
|
||||
object.
|
||||
|
||||
le (typing.Callable | None):
|
||||
Callable used to evaluate whether one object is less than or equal
|
||||
to another object.
|
||||
|
||||
gt (typing.Callable | None):
|
||||
Callable used to evaluate whether one object is greater than
|
||||
another object.
|
||||
|
||||
ge (typing.Callable | None):
|
||||
Callable used to evaluate whether one object is greater than or
|
||||
equal to another object.
|
||||
|
||||
require_same_type (bool):
|
||||
When `True`, equality and ordering methods will return
|
||||
`NotImplemented` if objects are not of the same type.
|
||||
|
||||
class_name (str | None): Name of class. Defaults to "Comparable".
|
||||
|
||||
See `comparison` for more details.
|
||||
|
||||
.. versionadded:: 21.1.0
|
||||
"""
|
||||
|
||||
body = {
|
||||
"__slots__": ["value"],
|
||||
"__init__": _make_init(),
|
||||
"_requirements": [],
|
||||
"_is_comparable_to": _is_comparable_to,
|
||||
}
|
||||
|
||||
# Add operations.
|
||||
num_order_functions = 0
|
||||
has_eq_function = False
|
||||
|
||||
if eq is not None:
|
||||
has_eq_function = True
|
||||
body["__eq__"] = _make_operator("eq", eq)
|
||||
body["__ne__"] = __ne__
|
||||
|
||||
if lt is not None:
|
||||
num_order_functions += 1
|
||||
body["__lt__"] = _make_operator("lt", lt)
|
||||
|
||||
if le is not None:
|
||||
num_order_functions += 1
|
||||
body["__le__"] = _make_operator("le", le)
|
||||
|
||||
if gt is not None:
|
||||
num_order_functions += 1
|
||||
body["__gt__"] = _make_operator("gt", gt)
|
||||
|
||||
if ge is not None:
|
||||
num_order_functions += 1
|
||||
body["__ge__"] = _make_operator("ge", ge)
|
||||
|
||||
type_ = types.new_class(
|
||||
class_name, (object,), {}, lambda ns: ns.update(body)
|
||||
)
|
||||
|
||||
# Add same type requirement.
|
||||
if require_same_type:
|
||||
type_._requirements.append(_check_same_type)
|
||||
|
||||
# Add total ordering if at least one operation was defined.
|
||||
if 0 < num_order_functions < 4:
|
||||
if not has_eq_function:
|
||||
# functools.total_ordering requires __eq__ to be defined,
|
||||
# so raise early error here to keep a nice stack.
|
||||
msg = "eq must be define is order to complete ordering from lt, le, gt, ge."
|
||||
raise ValueError(msg)
|
||||
type_ = functools.total_ordering(type_)
|
||||
|
||||
return type_
|
||||
|
||||
|
||||
def _make_init():
|
||||
"""
|
||||
Create __init__ method.
|
||||
"""
|
||||
|
||||
def __init__(self, value):
|
||||
"""
|
||||
Initialize object with *value*.
|
||||
"""
|
||||
self.value = value
|
||||
|
||||
return __init__
|
||||
|
||||
|
||||
def _make_operator(name, func):
|
||||
"""
|
||||
Create operator method.
|
||||
"""
|
||||
|
||||
def method(self, other):
|
||||
if not self._is_comparable_to(other):
|
||||
return NotImplemented
|
||||
|
||||
result = func(self.value, other.value)
|
||||
if result is NotImplemented:
|
||||
return NotImplemented
|
||||
|
||||
return result
|
||||
|
||||
method.__name__ = f"__{name}__"
|
||||
method.__doc__ = (
|
||||
f"Return a {_operation_names[name]} b. Computed by attrs."
|
||||
)
|
||||
|
||||
return method
|
||||
|
||||
|
||||
def _is_comparable_to(self, other):
|
||||
"""
|
||||
Check whether `other` is comparable to `self`.
|
||||
"""
|
||||
return all(func(self, other) for func in self._requirements)
|
||||
|
||||
|
||||
def _check_same_type(self, other):
|
||||
"""
|
||||
Return True if *self* and *other* are of the same type, False otherwise.
|
||||
"""
|
||||
return other.value.__class__ is self.value.__class__
|
||||
@@ -0,0 +1,13 @@
|
||||
from typing import Any, Callable
|
||||
|
||||
_CompareWithType = Callable[[Any, Any], bool]
|
||||
|
||||
def cmp_using(
|
||||
eq: _CompareWithType | None = ...,
|
||||
lt: _CompareWithType | None = ...,
|
||||
le: _CompareWithType | None = ...,
|
||||
gt: _CompareWithType | None = ...,
|
||||
ge: _CompareWithType | None = ...,
|
||||
require_same_type: bool = ...,
|
||||
class_name: str = ...,
|
||||
) -> type: ...
|
||||
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import inspect
|
||||
import platform
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from collections.abc import Mapping, Sequence # noqa: F401
|
||||
from typing import _GenericAlias
|
||||
|
||||
|
||||
PYPY = platform.python_implementation() == "PyPy"
|
||||
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
|
||||
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
|
||||
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
|
||||
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
|
||||
PY_3_13_PLUS = sys.version_info[:2] >= (3, 13)
|
||||
PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
|
||||
|
||||
|
||||
if PY_3_14_PLUS: # pragma: no cover
|
||||
import annotationlib
|
||||
|
||||
_get_annotations = annotationlib.get_annotations
|
||||
|
||||
else:
|
||||
|
||||
def _get_annotations(cls):
|
||||
"""
|
||||
Get annotations for *cls*.
|
||||
"""
|
||||
return cls.__dict__.get("__annotations__", {})
|
||||
|
||||
|
||||
class _AnnotationExtractor:
|
||||
"""
|
||||
Extract type annotations from a callable, returning None whenever there
|
||||
is none.
|
||||
"""
|
||||
|
||||
__slots__ = ["sig"]
|
||||
|
||||
def __init__(self, callable):
|
||||
try:
|
||||
self.sig = inspect.signature(callable)
|
||||
except (ValueError, TypeError): # inspect failed
|
||||
self.sig = None
|
||||
|
||||
def get_first_param_type(self):
|
||||
"""
|
||||
Return the type annotation of the first argument if it's not empty.
|
||||
"""
|
||||
if not self.sig:
|
||||
return None
|
||||
|
||||
params = list(self.sig.parameters.values())
|
||||
if params and params[0].annotation is not inspect.Parameter.empty:
|
||||
return params[0].annotation
|
||||
|
||||
return None
|
||||
|
||||
def get_return_type(self):
|
||||
"""
|
||||
Return the return type if it's not empty.
|
||||
"""
|
||||
if (
|
||||
self.sig
|
||||
and self.sig.return_annotation is not inspect.Signature.empty
|
||||
):
|
||||
return self.sig.return_annotation
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Thread-local global to track attrs instances which are already being repr'd.
|
||||
# This is needed because there is no other (thread-safe) way to pass info
|
||||
# about the instances that are already being repr'd through the call stack
|
||||
# in order to ensure we don't perform infinite recursion.
|
||||
#
|
||||
# For instance, if an instance contains a dict which contains that instance,
|
||||
# we need to know that we're already repr'ing the outside instance from within
|
||||
# the dict's repr() call.
|
||||
#
|
||||
# This lives here rather than in _make.py so that the functions in _make.py
|
||||
# don't have a direct reference to the thread-local in their globals dict.
|
||||
# If they have such a reference, it breaks cloudpickle.
|
||||
repr_context = threading.local()
|
||||
|
||||
|
||||
def get_generic_base(cl):
|
||||
"""If this is a generic class (A[str]), return the generic base for it."""
|
||||
if cl.__class__ is _GenericAlias:
|
||||
return cl.__origin__
|
||||
return None
|
||||
@@ -0,0 +1,31 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
__all__ = ["get_run_validators", "set_run_validators"]
|
||||
|
||||
_run_validators = True
|
||||
|
||||
|
||||
def set_run_validators(run):
|
||||
"""
|
||||
Set whether or not validators are run. By default, they are run.
|
||||
|
||||
.. deprecated:: 21.3.0 It will not be removed, but it also will not be
|
||||
moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()`
|
||||
instead.
|
||||
"""
|
||||
if not isinstance(run, bool):
|
||||
msg = "'run' must be bool."
|
||||
raise TypeError(msg)
|
||||
global _run_validators
|
||||
_run_validators = run
|
||||
|
||||
|
||||
def get_run_validators():
|
||||
"""
|
||||
Return whether or not validators are run.
|
||||
|
||||
.. deprecated:: 21.3.0 It will not be removed, but it also will not be
|
||||
moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()`
|
||||
instead.
|
||||
"""
|
||||
return _run_validators
|
||||
@@ -0,0 +1,468 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
from ._compat import PY_3_9_PLUS, get_generic_base
|
||||
from ._make import _OBJ_SETATTR, NOTHING, fields
|
||||
from .exceptions import AttrsAttributeNotFoundError
|
||||
|
||||
|
||||
def asdict(
|
||||
inst,
|
||||
recurse=True,
|
||||
filter=None,
|
||||
dict_factory=dict,
|
||||
retain_collection_types=False,
|
||||
value_serializer=None,
|
||||
):
|
||||
"""
|
||||
Return the *attrs* attribute values of *inst* as a dict.
|
||||
|
||||
Optionally recurse into other *attrs*-decorated classes.
|
||||
|
||||
Args:
|
||||
inst: Instance of an *attrs*-decorated class.
|
||||
|
||||
recurse (bool): Recurse into classes that are also *attrs*-decorated.
|
||||
|
||||
filter (~typing.Callable):
|
||||
A callable whose return code determines whether an attribute or
|
||||
element is included (`True`) or dropped (`False`). Is called with
|
||||
the `attrs.Attribute` as the first argument and the value as the
|
||||
second argument.
|
||||
|
||||
dict_factory (~typing.Callable):
|
||||
A callable to produce dictionaries from. For example, to produce
|
||||
ordered dictionaries instead of normal Python dictionaries, pass in
|
||||
``collections.OrderedDict``.
|
||||
|
||||
retain_collection_types (bool):
|
||||
Do not convert to `list` when encountering an attribute whose type
|
||||
is `tuple` or `set`. Only meaningful if *recurse* is `True`.
|
||||
|
||||
value_serializer (typing.Callable | None):
|
||||
A hook that is called for every attribute or dict key/value. It
|
||||
receives the current instance, field and value and must return the
|
||||
(updated) value. The hook is run *after* the optional *filter* has
|
||||
been applied.
|
||||
|
||||
Returns:
|
||||
Return type of *dict_factory*.
|
||||
|
||||
Raises:
|
||||
attrs.exceptions.NotAnAttrsClassError:
|
||||
If *cls* is not an *attrs* class.
|
||||
|
||||
.. versionadded:: 16.0.0 *dict_factory*
|
||||
.. versionadded:: 16.1.0 *retain_collection_types*
|
||||
.. versionadded:: 20.3.0 *value_serializer*
|
||||
.. versionadded:: 21.3.0
|
||||
If a dict has a collection for a key, it is serialized as a tuple.
|
||||
"""
|
||||
attrs = fields(inst.__class__)
|
||||
rv = dict_factory()
|
||||
for a in attrs:
|
||||
v = getattr(inst, a.name)
|
||||
if filter is not None and not filter(a, v):
|
||||
continue
|
||||
|
||||
if value_serializer is not None:
|
||||
v = value_serializer(inst, a, v)
|
||||
|
||||
if recurse is True:
|
||||
if has(v.__class__):
|
||||
rv[a.name] = asdict(
|
||||
v,
|
||||
recurse=True,
|
||||
filter=filter,
|
||||
dict_factory=dict_factory,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
)
|
||||
elif isinstance(v, (tuple, list, set, frozenset)):
|
||||
cf = v.__class__ if retain_collection_types is True else list
|
||||
items = [
|
||||
_asdict_anything(
|
||||
i,
|
||||
is_key=False,
|
||||
filter=filter,
|
||||
dict_factory=dict_factory,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
)
|
||||
for i in v
|
||||
]
|
||||
try:
|
||||
rv[a.name] = cf(items)
|
||||
except TypeError:
|
||||
if not issubclass(cf, tuple):
|
||||
raise
|
||||
# Workaround for TypeError: cf.__new__() missing 1 required
|
||||
# positional argument (which appears, for a namedturle)
|
||||
rv[a.name] = cf(*items)
|
||||
elif isinstance(v, dict):
|
||||
df = dict_factory
|
||||
rv[a.name] = df(
|
||||
(
|
||||
_asdict_anything(
|
||||
kk,
|
||||
is_key=True,
|
||||
filter=filter,
|
||||
dict_factory=df,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
),
|
||||
_asdict_anything(
|
||||
vv,
|
||||
is_key=False,
|
||||
filter=filter,
|
||||
dict_factory=df,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
),
|
||||
)
|
||||
for kk, vv in v.items()
|
||||
)
|
||||
else:
|
||||
rv[a.name] = v
|
||||
else:
|
||||
rv[a.name] = v
|
||||
return rv
|
||||
|
||||
|
||||
def _asdict_anything(
|
||||
val,
|
||||
is_key,
|
||||
filter,
|
||||
dict_factory,
|
||||
retain_collection_types,
|
||||
value_serializer,
|
||||
):
|
||||
"""
|
||||
``asdict`` only works on attrs instances, this works on anything.
|
||||
"""
|
||||
if getattr(val.__class__, "__attrs_attrs__", None) is not None:
|
||||
# Attrs class.
|
||||
rv = asdict(
|
||||
val,
|
||||
recurse=True,
|
||||
filter=filter,
|
||||
dict_factory=dict_factory,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
)
|
||||
elif isinstance(val, (tuple, list, set, frozenset)):
|
||||
if retain_collection_types is True:
|
||||
cf = val.__class__
|
||||
elif is_key:
|
||||
cf = tuple
|
||||
else:
|
||||
cf = list
|
||||
|
||||
rv = cf(
|
||||
[
|
||||
_asdict_anything(
|
||||
i,
|
||||
is_key=False,
|
||||
filter=filter,
|
||||
dict_factory=dict_factory,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
)
|
||||
for i in val
|
||||
]
|
||||
)
|
||||
elif isinstance(val, dict):
|
||||
df = dict_factory
|
||||
rv = df(
|
||||
(
|
||||
_asdict_anything(
|
||||
kk,
|
||||
is_key=True,
|
||||
filter=filter,
|
||||
dict_factory=df,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
),
|
||||
_asdict_anything(
|
||||
vv,
|
||||
is_key=False,
|
||||
filter=filter,
|
||||
dict_factory=df,
|
||||
retain_collection_types=retain_collection_types,
|
||||
value_serializer=value_serializer,
|
||||
),
|
||||
)
|
||||
for kk, vv in val.items()
|
||||
)
|
||||
else:
|
||||
rv = val
|
||||
if value_serializer is not None:
|
||||
rv = value_serializer(None, None, rv)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def astuple(
|
||||
inst,
|
||||
recurse=True,
|
||||
filter=None,
|
||||
tuple_factory=tuple,
|
||||
retain_collection_types=False,
|
||||
):
|
||||
"""
|
||||
Return the *attrs* attribute values of *inst* as a tuple.
|
||||
|
||||
Optionally recurse into other *attrs*-decorated classes.
|
||||
|
||||
Args:
|
||||
inst: Instance of an *attrs*-decorated class.
|
||||
|
||||
recurse (bool):
|
||||
Recurse into classes that are also *attrs*-decorated.
|
||||
|
||||
filter (~typing.Callable):
|
||||
A callable whose return code determines whether an attribute or
|
||||
element is included (`True`) or dropped (`False`). Is called with
|
||||
the `attrs.Attribute` as the first argument and the value as the
|
||||
second argument.
|
||||
|
||||
tuple_factory (~typing.Callable):
|
||||
A callable to produce tuples from. For example, to produce lists
|
||||
instead of tuples.
|
||||
|
||||
retain_collection_types (bool):
|
||||
Do not convert to `list` or `dict` when encountering an attribute
|
||||
which type is `tuple`, `dict` or `set`. Only meaningful if
|
||||
*recurse* is `True`.
|
||||
|
||||
Returns:
|
||||
Return type of *tuple_factory*
|
||||
|
||||
Raises:
|
||||
attrs.exceptions.NotAnAttrsClassError:
|
||||
If *cls* is not an *attrs* class.
|
||||
|
||||
.. versionadded:: 16.2.0
|
||||
"""
|
||||
attrs = fields(inst.__class__)
|
||||
rv = []
|
||||
retain = retain_collection_types # Very long. :/
|
||||
for a in attrs:
|
||||
v = getattr(inst, a.name)
|
||||
if filter is not None and not filter(a, v):
|
||||
continue
|
||||
if recurse is True:
|
||||
if has(v.__class__):
|
||||
rv.append(
|
||||
astuple(
|
||||
v,
|
||||
recurse=True,
|
||||
filter=filter,
|
||||
tuple_factory=tuple_factory,
|
||||
retain_collection_types=retain,
|
||||
)
|
||||
)
|
||||
elif isinstance(v, (tuple, list, set, frozenset)):
|
||||
cf = v.__class__ if retain is True else list
|
||||
items = [
|
||||
(
|
||||
astuple(
|
||||
j,
|
||||
recurse=True,
|
||||
filter=filter,
|
||||
tuple_factory=tuple_factory,
|
||||
retain_collection_types=retain,
|
||||
)
|
||||
if has(j.__class__)
|
||||
else j
|
||||
)
|
||||
for j in v
|
||||
]
|
||||
try:
|
||||
rv.append(cf(items))
|
||||
except TypeError:
|
||||
if not issubclass(cf, tuple):
|
||||
raise
|
||||
# Workaround for TypeError: cf.__new__() missing 1 required
|
||||
# positional argument (which appears, for a namedturle)
|
||||
rv.append(cf(*items))
|
||||
elif isinstance(v, dict):
|
||||
df = v.__class__ if retain is True else dict
|
||||
rv.append(
|
||||
df(
|
||||
(
|
||||
(
|
||||
astuple(
|
||||
kk,
|
||||
tuple_factory=tuple_factory,
|
||||
retain_collection_types=retain,
|
||||
)
|
||||
if has(kk.__class__)
|
||||
else kk
|
||||
),
|
||||
(
|
||||
astuple(
|
||||
vv,
|
||||
tuple_factory=tuple_factory,
|
||||
retain_collection_types=retain,
|
||||
)
|
||||
if has(vv.__class__)
|
||||
else vv
|
||||
),
|
||||
)
|
||||
for kk, vv in v.items()
|
||||
)
|
||||
)
|
||||
else:
|
||||
rv.append(v)
|
||||
else:
|
||||
rv.append(v)
|
||||
|
||||
return rv if tuple_factory is list else tuple_factory(rv)
|
||||
|
||||
|
||||
def has(cls):
|
||||
"""
|
||||
Check whether *cls* is a class with *attrs* attributes.
|
||||
|
||||
Args:
|
||||
cls (type): Class to introspect.
|
||||
|
||||
Raises:
|
||||
TypeError: If *cls* is not a class.
|
||||
|
||||
Returns:
|
||||
bool:
|
||||
"""
|
||||
attrs = getattr(cls, "__attrs_attrs__", None)
|
||||
if attrs is not None:
|
||||
return True
|
||||
|
||||
# No attrs, maybe it's a specialized generic (A[str])?
|
||||
generic_base = get_generic_base(cls)
|
||||
if generic_base is not None:
|
||||
generic_attrs = getattr(generic_base, "__attrs_attrs__", None)
|
||||
if generic_attrs is not None:
|
||||
# Stick it on here for speed next time.
|
||||
cls.__attrs_attrs__ = generic_attrs
|
||||
return generic_attrs is not None
|
||||
return False
|
||||
|
||||
|
||||
def assoc(inst, **changes):
|
||||
"""
|
||||
Copy *inst* and apply *changes*.
|
||||
|
||||
This is different from `evolve` that applies the changes to the arguments
|
||||
that create the new instance.
|
||||
|
||||
`evolve`'s behavior is preferable, but there are `edge cases`_ where it
|
||||
doesn't work. Therefore `assoc` is deprecated, but will not be removed.
|
||||
|
||||
.. _`edge cases`: https://github.com/python-attrs/attrs/issues/251
|
||||
|
||||
Args:
|
||||
inst: Instance of a class with *attrs* attributes.
|
||||
|
||||
changes: Keyword changes in the new copy.
|
||||
|
||||
Returns:
|
||||
A copy of inst with *changes* incorporated.
|
||||
|
||||
Raises:
|
||||
attrs.exceptions.AttrsAttributeNotFoundError:
|
||||
If *attr_name* couldn't be found on *cls*.
|
||||
|
||||
attrs.exceptions.NotAnAttrsClassError:
|
||||
If *cls* is not an *attrs* class.
|
||||
|
||||
.. deprecated:: 17.1.0
|
||||
Use `attrs.evolve` instead if you can. This function will not be
|
||||
removed du to the slightly different approach compared to
|
||||
`attrs.evolve`, though.
|
||||
"""
|
||||
new = copy.copy(inst)
|
||||
attrs = fields(inst.__class__)
|
||||
for k, v in changes.items():
|
||||
a = getattr(attrs, k, NOTHING)
|
||||
if a is NOTHING:
|
||||
msg = f"{k} is not an attrs attribute on {new.__class__}."
|
||||
raise AttrsAttributeNotFoundError(msg)
|
||||
_OBJ_SETATTR(new, k, v)
|
||||
return new
|
||||
|
||||
|
||||
def resolve_types(
|
||||
cls, globalns=None, localns=None, attribs=None, include_extras=True
|
||||
):
|
||||
"""
|
||||
Resolve any strings and forward annotations in type annotations.
|
||||
|
||||
This is only required if you need concrete types in :class:`Attribute`'s
|
||||
*type* field. In other words, you don't need to resolve your types if you
|
||||
only use them for static type checking.
|
||||
|
||||
With no arguments, names will be looked up in the module in which the class
|
||||
was created. If this is not what you want, for example, if the name only
|
||||
exists inside a method, you may pass *globalns* or *localns* to specify
|
||||
other dictionaries in which to look up these names. See the docs of
|
||||
`typing.get_type_hints` for more details.
|
||||
|
||||
Args:
|
||||
cls (type): Class to resolve.
|
||||
|
||||
globalns (dict | None): Dictionary containing global variables.
|
||||
|
||||
localns (dict | None): Dictionary containing local variables.
|
||||
|
||||
attribs (list | None):
|
||||
List of attribs for the given class. This is necessary when calling
|
||||
from inside a ``field_transformer`` since *cls* is not an *attrs*
|
||||
class yet.
|
||||
|
||||
include_extras (bool):
|
||||
Resolve more accurately, if possible. Pass ``include_extras`` to
|
||||
``typing.get_hints``, if supported by the typing module. On
|
||||
supported Python versions (3.9+), this resolves the types more
|
||||
accurately.
|
||||
|
||||
Raises:
|
||||
TypeError: If *cls* is not a class.
|
||||
|
||||
attrs.exceptions.NotAnAttrsClassError:
|
||||
If *cls* is not an *attrs* class and you didn't pass any attribs.
|
||||
|
||||
NameError: If types cannot be resolved because of missing variables.
|
||||
|
||||
Returns:
|
||||
*cls* so you can use this function also as a class decorator. Please
|
||||
note that you have to apply it **after** `attrs.define`. That means the
|
||||
decorator has to come in the line **before** `attrs.define`.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
.. versionadded:: 21.1.0 *attribs*
|
||||
.. versionadded:: 23.1.0 *include_extras*
|
||||
"""
|
||||
# Since calling get_type_hints is expensive we cache whether we've
|
||||
# done it already.
|
||||
if getattr(cls, "__attrs_types_resolved__", None) != cls:
|
||||
import typing
|
||||
|
||||
kwargs = {"globalns": globalns, "localns": localns}
|
||||
|
||||
if PY_3_9_PLUS:
|
||||
kwargs["include_extras"] = include_extras
|
||||
|
||||
hints = typing.get_type_hints(cls, **kwargs)
|
||||
for field in fields(cls) if attribs is None else attribs:
|
||||
if field.name in hints:
|
||||
# Since fields have been frozen we must work around it.
|
||||
_OBJ_SETATTR(field, "type", hints[field.name])
|
||||
# We store the class we resolved so that subclasses know they haven't
|
||||
# been resolved.
|
||||
cls.__attrs_types_resolved__ = cls
|
||||
|
||||
# Return the class so you can use it as a decorator too.
|
||||
return cls
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,623 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
These are keyword-only APIs that call `attr.s` and `attr.ib` with different
|
||||
default values.
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
|
||||
from . import setters
|
||||
from ._funcs import asdict as _asdict
|
||||
from ._funcs import astuple as _astuple
|
||||
from ._make import (
|
||||
_DEFAULT_ON_SETATTR,
|
||||
NOTHING,
|
||||
_frozen_setattrs,
|
||||
attrib,
|
||||
attrs,
|
||||
)
|
||||
from .exceptions import UnannotatedAttributeError
|
||||
|
||||
|
||||
def define(
|
||||
maybe_cls=None,
|
||||
*,
|
||||
these=None,
|
||||
repr=None,
|
||||
unsafe_hash=None,
|
||||
hash=None,
|
||||
init=None,
|
||||
slots=True,
|
||||
frozen=False,
|
||||
weakref_slot=True,
|
||||
str=False,
|
||||
auto_attribs=None,
|
||||
kw_only=False,
|
||||
cache_hash=False,
|
||||
auto_exc=True,
|
||||
eq=None,
|
||||
order=False,
|
||||
auto_detect=True,
|
||||
getstate_setstate=None,
|
||||
on_setattr=None,
|
||||
field_transformer=None,
|
||||
match_args=True,
|
||||
):
|
||||
r"""
|
||||
A class decorator that adds :term:`dunder methods` according to
|
||||
:term:`fields <field>` specified using :doc:`type annotations <types>`,
|
||||
`field()` calls, or the *these* argument.
|
||||
|
||||
Since *attrs* patches or replaces an existing class, you cannot use
|
||||
`object.__init_subclass__` with *attrs* classes, because it runs too early.
|
||||
As a replacement, you can define ``__attrs_init_subclass__`` on your class.
|
||||
It will be called by *attrs* classes that subclass it after they're
|
||||
created. See also :ref:`init-subclass`.
|
||||
|
||||
Args:
|
||||
slots (bool):
|
||||
Create a :term:`slotted class <slotted classes>` that's more
|
||||
memory-efficient. Slotted classes are generally superior to the
|
||||
default dict classes, but have some gotchas you should know about,
|
||||
so we encourage you to read the :term:`glossary entry <slotted
|
||||
classes>`.
|
||||
|
||||
auto_detect (bool):
|
||||
Instead of setting the *init*, *repr*, *eq*, and *hash* arguments
|
||||
explicitly, assume they are set to True **unless any** of the
|
||||
involved methods for one of the arguments is implemented in the
|
||||
*current* class (meaning, it is *not* inherited from some base
|
||||
class).
|
||||
|
||||
So, for example by implementing ``__eq__`` on a class yourself,
|
||||
*attrs* will deduce ``eq=False`` and will create *neither*
|
||||
``__eq__`` *nor* ``__ne__`` (but Python classes come with a
|
||||
sensible ``__ne__`` by default, so it *should* be enough to only
|
||||
implement ``__eq__`` in most cases).
|
||||
|
||||
Passing True or False` to *init*, *repr*, *eq*, or *hash*
|
||||
overrides whatever *auto_detect* would determine.
|
||||
|
||||
auto_exc (bool):
|
||||
If the class subclasses `BaseException` (which implicitly includes
|
||||
any subclass of any exception), the following happens to behave
|
||||
like a well-behaved Python exception class:
|
||||
|
||||
- the values for *eq*, *order*, and *hash* are ignored and the
|
||||
instances compare and hash by the instance's ids [#]_ ,
|
||||
- all attributes that are either passed into ``__init__`` or have a
|
||||
default value are additionally available as a tuple in the
|
||||
``args`` attribute,
|
||||
- the value of *str* is ignored leaving ``__str__`` to base
|
||||
classes.
|
||||
|
||||
.. [#]
|
||||
Note that *attrs* will *not* remove existing implementations of
|
||||
``__hash__`` or the equality methods. It just won't add own
|
||||
ones.
|
||||
|
||||
on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]):
|
||||
A callable that is run whenever the user attempts to set an
|
||||
attribute (either by assignment like ``i.x = 42`` or by using
|
||||
`setattr` like ``setattr(i, "x", 42)``). It receives the same
|
||||
arguments as validators: the instance, the attribute that is being
|
||||
modified, and the new value.
|
||||
|
||||
If no exception is raised, the attribute is set to the return value
|
||||
of the callable.
|
||||
|
||||
If a list of callables is passed, they're automatically wrapped in
|
||||
an `attrs.setters.pipe`.
|
||||
|
||||
If left None, the default behavior is to run converters and
|
||||
validators whenever an attribute is set.
|
||||
|
||||
init (bool):
|
||||
Create a ``__init__`` method that initializes the *attrs*
|
||||
attributes. Leading underscores are stripped for the argument name,
|
||||
unless an alias is set on the attribute.
|
||||
|
||||
.. seealso::
|
||||
`init` shows advanced ways to customize the generated
|
||||
``__init__`` method, including executing code before and after.
|
||||
|
||||
repr(bool):
|
||||
Create a ``__repr__`` method with a human readable representation
|
||||
of *attrs* attributes.
|
||||
|
||||
str (bool):
|
||||
Create a ``__str__`` method that is identical to ``__repr__``. This
|
||||
is usually not necessary except for `Exception`\ s.
|
||||
|
||||
eq (bool | None):
|
||||
If True or None (default), add ``__eq__`` and ``__ne__`` methods
|
||||
that check two instances for equality.
|
||||
|
||||
.. seealso::
|
||||
`comparison` describes how to customize the comparison behavior
|
||||
going as far comparing NumPy arrays.
|
||||
|
||||
order (bool | None):
|
||||
If True, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__``
|
||||
methods that behave like *eq* above and allow instances to be
|
||||
ordered.
|
||||
|
||||
They compare the instances as if they were tuples of their *attrs*
|
||||
attributes if and only if the types of both classes are
|
||||
*identical*.
|
||||
|
||||
If `None` mirror value of *eq*.
|
||||
|
||||
.. seealso:: `comparison`
|
||||
|
||||
unsafe_hash (bool | None):
|
||||
If None (default), the ``__hash__`` method is generated according
|
||||
how *eq* and *frozen* are set.
|
||||
|
||||
1. If *both* are True, *attrs* will generate a ``__hash__`` for
|
||||
you.
|
||||
2. If *eq* is True and *frozen* is False, ``__hash__`` will be set
|
||||
to None, marking it unhashable (which it is).
|
||||
3. If *eq* is False, ``__hash__`` will be left untouched meaning
|
||||
the ``__hash__`` method of the base class will be used. If the
|
||||
base class is `object`, this means it will fall back to id-based
|
||||
hashing.
|
||||
|
||||
Although not recommended, you can decide for yourself and force
|
||||
*attrs* to create one (for example, if the class is immutable even
|
||||
though you didn't freeze it programmatically) by passing True or
|
||||
not. Both of these cases are rather special and should be used
|
||||
carefully.
|
||||
|
||||
.. seealso::
|
||||
|
||||
- Our documentation on `hashing`,
|
||||
- Python's documentation on `object.__hash__`,
|
||||
- and the `GitHub issue that led to the default \ behavior
|
||||
<https://github.com/python-attrs/attrs/issues/136>`_ for more
|
||||
details.
|
||||
|
||||
hash (bool | None):
|
||||
Deprecated alias for *unsafe_hash*. *unsafe_hash* takes precedence.
|
||||
|
||||
cache_hash (bool):
|
||||
Ensure that the object's hash code is computed only once and stored
|
||||
on the object. If this is set to True, hashing must be either
|
||||
explicitly or implicitly enabled for this class. If the hash code
|
||||
is cached, avoid any reassignments of fields involved in hash code
|
||||
computation or mutations of the objects those fields point to after
|
||||
object creation. If such changes occur, the behavior of the
|
||||
object's hash code is undefined.
|
||||
|
||||
frozen (bool):
|
||||
Make instances immutable after initialization. If someone attempts
|
||||
to modify a frozen instance, `attrs.exceptions.FrozenInstanceError`
|
||||
is raised.
|
||||
|
||||
.. note::
|
||||
|
||||
1. This is achieved by installing a custom ``__setattr__``
|
||||
method on your class, so you can't implement your own.
|
||||
|
||||
2. True immutability is impossible in Python.
|
||||
|
||||
3. This *does* have a minor a runtime performance `impact
|
||||
<how-frozen>` when initializing new instances. In other
|
||||
words: ``__init__`` is slightly slower with ``frozen=True``.
|
||||
|
||||
4. If a class is frozen, you cannot modify ``self`` in
|
||||
``__attrs_post_init__`` or a self-written ``__init__``. You
|
||||
can circumvent that limitation by using
|
||||
``object.__setattr__(self, "attribute_name", value)``.
|
||||
|
||||
5. Subclasses of a frozen class are frozen too.
|
||||
|
||||
kw_only (bool):
|
||||
Make all attributes keyword-only in the generated ``__init__`` (if
|
||||
*init* is False, this parameter is ignored).
|
||||
|
||||
weakref_slot (bool):
|
||||
Make instances weak-referenceable. This has no effect unless
|
||||
*slots* is True.
|
||||
|
||||
field_transformer (~typing.Callable | None):
|
||||
A function that is called with the original class object and all
|
||||
fields right before *attrs* finalizes the class. You can use this,
|
||||
for example, to automatically add converters or validators to
|
||||
fields based on their types.
|
||||
|
||||
.. seealso:: `transform-fields`
|
||||
|
||||
match_args (bool):
|
||||
If True (default), set ``__match_args__`` on the class to support
|
||||
:pep:`634` (*Structural Pattern Matching*). It is a tuple of all
|
||||
non-keyword-only ``__init__`` parameter names on Python 3.10 and
|
||||
later. Ignored on older Python versions.
|
||||
|
||||
collect_by_mro (bool):
|
||||
If True, *attrs* collects attributes from base classes correctly
|
||||
according to the `method resolution order
|
||||
<https://docs.python.org/3/howto/mro.html>`_. If False, *attrs*
|
||||
will mimic the (wrong) behavior of `dataclasses` and :pep:`681`.
|
||||
|
||||
See also `issue #428
|
||||
<https://github.com/python-attrs/attrs/issues/428>`_.
|
||||
|
||||
getstate_setstate (bool | None):
|
||||
.. note::
|
||||
|
||||
This is usually only interesting for slotted classes and you
|
||||
should probably just set *auto_detect* to True.
|
||||
|
||||
If True, ``__getstate__`` and ``__setstate__`` are generated and
|
||||
attached to the class. This is necessary for slotted classes to be
|
||||
pickleable. If left None, it's True by default for slotted classes
|
||||
and False for dict classes.
|
||||
|
||||
If *auto_detect* is True, and *getstate_setstate* is left None, and
|
||||
**either** ``__getstate__`` or ``__setstate__`` is detected
|
||||
directly on the class (meaning: not inherited), it is set to False
|
||||
(this is usually what you want).
|
||||
|
||||
auto_attribs (bool | None):
|
||||
If True, look at type annotations to determine which attributes to
|
||||
use, like `dataclasses`. If False, it will only look for explicit
|
||||
:func:`field` class attributes, like classic *attrs*.
|
||||
|
||||
If left None, it will guess:
|
||||
|
||||
1. If any attributes are annotated and no unannotated
|
||||
`attrs.field`\ s are found, it assumes *auto_attribs=True*.
|
||||
2. Otherwise it assumes *auto_attribs=False* and tries to collect
|
||||
`attrs.field`\ s.
|
||||
|
||||
If *attrs* decides to look at type annotations, **all** fields
|
||||
**must** be annotated. If *attrs* encounters a field that is set to
|
||||
a :func:`field` / `attr.ib` but lacks a type annotation, an
|
||||
`attrs.exceptions.UnannotatedAttributeError` is raised. Use
|
||||
``field_name: typing.Any = field(...)`` if you don't want to set a
|
||||
type.
|
||||
|
||||
.. warning::
|
||||
|
||||
For features that use the attribute name to create decorators
|
||||
(for example, :ref:`validators <validators>`), you still *must*
|
||||
assign :func:`field` / `attr.ib` to them. Otherwise Python will
|
||||
either not find the name or try to use the default value to
|
||||
call, for example, ``validator`` on it.
|
||||
|
||||
Attributes annotated as `typing.ClassVar`, and attributes that are
|
||||
neither annotated nor set to an `field()` are **ignored**.
|
||||
|
||||
these (dict[str, object]):
|
||||
A dictionary of name to the (private) return value of `field()`
|
||||
mappings. This is useful to avoid the definition of your attributes
|
||||
within the class body because you can't (for example, if you want
|
||||
to add ``__repr__`` methods to Django models) or don't want to.
|
||||
|
||||
If *these* is not `None`, *attrs* will *not* search the class body
|
||||
for attributes and will *not* remove any attributes from it.
|
||||
|
||||
The order is deduced from the order of the attributes inside
|
||||
*these*.
|
||||
|
||||
Arguably, this is a rather obscure feature.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.
|
||||
.. versionadded:: 22.2.0
|
||||
*unsafe_hash* as an alias for *hash* (for :pep:`681` compliance).
|
||||
.. versionchanged:: 24.1.0
|
||||
Instances are not compared as tuples of attributes anymore, but using a
|
||||
big ``and`` condition. This is faster and has more correct behavior for
|
||||
uncomparable values like `math.nan`.
|
||||
.. versionadded:: 24.1.0
|
||||
If a class has an *inherited* classmethod called
|
||||
``__attrs_init_subclass__``, it is executed after the class is created.
|
||||
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
|
||||
.. versionadded:: 24.3.0
|
||||
Unless already present, a ``__replace__`` method is automatically
|
||||
created for `copy.replace` (Python 3.13+ only).
|
||||
|
||||
.. note::
|
||||
|
||||
The main differences to the classic `attr.s` are:
|
||||
|
||||
- Automatically detect whether or not *auto_attribs* should be `True`
|
||||
(c.f. *auto_attribs* parameter).
|
||||
- Converters and validators run when attributes are set by default --
|
||||
if *frozen* is `False`.
|
||||
- *slots=True*
|
||||
|
||||
Usually, this has only upsides and few visible effects in everyday
|
||||
programming. But it *can* lead to some surprising behaviors, so
|
||||
please make sure to read :term:`slotted classes`.
|
||||
|
||||
- *auto_exc=True*
|
||||
- *auto_detect=True*
|
||||
- *order=False*
|
||||
- Some options that were only relevant on Python 2 or were kept around
|
||||
for backwards-compatibility have been removed.
|
||||
|
||||
"""
|
||||
|
||||
def do_it(cls, auto_attribs):
|
||||
return attrs(
|
||||
maybe_cls=cls,
|
||||
these=these,
|
||||
repr=repr,
|
||||
hash=hash,
|
||||
unsafe_hash=unsafe_hash,
|
||||
init=init,
|
||||
slots=slots,
|
||||
frozen=frozen,
|
||||
weakref_slot=weakref_slot,
|
||||
str=str,
|
||||
auto_attribs=auto_attribs,
|
||||
kw_only=kw_only,
|
||||
cache_hash=cache_hash,
|
||||
auto_exc=auto_exc,
|
||||
eq=eq,
|
||||
order=order,
|
||||
auto_detect=auto_detect,
|
||||
collect_by_mro=True,
|
||||
getstate_setstate=getstate_setstate,
|
||||
on_setattr=on_setattr,
|
||||
field_transformer=field_transformer,
|
||||
match_args=match_args,
|
||||
)
|
||||
|
||||
def wrap(cls):
|
||||
"""
|
||||
Making this a wrapper ensures this code runs during class creation.
|
||||
|
||||
We also ensure that frozen-ness of classes is inherited.
|
||||
"""
|
||||
nonlocal frozen, on_setattr
|
||||
|
||||
had_on_setattr = on_setattr not in (None, setters.NO_OP)
|
||||
|
||||
# By default, mutable classes convert & validate on setattr.
|
||||
if frozen is False and on_setattr is None:
|
||||
on_setattr = _DEFAULT_ON_SETATTR
|
||||
|
||||
# However, if we subclass a frozen class, we inherit the immutability
|
||||
# and disable on_setattr.
|
||||
for base_cls in cls.__bases__:
|
||||
if base_cls.__setattr__ is _frozen_setattrs:
|
||||
if had_on_setattr:
|
||||
msg = "Frozen classes can't use on_setattr (frozen-ness was inherited)."
|
||||
raise ValueError(msg)
|
||||
|
||||
on_setattr = setters.NO_OP
|
||||
break
|
||||
|
||||
if auto_attribs is not None:
|
||||
return do_it(cls, auto_attribs)
|
||||
|
||||
try:
|
||||
return do_it(cls, True)
|
||||
except UnannotatedAttributeError:
|
||||
return do_it(cls, False)
|
||||
|
||||
# maybe_cls's type depends on the usage of the decorator. It's a class
|
||||
# if it's used as `@attrs` but `None` if used as `@attrs()`.
|
||||
if maybe_cls is None:
|
||||
return wrap
|
||||
|
||||
return wrap(maybe_cls)
|
||||
|
||||
|
||||
mutable = define
|
||||
frozen = partial(define, frozen=True, on_setattr=None)
|
||||
|
||||
|
||||
def field(
|
||||
*,
|
||||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
hash=None,
|
||||
init=True,
|
||||
metadata=None,
|
||||
type=None,
|
||||
converter=None,
|
||||
factory=None,
|
||||
kw_only=False,
|
||||
eq=None,
|
||||
order=None,
|
||||
on_setattr=None,
|
||||
alias=None,
|
||||
):
|
||||
"""
|
||||
Create a new :term:`field` / :term:`attribute` on a class.
|
||||
|
||||
.. warning::
|
||||
|
||||
Does **nothing** unless the class is also decorated with
|
||||
`attrs.define` (or similar)!
|
||||
|
||||
Args:
|
||||
default:
|
||||
A value that is used if an *attrs*-generated ``__init__`` is used
|
||||
and no value is passed while instantiating or the attribute is
|
||||
excluded using ``init=False``.
|
||||
|
||||
If the value is an instance of `attrs.Factory`, its callable will
|
||||
be used to construct a new value (useful for mutable data types
|
||||
like lists or dicts).
|
||||
|
||||
If a default is not set (or set manually to `attrs.NOTHING`), a
|
||||
value *must* be supplied when instantiating; otherwise a
|
||||
`TypeError` will be raised.
|
||||
|
||||
.. seealso:: `defaults`
|
||||
|
||||
factory (~typing.Callable):
|
||||
Syntactic sugar for ``default=attr.Factory(factory)``.
|
||||
|
||||
validator (~typing.Callable | list[~typing.Callable]):
|
||||
Callable that is called by *attrs*-generated ``__init__`` methods
|
||||
after the instance has been initialized. They receive the
|
||||
initialized instance, the :func:`~attrs.Attribute`, and the passed
|
||||
value.
|
||||
|
||||
The return value is *not* inspected so the validator has to throw
|
||||
an exception itself.
|
||||
|
||||
If a `list` is passed, its items are treated as validators and must
|
||||
all pass.
|
||||
|
||||
Validators can be globally disabled and re-enabled using
|
||||
`attrs.validators.get_disabled` / `attrs.validators.set_disabled`.
|
||||
|
||||
The validator can also be set using decorator notation as shown
|
||||
below.
|
||||
|
||||
.. seealso:: :ref:`validators`
|
||||
|
||||
repr (bool | ~typing.Callable):
|
||||
Include this attribute in the generated ``__repr__`` method. If
|
||||
True, include the attribute; if False, omit it. By default, the
|
||||
built-in ``repr()`` function is used. To override how the attribute
|
||||
value is formatted, pass a ``callable`` that takes a single value
|
||||
and returns a string. Note that the resulting string is used as-is,
|
||||
which means it will be used directly *instead* of calling
|
||||
``repr()`` (the default).
|
||||
|
||||
eq (bool | ~typing.Callable):
|
||||
If True (default), include this attribute in the generated
|
||||
``__eq__`` and ``__ne__`` methods that check two instances for
|
||||
equality. To override how the attribute value is compared, pass a
|
||||
callable that takes a single value and returns the value to be
|
||||
compared.
|
||||
|
||||
.. seealso:: `comparison`
|
||||
|
||||
order (bool | ~typing.Callable):
|
||||
If True (default), include this attributes in the generated
|
||||
``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. To
|
||||
override how the attribute value is ordered, pass a callable that
|
||||
takes a single value and returns the value to be ordered.
|
||||
|
||||
.. seealso:: `comparison`
|
||||
|
||||
hash (bool | None):
|
||||
Include this attribute in the generated ``__hash__`` method. If
|
||||
None (default), mirror *eq*'s value. This is the correct behavior
|
||||
according the Python spec. Setting this value to anything else
|
||||
than None is *discouraged*.
|
||||
|
||||
.. seealso:: `hashing`
|
||||
|
||||
init (bool):
|
||||
Include this attribute in the generated ``__init__`` method.
|
||||
|
||||
It is possible to set this to False and set a default value. In
|
||||
that case this attributed is unconditionally initialized with the
|
||||
specified default value or factory.
|
||||
|
||||
.. seealso:: `init`
|
||||
|
||||
converter (typing.Callable | Converter):
|
||||
A callable that is called by *attrs*-generated ``__init__`` methods
|
||||
to convert attribute's value to the desired format.
|
||||
|
||||
If a vanilla callable is passed, it is given the passed-in value as
|
||||
the only positional argument. It is possible to receive additional
|
||||
arguments by wrapping the callable in a `Converter`.
|
||||
|
||||
Either way, the returned value will be used as the new value of the
|
||||
attribute. The value is converted before being passed to the
|
||||
validator, if any.
|
||||
|
||||
.. seealso:: :ref:`converters`
|
||||
|
||||
metadata (dict | None):
|
||||
An arbitrary mapping, to be used by third-party code.
|
||||
|
||||
.. seealso:: `extending-metadata`.
|
||||
|
||||
type (type):
|
||||
The type of the attribute. Nowadays, the preferred method to
|
||||
specify the type is using a variable annotation (see :pep:`526`).
|
||||
This argument is provided for backwards-compatibility and for usage
|
||||
with `make_class`. Regardless of the approach used, the type will
|
||||
be stored on ``Attribute.type``.
|
||||
|
||||
Please note that *attrs* doesn't do anything with this metadata by
|
||||
itself. You can use it as part of your own code or for `static type
|
||||
checking <types>`.
|
||||
|
||||
kw_only (bool):
|
||||
Make this attribute keyword-only in the generated ``__init__`` (if
|
||||
``init`` is False, this parameter is ignored).
|
||||
|
||||
on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]):
|
||||
Allows to overwrite the *on_setattr* setting from `attr.s`. If left
|
||||
None, the *on_setattr* value from `attr.s` is used. Set to
|
||||
`attrs.setters.NO_OP` to run **no** `setattr` hooks for this
|
||||
attribute -- regardless of the setting in `define()`.
|
||||
|
||||
alias (str | None):
|
||||
Override this attribute's parameter name in the generated
|
||||
``__init__`` method. If left None, default to ``name`` stripped
|
||||
of leading underscores. See `private-attributes`.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
.. versionchanged:: 21.1.0
|
||||
*eq*, *order*, and *cmp* also accept a custom callable
|
||||
.. versionadded:: 22.2.0 *alias*
|
||||
.. versionadded:: 23.1.0
|
||||
The *type* parameter has been re-added; mostly for `attrs.make_class`.
|
||||
Please note that type checkers ignore this metadata.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`attr.ib`
|
||||
"""
|
||||
return attrib(
|
||||
default=default,
|
||||
validator=validator,
|
||||
repr=repr,
|
||||
hash=hash,
|
||||
init=init,
|
||||
metadata=metadata,
|
||||
type=type,
|
||||
converter=converter,
|
||||
factory=factory,
|
||||
kw_only=kw_only,
|
||||
eq=eq,
|
||||
order=order,
|
||||
on_setattr=on_setattr,
|
||||
alias=alias,
|
||||
)
|
||||
|
||||
|
||||
def asdict(inst, *, recurse=True, filter=None, value_serializer=None):
|
||||
"""
|
||||
Same as `attr.asdict`, except that collections types are always retained
|
||||
and dict is always used as *dict_factory*.
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return _asdict(
|
||||
inst=inst,
|
||||
recurse=recurse,
|
||||
filter=filter,
|
||||
value_serializer=value_serializer,
|
||||
retain_collection_types=True,
|
||||
)
|
||||
|
||||
|
||||
def astuple(inst, *, recurse=True, filter=None):
|
||||
"""
|
||||
Same as `attr.astuple`, except that collections types are always retained
|
||||
and `tuple` is always used as the *tuple_factory*.
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return _astuple(
|
||||
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
from typing import Any, ClassVar, Protocol
|
||||
|
||||
# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`.
|
||||
MYPY = False
|
||||
|
||||
if MYPY:
|
||||
# A protocol to be able to statically accept an attrs class.
|
||||
class AttrsInstance_(Protocol):
|
||||
__attrs_attrs__: ClassVar[Any]
|
||||
|
||||
else:
|
||||
# For type checkers without plug-in support use an empty protocol that
|
||||
# will (hopefully) be combined into a union.
|
||||
class AttrsInstance_(Protocol):
|
||||
pass
|
||||
@@ -0,0 +1,86 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from functools import total_ordering
|
||||
|
||||
from ._funcs import astuple
|
||||
from ._make import attrib, attrs
|
||||
|
||||
|
||||
@total_ordering
|
||||
@attrs(eq=False, order=False, slots=True, frozen=True)
|
||||
class VersionInfo:
|
||||
"""
|
||||
A version object that can be compared to tuple of length 1--4:
|
||||
|
||||
>>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2)
|
||||
True
|
||||
>>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1)
|
||||
True
|
||||
>>> vi = attr.VersionInfo(19, 2, 0, "final")
|
||||
>>> vi < (19, 1, 1)
|
||||
False
|
||||
>>> vi < (19,)
|
||||
False
|
||||
>>> vi == (19, 2,)
|
||||
True
|
||||
>>> vi == (19, 2, 1)
|
||||
False
|
||||
|
||||
.. versionadded:: 19.2
|
||||
"""
|
||||
|
||||
year = attrib(type=int)
|
||||
minor = attrib(type=int)
|
||||
micro = attrib(type=int)
|
||||
releaselevel = attrib(type=str)
|
||||
|
||||
@classmethod
|
||||
def _from_version_string(cls, s):
|
||||
"""
|
||||
Parse *s* and return a _VersionInfo.
|
||||
"""
|
||||
v = s.split(".")
|
||||
if len(v) == 3:
|
||||
v.append("final")
|
||||
|
||||
return cls(
|
||||
year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3]
|
||||
)
|
||||
|
||||
def _ensure_tuple(self, other):
|
||||
"""
|
||||
Ensure *other* is a tuple of a valid length.
|
||||
|
||||
Returns a possibly transformed *other* and ourselves as a tuple of
|
||||
the same length as *other*.
|
||||
"""
|
||||
|
||||
if self.__class__ is other.__class__:
|
||||
other = astuple(other)
|
||||
|
||||
if not isinstance(other, tuple):
|
||||
raise NotImplementedError
|
||||
|
||||
if not (1 <= len(other) <= 4):
|
||||
raise NotImplementedError
|
||||
|
||||
return astuple(self)[: len(other)], other
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
us, them = self._ensure_tuple(other)
|
||||
except NotImplementedError:
|
||||
return NotImplemented
|
||||
|
||||
return us == them
|
||||
|
||||
def __lt__(self, other):
|
||||
try:
|
||||
us, them = self._ensure_tuple(other)
|
||||
except NotImplementedError:
|
||||
return NotImplemented
|
||||
|
||||
# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
|
||||
# have to do anything special with releaselevel for now.
|
||||
return us < them
|
||||
@@ -0,0 +1,9 @@
|
||||
class VersionInfo:
|
||||
@property
|
||||
def year(self) -> int: ...
|
||||
@property
|
||||
def minor(self) -> int: ...
|
||||
@property
|
||||
def micro(self) -> int: ...
|
||||
@property
|
||||
def releaselevel(self) -> str: ...
|
||||
@@ -0,0 +1,162 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Commonly useful converters.
|
||||
"""
|
||||
|
||||
import typing
|
||||
|
||||
from ._compat import _AnnotationExtractor
|
||||
from ._make import NOTHING, Converter, Factory, pipe
|
||||
|
||||
|
||||
__all__ = [
|
||||
"default_if_none",
|
||||
"optional",
|
||||
"pipe",
|
||||
"to_bool",
|
||||
]
|
||||
|
||||
|
||||
def optional(converter):
|
||||
"""
|
||||
A converter that allows an attribute to be optional. An optional attribute
|
||||
is one which can be set to `None`.
|
||||
|
||||
Type annotations will be inferred from the wrapped converter's, if it has
|
||||
any.
|
||||
|
||||
Args:
|
||||
converter (typing.Callable):
|
||||
the converter that is used for non-`None` values.
|
||||
|
||||
.. versionadded:: 17.1.0
|
||||
"""
|
||||
|
||||
if isinstance(converter, Converter):
|
||||
|
||||
def optional_converter(val, inst, field):
|
||||
if val is None:
|
||||
return None
|
||||
return converter(val, inst, field)
|
||||
|
||||
else:
|
||||
|
||||
def optional_converter(val):
|
||||
if val is None:
|
||||
return None
|
||||
return converter(val)
|
||||
|
||||
xtr = _AnnotationExtractor(converter)
|
||||
|
||||
t = xtr.get_first_param_type()
|
||||
if t:
|
||||
optional_converter.__annotations__["val"] = typing.Optional[t]
|
||||
|
||||
rt = xtr.get_return_type()
|
||||
if rt:
|
||||
optional_converter.__annotations__["return"] = typing.Optional[rt]
|
||||
|
||||
if isinstance(converter, Converter):
|
||||
return Converter(optional_converter, takes_self=True, takes_field=True)
|
||||
|
||||
return optional_converter
|
||||
|
||||
|
||||
def default_if_none(default=NOTHING, factory=None):
|
||||
"""
|
||||
A converter that allows to replace `None` values by *default* or the result
|
||||
of *factory*.
|
||||
|
||||
Args:
|
||||
default:
|
||||
Value to be used if `None` is passed. Passing an instance of
|
||||
`attrs.Factory` is supported, however the ``takes_self`` option is
|
||||
*not*.
|
||||
|
||||
factory (typing.Callable):
|
||||
A callable that takes no parameters whose result is used if `None`
|
||||
is passed.
|
||||
|
||||
Raises:
|
||||
TypeError: If **neither** *default* or *factory* is passed.
|
||||
|
||||
TypeError: If **both** *default* and *factory* are passed.
|
||||
|
||||
ValueError:
|
||||
If an instance of `attrs.Factory` is passed with
|
||||
``takes_self=True``.
|
||||
|
||||
.. versionadded:: 18.2.0
|
||||
"""
|
||||
if default is NOTHING and factory is None:
|
||||
msg = "Must pass either `default` or `factory`."
|
||||
raise TypeError(msg)
|
||||
|
||||
if default is not NOTHING and factory is not None:
|
||||
msg = "Must pass either `default` or `factory` but not both."
|
||||
raise TypeError(msg)
|
||||
|
||||
if factory is not None:
|
||||
default = Factory(factory)
|
||||
|
||||
if isinstance(default, Factory):
|
||||
if default.takes_self:
|
||||
msg = "`takes_self` is not supported by default_if_none."
|
||||
raise ValueError(msg)
|
||||
|
||||
def default_if_none_converter(val):
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
return default.factory()
|
||||
|
||||
else:
|
||||
|
||||
def default_if_none_converter(val):
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
return default
|
||||
|
||||
return default_if_none_converter
|
||||
|
||||
|
||||
def to_bool(val):
|
||||
"""
|
||||
Convert "boolean" strings (for example, from environment variables) to real
|
||||
booleans.
|
||||
|
||||
Values mapping to `True`:
|
||||
|
||||
- ``True``
|
||||
- ``"true"`` / ``"t"``
|
||||
- ``"yes"`` / ``"y"``
|
||||
- ``"on"``
|
||||
- ``"1"``
|
||||
- ``1``
|
||||
|
||||
Values mapping to `False`:
|
||||
|
||||
- ``False``
|
||||
- ``"false"`` / ``"f"``
|
||||
- ``"no"`` / ``"n"``
|
||||
- ``"off"``
|
||||
- ``"0"``
|
||||
- ``0``
|
||||
|
||||
Raises:
|
||||
ValueError: For any other value.
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
if isinstance(val, str):
|
||||
val = val.lower()
|
||||
|
||||
if val in (True, "true", "t", "yes", "y", "on", "1", 1):
|
||||
return True
|
||||
if val in (False, "false", "f", "no", "n", "off", "0", 0):
|
||||
return False
|
||||
|
||||
msg = f"Cannot convert value to bool: {val!r}"
|
||||
raise ValueError(msg)
|
||||
@@ -0,0 +1,19 @@
|
||||
from typing import Callable, Any, overload
|
||||
|
||||
from attrs import _ConverterType, _CallableConverterType
|
||||
|
||||
@overload
|
||||
def pipe(*validators: _CallableConverterType) -> _CallableConverterType: ...
|
||||
@overload
|
||||
def pipe(*validators: _ConverterType) -> _ConverterType: ...
|
||||
@overload
|
||||
def optional(converter: _CallableConverterType) -> _CallableConverterType: ...
|
||||
@overload
|
||||
def optional(converter: _ConverterType) -> _ConverterType: ...
|
||||
@overload
|
||||
def default_if_none(default: Any) -> _CallableConverterType: ...
|
||||
@overload
|
||||
def default_if_none(
|
||||
*, factory: Callable[[], Any]
|
||||
) -> _CallableConverterType: ...
|
||||
def to_bool(val: str | int | bool) -> bool: ...
|
||||
@@ -0,0 +1,95 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
class FrozenError(AttributeError):
|
||||
"""
|
||||
A frozen/immutable instance or attribute have been attempted to be
|
||||
modified.
|
||||
|
||||
It mirrors the behavior of ``namedtuples`` by using the same error message
|
||||
and subclassing `AttributeError`.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
"""
|
||||
|
||||
msg = "can't set attribute"
|
||||
args: ClassVar[tuple[str]] = [msg]
|
||||
|
||||
|
||||
class FrozenInstanceError(FrozenError):
|
||||
"""
|
||||
A frozen instance has been attempted to be modified.
|
||||
|
||||
.. versionadded:: 16.1.0
|
||||
"""
|
||||
|
||||
|
||||
class FrozenAttributeError(FrozenError):
|
||||
"""
|
||||
A frozen attribute has been attempted to be modified.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
"""
|
||||
|
||||
|
||||
class AttrsAttributeNotFoundError(ValueError):
|
||||
"""
|
||||
An *attrs* function couldn't find an attribute that the user asked for.
|
||||
|
||||
.. versionadded:: 16.2.0
|
||||
"""
|
||||
|
||||
|
||||
class NotAnAttrsClassError(ValueError):
|
||||
"""
|
||||
A non-*attrs* class has been passed into an *attrs* function.
|
||||
|
||||
.. versionadded:: 16.2.0
|
||||
"""
|
||||
|
||||
|
||||
class DefaultAlreadySetError(RuntimeError):
|
||||
"""
|
||||
A default has been set when defining the field and is attempted to be reset
|
||||
using the decorator.
|
||||
|
||||
.. versionadded:: 17.1.0
|
||||
"""
|
||||
|
||||
|
||||
class UnannotatedAttributeError(RuntimeError):
|
||||
"""
|
||||
A class with ``auto_attribs=True`` has a field without a type annotation.
|
||||
|
||||
.. versionadded:: 17.3.0
|
||||
"""
|
||||
|
||||
|
||||
class PythonTooOldError(RuntimeError):
|
||||
"""
|
||||
It was attempted to use an *attrs* feature that requires a newer Python
|
||||
version.
|
||||
|
||||
.. versionadded:: 18.2.0
|
||||
"""
|
||||
|
||||
|
||||
class NotCallableError(TypeError):
|
||||
"""
|
||||
A field requiring a callable has been set with a value that is not
|
||||
callable.
|
||||
|
||||
.. versionadded:: 19.2.0
|
||||
"""
|
||||
|
||||
def __init__(self, msg, value):
|
||||
super(TypeError, self).__init__(msg, value)
|
||||
self.msg = msg
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return str(self.msg)
|
||||
@@ -0,0 +1,17 @@
|
||||
from typing import Any
|
||||
|
||||
class FrozenError(AttributeError):
|
||||
msg: str = ...
|
||||
|
||||
class FrozenInstanceError(FrozenError): ...
|
||||
class FrozenAttributeError(FrozenError): ...
|
||||
class AttrsAttributeNotFoundError(ValueError): ...
|
||||
class NotAnAttrsClassError(ValueError): ...
|
||||
class DefaultAlreadySetError(RuntimeError): ...
|
||||
class UnannotatedAttributeError(RuntimeError): ...
|
||||
class PythonTooOldError(RuntimeError): ...
|
||||
|
||||
class NotCallableError(TypeError):
|
||||
msg: str = ...
|
||||
value: Any = ...
|
||||
def __init__(self, msg: str, value: Any) -> None: ...
|
||||
@@ -0,0 +1,72 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Commonly useful filters for `attrs.asdict` and `attrs.astuple`.
|
||||
"""
|
||||
|
||||
from ._make import Attribute
|
||||
|
||||
|
||||
def _split_what(what):
|
||||
"""
|
||||
Returns a tuple of `frozenset`s of classes and attributes.
|
||||
"""
|
||||
return (
|
||||
frozenset(cls for cls in what if isinstance(cls, type)),
|
||||
frozenset(cls for cls in what if isinstance(cls, str)),
|
||||
frozenset(cls for cls in what if isinstance(cls, Attribute)),
|
||||
)
|
||||
|
||||
|
||||
def include(*what):
|
||||
"""
|
||||
Create a filter that only allows *what*.
|
||||
|
||||
Args:
|
||||
what (list[type, str, attrs.Attribute]):
|
||||
What to include. Can be a type, a name, or an attribute.
|
||||
|
||||
Returns:
|
||||
Callable:
|
||||
A callable that can be passed to `attrs.asdict`'s and
|
||||
`attrs.astuple`'s *filter* argument.
|
||||
|
||||
.. versionchanged:: 23.1.0 Accept strings with field names.
|
||||
"""
|
||||
cls, names, attrs = _split_what(what)
|
||||
|
||||
def include_(attribute, value):
|
||||
return (
|
||||
value.__class__ in cls
|
||||
or attribute.name in names
|
||||
or attribute in attrs
|
||||
)
|
||||
|
||||
return include_
|
||||
|
||||
|
||||
def exclude(*what):
|
||||
"""
|
||||
Create a filter that does **not** allow *what*.
|
||||
|
||||
Args:
|
||||
what (list[type, str, attrs.Attribute]):
|
||||
What to exclude. Can be a type, a name, or an attribute.
|
||||
|
||||
Returns:
|
||||
Callable:
|
||||
A callable that can be passed to `attrs.asdict`'s and
|
||||
`attrs.astuple`'s *filter* argument.
|
||||
|
||||
.. versionchanged:: 23.3.0 Accept field name string as input argument
|
||||
"""
|
||||
cls, names, attrs = _split_what(what)
|
||||
|
||||
def exclude_(attribute, value):
|
||||
return not (
|
||||
value.__class__ in cls
|
||||
or attribute.name in names
|
||||
or attribute in attrs
|
||||
)
|
||||
|
||||
return exclude_
|
||||
@@ -0,0 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from . import Attribute, _FilterType
|
||||
|
||||
def include(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ...
|
||||
def exclude(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ...
|
||||
@@ -0,0 +1,79 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Commonly used hooks for on_setattr.
|
||||
"""
|
||||
|
||||
from . import _config
|
||||
from .exceptions import FrozenAttributeError
|
||||
|
||||
|
||||
def pipe(*setters):
|
||||
"""
|
||||
Run all *setters* and return the return value of the last one.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
"""
|
||||
|
||||
def wrapped_pipe(instance, attrib, new_value):
|
||||
rv = new_value
|
||||
|
||||
for setter in setters:
|
||||
rv = setter(instance, attrib, rv)
|
||||
|
||||
return rv
|
||||
|
||||
return wrapped_pipe
|
||||
|
||||
|
||||
def frozen(_, __, ___):
|
||||
"""
|
||||
Prevent an attribute to be modified.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
"""
|
||||
raise FrozenAttributeError
|
||||
|
||||
|
||||
def validate(instance, attrib, new_value):
|
||||
"""
|
||||
Run *attrib*'s validator on *new_value* if it has one.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
"""
|
||||
if _config._run_validators is False:
|
||||
return new_value
|
||||
|
||||
v = attrib.validator
|
||||
if not v:
|
||||
return new_value
|
||||
|
||||
v(instance, attrib, new_value)
|
||||
|
||||
return new_value
|
||||
|
||||
|
||||
def convert(instance, attrib, new_value):
|
||||
"""
|
||||
Run *attrib*'s converter -- if it has one -- on *new_value* and return the
|
||||
result.
|
||||
|
||||
.. versionadded:: 20.1.0
|
||||
"""
|
||||
c = attrib.converter
|
||||
if c:
|
||||
# This can be removed once we drop 3.8 and use attrs.Converter instead.
|
||||
from ._make import Converter
|
||||
|
||||
if not isinstance(c, Converter):
|
||||
return c(new_value)
|
||||
|
||||
return c(new_value, instance, attrib)
|
||||
|
||||
return new_value
|
||||
|
||||
|
||||
# Sentinel for disabling class-wide *on_setattr* hooks for certain attributes.
|
||||
# Sphinx's autodata stopped working, so the docstring is inlined in the API
|
||||
# docs.
|
||||
NO_OP = object()
|
||||
@@ -0,0 +1,20 @@
|
||||
from typing import Any, NewType, NoReturn, TypeVar
|
||||
|
||||
from . import Attribute
|
||||
from attrs import _OnSetAttrType
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
def frozen(
|
||||
instance: Any, attribute: Attribute[Any], new_value: Any
|
||||
) -> NoReturn: ...
|
||||
def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ...
|
||||
def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ...
|
||||
|
||||
# convert is allowed to return Any, because they can be chained using pipe.
|
||||
def convert(
|
||||
instance: Any, attribute: Attribute[Any], new_value: Any
|
||||
) -> Any: ...
|
||||
|
||||
_NoOpType = NewType("_NoOpType", object)
|
||||
NO_OP: _NoOpType
|
||||
@@ -0,0 +1,710 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Commonly useful validators.
|
||||
"""
|
||||
|
||||
import operator
|
||||
import re
|
||||
|
||||
from contextlib import contextmanager
|
||||
from re import Pattern
|
||||
|
||||
from ._config import get_run_validators, set_run_validators
|
||||
from ._make import _AndValidator, and_, attrib, attrs
|
||||
from .converters import default_if_none
|
||||
from .exceptions import NotCallableError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"and_",
|
||||
"deep_iterable",
|
||||
"deep_mapping",
|
||||
"disabled",
|
||||
"ge",
|
||||
"get_disabled",
|
||||
"gt",
|
||||
"in_",
|
||||
"instance_of",
|
||||
"is_callable",
|
||||
"le",
|
||||
"lt",
|
||||
"matches_re",
|
||||
"max_len",
|
||||
"min_len",
|
||||
"not_",
|
||||
"optional",
|
||||
"or_",
|
||||
"set_disabled",
|
||||
]
|
||||
|
||||
|
||||
def set_disabled(disabled):
|
||||
"""
|
||||
Globally disable or enable running validators.
|
||||
|
||||
By default, they are run.
|
||||
|
||||
Args:
|
||||
disabled (bool): If `True`, disable running all validators.
|
||||
|
||||
.. warning::
|
||||
|
||||
This function is not thread-safe!
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
set_run_validators(not disabled)
|
||||
|
||||
|
||||
def get_disabled():
|
||||
"""
|
||||
Return a bool indicating whether validators are currently disabled or not.
|
||||
|
||||
Returns:
|
||||
bool:`True` if validators are currently disabled.
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return not get_run_validators()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def disabled():
|
||||
"""
|
||||
Context manager that disables running validators within its context.
|
||||
|
||||
.. warning::
|
||||
|
||||
This context manager is not thread-safe!
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
set_run_validators(False)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
set_run_validators(True)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _InstanceOfValidator:
|
||||
type = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if not isinstance(value, self.type):
|
||||
msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})."
|
||||
raise TypeError(
|
||||
msg,
|
||||
attr,
|
||||
self.type,
|
||||
value,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<instance_of validator for type {self.type!r}>"
|
||||
|
||||
|
||||
def instance_of(type):
|
||||
"""
|
||||
A validator that raises a `TypeError` if the initializer is called with a
|
||||
wrong type for this particular attribute (checks are performed using
|
||||
`isinstance` therefore it's also valid to pass a tuple of types).
|
||||
|
||||
Args:
|
||||
type (type | tuple[type]): The type to check for.
|
||||
|
||||
Raises:
|
||||
TypeError:
|
||||
With a human readable error message, the attribute (of type
|
||||
`attrs.Attribute`), the expected type, and the value it got.
|
||||
"""
|
||||
return _InstanceOfValidator(type)
|
||||
|
||||
|
||||
@attrs(repr=False, frozen=True, slots=True)
|
||||
class _MatchesReValidator:
|
||||
pattern = attrib()
|
||||
match_func = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if not self.match_func(value):
|
||||
msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)"
|
||||
raise ValueError(
|
||||
msg,
|
||||
attr,
|
||||
self.pattern,
|
||||
value,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<matches_re validator for pattern {self.pattern!r}>"
|
||||
|
||||
|
||||
def matches_re(regex, flags=0, func=None):
|
||||
r"""
|
||||
A validator that raises `ValueError` if the initializer is called with a
|
||||
string that doesn't match *regex*.
|
||||
|
||||
Args:
|
||||
regex (str, re.Pattern):
|
||||
A regex string or precompiled pattern to match against
|
||||
|
||||
flags (int):
|
||||
Flags that will be passed to the underlying re function (default 0)
|
||||
|
||||
func (typing.Callable):
|
||||
Which underlying `re` function to call. Valid options are
|
||||
`re.fullmatch`, `re.search`, and `re.match`; the default `None`
|
||||
means `re.fullmatch`. For performance reasons, the pattern is
|
||||
always precompiled using `re.compile`.
|
||||
|
||||
.. versionadded:: 19.2.0
|
||||
.. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern.
|
||||
"""
|
||||
valid_funcs = (re.fullmatch, None, re.search, re.match)
|
||||
if func not in valid_funcs:
|
||||
msg = "'func' must be one of {}.".format(
|
||||
", ".join(
|
||||
sorted((e and e.__name__) or "None" for e in set(valid_funcs))
|
||||
)
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
if isinstance(regex, Pattern):
|
||||
if flags:
|
||||
msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead"
|
||||
raise TypeError(msg)
|
||||
pattern = regex
|
||||
else:
|
||||
pattern = re.compile(regex, flags)
|
||||
|
||||
if func is re.match:
|
||||
match_func = pattern.match
|
||||
elif func is re.search:
|
||||
match_func = pattern.search
|
||||
else:
|
||||
match_func = pattern.fullmatch
|
||||
|
||||
return _MatchesReValidator(pattern, match_func)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _OptionalValidator:
|
||||
validator = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
if value is None:
|
||||
return
|
||||
|
||||
self.validator(inst, attr, value)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<optional validator for {self.validator!r} or None>"
|
||||
|
||||
|
||||
def optional(validator):
|
||||
"""
|
||||
A validator that makes an attribute optional. An optional attribute is one
|
||||
which can be set to `None` in addition to satisfying the requirements of
|
||||
the sub-validator.
|
||||
|
||||
Args:
|
||||
validator
|
||||
(typing.Callable | tuple[typing.Callable] | list[typing.Callable]):
|
||||
A validator (or validators) that is used for non-`None` values.
|
||||
|
||||
.. versionadded:: 15.1.0
|
||||
.. versionchanged:: 17.1.0 *validator* can be a list of validators.
|
||||
.. versionchanged:: 23.1.0 *validator* can also be a tuple of validators.
|
||||
"""
|
||||
if isinstance(validator, (list, tuple)):
|
||||
return _OptionalValidator(_AndValidator(validator))
|
||||
|
||||
return _OptionalValidator(validator)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _InValidator:
|
||||
options = attrib()
|
||||
_original_options = attrib(hash=False)
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
try:
|
||||
in_options = value in self.options
|
||||
except TypeError: # e.g. `1 in "abc"`
|
||||
in_options = False
|
||||
|
||||
if not in_options:
|
||||
msg = f"'{attr.name}' must be in {self._original_options!r} (got {value!r})"
|
||||
raise ValueError(
|
||||
msg,
|
||||
attr,
|
||||
self._original_options,
|
||||
value,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<in_ validator with options {self._original_options!r}>"
|
||||
|
||||
|
||||
def in_(options):
|
||||
"""
|
||||
A validator that raises a `ValueError` if the initializer is called with a
|
||||
value that does not belong in the *options* provided.
|
||||
|
||||
The check is performed using ``value in options``, so *options* has to
|
||||
support that operation.
|
||||
|
||||
To keep the validator hashable, dicts, lists, and sets are transparently
|
||||
transformed into a `tuple`.
|
||||
|
||||
Args:
|
||||
options: Allowed options.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
With a human readable error message, the attribute (of type
|
||||
`attrs.Attribute`), the expected options, and the value it got.
|
||||
|
||||
.. versionadded:: 17.1.0
|
||||
.. versionchanged:: 22.1.0
|
||||
The ValueError was incomplete until now and only contained the human
|
||||
readable error message. Now it contains all the information that has
|
||||
been promised since 17.1.0.
|
||||
.. versionchanged:: 24.1.0
|
||||
*options* that are a list, dict, or a set are now transformed into a
|
||||
tuple to keep the validator hashable.
|
||||
"""
|
||||
repr_options = options
|
||||
if isinstance(options, (list, dict, set)):
|
||||
options = tuple(options)
|
||||
|
||||
return _InValidator(options, repr_options)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=False, unsafe_hash=True)
|
||||
class _IsCallableValidator:
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if not callable(value):
|
||||
message = (
|
||||
"'{name}' must be callable "
|
||||
"(got {value!r} that is a {actual!r})."
|
||||
)
|
||||
raise NotCallableError(
|
||||
msg=message.format(
|
||||
name=attr.name, value=value, actual=value.__class__
|
||||
),
|
||||
value=value,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<is_callable validator>"
|
||||
|
||||
|
||||
def is_callable():
|
||||
"""
|
||||
A validator that raises a `attrs.exceptions.NotCallableError` if the
|
||||
initializer is called with a value for this particular attribute that is
|
||||
not callable.
|
||||
|
||||
.. versionadded:: 19.1.0
|
||||
|
||||
Raises:
|
||||
attrs.exceptions.NotCallableError:
|
||||
With a human readable error message containing the attribute
|
||||
(`attrs.Attribute`) name, and the value it got.
|
||||
"""
|
||||
return _IsCallableValidator()
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _DeepIterable:
|
||||
member_validator = attrib(validator=is_callable())
|
||||
iterable_validator = attrib(
|
||||
default=None, validator=optional(is_callable())
|
||||
)
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if self.iterable_validator is not None:
|
||||
self.iterable_validator(inst, attr, value)
|
||||
|
||||
for member in value:
|
||||
self.member_validator(inst, attr, member)
|
||||
|
||||
def __repr__(self):
|
||||
iterable_identifier = (
|
||||
""
|
||||
if self.iterable_validator is None
|
||||
else f" {self.iterable_validator!r}"
|
||||
)
|
||||
return (
|
||||
f"<deep_iterable validator for{iterable_identifier}"
|
||||
f" iterables of {self.member_validator!r}>"
|
||||
)
|
||||
|
||||
|
||||
def deep_iterable(member_validator, iterable_validator=None):
|
||||
"""
|
||||
A validator that performs deep validation of an iterable.
|
||||
|
||||
Args:
|
||||
member_validator: Validator to apply to iterable members.
|
||||
|
||||
iterable_validator:
|
||||
Validator to apply to iterable itself (optional).
|
||||
|
||||
Raises
|
||||
TypeError: if any sub-validators fail
|
||||
|
||||
.. versionadded:: 19.1.0
|
||||
"""
|
||||
if isinstance(member_validator, (list, tuple)):
|
||||
member_validator = and_(*member_validator)
|
||||
return _DeepIterable(member_validator, iterable_validator)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _DeepMapping:
|
||||
key_validator = attrib(validator=is_callable())
|
||||
value_validator = attrib(validator=is_callable())
|
||||
mapping_validator = attrib(default=None, validator=optional(is_callable()))
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if self.mapping_validator is not None:
|
||||
self.mapping_validator(inst, attr, value)
|
||||
|
||||
for key in value:
|
||||
self.key_validator(inst, attr, key)
|
||||
self.value_validator(inst, attr, value[key])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
|
||||
|
||||
|
||||
def deep_mapping(key_validator, value_validator, mapping_validator=None):
|
||||
"""
|
||||
A validator that performs deep validation of a dictionary.
|
||||
|
||||
Args:
|
||||
key_validator: Validator to apply to dictionary keys.
|
||||
|
||||
value_validator: Validator to apply to dictionary values.
|
||||
|
||||
mapping_validator:
|
||||
Validator to apply to top-level mapping attribute (optional).
|
||||
|
||||
.. versionadded:: 19.1.0
|
||||
|
||||
Raises:
|
||||
TypeError: if any sub-validators fail
|
||||
"""
|
||||
return _DeepMapping(key_validator, value_validator, mapping_validator)
|
||||
|
||||
|
||||
@attrs(repr=False, frozen=True, slots=True)
|
||||
class _NumberValidator:
|
||||
bound = attrib()
|
||||
compare_op = attrib()
|
||||
compare_func = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if not self.compare_func(value, self.bound):
|
||||
msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}"
|
||||
raise ValueError(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Validator for x {self.compare_op} {self.bound}>"
|
||||
|
||||
|
||||
def lt(val):
|
||||
"""
|
||||
A validator that raises `ValueError` if the initializer is called with a
|
||||
number larger or equal to *val*.
|
||||
|
||||
The validator uses `operator.lt` to compare the values.
|
||||
|
||||
Args:
|
||||
val: Exclusive upper bound for values.
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return _NumberValidator(val, "<", operator.lt)
|
||||
|
||||
|
||||
def le(val):
|
||||
"""
|
||||
A validator that raises `ValueError` if the initializer is called with a
|
||||
number greater than *val*.
|
||||
|
||||
The validator uses `operator.le` to compare the values.
|
||||
|
||||
Args:
|
||||
val: Inclusive upper bound for values.
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return _NumberValidator(val, "<=", operator.le)
|
||||
|
||||
|
||||
def ge(val):
|
||||
"""
|
||||
A validator that raises `ValueError` if the initializer is called with a
|
||||
number smaller than *val*.
|
||||
|
||||
The validator uses `operator.ge` to compare the values.
|
||||
|
||||
Args:
|
||||
val: Inclusive lower bound for values
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return _NumberValidator(val, ">=", operator.ge)
|
||||
|
||||
|
||||
def gt(val):
|
||||
"""
|
||||
A validator that raises `ValueError` if the initializer is called with a
|
||||
number smaller or equal to *val*.
|
||||
|
||||
The validator uses `operator.ge` to compare the values.
|
||||
|
||||
Args:
|
||||
val: Exclusive lower bound for values
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return _NumberValidator(val, ">", operator.gt)
|
||||
|
||||
|
||||
@attrs(repr=False, frozen=True, slots=True)
|
||||
class _MaxLengthValidator:
|
||||
max_length = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if len(value) > self.max_length:
|
||||
msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<max_len validator for {self.max_length}>"
|
||||
|
||||
|
||||
def max_len(length):
|
||||
"""
|
||||
A validator that raises `ValueError` if the initializer is called
|
||||
with a string or iterable that is longer than *length*.
|
||||
|
||||
Args:
|
||||
length (int): Maximum length of the string or iterable
|
||||
|
||||
.. versionadded:: 21.3.0
|
||||
"""
|
||||
return _MaxLengthValidator(length)
|
||||
|
||||
|
||||
@attrs(repr=False, frozen=True, slots=True)
|
||||
class _MinLengthValidator:
|
||||
min_length = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if len(value) < self.min_length:
|
||||
msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<min_len validator for {self.min_length}>"
|
||||
|
||||
|
||||
def min_len(length):
|
||||
"""
|
||||
A validator that raises `ValueError` if the initializer is called
|
||||
with a string or iterable that is shorter than *length*.
|
||||
|
||||
Args:
|
||||
length (int): Minimum length of the string or iterable
|
||||
|
||||
.. versionadded:: 22.1.0
|
||||
"""
|
||||
return _MinLengthValidator(length)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _SubclassOfValidator:
|
||||
type = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
"""
|
||||
We use a callable class to be able to change the ``__repr__``.
|
||||
"""
|
||||
if not issubclass(value, self.type):
|
||||
msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})."
|
||||
raise TypeError(
|
||||
msg,
|
||||
attr,
|
||||
self.type,
|
||||
value,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<subclass_of validator for type {self.type!r}>"
|
||||
|
||||
|
||||
def _subclass_of(type):
|
||||
"""
|
||||
A validator that raises a `TypeError` if the initializer is called with a
|
||||
wrong type for this particular attribute (checks are performed using
|
||||
`issubclass` therefore it's also valid to pass a tuple of types).
|
||||
|
||||
Args:
|
||||
type (type | tuple[type, ...]): The type(s) to check for.
|
||||
|
||||
Raises:
|
||||
TypeError:
|
||||
With a human readable error message, the attribute (of type
|
||||
`attrs.Attribute`), the expected type, and the value it got.
|
||||
"""
|
||||
return _SubclassOfValidator(type)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _NotValidator:
|
||||
validator = attrib()
|
||||
msg = attrib(
|
||||
converter=default_if_none(
|
||||
"not_ validator child '{validator!r}' "
|
||||
"did not raise a captured error"
|
||||
)
|
||||
)
|
||||
exc_types = attrib(
|
||||
validator=deep_iterable(
|
||||
member_validator=_subclass_of(Exception),
|
||||
iterable_validator=instance_of(tuple),
|
||||
),
|
||||
)
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
try:
|
||||
self.validator(inst, attr, value)
|
||||
except self.exc_types:
|
||||
pass # suppress error to invert validity
|
||||
else:
|
||||
raise ValueError(
|
||||
self.msg.format(
|
||||
validator=self.validator,
|
||||
exc_types=self.exc_types,
|
||||
),
|
||||
attr,
|
||||
self.validator,
|
||||
value,
|
||||
self.exc_types,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<not_ validator wrapping {self.validator!r}, capturing {self.exc_types!r}>"
|
||||
|
||||
|
||||
def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)):
|
||||
"""
|
||||
A validator that wraps and logically 'inverts' the validator passed to it.
|
||||
It will raise a `ValueError` if the provided validator *doesn't* raise a
|
||||
`ValueError` or `TypeError` (by default), and will suppress the exception
|
||||
if the provided validator *does*.
|
||||
|
||||
Intended to be used with existing validators to compose logic without
|
||||
needing to create inverted variants, for example, ``not_(in_(...))``.
|
||||
|
||||
Args:
|
||||
validator: A validator to be logically inverted.
|
||||
|
||||
msg (str):
|
||||
Message to raise if validator fails. Formatted with keys
|
||||
``exc_types`` and ``validator``.
|
||||
|
||||
exc_types (tuple[type, ...]):
|
||||
Exception type(s) to capture. Other types raised by child
|
||||
validators will not be intercepted and pass through.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
With a human readable error message, the attribute (of type
|
||||
`attrs.Attribute`), the validator that failed to raise an
|
||||
exception, the value it got, and the expected exception types.
|
||||
|
||||
.. versionadded:: 22.2.0
|
||||
"""
|
||||
try:
|
||||
exc_types = tuple(exc_types)
|
||||
except TypeError:
|
||||
exc_types = (exc_types,)
|
||||
return _NotValidator(validator, msg, exc_types)
|
||||
|
||||
|
||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||
class _OrValidator:
|
||||
validators = attrib()
|
||||
|
||||
def __call__(self, inst, attr, value):
|
||||
for v in self.validators:
|
||||
try:
|
||||
v(inst, attr, value)
|
||||
except Exception: # noqa: BLE001, PERF203, S112
|
||||
continue
|
||||
else:
|
||||
return
|
||||
|
||||
msg = f"None of {self.validators!r} satisfied for value {value!r}"
|
||||
raise ValueError(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<or validator wrapping {self.validators!r}>"
|
||||
|
||||
|
||||
def or_(*validators):
|
||||
"""
|
||||
A validator that composes multiple validators into one.
|
||||
|
||||
When called on a value, it runs all wrapped validators until one of them is
|
||||
satisfied.
|
||||
|
||||
Args:
|
||||
validators (~collections.abc.Iterable[typing.Callable]):
|
||||
Arbitrary number of validators.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
If no validator is satisfied. Raised with a human-readable error
|
||||
message listing all the wrapped validators and the value that
|
||||
failed all of them.
|
||||
|
||||
.. versionadded:: 24.1.0
|
||||
"""
|
||||
vals = []
|
||||
for v in validators:
|
||||
vals.extend(v.validators if isinstance(v, _OrValidator) else [v])
|
||||
|
||||
return _OrValidator(tuple(vals))
|
||||
@@ -0,0 +1,86 @@
|
||||
from types import UnionType
|
||||
from typing import (
|
||||
Any,
|
||||
AnyStr,
|
||||
Callable,
|
||||
Container,
|
||||
ContextManager,
|
||||
Iterable,
|
||||
Mapping,
|
||||
Match,
|
||||
Pattern,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
|
||||
from attrs import _ValidatorType
|
||||
from attrs import _ValidatorArgType
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T1 = TypeVar("_T1")
|
||||
_T2 = TypeVar("_T2")
|
||||
_T3 = TypeVar("_T3")
|
||||
_I = TypeVar("_I", bound=Iterable)
|
||||
_K = TypeVar("_K")
|
||||
_V = TypeVar("_V")
|
||||
_M = TypeVar("_M", bound=Mapping)
|
||||
|
||||
def set_disabled(run: bool) -> None: ...
|
||||
def get_disabled() -> bool: ...
|
||||
def disabled() -> ContextManager[None]: ...
|
||||
|
||||
# To be more precise on instance_of use some overloads.
|
||||
# If there are more than 3 items in the tuple then we fall back to Any
|
||||
@overload
|
||||
def instance_of(type: type[_T]) -> _ValidatorType[_T]: ...
|
||||
@overload
|
||||
def instance_of(type: tuple[type[_T]]) -> _ValidatorType[_T]: ...
|
||||
@overload
|
||||
def instance_of(
|
||||
type: tuple[type[_T1], type[_T2]],
|
||||
) -> _ValidatorType[_T1 | _T2]: ...
|
||||
@overload
|
||||
def instance_of(
|
||||
type: tuple[type[_T1], type[_T2], type[_T3]],
|
||||
) -> _ValidatorType[_T1 | _T2 | _T3]: ...
|
||||
@overload
|
||||
def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ...
|
||||
@overload
|
||||
def instance_of(type: UnionType) -> _ValidatorType[Any]: ...
|
||||
def optional(
|
||||
validator: (
|
||||
_ValidatorType[_T]
|
||||
| list[_ValidatorType[_T]]
|
||||
| tuple[_ValidatorType[_T]]
|
||||
),
|
||||
) -> _ValidatorType[_T | None]: ...
|
||||
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
|
||||
def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
|
||||
def matches_re(
|
||||
regex: Pattern[AnyStr] | AnyStr,
|
||||
flags: int = ...,
|
||||
func: Callable[[AnyStr, AnyStr, int], Match[AnyStr] | None] | None = ...,
|
||||
) -> _ValidatorType[AnyStr]: ...
|
||||
def deep_iterable(
|
||||
member_validator: _ValidatorArgType[_T],
|
||||
iterable_validator: _ValidatorType[_I] | None = ...,
|
||||
) -> _ValidatorType[_I]: ...
|
||||
def deep_mapping(
|
||||
key_validator: _ValidatorType[_K],
|
||||
value_validator: _ValidatorType[_V],
|
||||
mapping_validator: _ValidatorType[_M] | None = ...,
|
||||
) -> _ValidatorType[_M]: ...
|
||||
def is_callable() -> _ValidatorType[_T]: ...
|
||||
def lt(val: _T) -> _ValidatorType[_T]: ...
|
||||
def le(val: _T) -> _ValidatorType[_T]: ...
|
||||
def ge(val: _T) -> _ValidatorType[_T]: ...
|
||||
def gt(val: _T) -> _ValidatorType[_T]: ...
|
||||
def max_len(length: int) -> _ValidatorType[_T]: ...
|
||||
def min_len(length: int) -> _ValidatorType[_T]: ...
|
||||
def not_(
|
||||
validator: _ValidatorType[_T],
|
||||
*,
|
||||
msg: str | None = None,
|
||||
exc_types: type[Exception] | Iterable[type[Exception]] = ...,
|
||||
) -> _ValidatorType[_T]: ...
|
||||
def or_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,232 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: attrs
|
||||
Version: 25.3.0
|
||||
Summary: Classes Without Boilerplate
|
||||
Project-URL: Documentation, https://www.attrs.org/
|
||||
Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html
|
||||
Project-URL: GitHub, https://github.com/python-attrs/attrs
|
||||
Project-URL: Funding, https://github.com/sponsors/hynek
|
||||
Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi
|
||||
Author-email: Hynek Schlawack <hs@ox.cx>
|
||||
License-Expression: MIT
|
||||
License-File: LICENSE
|
||||
Keywords: attribute,boilerplate,class
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.8
|
||||
Provides-Extra: benchmark
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'benchmark'
|
||||
Requires-Dist: hypothesis; extra == 'benchmark'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
|
||||
Requires-Dist: pympler; extra == 'benchmark'
|
||||
Requires-Dist: pytest-codspeed; extra == 'benchmark'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'benchmark'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'benchmark'
|
||||
Provides-Extra: cov
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'cov'
|
||||
Requires-Dist: coverage[toml]>=5.3; extra == 'cov'
|
||||
Requires-Dist: hypothesis; extra == 'cov'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
|
||||
Requires-Dist: pympler; extra == 'cov'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'cov'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'cov'
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'dev'
|
||||
Requires-Dist: hypothesis; extra == 'dev'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
|
||||
Requires-Dist: pre-commit-uv; extra == 'dev'
|
||||
Requires-Dist: pympler; extra == 'dev'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'dev'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'dev'
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: cogapp; extra == 'docs'
|
||||
Requires-Dist: furo; extra == 'docs'
|
||||
Requires-Dist: myst-parser; extra == 'docs'
|
||||
Requires-Dist: sphinx; extra == 'docs'
|
||||
Requires-Dist: sphinx-notfound-page; extra == 'docs'
|
||||
Requires-Dist: sphinxcontrib-towncrier; extra == 'docs'
|
||||
Requires-Dist: towncrier; extra == 'docs'
|
||||
Provides-Extra: tests
|
||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'tests'
|
||||
Requires-Dist: hypothesis; extra == 'tests'
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
|
||||
Requires-Dist: pympler; extra == 'tests'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
|
||||
Requires-Dist: pytest-xdist[psutil]; extra == 'tests'
|
||||
Requires-Dist: pytest>=4.3.0; extra == 'tests'
|
||||
Provides-Extra: tests-mypy
|
||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
|
||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.attrs.org/">
|
||||
<img src="https://raw.githubusercontent.com/python-attrs/attrs/main/docs/_static/attrs_logo.svg" width="35%" alt="attrs" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)).
|
||||
[Trusted by NASA](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/personalizing-your-profile#list-of-qualifying-repositories-for-mars-2020-helicopter-contributor-achievement) for Mars missions since 2020!
|
||||
|
||||
Its main goal is to help you to write **concise** and **correct** software without slowing down your code.
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
*attrs* would not be possible without our [amazing sponsors](https://github.com/sponsors/hynek).
|
||||
Especially those generously supporting us at the *The Organization* tier and higher:
|
||||
|
||||
<!-- sponsor-break-begin -->
|
||||
|
||||
<p align="center">
|
||||
|
||||
<!-- [[[cog
|
||||
import pathlib, tomllib
|
||||
|
||||
for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]:
|
||||
print(f'<a href="{sponsor["url"]}"><img title="{sponsor["title"]}" src="https://www.attrs.org/en/25.3.0/_static/sponsors/{sponsor["img"]}" width="190" /></a>')
|
||||
]]] -->
|
||||
<a href="https://www.variomedia.de/"><img title="Variomedia AG" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Variomedia.svg" width="190" /></a>
|
||||
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Tidelift.svg" width="190" /></a>
|
||||
<a href="https://klaviyo.com/"><img title="Klaviyo" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Klaviyo.svg" width="190" /></a>
|
||||
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
|
||||
<a href="https://www.emsys-renewables.com/"><img title="emsys renewables" src="https://www.attrs.org/en/25.3.0/_static/sponsors/emsys-renewables.svg" width="190" /></a>
|
||||
<a href="https://filepreviews.io/"><img title="FilePreviews" src="https://www.attrs.org/en/25.3.0/_static/sponsors/FilePreviews.svg" width="190" /></a>
|
||||
<a href="https://polar.sh/"><img title="Polar" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Polar.svg" width="190" /></a>
|
||||
<!-- [[[end]]] -->
|
||||
|
||||
</p>
|
||||
|
||||
<!-- sponsor-break-end -->
|
||||
|
||||
<p align="center">
|
||||
<strong>Please consider <a href="https://github.com/sponsors/hynek">joining them</a> to help make <em>attrs</em>’s maintenance more sustainable!</strong>
|
||||
</p>
|
||||
|
||||
<!-- teaser-end -->
|
||||
|
||||
## Example
|
||||
|
||||
*attrs* gives you a class decorator and a way to declaratively define the attributes on that class:
|
||||
|
||||
<!-- code-begin -->
|
||||
|
||||
```pycon
|
||||
>>> from attrs import asdict, define, make_class, Factory
|
||||
|
||||
>>> @define
|
||||
... class SomeClass:
|
||||
... a_number: int = 42
|
||||
... list_of_numbers: list[int] = Factory(list)
|
||||
...
|
||||
... def hard_math(self, another_number):
|
||||
... return self.a_number + sum(self.list_of_numbers) * another_number
|
||||
|
||||
|
||||
>>> sc = SomeClass(1, [1, 2, 3])
|
||||
>>> sc
|
||||
SomeClass(a_number=1, list_of_numbers=[1, 2, 3])
|
||||
|
||||
>>> sc.hard_math(3)
|
||||
19
|
||||
>>> sc == SomeClass(1, [1, 2, 3])
|
||||
True
|
||||
>>> sc != SomeClass(2, [3, 2, 1])
|
||||
True
|
||||
|
||||
>>> asdict(sc)
|
||||
{'a_number': 1, 'list_of_numbers': [1, 2, 3]}
|
||||
|
||||
>>> SomeClass()
|
||||
SomeClass(a_number=42, list_of_numbers=[])
|
||||
|
||||
>>> C = make_class("C", ["a", "b"])
|
||||
>>> C("foo", "bar")
|
||||
C(a='foo', b='bar')
|
||||
```
|
||||
|
||||
After *declaring* your attributes, *attrs* gives you:
|
||||
|
||||
- a concise and explicit overview of the class's attributes,
|
||||
- a nice human-readable `__repr__`,
|
||||
- equality-checking methods,
|
||||
- an initializer,
|
||||
- and much more,
|
||||
|
||||
*without* writing dull boilerplate code again and again and *without* runtime performance penalties.
|
||||
|
||||
---
|
||||
|
||||
This example uses *attrs*'s modern APIs that have been introduced in version 20.1.0, and the *attrs* package import name that has been added in version 21.3.0.
|
||||
The classic APIs (`@attr.s`, `attr.ib`, plus their serious-business aliases) and the `attr` package import name will remain **indefinitely**.
|
||||
|
||||
Check out [*On The Core API Names*](https://www.attrs.org/en/latest/names.html) for an in-depth explanation!
|
||||
|
||||
|
||||
### Hate Type Annotations!?
|
||||
|
||||
No problem!
|
||||
Types are entirely **optional** with *attrs*.
|
||||
Simply assign `attrs.field()` to the attributes instead of annotating them with types:
|
||||
|
||||
```python
|
||||
from attrs import define, field
|
||||
|
||||
@define
|
||||
class SomeClass:
|
||||
a_number = field(default=42)
|
||||
list_of_numbers = field(factory=list)
|
||||
```
|
||||
|
||||
|
||||
## Data Classes
|
||||
|
||||
On the tin, *attrs* might remind you of `dataclasses` (and indeed, `dataclasses` [are a descendant](https://hynek.me/articles/import-attrs/) of *attrs*).
|
||||
In practice it does a lot more and is more flexible.
|
||||
For instance, it allows you to define [special handling of NumPy arrays for equality checks](https://www.attrs.org/en/stable/comparison.html#customization), allows more ways to [plug into the initialization process](https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization), has a replacement for `__init_subclass__`, and allows for stepping through the generated methods using a debugger.
|
||||
|
||||
For more details, please refer to our [comparison page](https://www.attrs.org/en/stable/why.html#data-classes), but generally speaking, we are more likely to commit crimes against nature to make things work that one would expect to work, but that are quite complicated in practice.
|
||||
|
||||
|
||||
## Project Information
|
||||
|
||||
- [**Changelog**](https://www.attrs.org/en/stable/changelog.html)
|
||||
- [**Documentation**](https://www.attrs.org/)
|
||||
- [**PyPI**](https://pypi.org/project/attrs/)
|
||||
- [**Source Code**](https://github.com/python-attrs/attrs)
|
||||
- [**Contributing**](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md)
|
||||
- [**Third-party Extensions**](https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs)
|
||||
- **Get Help**: use the `python-attrs` tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-attrs)
|
||||
|
||||
|
||||
### *attrs* for Enterprise
|
||||
|
||||
Available as part of the [Tidelift Subscription](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek).
|
||||
|
||||
The maintainers of *attrs* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications.
|
||||
Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.
|
||||
|
||||
## Release Information
|
||||
|
||||
### Changes
|
||||
|
||||
- Restore support for generator-based `field_transformer`s.
|
||||
[#1417](https://github.com/python-attrs/attrs/issues/1417)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
[Full changelog →](https://www.attrs.org/en/stable/changelog.html)
|
||||
@@ -0,0 +1,56 @@
|
||||
attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057
|
||||
attr/__init__.pyi,sha256=QIXnnHPoucmDWkbpNsWTP-cgJ1bn8le7DjyRa_wYdew,11281
|
||||
attr/__pycache__/__init__.cpython-311.pyc,,
|
||||
attr/__pycache__/_cmp.cpython-311.pyc,,
|
||||
attr/__pycache__/_compat.cpython-311.pyc,,
|
||||
attr/__pycache__/_config.cpython-311.pyc,,
|
||||
attr/__pycache__/_funcs.cpython-311.pyc,,
|
||||
attr/__pycache__/_make.cpython-311.pyc,,
|
||||
attr/__pycache__/_next_gen.cpython-311.pyc,,
|
||||
attr/__pycache__/_version_info.cpython-311.pyc,,
|
||||
attr/__pycache__/converters.cpython-311.pyc,,
|
||||
attr/__pycache__/exceptions.cpython-311.pyc,,
|
||||
attr/__pycache__/filters.cpython-311.pyc,,
|
||||
attr/__pycache__/setters.cpython-311.pyc,,
|
||||
attr/__pycache__/validators.cpython-311.pyc,,
|
||||
attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117
|
||||
attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368
|
||||
attr/_compat.py,sha256=4hlXbWhdDjQCDK6FKF1EgnZ3POiHgtpp54qE0nxaGHg,2704
|
||||
attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843
|
||||
attr/_funcs.py,sha256=5-tUKJtp3h5El55EcDl6GWXFp68fT8D8U7uCRN6497I,15854
|
||||
attr/_make.py,sha256=lBUPPmxiA1BeHzB6OlHoCEh--tVvM1ozXO8eXOa6g4c,96664
|
||||
attr/_next_gen.py,sha256=7FRkbtl_N017SuBhf_Vw3mw2c2pGZhtCGOzadgz7tp4,24395
|
||||
attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469
|
||||
attr/_version_info.py,sha256=exSqb3b5E-fMSsgZAlEw9XcLpEgobPORCZpcaEglAM4,2121
|
||||
attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209
|
||||
attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861
|
||||
attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643
|
||||
attr/exceptions.py,sha256=HRFq4iybmv7-DcZwyjl6M1euM2YeJVK_hFxuaBGAngI,1977
|
||||
attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539
|
||||
attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795
|
||||
attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208
|
||||
attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617
|
||||
attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584
|
||||
attr/validators.py,sha256=WaB1HLAHHqRHWsrv_K9H-sJ7ESil3H3Cmv2d8TtVZx4,20046
|
||||
attr/validators.pyi,sha256=s2WhKPqskxbsckJfKk8zOuuB088GfgpyxcCYSNFLqNU,2603
|
||||
attrs-25.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
attrs-25.3.0.dist-info/METADATA,sha256=W38cREj7s1wqNf1fg4hVwZmL1xh0AdSp4IhtTMROinw,10993
|
||||
attrs-25.3.0.dist-info/RECORD,,
|
||||
attrs-25.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attrs-25.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
||||
attrs-25.3.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109
|
||||
attrs/__init__.py,sha256=qeQJZ4O08yczSn840v9bYOaZyRE81WsVi-QCrY3krCU,1107
|
||||
attrs/__init__.pyi,sha256=nZmInocjM7tHV4AQw0vxO_fo6oJjL_PonlV9zKKW8DY,7931
|
||||
attrs/__pycache__/__init__.cpython-311.pyc,,
|
||||
attrs/__pycache__/converters.cpython-311.pyc,,
|
||||
attrs/__pycache__/exceptions.cpython-311.pyc,,
|
||||
attrs/__pycache__/filters.cpython-311.pyc,,
|
||||
attrs/__pycache__/setters.cpython-311.pyc,,
|
||||
attrs/__pycache__/validators.cpython-311.pyc,,
|
||||
attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76
|
||||
attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76
|
||||
attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73
|
||||
attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73
|
||||
attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76
|
||||
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.27.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Hynek Schlawack and the attrs contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,69 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from attr import (
|
||||
NOTHING,
|
||||
Attribute,
|
||||
AttrsInstance,
|
||||
Converter,
|
||||
Factory,
|
||||
NothingType,
|
||||
_make_getattr,
|
||||
assoc,
|
||||
cmp_using,
|
||||
define,
|
||||
evolve,
|
||||
field,
|
||||
fields,
|
||||
fields_dict,
|
||||
frozen,
|
||||
has,
|
||||
make_class,
|
||||
mutable,
|
||||
resolve_types,
|
||||
validate,
|
||||
)
|
||||
from attr._next_gen import asdict, astuple
|
||||
|
||||
from . import converters, exceptions, filters, setters, validators
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NOTHING",
|
||||
"Attribute",
|
||||
"AttrsInstance",
|
||||
"Converter",
|
||||
"Factory",
|
||||
"NothingType",
|
||||
"__author__",
|
||||
"__copyright__",
|
||||
"__description__",
|
||||
"__doc__",
|
||||
"__email__",
|
||||
"__license__",
|
||||
"__title__",
|
||||
"__url__",
|
||||
"__version__",
|
||||
"__version_info__",
|
||||
"asdict",
|
||||
"assoc",
|
||||
"astuple",
|
||||
"cmp_using",
|
||||
"converters",
|
||||
"define",
|
||||
"evolve",
|
||||
"exceptions",
|
||||
"field",
|
||||
"fields",
|
||||
"fields_dict",
|
||||
"filters",
|
||||
"frozen",
|
||||
"has",
|
||||
"make_class",
|
||||
"mutable",
|
||||
"resolve_types",
|
||||
"setters",
|
||||
"validate",
|
||||
"validators",
|
||||
]
|
||||
|
||||
__getattr__ = _make_getattr(__name__)
|
||||
@@ -0,0 +1,263 @@
|
||||
import sys
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Mapping,
|
||||
Sequence,
|
||||
overload,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
# Because we need to type our own stuff, we have to make everything from
|
||||
# attr explicitly public too.
|
||||
from attr import __author__ as __author__
|
||||
from attr import __copyright__ as __copyright__
|
||||
from attr import __description__ as __description__
|
||||
from attr import __email__ as __email__
|
||||
from attr import __license__ as __license__
|
||||
from attr import __title__ as __title__
|
||||
from attr import __url__ as __url__
|
||||
from attr import __version__ as __version__
|
||||
from attr import __version_info__ as __version_info__
|
||||
from attr import assoc as assoc
|
||||
from attr import Attribute as Attribute
|
||||
from attr import AttrsInstance as AttrsInstance
|
||||
from attr import cmp_using as cmp_using
|
||||
from attr import converters as converters
|
||||
from attr import Converter as Converter
|
||||
from attr import evolve as evolve
|
||||
from attr import exceptions as exceptions
|
||||
from attr import Factory as Factory
|
||||
from attr import fields as fields
|
||||
from attr import fields_dict as fields_dict
|
||||
from attr import filters as filters
|
||||
from attr import has as has
|
||||
from attr import make_class as make_class
|
||||
from attr import NOTHING as NOTHING
|
||||
from attr import resolve_types as resolve_types
|
||||
from attr import setters as setters
|
||||
from attr import validate as validate
|
||||
from attr import validators as validators
|
||||
from attr import attrib, asdict as asdict, astuple as astuple
|
||||
from attr import NothingType as NothingType
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import dataclass_transform
|
||||
else:
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_C = TypeVar("_C", bound=type)
|
||||
|
||||
_EqOrderType = bool | Callable[[Any], Any]
|
||||
_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any]
|
||||
_CallableConverterType = Callable[[Any], Any]
|
||||
_ConverterType = _CallableConverterType | Converter[Any, Any]
|
||||
_ReprType = Callable[[Any], str]
|
||||
_ReprArgType = bool | _ReprType
|
||||
_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any]
|
||||
_OnSetAttrArgType = _OnSetAttrType | list[_OnSetAttrType] | setters._NoOpType
|
||||
_FieldTransformer = Callable[
|
||||
[type, list["Attribute[Any]"]], list["Attribute[Any]"]
|
||||
]
|
||||
# FIXME: in reality, if multiple validators are passed they must be in a list
|
||||
# or tuple, but those are invariant and so would prevent subtypes of
|
||||
# _ValidatorType from working when passed in a list or tuple.
|
||||
_ValidatorArgType = _ValidatorType[_T] | Sequence[_ValidatorType[_T]]
|
||||
|
||||
@overload
|
||||
def field(
|
||||
*,
|
||||
default: None = ...,
|
||||
validator: None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: None = ...,
|
||||
factory: None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: bool | None = ...,
|
||||
order: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
type: type | None = ...,
|
||||
) -> Any: ...
|
||||
|
||||
# This form catches an explicit None or no default and infers the type from the
|
||||
# other arguments.
|
||||
@overload
|
||||
def field(
|
||||
*,
|
||||
default: None = ...,
|
||||
validator: _ValidatorArgType[_T] | None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
type: type | None = ...,
|
||||
) -> _T: ...
|
||||
|
||||
# This form catches an explicit default argument.
|
||||
@overload
|
||||
def field(
|
||||
*,
|
||||
default: _T,
|
||||
validator: _ValidatorArgType[_T] | None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
type: type | None = ...,
|
||||
) -> _T: ...
|
||||
|
||||
# This form covers type=non-Type: e.g. forward references (str), Any
|
||||
@overload
|
||||
def field(
|
||||
*,
|
||||
default: _T | None = ...,
|
||||
validator: _ValidatorArgType[_T] | None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
metadata: Mapping[Any, Any] | None = ...,
|
||||
converter: _ConverterType
|
||||
| list[_ConverterType]
|
||||
| tuple[_ConverterType]
|
||||
| None = ...,
|
||||
factory: Callable[[], _T] | None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: _EqOrderType | None = ...,
|
||||
order: _EqOrderType | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
alias: str | None = ...,
|
||||
type: type | None = ...,
|
||||
) -> Any: ...
|
||||
@overload
|
||||
@dataclass_transform(field_specifiers=(attrib, field))
|
||||
def define(
|
||||
maybe_cls: _C,
|
||||
*,
|
||||
these: dict[str, Any] | None = ...,
|
||||
repr: bool = ...,
|
||||
unsafe_hash: bool | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
frozen: bool = ...,
|
||||
weakref_slot: bool = ...,
|
||||
str: bool = ...,
|
||||
auto_attribs: bool = ...,
|
||||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: bool | None = ...,
|
||||
order: bool | None = ...,
|
||||
auto_detect: bool = ...,
|
||||
getstate_setstate: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
match_args: bool = ...,
|
||||
) -> _C: ...
|
||||
@overload
|
||||
@dataclass_transform(field_specifiers=(attrib, field))
|
||||
def define(
|
||||
maybe_cls: None = ...,
|
||||
*,
|
||||
these: dict[str, Any] | None = ...,
|
||||
repr: bool = ...,
|
||||
unsafe_hash: bool | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
frozen: bool = ...,
|
||||
weakref_slot: bool = ...,
|
||||
str: bool = ...,
|
||||
auto_attribs: bool = ...,
|
||||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: bool | None = ...,
|
||||
order: bool | None = ...,
|
||||
auto_detect: bool = ...,
|
||||
getstate_setstate: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
match_args: bool = ...,
|
||||
) -> Callable[[_C], _C]: ...
|
||||
|
||||
mutable = define
|
||||
|
||||
@overload
|
||||
@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field))
|
||||
def frozen(
|
||||
maybe_cls: _C,
|
||||
*,
|
||||
these: dict[str, Any] | None = ...,
|
||||
repr: bool = ...,
|
||||
unsafe_hash: bool | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
frozen: bool = ...,
|
||||
weakref_slot: bool = ...,
|
||||
str: bool = ...,
|
||||
auto_attribs: bool = ...,
|
||||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: bool | None = ...,
|
||||
order: bool | None = ...,
|
||||
auto_detect: bool = ...,
|
||||
getstate_setstate: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
match_args: bool = ...,
|
||||
) -> _C: ...
|
||||
@overload
|
||||
@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field))
|
||||
def frozen(
|
||||
maybe_cls: None = ...,
|
||||
*,
|
||||
these: dict[str, Any] | None = ...,
|
||||
repr: bool = ...,
|
||||
unsafe_hash: bool | None = ...,
|
||||
hash: bool | None = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
frozen: bool = ...,
|
||||
weakref_slot: bool = ...,
|
||||
str: bool = ...,
|
||||
auto_attribs: bool = ...,
|
||||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: bool | None = ...,
|
||||
order: bool | None = ...,
|
||||
auto_detect: bool = ...,
|
||||
getstate_setstate: bool | None = ...,
|
||||
on_setattr: _OnSetAttrArgType | None = ...,
|
||||
field_transformer: _FieldTransformer | None = ...,
|
||||
match_args: bool = ...,
|
||||
) -> Callable[[_C], _C]: ...
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from attr.converters import * # noqa: F403
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from attr.exceptions import * # noqa: F403
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from attr.filters import * # noqa: F403
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from attr.setters import * # noqa: F403
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from attr.validators import * # noqa: F403
|
||||
@@ -0,0 +1,25 @@
|
||||
from .converters import BaseConverter, Converter, GenConverter, UnstructureStrategy
|
||||
from .gen import override
|
||||
|
||||
__all__ = (
|
||||
"BaseConverter",
|
||||
"Converter",
|
||||
"GenConverter",
|
||||
"UnstructureStrategy",
|
||||
"global_converter",
|
||||
"override",
|
||||
"structure",
|
||||
"structure_attrs_fromdict",
|
||||
"structure_attrs_fromtuple",
|
||||
"unstructure",
|
||||
)
|
||||
from cattrs import global_converter
|
||||
|
||||
unstructure = global_converter.unstructure
|
||||
structure = global_converter.structure
|
||||
structure_attrs_fromtuple = global_converter.structure_attrs_fromtuple
|
||||
structure_attrs_fromdict = global_converter.structure_attrs_fromdict
|
||||
register_structure_hook = global_converter.register_structure_hook
|
||||
register_structure_hook_func = global_converter.register_structure_hook_func
|
||||
register_unstructure_hook = global_converter.register_unstructure_hook
|
||||
register_unstructure_hook_func = global_converter.register_unstructure_hook_func
|
||||
@@ -0,0 +1,8 @@
|
||||
from cattrs.converters import (
|
||||
BaseConverter,
|
||||
Converter,
|
||||
GenConverter,
|
||||
UnstructureStrategy,
|
||||
)
|
||||
|
||||
__all__ = ["BaseConverter", "Converter", "GenConverter", "UnstructureStrategy"]
|
||||
@@ -0,0 +1,3 @@
|
||||
from cattrs.disambiguators import create_uniq_field_dis_func
|
||||
|
||||
__all__ = ["create_uniq_field_dis_func"]
|
||||
@@ -0,0 +1,3 @@
|
||||
from cattrs.dispatch import FunctionDispatch, MultiStrategyDispatch
|
||||
|
||||
__all__ = ["FunctionDispatch", "MultiStrategyDispatch"]
|
||||
@@ -0,0 +1,15 @@
|
||||
from cattrs.errors import (
|
||||
BaseValidationError,
|
||||
ClassValidationError,
|
||||
ForbiddenExtraKeysError,
|
||||
IterableValidationError,
|
||||
StructureHandlerNotFoundError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BaseValidationError",
|
||||
"ClassValidationError",
|
||||
"ForbiddenExtraKeysError",
|
||||
"IterableValidationError",
|
||||
"StructureHandlerNotFoundError",
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
from cattrs.cols import iterable_unstructure_factory as make_iterable_unstructure_fn
|
||||
from cattrs.gen import (
|
||||
make_dict_structure_fn,
|
||||
make_dict_unstructure_fn,
|
||||
make_hetero_tuple_unstructure_fn,
|
||||
make_mapping_structure_fn,
|
||||
make_mapping_unstructure_fn,
|
||||
override,
|
||||
)
|
||||
from cattrs.gen._consts import AttributeOverride
|
||||
|
||||
__all__ = [
|
||||
"AttributeOverride",
|
||||
"make_dict_structure_fn",
|
||||
"make_dict_unstructure_fn",
|
||||
"make_hetero_tuple_unstructure_fn",
|
||||
"make_iterable_unstructure_fn",
|
||||
"make_mapping_structure_fn",
|
||||
"make_mapping_unstructure_fn",
|
||||
"override",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
from cattrs.preconf import validate_datetime
|
||||
|
||||
__all__ = ["validate_datetime"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Preconfigured converters for bson."""
|
||||
|
||||
from cattrs.preconf.bson import BsonConverter, configure_converter, make_converter
|
||||
|
||||
__all__ = ["BsonConverter", "configure_converter", "make_converter"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Preconfigured converters for the stdlib json."""
|
||||
|
||||
from cattrs.preconf.json import JsonConverter, configure_converter, make_converter
|
||||
|
||||
__all__ = ["JsonConverter", "configure_converter", "make_converter"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Preconfigured converters for msgpack."""
|
||||
|
||||
from cattrs.preconf.msgpack import MsgpackConverter, configure_converter, make_converter
|
||||
|
||||
__all__ = ["MsgpackConverter", "configure_converter", "make_converter"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Preconfigured converters for orjson."""
|
||||
|
||||
from cattrs.preconf.orjson import OrjsonConverter, configure_converter, make_converter
|
||||
|
||||
__all__ = ["OrjsonConverter", "configure_converter", "make_converter"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Preconfigured converters for pyyaml."""
|
||||
|
||||
from cattrs.preconf.pyyaml import PyyamlConverter, configure_converter, make_converter
|
||||
|
||||
__all__ = ["PyyamlConverter", "configure_converter", "make_converter"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Preconfigured converters for tomlkit."""
|
||||
|
||||
from cattrs.preconf.tomlkit import TomlkitConverter, configure_converter, make_converter
|
||||
|
||||
__all__ = ["TomlkitConverter", "configure_converter", "make_converter"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Preconfigured converters for ujson."""
|
||||
|
||||
from cattrs.preconf.ujson import UjsonConverter, configure_converter, make_converter
|
||||
|
||||
__all__ = ["UjsonConverter", "configure_converter", "make_converter"]
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,161 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: cattrs
|
||||
Version: 25.1.1
|
||||
Summary: Composable complex class support for attrs and dataclasses.
|
||||
Project-URL: Homepage, https://catt.rs
|
||||
Project-URL: Changelog, https://catt.rs/en/latest/history.html
|
||||
Project-URL: Bug Tracker, https://github.com/python-attrs/cattrs/issues
|
||||
Project-URL: Repository, https://github.com/python-attrs/cattrs
|
||||
Project-URL: Documentation, https://catt.rs/en/stable/
|
||||
Author-email: Tin Tvrtkovic <tinchester@gmail.com>
|
||||
License: MIT
|
||||
License-File: LICENSE
|
||||
Keywords: attrs,dataclasses,serialization
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.9
|
||||
Requires-Dist: attrs>=24.3.0
|
||||
Requires-Dist: exceptiongroup>=1.1.1; python_version < '3.11'
|
||||
Requires-Dist: typing-extensions>=4.12.2
|
||||
Provides-Extra: bson
|
||||
Requires-Dist: pymongo>=4.4.0; extra == 'bson'
|
||||
Provides-Extra: cbor2
|
||||
Requires-Dist: cbor2>=5.4.6; extra == 'cbor2'
|
||||
Provides-Extra: msgpack
|
||||
Requires-Dist: msgpack>=1.0.5; extra == 'msgpack'
|
||||
Provides-Extra: msgspec
|
||||
Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
|
||||
Provides-Extra: orjson
|
||||
Requires-Dist: orjson>=3.10.7; (implementation_name == 'cpython') and extra == 'orjson'
|
||||
Provides-Extra: pyyaml
|
||||
Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
|
||||
Provides-Extra: tomlkit
|
||||
Requires-Dist: tomlkit>=0.11.8; extra == 'tomlkit'
|
||||
Provides-Extra: ujson
|
||||
Requires-Dist: ujson>=5.10.0; extra == 'ujson'
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
# *cattrs*: Flexible Object Serialization and Validation
|
||||
|
||||
*Because validation belongs to the edges.*
|
||||
|
||||
[](https://catt.rs/)
|
||||
[](https://github.com/hynek/stamina/blob/main/LICENSE)
|
||||
[](https://pypi.python.org/pypi/cattrs)
|
||||
[](https://github.com/python-attrs/cattrs)
|
||||
[](https://pepy.tech/project/cattrs)
|
||||
[](https://github.com/python-attrs/cattrs/actions/workflows/main.yml)
|
||||
|
||||
---
|
||||
|
||||
<!-- begin-teaser -->
|
||||
|
||||
**cattrs** is a Swiss Army knife for (un)structuring and validating data in Python.
|
||||
In practice, that means it converts **unstructured dictionaries** into **proper classes** and back, while **validating** their contents.
|
||||
|
||||
<!-- end-teaser -->
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
<!-- begin-example -->
|
||||
|
||||
_cattrs_ works best with [_attrs_](https://www.attrs.org/) classes, and [dataclasses](https://docs.python.org/3/library/dataclasses.html) where simple (un-)structuring works out of the box, even for nested data, without polluting your data model with serialization details:
|
||||
|
||||
```python
|
||||
>>> from attrs import define
|
||||
>>> from cattrs import structure, unstructure
|
||||
>>> @define
|
||||
... class C:
|
||||
... a: int
|
||||
... b: list[str]
|
||||
>>> instance = structure({'a': 1, 'b': ['x', 'y']}, C)
|
||||
>>> instance
|
||||
C(a=1, b=['x', 'y'])
|
||||
>>> unstructure(instance)
|
||||
{'a': 1, 'b': ['x', 'y']}
|
||||
```
|
||||
|
||||
<!-- end-teaser -->
|
||||
<!-- end-example -->
|
||||
|
||||
Have a look at [*Why *cattrs*?*](https://catt.rs/en/latest/why.html) for more examples!
|
||||
|
||||
<!-- begin-why -->
|
||||
|
||||
## Features
|
||||
|
||||
### Recursive Unstructuring
|
||||
|
||||
- _attrs_ classes and dataclasses are converted into dictionaries in a way similar to `attrs.asdict()`, or into tuples in a way similar to `attrs.astuple()`.
|
||||
- Enumeration instances are converted to their values.
|
||||
- Other types are let through without conversion. This includes types such as integers, dictionaries, lists and instances of non-_attrs_ classes.
|
||||
- Custom converters for any type can be registered using `register_unstructure_hook`.
|
||||
|
||||
|
||||
### Recursive Structuring
|
||||
|
||||
Converts unstructured data into structured data, recursively, according to your specification given as a type.
|
||||
The following types are supported:
|
||||
|
||||
- `typing.Optional[T]` and its 3.10+ form, `T | None`.
|
||||
- `list[T]`, `typing.List[T]`, `typing.MutableSequence[T]`, `typing.Sequence[T]` convert to lists.
|
||||
- `tuple` and `typing.Tuple` (both variants, `tuple[T, ...]` and `tuple[X, Y, Z]`).
|
||||
- `set[T]`, `typing.MutableSet[T]`, and `typing.Set[T]` convert to sets.
|
||||
- `frozenset[T]`, and `typing.FrozenSet[T]` convert to frozensets.
|
||||
- `dict[K, V]`, `typing.Dict[K, V]`, `typing.MutableMapping[K, V]`, and `typing.Mapping[K, V]` convert to dictionaries.
|
||||
- [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict), both ordinary and generic.
|
||||
- [`typing.NewType`](https://docs.python.org/3/library/typing.html#newtype)
|
||||
- [PEP 695 type aliases](https://docs.python.org/3/library/typing.html#type-aliases) on 3.12+
|
||||
- _attrs_ classes with simple attributes and the usual `__init__`[^simple].
|
||||
- All _attrs_ classes and dataclasses with the usual `__init__`, if their complex attributes have type metadata.
|
||||
- Unions of supported _attrs_ classes, given that all of the classes have a unique field.
|
||||
- Unions of anything, if you provide a disambiguation function for it.
|
||||
- Custom converters for any type can be registered using `register_structure_hook`.
|
||||
|
||||
[^simple]: Simple attributes are attributes that can be assigned unstructured data, like numbers, strings, and collections of unstructured data.
|
||||
|
||||
|
||||
### Batteries Included
|
||||
|
||||
_cattrs_ comes with pre-configured converters for a number of serialization libraries, including JSON (standard library, [_orjson_](https://pypi.org/project/orjson/), [UltraJSON](https://pypi.org/project/ujson/)), [_msgpack_](https://pypi.org/project/msgpack/), [_cbor2_](https://pypi.org/project/cbor2/), [_bson_](https://pypi.org/project/bson/), [PyYAML](https://pypi.org/project/PyYAML/), [_tomlkit_](https://pypi.org/project/tomlkit/) and [_msgspec_](https://pypi.org/project/msgspec/) (supports only JSON at this time).
|
||||
|
||||
For details, see the [cattrs.preconf package](https://catt.rs/en/stable/preconf.html).
|
||||
|
||||
|
||||
## Design Decisions
|
||||
|
||||
_cattrs_ is based on a few fundamental design decisions:
|
||||
|
||||
- Un/structuring rules are separate from the models.
|
||||
This allows models to have a one-to-many relationship with un/structuring rules, and to create un/structuring rules for models which you do not own and you cannot change.
|
||||
(_cattrs_ can be configured to use un/structuring rules from models using the [`use_class_methods` strategy](https://catt.rs/en/latest/strategies.html#using-class-specific-structure-and-unstructure-methods).)
|
||||
- Invent as little as possible; reuse existing ordinary Python instead.
|
||||
For example, _cattrs_ did not have a custom exception type to group exceptions until the sanctioned Python [`exceptiongroups`](https://docs.python.org/3/library/exceptions.html#ExceptionGroup).
|
||||
A side-effect of this design decision is that, in a lot of cases, when you're solving _cattrs_ problems you're actually learning Python instead of learning _cattrs_.
|
||||
- Resist the temptation to guess.
|
||||
If there are two ways of solving a problem, _cattrs_ should refuse to guess and let the user configure it themselves.
|
||||
|
||||
A foolish consistency is the hobgoblin of little minds, so these decisions can and are sometimes broken, but they have proven to be a good foundation.
|
||||
|
||||
|
||||
<!-- end-why -->
|
||||
|
||||
## Credits
|
||||
|
||||
Major credits to Hynek Schlawack for creating [attrs](https://attrs.org) and its predecessor, [characteristic](https://github.com/hynek/characteristic).
|
||||
|
||||
_cattrs_ is tested with [Hypothesis](http://hypothesis.readthedocs.io/en/latest/), by David R. MacIver.
|
||||
|
||||
_cattrs_ is benchmarked using [perf](https://github.com/haypo/perf) and [pytest-benchmark](https://pytest-benchmark.readthedocs.io/en/latest/index.html).
|
||||
|
||||
This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [`audreyr/cookiecutter-pypackage`](https://github.com/audreyr/cookiecutter-pypackage) project template.
|
||||
@@ -0,0 +1,102 @@
|
||||
cattr/__init__.py,sha256=bYrmwTYSdYC_ut1xW31V7mxhXBlJQKs8EECgtUBgAuc,906
|
||||
cattr/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattr/__pycache__/converters.cpython-311.pyc,,
|
||||
cattr/__pycache__/disambiguators.cpython-311.pyc,,
|
||||
cattr/__pycache__/dispatch.cpython-311.pyc,,
|
||||
cattr/__pycache__/errors.cpython-311.pyc,,
|
||||
cattr/__pycache__/gen.cpython-311.pyc,,
|
||||
cattr/converters.py,sha256=rQhY4J8r7QTZh5WICuFe4GWO1v0DS3DgQ9r569zd6jg,192
|
||||
cattr/disambiguators.py,sha256=ugD1fq1Z5x1pGu5P1lMzcT-IEi1q7IfQJIHEdmg62vM,103
|
||||
cattr/dispatch.py,sha256=uVEOgHWR9Hn5tm-wIw-bDccqrxJByVi8yRKaYyvL67k,125
|
||||
cattr/errors.py,sha256=V4RhoCObwGrlaM3oyn1H_FYxGR8iAB9dG5NxFDYM548,343
|
||||
cattr/gen.py,sha256=hWyKoZ_d2D36Jz_npspyGw8s9pWtUA69sXf0R3uOvgM,597
|
||||
cattr/preconf/__init__.py,sha256=NqPE7uhVfcP-PggkUpsbfAutMo8oHjcoB1cvjgLft-s,78
|
||||
cattr/preconf/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/bson.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/json.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/msgpack.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/orjson.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/pyyaml.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/tomlkit.cpython-311.pyc,,
|
||||
cattr/preconf/__pycache__/ujson.cpython-311.pyc,,
|
||||
cattr/preconf/bson.py,sha256=Bn4hJxac7OthGg_CR4LCPeBp_fz4kx3QniBVOZhguGs,195
|
||||
cattr/preconf/json.py,sha256=LpqYuO3oePDxbQtKFKB0SaoeAi3Z_agIgyNn1VQSIVo,206
|
||||
cattr/preconf/msgpack.py,sha256=pyJ9L9ekNlZ0IQHbJ9Ay_fi_NOqY5_rE_q-UnD94-RM,207
|
||||
cattr/preconf/orjson.py,sha256=Adh-7csx4eqCjx22zipMFgSlDXbR554wvgNHEb8Q5JM,203
|
||||
cattr/preconf/pyyaml.py,sha256=Fy40bejjp7uqgoLhTA_p4wZYF0uFaguHbUK9zs9LoC0,203
|
||||
cattr/preconf/tomlkit.py,sha256=_gADJ_UYpj3EiNXGYjAfSOkcoFIkLpYVOFfLEqBfIJQ,207
|
||||
cattr/preconf/ujson.py,sha256=IzEa7QUcYOaSUMiLQsFEWJnBihmmOLhehsM-5cPY9NI,199
|
||||
cattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs-25.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
cattrs-25.1.1.dist-info/METADATA,sha256=ODqSak3dhIZZjmFa-SZT8Si32_3ey_oo2tUefYx0QtU,8388
|
||||
cattrs-25.1.1.dist-info/RECORD,,
|
||||
cattrs-25.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs-25.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
||||
cattrs-25.1.1.dist-info/licenses/LICENSE,sha256=9fudHt43qIykf0IMSZ3KD0oFvJk-Esd9I1IKrSkcAb8,1074
|
||||
cattrs/__init__.py,sha256=UhiFdxf81gCuBBA6FutoE1oOzthzF_PkAdoE2AVslIo,1901
|
||||
cattrs/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/__pycache__/_compat.cpython-311.pyc,,
|
||||
cattrs/__pycache__/_generics.cpython-311.pyc,,
|
||||
cattrs/__pycache__/cols.cpython-311.pyc,,
|
||||
cattrs/__pycache__/converters.cpython-311.pyc,,
|
||||
cattrs/__pycache__/disambiguators.cpython-311.pyc,,
|
||||
cattrs/__pycache__/dispatch.cpython-311.pyc,,
|
||||
cattrs/__pycache__/errors.cpython-311.pyc,,
|
||||
cattrs/__pycache__/fns.cpython-311.pyc,,
|
||||
cattrs/__pycache__/literals.cpython-311.pyc,,
|
||||
cattrs/__pycache__/typealiases.cpython-311.pyc,,
|
||||
cattrs/__pycache__/types.cpython-311.pyc,,
|
||||
cattrs/__pycache__/v.cpython-311.pyc,,
|
||||
cattrs/_compat.py,sha256=dMRB8a8RkxFdnQDKRpamresVF_SBkksM2_ZMAuL0s2w,11987
|
||||
cattrs/_generics.py,sha256=keExDE2CGIer8ci12SoJ_rXYTLva9P29uLlvyb_fxtM,966
|
||||
cattrs/cols.py,sha256=mWDchfvjMQ6uACKSfdZs05YiXrWdph2HOJfaqY3D_EI,8848
|
||||
cattrs/converters.py,sha256=ui4BSAxnV1J6Oh0pYN2oOHgp-mIybsXW-M8SXPDNzlo,54262
|
||||
cattrs/disambiguators.py,sha256=eUyWMtW6bQJcXGOWiraq1pFcMlUbo9BJHxFY--KF6Lk,6867
|
||||
cattrs/dispatch.py,sha256=9qA-pmsPvgrM6MGP8Ev2gVP6YL2rXvoBla_C0VgHxQ0,6780
|
||||
cattrs/errors.py,sha256=6IGfE-wVQbOaDNN4xAQJf7Hk_t2QNV0Zem64D6yMZrU,4168
|
||||
cattrs/fns.py,sha256=z5z1VZOZv8t5LwG8cBM_tIXg-_PlQUOyZb9wIrXNqlw,626
|
||||
cattrs/gen/__init__.py,sha256=bpRGHd3G0UTpWcHRcJTNAUcWmX-evoPHGvpH6u29FDU,38842
|
||||
cattrs/gen/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_consts.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_generics.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_lc.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/_shared.cpython-311.pyc,,
|
||||
cattrs/gen/__pycache__/typeddicts.cpython-311.pyc,,
|
||||
cattrs/gen/_consts.py,sha256=ZwT_m2J3S7p-UjltpbA1WtfQZLNj9KhmFYCAv6Zl-g0,511
|
||||
cattrs/gen/_generics.py,sha256=_DyXCGql2QIxGhAv3_B1hsi80uPK8PhK2hhZa95YOlo,3011
|
||||
cattrs/gen/_lc.py,sha256=4fjeUsmgQcCAIjnNndBic0gf5qKmxVS3CZHqUQ9Rw5g,882
|
||||
cattrs/gen/_shared.py,sha256=xKsfcVtpyYIir9AW8VuOVoiSbaEI7tsSL0JpUCIUX-g,2296
|
||||
cattrs/gen/typeddicts.py,sha256=Ck3QMr_B1T7vwxyRjfZPHafKphN2hndL181dpQNxzPs,21254
|
||||
cattrs/literals.py,sha256=0kzAewmWk9ikJGoKq4ysnAR22DMawG3iNqLl8NLgpk0,331
|
||||
cattrs/preconf/__init__.py,sha256=P7czFRcjeN6zBcdwUyeBloniltlJptCa8Yd2uFGlz9w,1527
|
||||
cattrs/preconf/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/bson.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/cbor2.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/json.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/msgpack.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/msgspec.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/orjson.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/pyyaml.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/tomlkit.cpython-311.pyc,,
|
||||
cattrs/preconf/__pycache__/ujson.cpython-311.pyc,,
|
||||
cattrs/preconf/bson.py,sha256=6p1kmOFMjswSXFCb1hKJeNvr3kNsAm1gfX_DA6igq8E,4201
|
||||
cattrs/preconf/cbor2.py,sha256=LnREcjpOp_402poUGRVIhDWI4f_R1wvJkdKvs5MrTGU,2022
|
||||
cattrs/preconf/json.py,sha256=zTrkfjOxXZFwwabNESeafy-C7MEpn7Cw5AdhkkeOjU4,2631
|
||||
cattrs/preconf/msgpack.py,sha256=dZE9tsAA5qX3pSc3MZmlGuvJ5q_wI6mANDyugKXKj-E,2325
|
||||
cattrs/preconf/msgspec.py,sha256=Ds0rPW4900zsBLqfupF-smkg9_Kwyx7D_Vh9a0yJB8M,7250
|
||||
cattrs/preconf/orjson.py,sha256=5MBcUsyp3eGsHgLfLtt8-q90L2mxjD0ttnrWBUIwouo,3870
|
||||
cattrs/preconf/pyyaml.py,sha256=w0aM_gJ6VhZf-Zpu_UlJki7rdgv4mfaSXElPofB3nlE,2378
|
||||
cattrs/preconf/tomlkit.py,sha256=gJWGJjMONCViTMZuphOg2xXzjQt3SCEVFVdoKgDjqc8,3148
|
||||
cattrs/preconf/ujson.py,sha256=wRLidBM8aWucFkCQ9haiktY8xYoCdanDhQuKJLQJgGM,2425
|
||||
cattrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cattrs/strategies/__init__.py,sha256=nkZWCzSRYcS-75FMfk52mioZSuWykaN8hB39Vig5Xkg,339
|
||||
cattrs/strategies/__pycache__/__init__.cpython-311.pyc,,
|
||||
cattrs/strategies/__pycache__/_class_methods.cpython-311.pyc,,
|
||||
cattrs/strategies/__pycache__/_subclasses.cpython-311.pyc,,
|
||||
cattrs/strategies/__pycache__/_unions.cpython-311.pyc,,
|
||||
cattrs/strategies/_class_methods.py,sha256=O5xhQCzNpuFiDNDMlbcyeOVqyrV65NhMZNRsG3jnoBU,2591
|
||||
cattrs/strategies/_subclasses.py,sha256=aCE2UQjevZQHMnPOPyl2qR_hgRpgRUt1j9lE4qZ3hNc,9365
|
||||
cattrs/strategies/_unions.py,sha256=YBBklVSWJ-7DSkLDLpumwAJJ39ALuSGyB6W0Ptz5Rz4,9355
|
||||
cattrs/typealiases.py,sha256=toHavC2kJsIcxThwvATPO5JShzKeC8kIl9KqteFohbw,1619
|
||||
cattrs/types.py,sha256=cqvfmzliYfrvPswxlW_tN4DmhQ2xpAKQvVbNBJaxiWs,278
|
||||
cattrs/v.py,sha256=IqUajgJFCKJYf-4S9TCKRtJcmmK4c3En69TGuf2FKOs,4126
|
||||
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.27.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016, Tin Tvrtković
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from typing import Final
|
||||
|
||||
from .converters import BaseConverter, Converter, GenConverter, UnstructureStrategy
|
||||
from .errors import (
|
||||
AttributeValidationNote,
|
||||
BaseValidationError,
|
||||
ClassValidationError,
|
||||
ForbiddenExtraKeysError,
|
||||
IterableValidationError,
|
||||
IterableValidationNote,
|
||||
StructureHandlerNotFoundError,
|
||||
)
|
||||
from .gen import override
|
||||
from .types import SimpleStructureHook
|
||||
from .v import transform_error
|
||||
|
||||
__all__ = [
|
||||
"AttributeValidationNote",
|
||||
"BaseConverter",
|
||||
"BaseValidationError",
|
||||
"ClassValidationError",
|
||||
"Converter",
|
||||
"ForbiddenExtraKeysError",
|
||||
"GenConverter",
|
||||
"IterableValidationError",
|
||||
"IterableValidationNote",
|
||||
"SimpleStructureHook",
|
||||
"StructureHandlerNotFoundError",
|
||||
"UnstructureStrategy",
|
||||
"get_structure_hook",
|
||||
"get_unstructure_hook",
|
||||
"global_converter",
|
||||
"override",
|
||||
"register_structure_hook",
|
||||
"register_structure_hook_func",
|
||||
"register_unstructure_hook",
|
||||
"register_unstructure_hook_func",
|
||||
"structure",
|
||||
"structure_attrs_fromdict",
|
||||
"structure_attrs_fromtuple",
|
||||
"transform_error",
|
||||
"unstructure",
|
||||
]
|
||||
|
||||
#: The global converter. Prefer creating your own if customizations are required.
|
||||
global_converter: Final = Converter()
|
||||
|
||||
unstructure = global_converter.unstructure
|
||||
structure = global_converter.structure
|
||||
structure_attrs_fromtuple = global_converter.structure_attrs_fromtuple
|
||||
structure_attrs_fromdict = global_converter.structure_attrs_fromdict
|
||||
register_structure_hook = global_converter.register_structure_hook
|
||||
register_structure_hook_func = global_converter.register_structure_hook_func
|
||||
register_unstructure_hook = global_converter.register_unstructure_hook
|
||||
register_unstructure_hook_func = global_converter.register_unstructure_hook_func
|
||||
get_structure_hook: Final = global_converter.get_structure_hook
|
||||
get_unstructure_hook: Final = global_converter.get_unstructure_hook
|
||||
@@ -0,0 +1,428 @@
|
||||
import sys
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Mapping as AbcMapping
|
||||
from collections.abc import MutableMapping as AbcMutableMapping
|
||||
from collections.abc import MutableSequence as AbcMutableSequence
|
||||
from collections.abc import MutableSet as AbcMutableSet
|
||||
from collections.abc import Sequence as AbcSequence
|
||||
from collections.abc import Set as AbcSet
|
||||
from dataclasses import MISSING, Field, is_dataclass
|
||||
from dataclasses import fields as dataclass_fields
|
||||
from functools import partial
|
||||
from inspect import signature as _signature
|
||||
from types import GenericAlias
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Deque,
|
||||
Dict,
|
||||
Final,
|
||||
FrozenSet,
|
||||
Generic,
|
||||
List,
|
||||
Literal,
|
||||
NewType,
|
||||
Optional,
|
||||
Protocol,
|
||||
Tuple,
|
||||
Union,
|
||||
_AnnotatedAlias,
|
||||
_GenericAlias,
|
||||
_SpecialGenericAlias,
|
||||
_UnionGenericAlias,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
from typing import Counter as TypingCounter
|
||||
from typing import Mapping as TypingMapping
|
||||
from typing import MutableMapping as TypingMutableMapping
|
||||
from typing import MutableSequence as TypingMutableSequence
|
||||
from typing import MutableSet as TypingMutableSet
|
||||
from typing import Sequence as TypingSequence
|
||||
from typing import Set as TypingSet
|
||||
|
||||
from attrs import NOTHING, Attribute, Factory, NothingType, resolve_types
|
||||
from attrs import fields as attrs_fields
|
||||
from attrs import fields_dict as attrs_fields_dict
|
||||
|
||||
__all__ = [
|
||||
"ANIES",
|
||||
"ExceptionGroup",
|
||||
"ExtensionsTypedDict",
|
||||
"TypeAlias",
|
||||
"adapted_fields",
|
||||
"fields_dict",
|
||||
"has",
|
||||
"is_typeddict",
|
||||
]
|
||||
|
||||
try:
|
||||
from typing_extensions import TypedDict as ExtensionsTypedDict
|
||||
except ImportError: # pragma: no cover
|
||||
ExtensionsTypedDict = None
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from builtins import ExceptionGroup
|
||||
else:
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
try:
|
||||
from typing_extensions import is_typeddict as _is_typeddict
|
||||
except ImportError: # pragma: no cover
|
||||
assert sys.version_info >= (3, 10)
|
||||
from typing import is_typeddict as _is_typeddict
|
||||
|
||||
try:
|
||||
from typing_extensions import TypeAlias
|
||||
except ImportError: # pragma: no cover
|
||||
assert sys.version_info >= (3, 11)
|
||||
from typing import TypeAlias
|
||||
|
||||
LITERALS = {Literal}
|
||||
try:
|
||||
from typing_extensions import Literal as teLiteral
|
||||
|
||||
LITERALS.add(teLiteral)
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# On some Python versions, `typing_extensions.Any` is different than
|
||||
# `typing.Any`.
|
||||
try:
|
||||
from typing_extensions import Any as teAny
|
||||
|
||||
ANIES = frozenset([Any, teAny])
|
||||
except ImportError: # pragma: no cover
|
||||
ANIES = frozenset([Any])
|
||||
|
||||
NoneType = type(None)
|
||||
|
||||
|
||||
def is_optional(typ: Any) -> bool:
|
||||
return is_union_type(typ) and NoneType in typ.__args__ and len(typ.__args__) == 2
|
||||
|
||||
|
||||
def is_typeddict(cls: Any):
|
||||
"""Thin wrapper around typing(_extensions).is_typeddict"""
|
||||
return _is_typeddict(getattr(cls, "__origin__", cls))
|
||||
|
||||
|
||||
def has(cls):
|
||||
return hasattr(cls, "__attrs_attrs__") or hasattr(cls, "__dataclass_fields__")
|
||||
|
||||
|
||||
def has_with_generic(cls):
|
||||
"""Test whether the class if a normal or generic attrs or dataclass."""
|
||||
return has(cls) or has(get_origin(cls))
|
||||
|
||||
|
||||
def fields(type):
|
||||
try:
|
||||
return type.__attrs_attrs__
|
||||
except AttributeError:
|
||||
return dataclass_fields(type)
|
||||
|
||||
|
||||
def fields_dict(type) -> dict[str, Union[Attribute, Field]]:
|
||||
"""Return the fields_dict for attrs and dataclasses."""
|
||||
if is_dataclass(type):
|
||||
return {f.name: f for f in dataclass_fields(type)}
|
||||
return attrs_fields_dict(type)
|
||||
|
||||
|
||||
def adapted_fields(cl: type) -> list[Attribute]:
|
||||
"""Return the attrs format of `fields()` for attrs and dataclasses.
|
||||
|
||||
Resolves `attrs` stringified annotations, if present.
|
||||
"""
|
||||
if is_dataclass(cl):
|
||||
attrs = dataclass_fields(cl)
|
||||
if any(isinstance(a.type, str) for a in attrs):
|
||||
# Do this conditionally in case `get_type_hints` fails, so
|
||||
# users can resolve on their own first.
|
||||
type_hints = get_type_hints(cl)
|
||||
else:
|
||||
type_hints = {}
|
||||
return [
|
||||
Attribute(
|
||||
attr.name,
|
||||
(
|
||||
attr.default
|
||||
if attr.default is not MISSING
|
||||
else (
|
||||
Factory(attr.default_factory)
|
||||
if attr.default_factory is not MISSING
|
||||
else NOTHING
|
||||
)
|
||||
),
|
||||
None,
|
||||
True,
|
||||
None,
|
||||
True,
|
||||
attr.init,
|
||||
True,
|
||||
type=type_hints.get(attr.name, attr.type),
|
||||
alias=attr.name,
|
||||
kw_only=getattr(attr, "kw_only", False),
|
||||
)
|
||||
for attr in attrs
|
||||
]
|
||||
attribs = attrs_fields(cl)
|
||||
if any(isinstance(a.type, str) for a in attribs):
|
||||
# PEP 563 annotations - need to be resolved.
|
||||
resolve_types(cl)
|
||||
attribs = attrs_fields(cl)
|
||||
return attribs
|
||||
|
||||
|
||||
def is_subclass(obj: type, bases) -> bool:
|
||||
"""A safe version of issubclass (won't raise)."""
|
||||
try:
|
||||
return issubclass(obj, bases)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
def is_hetero_tuple(type: Any) -> bool:
|
||||
origin = getattr(type, "__origin__", None)
|
||||
return origin is tuple and ... not in type.__args__
|
||||
|
||||
|
||||
def is_protocol(type: Any) -> bool:
|
||||
return is_subclass(type, Protocol) and getattr(type, "_is_protocol", False)
|
||||
|
||||
|
||||
def is_bare_final(type) -> bool:
|
||||
return type is Final
|
||||
|
||||
|
||||
def get_final_base(type) -> Optional[type]:
|
||||
"""Return the base of the Final annotation, if it is Final."""
|
||||
if type is Final:
|
||||
return Any
|
||||
if type.__class__ is _GenericAlias and type.__origin__ is Final:
|
||||
return type.__args__[0]
|
||||
return None
|
||||
|
||||
|
||||
OriginAbstractSet = AbcSet
|
||||
OriginMutableSet = AbcMutableSet
|
||||
|
||||
signature = _signature
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
signature = partial(_signature, eval_str=True)
|
||||
|
||||
|
||||
try:
|
||||
# Not present on 3.9.0, so we try carefully.
|
||||
from typing import _LiteralGenericAlias
|
||||
|
||||
def is_literal(type: Any) -> bool:
|
||||
"""Is this a literal?"""
|
||||
return type in LITERALS or (
|
||||
isinstance(
|
||||
type, (_GenericAlias, _LiteralGenericAlias, _SpecialGenericAlias)
|
||||
)
|
||||
and type.__origin__ in LITERALS
|
||||
)
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
|
||||
def is_literal(_) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
Set = AbcSet
|
||||
MutableSet = AbcMutableSet
|
||||
Sequence = AbcSequence
|
||||
MutableSequence = AbcMutableSequence
|
||||
MutableMapping = AbcMutableMapping
|
||||
Mapping = AbcMapping
|
||||
FrozenSetSubscriptable = frozenset
|
||||
TupleSubscriptable = tuple
|
||||
|
||||
|
||||
def is_annotated(type) -> bool:
|
||||
return getattr(type, "__class__", None) is _AnnotatedAlias
|
||||
|
||||
|
||||
def is_tuple(type):
|
||||
return (
|
||||
type in (Tuple, tuple)
|
||||
or (type.__class__ is _GenericAlias and is_subclass(type.__origin__, Tuple))
|
||||
or (getattr(type, "__origin__", None) is tuple)
|
||||
)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
|
||||
def is_union_type(obj):
|
||||
from types import UnionType
|
||||
|
||||
return (
|
||||
obj is Union
|
||||
or (isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union)
|
||||
or isinstance(obj, UnionType)
|
||||
)
|
||||
|
||||
def get_newtype_base(typ: Any) -> Optional[type]:
|
||||
if typ is NewType or isinstance(typ, NewType):
|
||||
return typ.__supertype__
|
||||
return None
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import NotRequired, Required
|
||||
else:
|
||||
from typing_extensions import NotRequired, Required
|
||||
|
||||
else:
|
||||
# 3.9
|
||||
from typing_extensions import NotRequired, Required
|
||||
|
||||
def is_union_type(obj):
|
||||
return obj is Union or (
|
||||
isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union
|
||||
)
|
||||
|
||||
def get_newtype_base(typ: Any) -> Optional[type]:
|
||||
supertype = getattr(typ, "__supertype__", None)
|
||||
if (
|
||||
supertype is not None
|
||||
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
|
||||
and typ.__module__ in ("typing", "typing_extensions")
|
||||
):
|
||||
return supertype
|
||||
return None
|
||||
|
||||
|
||||
def get_notrequired_base(type) -> Union[Any, NothingType]:
|
||||
if is_annotated(type):
|
||||
# Handle `Annotated[NotRequired[int]]`
|
||||
type = get_args(type)[0]
|
||||
if get_origin(type) in (NotRequired, Required):
|
||||
return get_args(type)[0]
|
||||
return NOTHING
|
||||
|
||||
|
||||
def is_sequence(type: Any) -> bool:
|
||||
"""A predicate function for sequences.
|
||||
|
||||
Matches lists, sequences, mutable sequences, deques and homogenous
|
||||
tuples.
|
||||
"""
|
||||
origin = getattr(type, "__origin__", None)
|
||||
return (
|
||||
type
|
||||
in (
|
||||
List,
|
||||
list,
|
||||
TypingSequence,
|
||||
TypingMutableSequence,
|
||||
AbcMutableSequence,
|
||||
tuple,
|
||||
Tuple,
|
||||
deque,
|
||||
Deque,
|
||||
)
|
||||
or (
|
||||
type.__class__ is _GenericAlias
|
||||
and (
|
||||
((origin is not tuple) and is_subclass(origin, TypingSequence))
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
)
|
||||
or (origin in (list, deque, AbcMutableSequence, AbcSequence))
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
|
||||
|
||||
def is_deque(type):
|
||||
return (
|
||||
type in (deque, Deque)
|
||||
or (type.__class__ is _GenericAlias and is_subclass(type.__origin__, deque))
|
||||
or (getattr(type, "__origin__", None) is deque)
|
||||
)
|
||||
|
||||
|
||||
def is_mutable_set(type: Any) -> bool:
|
||||
"""A predicate function for (mutable) sets.
|
||||
|
||||
Matches built-in sets and sets from the typing module.
|
||||
"""
|
||||
return (
|
||||
type in (TypingSet, TypingMutableSet, set)
|
||||
or (
|
||||
type.__class__ is _GenericAlias
|
||||
and is_subclass(type.__origin__, TypingMutableSet)
|
||||
)
|
||||
or (getattr(type, "__origin__", None) in (set, AbcMutableSet, AbcSet))
|
||||
)
|
||||
|
||||
|
||||
def is_frozenset(type: Any) -> bool:
|
||||
"""A predicate function for frozensets.
|
||||
|
||||
Matches built-in frozensets and frozensets from the typing module.
|
||||
"""
|
||||
return (
|
||||
type in (FrozenSet, frozenset)
|
||||
or (type.__class__ is _GenericAlias and is_subclass(type.__origin__, FrozenSet))
|
||||
or (getattr(type, "__origin__", None) is frozenset)
|
||||
)
|
||||
|
||||
|
||||
def is_bare(type):
|
||||
return isinstance(type, _SpecialGenericAlias) or (
|
||||
not hasattr(type, "__origin__") and not hasattr(type, "__args__")
|
||||
)
|
||||
|
||||
|
||||
def is_mapping(type: Any) -> bool:
|
||||
"""A predicate function for mappings."""
|
||||
return (
|
||||
type in (dict, Dict, TypingMapping, TypingMutableMapping, AbcMutableMapping)
|
||||
or (
|
||||
type.__class__ is _GenericAlias
|
||||
and is_subclass(type.__origin__, TypingMapping)
|
||||
)
|
||||
or is_subclass(
|
||||
getattr(type, "__origin__", type), (dict, AbcMutableMapping, AbcMapping)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_counter(type):
|
||||
return (
|
||||
type in (Counter, TypingCounter) or getattr(type, "__origin__", None) is Counter
|
||||
)
|
||||
|
||||
|
||||
def is_generic(type) -> bool:
|
||||
"""Whether `type` is a generic type."""
|
||||
# Inheriting from protocol will inject `Generic` into the MRO
|
||||
# without `__orig_bases__`.
|
||||
return isinstance(type, (_GenericAlias, GenericAlias)) or (
|
||||
is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
|
||||
)
|
||||
|
||||
|
||||
def copy_with(type, args):
|
||||
"""Replace a generic type's arguments."""
|
||||
if is_annotated(type):
|
||||
# typing.Annotated requires a special case.
|
||||
return Annotated[args]
|
||||
if isinstance(args, tuple) and len(args) == 1:
|
||||
# Some annotations can't handle 1-tuples.
|
||||
args = args[0]
|
||||
return type.__origin__[args]
|
||||
|
||||
|
||||
def get_full_type_hints(obj, globalns=None, localns=None):
|
||||
return get_type_hints(obj, globalns, localns, include_extras=True)
|
||||
|
||||
|
||||
def is_generic_attrs(type) -> bool:
|
||||
"""Return True for both specialized (A[int]) and unspecialized (A) generics."""
|
||||
return is_generic(type) and has(type.__origin__)
|
||||
@@ -0,0 +1,32 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from attrs import NOTHING
|
||||
from typing_extensions import Self
|
||||
|
||||
from ._compat import copy_with, get_args, is_annotated, is_generic
|
||||
|
||||
|
||||
def deep_copy_with(t, mapping: Mapping[str, Any], self_is=NOTHING):
|
||||
args = get_args(t)
|
||||
rest = ()
|
||||
if is_annotated(t) and args:
|
||||
# If we're dealing with `Annotated`, we only map the first type parameter
|
||||
rest = tuple(args[1:])
|
||||
args = (args[0],)
|
||||
new_args = (
|
||||
tuple(
|
||||
(
|
||||
self_is
|
||||
if a is Self and self_is is not NOTHING
|
||||
else (
|
||||
mapping[a.__name__]
|
||||
if hasattr(a, "__name__") and a.__name__ in mapping
|
||||
else (deep_copy_with(a, mapping, self_is) if is_generic(a) else a)
|
||||
)
|
||||
)
|
||||
for a in args
|
||||
)
|
||||
+ rest
|
||||
)
|
||||
return copy_with(t, new_args) if new_args != args else t
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Utility functions for collections."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
DefaultDict,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
TypeVar,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from attrs import NOTHING, Attribute, NothingType
|
||||
|
||||
from ._compat import (
|
||||
ANIES,
|
||||
get_args,
|
||||
get_origin,
|
||||
is_bare,
|
||||
is_frozenset,
|
||||
is_mapping,
|
||||
is_sequence,
|
||||
is_subclass,
|
||||
)
|
||||
from ._compat import is_mutable_set as is_set
|
||||
from .dispatch import StructureHook, UnstructureHook
|
||||
from .errors import IterableValidationError, IterableValidationNote
|
||||
from .fns import identity
|
||||
from .gen import (
|
||||
AttributeOverride,
|
||||
already_generating,
|
||||
make_dict_structure_fn_from_attrs,
|
||||
make_dict_unstructure_fn_from_attrs,
|
||||
make_hetero_tuple_unstructure_fn,
|
||||
mapping_structure_factory,
|
||||
mapping_unstructure_factory,
|
||||
)
|
||||
from .gen import make_iterable_unstructure_fn as iterable_unstructure_factory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .converters import BaseConverter
|
||||
|
||||
__all__ = [
|
||||
"defaultdict_structure_factory",
|
||||
"is_any_set",
|
||||
"is_defaultdict",
|
||||
"is_frozenset",
|
||||
"is_mapping",
|
||||
"is_namedtuple",
|
||||
"is_sequence",
|
||||
"is_set",
|
||||
"iterable_unstructure_factory",
|
||||
"list_structure_factory",
|
||||
"mapping_structure_factory",
|
||||
"mapping_unstructure_factory",
|
||||
"namedtuple_dict_structure_factory",
|
||||
"namedtuple_dict_unstructure_factory",
|
||||
"namedtuple_structure_factory",
|
||||
"namedtuple_unstructure_factory",
|
||||
]
|
||||
|
||||
|
||||
def is_any_set(type) -> bool:
|
||||
"""A predicate function for both mutable and frozensets."""
|
||||
return is_set(type) or is_frozenset(type)
|
||||
|
||||
|
||||
def is_namedtuple(type: Any) -> bool:
|
||||
"""A predicate function for named tuples."""
|
||||
|
||||
if is_subclass(type, tuple):
|
||||
for cl in type.mro():
|
||||
orig_bases = cl.__dict__.get("__orig_bases__", ())
|
||||
if NamedTuple in orig_bases:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_passthrough(type: type[tuple], converter: BaseConverter) -> bool:
|
||||
"""If all fields would be passed through, this class should not be processed
|
||||
either.
|
||||
"""
|
||||
return all(
|
||||
converter.get_unstructure_hook(t) == identity
|
||||
for t in type.__annotations__.values()
|
||||
)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def list_structure_factory(type: type, converter: BaseConverter) -> StructureHook:
|
||||
"""A hook factory for structuring lists.
|
||||
|
||||
Converts any given iterable into a list.
|
||||
"""
|
||||
|
||||
if is_bare(type) or type.__args__[0] in ANIES:
|
||||
|
||||
def structure_list(obj: Iterable[T], _: type = type) -> list[T]:
|
||||
return list(obj)
|
||||
|
||||
return structure_list
|
||||
|
||||
elem_type = type.__args__[0]
|
||||
|
||||
try:
|
||||
handler = converter.get_structure_hook(elem_type)
|
||||
except RecursionError:
|
||||
# Break the cycle by using late binding.
|
||||
handler = converter.structure
|
||||
|
||||
if converter.detailed_validation:
|
||||
|
||||
def structure_list(
|
||||
obj: Iterable[T], _: type = type, _handler=handler, _elem_type=elem_type
|
||||
) -> list[T]:
|
||||
errors = []
|
||||
res = []
|
||||
ix = 0 # Avoid `enumerate` for performance.
|
||||
for e in obj:
|
||||
try:
|
||||
res.append(handler(e, _elem_type))
|
||||
except Exception as e:
|
||||
msg = IterableValidationNote(
|
||||
f"Structuring {type} @ index {ix}", ix, elem_type
|
||||
)
|
||||
e.__notes__ = [*getattr(e, "__notes__", []), msg]
|
||||
errors.append(e)
|
||||
finally:
|
||||
ix += 1
|
||||
if errors:
|
||||
raise IterableValidationError(
|
||||
f"While structuring {type!r}", errors, type
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
else:
|
||||
|
||||
def structure_list(
|
||||
obj: Iterable[T], _: type = type, _handler=handler, _elem_type=elem_type
|
||||
) -> list[T]:
|
||||
return [_handler(e, _elem_type) for e in obj]
|
||||
|
||||
return structure_list
|
||||
|
||||
|
||||
def namedtuple_unstructure_factory(
|
||||
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
|
||||
) -> UnstructureHook:
|
||||
"""A hook factory for unstructuring namedtuples.
|
||||
|
||||
:param unstructure_to: Force unstructuring to this type, if provided.
|
||||
"""
|
||||
|
||||
if unstructure_to is None and _is_passthrough(cl, converter):
|
||||
return identity
|
||||
|
||||
return make_hetero_tuple_unstructure_fn(
|
||||
cl,
|
||||
converter,
|
||||
unstructure_to=tuple if unstructure_to is None else unstructure_to,
|
||||
type_args=tuple(cl.__annotations__.values()),
|
||||
)
|
||||
|
||||
|
||||
def namedtuple_structure_factory(
|
||||
cl: type[tuple], converter: BaseConverter
|
||||
) -> StructureHook:
|
||||
"""A hook factory for structuring namedtuples from iterables."""
|
||||
# We delegate to the existing infrastructure for heterogenous tuples.
|
||||
hetero_tuple_type = tuple[tuple(cl.__annotations__.values())]
|
||||
base_hook = converter.get_structure_hook(hetero_tuple_type)
|
||||
return lambda v, _: cl(*base_hook(v, hetero_tuple_type))
|
||||
|
||||
|
||||
def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
|
||||
"""Generate pseudo attributes for a namedtuple."""
|
||||
return [
|
||||
Attribute(
|
||||
name,
|
||||
cl._field_defaults.get(name, NOTHING),
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
type=a,
|
||||
alias=name,
|
||||
)
|
||||
for name, a in get_type_hints(cl).items()
|
||||
]
|
||||
|
||||
|
||||
def namedtuple_dict_structure_factory(
|
||||
cl: type[tuple],
|
||||
converter: BaseConverter,
|
||||
detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||
forbid_extra_keys: bool = False,
|
||||
use_linecache: bool = True,
|
||||
/,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> StructureHook:
|
||||
"""A hook factory for hooks structuring namedtuples from dictionaries.
|
||||
|
||||
:param forbid_extra_keys: Whether the hook should raise a `ForbiddenExtraKeysError`
|
||||
if unknown keys are encountered.
|
||||
:param use_linecache: Whether to store the source code in the Python linecache.
|
||||
|
||||
.. versionadded:: 24.1.0
|
||||
"""
|
||||
try:
|
||||
working_set = already_generating.working_set
|
||||
except AttributeError:
|
||||
working_set = set()
|
||||
already_generating.working_set = working_set
|
||||
else:
|
||||
if cl in working_set:
|
||||
raise RecursionError()
|
||||
|
||||
working_set.add(cl)
|
||||
|
||||
try:
|
||||
return make_dict_structure_fn_from_attrs(
|
||||
_namedtuple_to_attrs(cl),
|
||||
cl,
|
||||
converter,
|
||||
_cattrs_forbid_extra_keys=forbid_extra_keys,
|
||||
_cattrs_use_detailed_validation=detailed_validation,
|
||||
_cattrs_use_linecache=use_linecache,
|
||||
**kwargs,
|
||||
)
|
||||
finally:
|
||||
working_set.remove(cl)
|
||||
if not working_set:
|
||||
del already_generating.working_set
|
||||
|
||||
|
||||
def namedtuple_dict_unstructure_factory(
|
||||
cl: type[tuple],
|
||||
converter: BaseConverter,
|
||||
omit_if_default: bool = False,
|
||||
use_linecache: bool = True,
|
||||
/,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> UnstructureHook:
|
||||
"""A hook factory for hooks unstructuring namedtuples to dictionaries.
|
||||
|
||||
:param omit_if_default: When true, attributes equal to their default values
|
||||
will be omitted in the result dictionary.
|
||||
:param use_linecache: Whether to store the source code in the Python linecache.
|
||||
|
||||
.. versionadded:: 24.1.0
|
||||
"""
|
||||
try:
|
||||
working_set = already_generating.working_set
|
||||
except AttributeError:
|
||||
working_set = set()
|
||||
already_generating.working_set = working_set
|
||||
if cl in working_set:
|
||||
raise RecursionError()
|
||||
|
||||
working_set.add(cl)
|
||||
|
||||
try:
|
||||
return make_dict_unstructure_fn_from_attrs(
|
||||
_namedtuple_to_attrs(cl),
|
||||
cl,
|
||||
converter,
|
||||
_cattrs_omit_if_default=omit_if_default,
|
||||
_cattrs_use_linecache=use_linecache,
|
||||
**kwargs,
|
||||
)
|
||||
finally:
|
||||
working_set.remove(cl)
|
||||
if not working_set:
|
||||
del already_generating.working_set
|
||||
|
||||
|
||||
def is_defaultdict(type: Any) -> bool:
|
||||
"""Is this type a defaultdict?
|
||||
|
||||
Bare defaultdicts (defaultdicts with no type arguments) are not supported
|
||||
since there's no way to discover their _default_factory_.
|
||||
"""
|
||||
return is_subclass(get_origin(type), (defaultdict, DefaultDict))
|
||||
|
||||
|
||||
def defaultdict_structure_factory(
|
||||
type: type[defaultdict],
|
||||
converter: BaseConverter,
|
||||
default_factory: Callable[[], Any] | NothingType = NOTHING,
|
||||
) -> StructureHook:
|
||||
"""A structure hook factory for defaultdicts.
|
||||
|
||||
The value type parameter will be used as the _default factory_.
|
||||
"""
|
||||
if default_factory is NOTHING:
|
||||
default_factory = get_args(type)[1]
|
||||
return mapping_structure_factory(
|
||||
type, converter, partial(defaultdict, default_factory)
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
"""Utilities for union (sum type) disambiguation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import MISSING
|
||||
from functools import reduce
|
||||
from operator import or_
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, Union
|
||||
|
||||
from attrs import NOTHING, Attribute, AttrsInstance
|
||||
|
||||
from ._compat import (
|
||||
NoneType,
|
||||
adapted_fields,
|
||||
fields_dict,
|
||||
get_args,
|
||||
get_origin,
|
||||
has,
|
||||
is_literal,
|
||||
is_union_type,
|
||||
)
|
||||
from .gen import AttributeOverride
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .converters import BaseConverter
|
||||
|
||||
__all__ = ["create_default_dis_func", "is_supported_union"]
|
||||
|
||||
|
||||
def is_supported_union(typ: Any) -> bool:
|
||||
"""Whether the type is a union of attrs classes or dataclasses."""
|
||||
return is_union_type(typ) and all(
|
||||
e is NoneType or has(get_origin(e) or e) for e in typ.__args__
|
||||
)
|
||||
|
||||
|
||||
def create_default_dis_func(
|
||||
converter: BaseConverter,
|
||||
*classes: type[AttrsInstance],
|
||||
use_literals: bool = True,
|
||||
overrides: (
|
||||
dict[str, AttributeOverride] | Literal["from_converter"]
|
||||
) = "from_converter",
|
||||
) -> Callable[[Mapping[Any, Any]], type[Any] | None]:
|
||||
"""Given attrs classes or dataclasses, generate a disambiguation function.
|
||||
|
||||
The function is based on unique fields without defaults or unique values.
|
||||
|
||||
:param use_literals: Whether to try using fields annotated as literals for
|
||||
disambiguation.
|
||||
:param overrides: Attribute overrides to apply.
|
||||
|
||||
.. versionchanged:: 24.1.0
|
||||
Dataclasses are now supported.
|
||||
"""
|
||||
if len(classes) < 2:
|
||||
raise ValueError("At least two classes required.")
|
||||
|
||||
if overrides == "from_converter":
|
||||
overrides = [
|
||||
getattr(converter.get_structure_hook(c), "overrides", {}) for c in classes
|
||||
]
|
||||
else:
|
||||
overrides = [overrides for _ in classes]
|
||||
|
||||
# first, attempt for unique values
|
||||
if use_literals:
|
||||
# requirements for a discriminator field:
|
||||
# (... TODO: a single fallback is OK)
|
||||
# - it must always be enumerated
|
||||
cls_candidates = [
|
||||
{
|
||||
at.name
|
||||
for at in adapted_fields(get_origin(cl) or cl)
|
||||
if is_literal(at.type)
|
||||
}
|
||||
for cl in classes
|
||||
]
|
||||
|
||||
# literal field names common to all members
|
||||
discriminators: set[str] = cls_candidates[0]
|
||||
for possible_discriminators in cls_candidates:
|
||||
discriminators &= possible_discriminators
|
||||
|
||||
best_result = None
|
||||
best_discriminator = None
|
||||
for discriminator in discriminators:
|
||||
# maps Literal values (strings, ints...) to classes
|
||||
mapping = defaultdict(list)
|
||||
|
||||
for cl in classes:
|
||||
for key in get_args(
|
||||
fields_dict(get_origin(cl) or cl)[discriminator].type
|
||||
):
|
||||
mapping[key].append(cl)
|
||||
|
||||
if best_result is None or max(len(v) for v in mapping.values()) <= max(
|
||||
len(v) for v in best_result.values()
|
||||
):
|
||||
best_result = mapping
|
||||
best_discriminator = discriminator
|
||||
|
||||
if (
|
||||
best_result
|
||||
and best_discriminator
|
||||
and max(len(v) for v in best_result.values()) != len(classes)
|
||||
):
|
||||
final_mapping = {
|
||||
k: v[0] if len(v) == 1 else Union[tuple(v)]
|
||||
for k, v in best_result.items()
|
||||
}
|
||||
|
||||
def dis_func(data: Mapping[Any, Any]) -> type | None:
|
||||
if not isinstance(data, Mapping):
|
||||
raise ValueError("Only input mappings are supported.")
|
||||
return final_mapping[data[best_discriminator]]
|
||||
|
||||
return dis_func
|
||||
|
||||
# next, attempt for unique keys
|
||||
|
||||
# NOTE: This could just as well work with just field availability and not
|
||||
# uniqueness, returning Unions ... it doesn't do that right now.
|
||||
cls_and_attrs = [
|
||||
(cl, *_usable_attribute_names(cl, override))
|
||||
for cl, override in zip(classes, overrides)
|
||||
]
|
||||
# For each class, attempt to generate a single unique required field.
|
||||
uniq_attrs_dict: dict[str, type] = {}
|
||||
|
||||
# We start from classes with the largest number of unique fields
|
||||
# so we can do easy picks first, making later picks easier.
|
||||
cls_and_attrs.sort(key=lambda c_a: len(c_a[1]), reverse=True)
|
||||
|
||||
fallback = None # If none match, try this.
|
||||
|
||||
for cl, cl_reqs, back_map in cls_and_attrs:
|
||||
# We do not have to consider classes we've already processed, since
|
||||
# they will have been eliminated by the match dictionary already.
|
||||
other_classes = [
|
||||
c_and_a
|
||||
for c_and_a in cls_and_attrs
|
||||
if c_and_a[0] is not cl and c_and_a[0] not in uniq_attrs_dict.values()
|
||||
]
|
||||
other_reqs = reduce(or_, (c_a[1] for c_a in other_classes), set())
|
||||
uniq = cl_reqs - other_reqs
|
||||
|
||||
# We want a unique attribute with no default.
|
||||
cl_fields = fields_dict(get_origin(cl) or cl)
|
||||
for maybe_renamed_attr_name in uniq:
|
||||
orig_name = back_map[maybe_renamed_attr_name]
|
||||
if cl_fields[orig_name].default in (NOTHING, MISSING):
|
||||
break
|
||||
else:
|
||||
if fallback is None:
|
||||
fallback = cl
|
||||
continue
|
||||
raise TypeError(f"{cl} has no usable non-default attributes")
|
||||
uniq_attrs_dict[maybe_renamed_attr_name] = cl
|
||||
|
||||
if fallback is None:
|
||||
|
||||
def dis_func(data: Mapping[Any, Any]) -> type[AttrsInstance] | None:
|
||||
if not isinstance(data, Mapping):
|
||||
raise ValueError("Only input mappings are supported")
|
||||
for k, v in uniq_attrs_dict.items():
|
||||
if k in data:
|
||||
return v
|
||||
raise ValueError("Couldn't disambiguate")
|
||||
|
||||
else:
|
||||
|
||||
def dis_func(data: Mapping[Any, Any]) -> type[AttrsInstance] | None:
|
||||
if not isinstance(data, Mapping):
|
||||
raise ValueError("Only input mappings are supported")
|
||||
for k, v in uniq_attrs_dict.items():
|
||||
if k in data:
|
||||
return v
|
||||
return fallback
|
||||
|
||||
return dis_func
|
||||
|
||||
|
||||
create_uniq_field_dis_func = create_default_dis_func
|
||||
|
||||
|
||||
def _overriden_name(at: Attribute, override: AttributeOverride | None) -> str:
|
||||
if override is None or override.rename is None:
|
||||
return at.name
|
||||
return override.rename
|
||||
|
||||
|
||||
def _usable_attribute_names(
|
||||
cl: type[Any], overrides: dict[str, AttributeOverride]
|
||||
) -> tuple[set[str], dict[str, str]]:
|
||||
"""Return renamed fields and a mapping to original field names."""
|
||||
res = set()
|
||||
mapping = {}
|
||||
|
||||
for at in adapted_fields(get_origin(cl) or cl):
|
||||
res.add(n := _overriden_name(at, overrides.get(at.name)))
|
||||
mapping[n] = at.name
|
||||
|
||||
return res, mapping
|
||||
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache, singledispatch
|
||||
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, TypeVar
|
||||
|
||||
from attrs import Factory, define
|
||||
|
||||
from ._compat import TypeAlias
|
||||
from .fns import Predicate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .converters import BaseConverter
|
||||
|
||||
TargetType: TypeAlias = Any
|
||||
UnstructuredValue: TypeAlias = Any
|
||||
StructuredValue: TypeAlias = Any
|
||||
|
||||
StructureHook: TypeAlias = Callable[[UnstructuredValue, TargetType], StructuredValue]
|
||||
UnstructureHook: TypeAlias = Callable[[StructuredValue], UnstructuredValue]
|
||||
|
||||
Hook = TypeVar("Hook", StructureHook, UnstructureHook)
|
||||
HookFactory: TypeAlias = Callable[[TargetType], Hook]
|
||||
|
||||
|
||||
@define
|
||||
class _DispatchNotFound:
|
||||
"""A dummy object to help signify a dispatch not found."""
|
||||
|
||||
|
||||
@define
|
||||
class FunctionDispatch:
|
||||
"""
|
||||
FunctionDispatch is similar to functools.singledispatch, but
|
||||
instead dispatches based on functions that take the type of the
|
||||
first argument in the method, and return True or False.
|
||||
|
||||
objects that help determine dispatch should be instantiated objects.
|
||||
|
||||
:param converter: A converter to be used for factories that require converters.
|
||||
|
||||
.. versionchanged:: 24.1.0
|
||||
Support for factories that require converters, hence this requires a
|
||||
converter when creating.
|
||||
"""
|
||||
|
||||
_converter: BaseConverter
|
||||
_handler_pairs: list[tuple[Predicate, Callable[[Any, Any], Any], bool, bool]] = (
|
||||
Factory(list)
|
||||
)
|
||||
|
||||
def register(
|
||||
self,
|
||||
predicate: Predicate,
|
||||
func: Callable[..., Any],
|
||||
is_generator=False,
|
||||
takes_converter=False,
|
||||
) -> None:
|
||||
self._handler_pairs.insert(0, (predicate, func, is_generator, takes_converter))
|
||||
|
||||
def dispatch(self, typ: Any) -> Callable[..., Any] | None:
|
||||
"""
|
||||
Return the appropriate handler for the object passed.
|
||||
"""
|
||||
for can_handle, handler, is_generator, takes_converter in self._handler_pairs:
|
||||
# can handle could raise an exception here
|
||||
# such as issubclass being called on an instance.
|
||||
# it's easier to just ignore that case.
|
||||
try:
|
||||
ch = can_handle(typ)
|
||||
except Exception: # noqa: S112
|
||||
continue
|
||||
if ch:
|
||||
if is_generator:
|
||||
if takes_converter:
|
||||
return handler(typ, self._converter)
|
||||
return handler(typ)
|
||||
|
||||
return handler
|
||||
return None
|
||||
|
||||
def get_num_fns(self) -> int:
|
||||
return len(self._handler_pairs)
|
||||
|
||||
def copy_to(self, other: FunctionDispatch, skip: int = 0) -> None:
|
||||
other._handler_pairs = self._handler_pairs[:-skip] + other._handler_pairs
|
||||
|
||||
|
||||
@define(init=False)
|
||||
class MultiStrategyDispatch(Generic[Hook]):
|
||||
"""
|
||||
MultiStrategyDispatch uses a combination of exact-match dispatch,
|
||||
singledispatch, and FunctionDispatch.
|
||||
|
||||
:param fallback_factory: A hook factory to be called when a hook cannot be
|
||||
produced.
|
||||
:param converter: A converter to be used for factories that require converters.
|
||||
|
||||
.. versionchanged:: 23.2.0
|
||||
Fallbacks are now factories.
|
||||
.. versionchanged:: 24.1.0
|
||||
Support for factories that require converters, hence this requires a
|
||||
converter when creating.
|
||||
"""
|
||||
|
||||
_fallback_factory: HookFactory[Hook]
|
||||
_direct_dispatch: dict[TargetType, Hook]
|
||||
_function_dispatch: FunctionDispatch
|
||||
_single_dispatch: Any
|
||||
dispatch: Callable[[TargetType, BaseConverter], Hook]
|
||||
|
||||
def __init__(
|
||||
self, fallback_factory: HookFactory[Hook], converter: BaseConverter
|
||||
) -> None:
|
||||
self._fallback_factory = fallback_factory
|
||||
self._direct_dispatch = {}
|
||||
self._function_dispatch = FunctionDispatch(converter)
|
||||
self._single_dispatch = singledispatch(_DispatchNotFound)
|
||||
self.dispatch = lru_cache(maxsize=None)(self.dispatch_without_caching)
|
||||
|
||||
def dispatch_without_caching(self, typ: TargetType) -> Hook:
|
||||
"""Dispatch on the type but without caching the result."""
|
||||
try:
|
||||
dispatch = self._single_dispatch.dispatch(typ)
|
||||
if dispatch is not _DispatchNotFound:
|
||||
return dispatch
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
direct_dispatch = self._direct_dispatch.get(typ)
|
||||
if direct_dispatch is not None:
|
||||
return direct_dispatch
|
||||
|
||||
res = self._function_dispatch.dispatch(typ)
|
||||
return res if res is not None else self._fallback_factory(typ)
|
||||
|
||||
def register_cls_list(self, cls_and_handler, direct: bool = False) -> None:
|
||||
"""Register a class to direct or singledispatch."""
|
||||
for cls, handler in cls_and_handler:
|
||||
if direct:
|
||||
self._direct_dispatch[cls] = handler
|
||||
else:
|
||||
self._single_dispatch.register(cls, handler)
|
||||
self.clear_direct()
|
||||
self.dispatch.cache_clear()
|
||||
|
||||
def register_func_list(
|
||||
self,
|
||||
pred_and_handler: list[
|
||||
tuple[Predicate, Any]
|
||||
| tuple[Predicate, Any, bool]
|
||||
| tuple[Predicate, Callable[[Any, BaseConverter], Any], Literal["extended"]]
|
||||
],
|
||||
):
|
||||
"""
|
||||
Register a predicate function to determine if the handler
|
||||
should be used for the type.
|
||||
|
||||
:param pred_and_handler: The list of predicates and their associated
|
||||
handlers. If a handler is registered in `extended` mode, it's a
|
||||
factory that requires a converter.
|
||||
"""
|
||||
for tup in pred_and_handler:
|
||||
if len(tup) == 2:
|
||||
func, handler = tup
|
||||
self._function_dispatch.register(func, handler)
|
||||
else:
|
||||
func, handler, is_gen = tup
|
||||
if is_gen == "extended":
|
||||
self._function_dispatch.register(
|
||||
func, handler, is_generator=is_gen, takes_converter=True
|
||||
)
|
||||
else:
|
||||
self._function_dispatch.register(func, handler, is_generator=is_gen)
|
||||
self.clear_direct()
|
||||
self.dispatch.cache_clear()
|
||||
|
||||
def clear_direct(self) -> None:
|
||||
"""Clear the direct dispatch."""
|
||||
self._direct_dispatch.clear()
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear all caches."""
|
||||
self._direct_dispatch.clear()
|
||||
self.dispatch.cache_clear()
|
||||
|
||||
def get_num_fns(self) -> int:
|
||||
return self._function_dispatch.get_num_fns()
|
||||
|
||||
def copy_to(self, other: MultiStrategyDispatch, skip: int = 0) -> None:
|
||||
self._function_dispatch.copy_to(other._function_dispatch, skip=skip)
|
||||
for cls, fn in self._single_dispatch.registry.items():
|
||||
other._single_dispatch.register(cls, fn)
|
||||
other.clear_cache()
|
||||
@@ -0,0 +1,132 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from cattrs._compat import ExceptionGroup
|
||||
|
||||
|
||||
class StructureHandlerNotFoundError(Exception):
|
||||
"""
|
||||
Error raised when structuring cannot find a handler for converting inputs into
|
||||
:attr:`type_`.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, type_: type) -> None:
|
||||
super().__init__(message)
|
||||
self.type_ = type_
|
||||
|
||||
|
||||
class BaseValidationError(ExceptionGroup):
|
||||
cl: type
|
||||
|
||||
def __new__(cls, message: str, excs: Sequence[Exception], cl: type):
|
||||
obj = super().__new__(cls, message, excs)
|
||||
obj.cl = cl
|
||||
return obj
|
||||
|
||||
def derive(self, excs: Sequence[Exception]) -> Self:
|
||||
return self.__class__(self.message, excs, self.cl)
|
||||
|
||||
|
||||
class IterableValidationNote(str):
|
||||
"""Attached as a note to an exception when an iterable element fails structuring."""
|
||||
|
||||
index: Union[int, str] # Ints for list indices, strs for dict keys
|
||||
type: Any
|
||||
|
||||
def __new__(
|
||||
cls, string: str, index: Union[int, str], type: Any
|
||||
) -> "IterableValidationNote":
|
||||
instance = str.__new__(cls, string)
|
||||
instance.index = index
|
||||
instance.type = type
|
||||
return instance
|
||||
|
||||
def __getnewargs__(self) -> tuple[str, Union[int, str], Any]:
|
||||
return (str(self), self.index, self.type)
|
||||
|
||||
|
||||
class IterableValidationError(BaseValidationError):
|
||||
"""Raised when structuring an iterable."""
|
||||
|
||||
def group_exceptions(
|
||||
self,
|
||||
) -> tuple[list[tuple[Exception, IterableValidationNote]], list[Exception]]:
|
||||
"""Split the exceptions into two groups: with and without validation notes."""
|
||||
excs_with_notes = []
|
||||
other_excs = []
|
||||
for subexc in self.exceptions:
|
||||
if hasattr(subexc, "__notes__"):
|
||||
for note in subexc.__notes__:
|
||||
if note.__class__ is IterableValidationNote:
|
||||
excs_with_notes.append((subexc, note))
|
||||
break
|
||||
else:
|
||||
other_excs.append(subexc)
|
||||
else:
|
||||
other_excs.append(subexc)
|
||||
|
||||
return excs_with_notes, other_excs
|
||||
|
||||
|
||||
class AttributeValidationNote(str):
|
||||
"""Attached as a note to an exception when an attribute fails structuring."""
|
||||
|
||||
name: str
|
||||
type: Any
|
||||
|
||||
def __new__(cls, string: str, name: str, type: Any) -> "AttributeValidationNote":
|
||||
instance = str.__new__(cls, string)
|
||||
instance.name = name
|
||||
instance.type = type
|
||||
return instance
|
||||
|
||||
def __getnewargs__(self) -> tuple[str, str, Any]:
|
||||
return (str(self), self.name, self.type)
|
||||
|
||||
|
||||
class ClassValidationError(BaseValidationError):
|
||||
"""Raised when validating a class if any attributes are invalid."""
|
||||
|
||||
def group_exceptions(
|
||||
self,
|
||||
) -> tuple[list[tuple[Exception, AttributeValidationNote]], list[Exception]]:
|
||||
"""Split the exceptions into two groups: with and without validation notes."""
|
||||
excs_with_notes = []
|
||||
other_excs = []
|
||||
for subexc in self.exceptions:
|
||||
if hasattr(subexc, "__notes__"):
|
||||
for note in subexc.__notes__:
|
||||
if note.__class__ is AttributeValidationNote:
|
||||
excs_with_notes.append((subexc, note))
|
||||
break
|
||||
else:
|
||||
other_excs.append(subexc)
|
||||
else:
|
||||
other_excs.append(subexc)
|
||||
|
||||
return excs_with_notes, other_excs
|
||||
|
||||
|
||||
class ForbiddenExtraKeysError(Exception):
|
||||
"""
|
||||
Raised when `forbid_extra_keys` is activated and such extra keys are detected
|
||||
during structuring.
|
||||
|
||||
The attribute `extra_fields` is a sequence of those extra keys, which were the
|
||||
cause of this error, and `cl` is the class which was structured with those extra
|
||||
keys.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, message: Optional[str], cl: type, extra_fields: set[str]
|
||||
) -> None:
|
||||
self.cl = cl
|
||||
self.extra_fields = extra_fields
|
||||
cln = cl.__name__
|
||||
|
||||
super().__init__(
|
||||
message
|
||||
or f"Extra fields in constructor for {cln}: {', '.join(extra_fields)}"
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Useful internal functions."""
|
||||
|
||||
from typing import Any, Callable, NoReturn, TypeVar
|
||||
|
||||
from ._compat import TypeAlias
|
||||
from .errors import StructureHandlerNotFoundError
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
Predicate: TypeAlias = Callable[[Any], bool]
|
||||
"""A predicate function determines if a type can be handled."""
|
||||
|
||||
|
||||
def identity(obj: T) -> T:
|
||||
"""The identity function."""
|
||||
return obj
|
||||
|
||||
|
||||
def raise_error(_, cl: Any) -> NoReturn:
|
||||
"""At the bottom of the condition stack, we explode if we can't handle it."""
|
||||
msg = f"Unsupported type: {cl!r}. Register a structure hook for it."
|
||||
raise StructureHandlerNotFoundError(msg, type_=cl)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from threading import local
|
||||
from typing import Any, Callable
|
||||
|
||||
from attrs import frozen
|
||||
|
||||
|
||||
@frozen
|
||||
class AttributeOverride:
|
||||
omit_if_default: bool | None = None
|
||||
rename: str | None = None
|
||||
omit: bool | None = None # Omit the field completely.
|
||||
struct_hook: Callable[[Any, Any], Any] | None = None # Structure hook to use.
|
||||
unstruct_hook: Callable[[Any], Any] | None = None # Structure hook to use.
|
||||
|
||||
|
||||
neutral = AttributeOverride()
|
||||
already_generating = local()
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
from .._compat import get_args, get_origin, is_generic
|
||||
|
||||
|
||||
def _tvar_has_default(tvar) -> bool:
|
||||
"""Does `tvar` have a default?
|
||||
|
||||
In CPython 3.13+ and typing_extensions>=4.12.0:
|
||||
- TypeVars have a `no_default()` method for detecting
|
||||
if a TypeVar has a default
|
||||
- TypeVars with `default=None` have `__default__` set to `None`
|
||||
- TypeVars with no `default` parameter passed
|
||||
have `__default__` set to `typing(_extensions).NoDefault
|
||||
|
||||
On typing_exensions<4.12.0:
|
||||
- TypeVars do not have a `no_default()` method for detecting
|
||||
if a TypeVar has a default
|
||||
- TypeVars with `default=None` have `__default__` set to `NoneType`
|
||||
- TypeVars with no `default` parameter passed
|
||||
have `__default__` set to `typing(_extensions).NoDefault
|
||||
"""
|
||||
try:
|
||||
return tvar.has_default()
|
||||
except AttributeError:
|
||||
# compatibility for typing_extensions<4.12.0
|
||||
return getattr(tvar, "__default__", None) is not None
|
||||
|
||||
|
||||
def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, type]:
|
||||
"""Generate a mapping of typevars to actual types for a generic class."""
|
||||
mapping = dict(old_mapping)
|
||||
|
||||
origin = get_origin(cl)
|
||||
|
||||
if origin is not None:
|
||||
# To handle the cases where classes in the typing module are using
|
||||
# the GenericAlias structure but aren't a Generic and hence
|
||||
# end up in this function but do not have an `__parameters__`
|
||||
# attribute. These classes are interface types, for example
|
||||
# `typing.Hashable`.
|
||||
parameters = getattr(get_origin(cl), "__parameters__", None)
|
||||
if parameters is None:
|
||||
return dict(old_mapping)
|
||||
|
||||
for p, t in zip(parameters, get_args(cl)):
|
||||
if isinstance(t, TypeVar):
|
||||
continue
|
||||
mapping[p.__name__] = t
|
||||
|
||||
elif is_generic(cl):
|
||||
# Origin is None, so this may be a subclass of a generic class.
|
||||
orig_bases = cl.__orig_bases__
|
||||
for base in orig_bases:
|
||||
if not hasattr(base, "__args__"):
|
||||
continue
|
||||
base_args = base.__args__
|
||||
if hasattr(base.__origin__, "__parameters__"):
|
||||
base_params = base.__origin__.__parameters__
|
||||
elif any(_tvar_has_default(base_arg) for base_arg in base_args):
|
||||
# TypeVar with a default e.g. PEP 696
|
||||
# https://www.python.org/dev/peps/pep-0696/
|
||||
# Extract the defaults for the TypeVars and insert
|
||||
# them into the mapping
|
||||
mapping_params = [
|
||||
(base_arg, base_arg.__default__)
|
||||
for base_arg in base_args
|
||||
if _tvar_has_default(base_arg)
|
||||
]
|
||||
base_params, base_args = zip(*mapping_params)
|
||||
else:
|
||||
continue
|
||||
|
||||
for param, arg in zip(base_params, base_args):
|
||||
mapping[param.__name__] = arg
|
||||
|
||||
return mapping
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Line-cache functionality."""
|
||||
|
||||
import linecache
|
||||
|
||||
|
||||
def generate_unique_filename(cls: type, func_name: str, lines: list[str] = []) -> str:
|
||||
"""
|
||||
Create a "filename" suitable for a function being generated.
|
||||
|
||||
If *lines* are provided, insert them in the first free spot or stop
|
||||
if a duplicate is found.
|
||||
"""
|
||||
extra = ""
|
||||
count = 1
|
||||
|
||||
while True:
|
||||
unique_filename = "<cattrs generated {} {}.{}{}>".format(
|
||||
func_name, cls.__module__, getattr(cls, "__qualname__", cls.__name__), extra
|
||||
)
|
||||
if not lines:
|
||||
return unique_filename
|
||||
cache_line = (len("\n".join(lines)), None, lines, unique_filename)
|
||||
if linecache.cache.setdefault(unique_filename, cache_line) == cache_line:
|
||||
return unique_filename
|
||||
|
||||
# Looks like this spot is taken. Try again.
|
||||
count += 1
|
||||
extra = f"-{count}"
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from attrs import NOTHING, Attribute, Factory
|
||||
|
||||
from .._compat import is_bare_final
|
||||
from ..dispatch import StructureHook
|
||||
from ..errors import StructureHandlerNotFoundError
|
||||
from ..fns import raise_error
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
|
||||
|
||||
def find_structure_handler(
|
||||
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
|
||||
) -> StructureHook | None:
|
||||
"""Find the appropriate structure handler to use.
|
||||
|
||||
Return `None` if no handler should be used.
|
||||
"""
|
||||
try:
|
||||
if a.converter is not None and prefer_attrs_converters:
|
||||
# If the user as requested to use attrib converters, use nothing
|
||||
# so it falls back to that.
|
||||
handler = None
|
||||
elif (
|
||||
a.converter is not None and not prefer_attrs_converters and type is not None
|
||||
):
|
||||
try:
|
||||
handler = c.get_structure_hook(type, cache_result=False)
|
||||
except StructureHandlerNotFoundError:
|
||||
handler = None
|
||||
else:
|
||||
# The legacy way, should still work.
|
||||
if handler == raise_error:
|
||||
handler = None
|
||||
elif type is not None:
|
||||
if (
|
||||
is_bare_final(type)
|
||||
and a.default is not NOTHING
|
||||
and not isinstance(a.default, Factory)
|
||||
):
|
||||
# This is a special case where we can use the
|
||||
# type of the default to dispatch on.
|
||||
type = a.default.__class__
|
||||
handler = c.get_structure_hook(type, cache_result=False)
|
||||
if handler == c._structure_call:
|
||||
# Finals can't really be used with _structure_call, so
|
||||
# we wrap it so the rest of the toolchain doesn't get
|
||||
# confused.
|
||||
|
||||
def handler(v, _, _h=handler):
|
||||
return _h(v, type)
|
||||
|
||||
else:
|
||||
handler = c.get_structure_hook(type, cache_result=False)
|
||||
else:
|
||||
handler = c.structure
|
||||
return handler
|
||||
except RecursionError:
|
||||
# This means we're dealing with a reference cycle, so use late binding.
|
||||
return c.structure
|
||||
@@ -0,0 +1,582 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
|
||||
|
||||
from attrs import NOTHING, Attribute
|
||||
from typing_extensions import _TypedDictMeta
|
||||
|
||||
try:
|
||||
from inspect import get_annotations
|
||||
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return get_annotations(cl, eval_str=True)
|
||||
|
||||
except ImportError:
|
||||
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return cl.__dict__.get("__annotations__", {})
|
||||
|
||||
|
||||
from .._compat import (
|
||||
get_full_type_hints,
|
||||
get_notrequired_base,
|
||||
get_origin,
|
||||
is_annotated,
|
||||
is_bare,
|
||||
is_generic,
|
||||
)
|
||||
from .._generics import deep_copy_with
|
||||
from ..errors import (
|
||||
AttributeValidationNote,
|
||||
ClassValidationError,
|
||||
ForbiddenExtraKeysError,
|
||||
StructureHandlerNotFoundError,
|
||||
)
|
||||
from ..fns import identity
|
||||
from . import AttributeOverride
|
||||
from ._consts import already_generating, neutral
|
||||
from ._generics import generate_mapping
|
||||
from ._lc import generate_unique_filename
|
||||
from ._shared import find_structure_handler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
|
||||
__all__ = ["make_dict_structure_fn", "make_dict_unstructure_fn"]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def make_dict_unstructure_fn(
|
||||
cl: type[T],
|
||||
converter: BaseConverter,
|
||||
_cattrs_use_linecache: bool = True,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> Callable[[T], dict[str, Any]]:
|
||||
"""
|
||||
Generate a specialized dict unstructuring function for a TypedDict.
|
||||
|
||||
:param cl: A `TypedDict` class.
|
||||
:param converter: A Converter instance to use for unstructuring nested fields.
|
||||
:param kwargs: A mapping of field names to an `AttributeOverride`, for
|
||||
customization.
|
||||
:param _cattrs_detailed_validation: Whether to store the generated code in the
|
||||
_linecache_, for easier debugging and better stack traces.
|
||||
"""
|
||||
origin = get_origin(cl)
|
||||
attrs = _adapted_fields(origin or cl) # type: ignore
|
||||
req_keys = _required_keys(origin or cl)
|
||||
|
||||
mapping = {}
|
||||
if is_generic(cl):
|
||||
mapping = generate_mapping(cl, mapping)
|
||||
|
||||
for base in getattr(origin, "__orig_bases__", ()):
|
||||
if is_generic(base) and not str(base).startswith("typing.Generic"):
|
||||
mapping = generate_mapping(base, mapping)
|
||||
break
|
||||
|
||||
# It's possible for origin to be None if this is a subclass
|
||||
# of a generic class.
|
||||
if origin is not None:
|
||||
cl = origin
|
||||
|
||||
cl_name = cl.__name__
|
||||
fn_name = "unstructure_typeddict_" + cl_name
|
||||
globs = {}
|
||||
lines = []
|
||||
internal_arg_parts = {}
|
||||
|
||||
# We keep track of what we're generating to help with recursive
|
||||
# class graphs.
|
||||
try:
|
||||
working_set = already_generating.working_set
|
||||
except AttributeError:
|
||||
working_set = set()
|
||||
already_generating.working_set = working_set
|
||||
if cl in working_set:
|
||||
raise RecursionError()
|
||||
working_set.add(cl)
|
||||
|
||||
try:
|
||||
# We want to short-circuit in certain cases and return the identity
|
||||
# function.
|
||||
# We short-circuit if all of these are true:
|
||||
# * no attributes have been overridden
|
||||
# * all attributes resolve to `converter._unstructure_identity`
|
||||
for a in attrs:
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
if override != neutral:
|
||||
break
|
||||
handler = None
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
if t.__name__ in mapping:
|
||||
t = mapping[t.__name__]
|
||||
else:
|
||||
# Unbound typevars use late binding.
|
||||
handler = converter.unstructure
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
if handler is None:
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
try:
|
||||
handler = converter.get_unstructure_hook(t)
|
||||
except RecursionError:
|
||||
# There's a circular reference somewhere down the line
|
||||
handler = converter.unstructure
|
||||
is_identity = handler == identity
|
||||
if not is_identity:
|
||||
break
|
||||
else:
|
||||
# We've not broken the loop.
|
||||
return identity
|
||||
|
||||
for ix, a in enumerate(attrs):
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
if override.omit:
|
||||
lines.append(f" res.pop('{attr_name}', None)")
|
||||
continue
|
||||
if override.rename is not None:
|
||||
# We also need to pop when renaming, since we're copying
|
||||
# the original.
|
||||
lines.append(f" res.pop('{attr_name}', None)")
|
||||
kn = attr_name if override.rename is None else override.rename
|
||||
attr_required = attr_name in req_keys
|
||||
|
||||
# For each attribute, we try resolving the type here and now.
|
||||
# If a type is manually overwritten, this function should be
|
||||
# regenerated.
|
||||
handler = None
|
||||
if override.unstruct_hook is not None:
|
||||
handler = override.unstruct_hook
|
||||
else:
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
if t.__name__ in mapping:
|
||||
t = mapping[t.__name__]
|
||||
else:
|
||||
handler = converter.unstructure
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
if handler is None:
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
try:
|
||||
handler = converter.get_unstructure_hook(t)
|
||||
except RecursionError:
|
||||
# There's a circular reference somewhere down the line
|
||||
handler = converter.unstructure
|
||||
|
||||
is_identity = handler == identity
|
||||
|
||||
if not is_identity:
|
||||
unstruct_handler_name = f"__c_unstr_{ix}"
|
||||
globs[unstruct_handler_name] = handler
|
||||
internal_arg_parts[unstruct_handler_name] = handler
|
||||
invoke = f"{unstruct_handler_name}(instance['{attr_name}'])"
|
||||
elif override.rename is None:
|
||||
# We're not doing anything to this attribute, so
|
||||
# it'll already be present in the input dict.
|
||||
continue
|
||||
else:
|
||||
# Probably renamed, we just fetch it.
|
||||
invoke = f"instance['{attr_name}']"
|
||||
|
||||
if attr_required:
|
||||
# No default or no override.
|
||||
lines.append(f" res['{kn}'] = {invoke}")
|
||||
else:
|
||||
lines.append(f" if '{attr_name}' in instance: res['{kn}'] = {invoke}")
|
||||
|
||||
internal_arg_line = ", ".join([f"{i}={i}" for i in internal_arg_parts])
|
||||
if internal_arg_line:
|
||||
internal_arg_line = f", {internal_arg_line}"
|
||||
for k, v in internal_arg_parts.items():
|
||||
globs[k] = v
|
||||
|
||||
total_lines = [
|
||||
f"def {fn_name}(instance{internal_arg_line}):",
|
||||
" res = instance.copy()",
|
||||
*lines,
|
||||
" return res",
|
||||
]
|
||||
script = "\n".join(total_lines)
|
||||
|
||||
fname = generate_unique_filename(
|
||||
cl, "unstructure", lines=total_lines if _cattrs_use_linecache else []
|
||||
)
|
||||
|
||||
eval(compile(script, fname, "exec"), globs)
|
||||
finally:
|
||||
working_set.remove(cl)
|
||||
if not working_set:
|
||||
del already_generating.working_set
|
||||
|
||||
return globs[fn_name]
|
||||
|
||||
|
||||
def make_dict_structure_fn(
|
||||
cl: Any,
|
||||
converter: BaseConverter,
|
||||
_cattrs_forbid_extra_keys: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_use_linecache: bool = True,
|
||||
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||
**kwargs: AttributeOverride,
|
||||
) -> Callable[[dict, Any], Any]:
|
||||
"""Generate a specialized dict structuring function for typed dicts.
|
||||
|
||||
:param cl: A `TypedDict` class.
|
||||
:param converter: A Converter instance to use for structuring nested fields.
|
||||
:param kwargs: A mapping of field names to an `AttributeOverride`, for
|
||||
customization.
|
||||
:param _cattrs_detailed_validation: Whether to use a slower mode that produces
|
||||
more detailed errors.
|
||||
:param _cattrs_forbid_extra_keys: Whether the structuring function should raise a
|
||||
`ForbiddenExtraKeysError` if unknown keys are encountered.
|
||||
:param _cattrs_detailed_validation: Whether to store the generated code in the
|
||||
_linecache_, for easier debugging and better stack traces.
|
||||
|
||||
.. versionchanged:: 23.2.0
|
||||
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
|
||||
take their values from the given converter by default.
|
||||
"""
|
||||
|
||||
mapping = {}
|
||||
if is_generic(cl):
|
||||
base = get_origin(cl)
|
||||
mapping = generate_mapping(cl, mapping)
|
||||
if base is not None:
|
||||
# It's possible for this to be a subclass of a generic,
|
||||
# so no origin.
|
||||
cl = base
|
||||
|
||||
for base in getattr(cl, "__orig_bases__", ()):
|
||||
if is_generic(base) and not str(base).startswith("typing.Generic"):
|
||||
mapping = generate_mapping(base, mapping)
|
||||
break
|
||||
|
||||
cl_name = cl.__name__
|
||||
fn_name = "structure_" + cl_name
|
||||
|
||||
# We have generic parameters and need to generate a unique name for the function
|
||||
for p in getattr(cl, "__parameters__", ()):
|
||||
try:
|
||||
name_base = mapping[p.__name__]
|
||||
except KeyError:
|
||||
pn = p.__name__
|
||||
raise StructureHandlerNotFoundError(
|
||||
f"Missing type for generic argument {pn}, specify it when structuring.",
|
||||
p,
|
||||
) from None
|
||||
name = getattr(name_base, "__name__", None) or str(name_base)
|
||||
# `<>` can be present in lambdas
|
||||
# `|` can be present in unions
|
||||
name = re.sub(r"[\[\.\] ,<>]", "_", name)
|
||||
name = re.sub(r"\|", "u", name)
|
||||
fn_name += f"_{name}"
|
||||
|
||||
internal_arg_parts = {"__cl": cl}
|
||||
globs = {}
|
||||
lines = []
|
||||
post_lines = []
|
||||
|
||||
attrs = _adapted_fields(cl)
|
||||
req_keys = _required_keys(cl)
|
||||
|
||||
allowed_fields = set()
|
||||
if _cattrs_forbid_extra_keys == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
|
||||
if _cattrs_detailed_validation == "from_converter":
|
||||
_cattrs_detailed_validation = converter.detailed_validation
|
||||
|
||||
if _cattrs_forbid_extra_keys:
|
||||
globs["__c_a"] = allowed_fields
|
||||
globs["__c_feke"] = ForbiddenExtraKeysError
|
||||
|
||||
if _cattrs_detailed_validation:
|
||||
# When running under detailed validation, be extra careful about the
|
||||
# input type so that the correct error is raised if the input isn't a dict.
|
||||
internal_arg_parts["__c_mapping"] = Mapping
|
||||
lines.append(" if not isinstance(o, __c_mapping):")
|
||||
te = "TypeError(f'expected a mapping, not {o.__class__.__name__}')"
|
||||
lines.append(
|
||||
f" raise __c_cve('While structuring ' + {cl.__name__!r}, [{te}], __cl)"
|
||||
)
|
||||
|
||||
lines.append(" res = o.copy()")
|
||||
|
||||
if _cattrs_detailed_validation:
|
||||
lines.append(" errors = []")
|
||||
internal_arg_parts["__c_cve"] = ClassValidationError
|
||||
internal_arg_parts["__c_avn"] = AttributeValidationNote
|
||||
for ix, a in enumerate(attrs):
|
||||
an = a.name
|
||||
attr_required = an in req_keys
|
||||
override = kwargs.get(an, neutral)
|
||||
if override.omit:
|
||||
continue
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
t = mapping.get(t.__name__, t)
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
# For each attribute, we try resolving the type here and now.
|
||||
# If a type is manually overwritten, this function should be
|
||||
# regenerated.
|
||||
if override.struct_hook is not None:
|
||||
# If the user has requested an override, just use that.
|
||||
handler = override.struct_hook
|
||||
else:
|
||||
handler = find_structure_handler(a, t, converter)
|
||||
|
||||
struct_handler_name = f"__c_structure_{ix}"
|
||||
internal_arg_parts[struct_handler_name] = handler
|
||||
|
||||
kn = an if override.rename is None else override.rename
|
||||
allowed_fields.add(kn)
|
||||
i = " "
|
||||
if not attr_required:
|
||||
lines.append(f"{i}if '{kn}' in o:")
|
||||
i = f"{i} "
|
||||
lines.append(f"{i}try:")
|
||||
i = f"{i} "
|
||||
|
||||
tn = f"__c_type_{ix}"
|
||||
internal_arg_parts[tn] = t
|
||||
|
||||
if handler == converter._structure_call:
|
||||
internal_arg_parts[struct_handler_name] = t
|
||||
lines.append(f"{i}res['{an}'] = {struct_handler_name}(o['{kn}'])")
|
||||
else:
|
||||
lines.append(f"{i}res['{an}'] = {struct_handler_name}(o['{kn}'], {tn})")
|
||||
if override.rename is not None:
|
||||
lines.append(f"{i}del res['{kn}']")
|
||||
i = i[:-2]
|
||||
lines.append(f"{i}except Exception as e:")
|
||||
i = f"{i} "
|
||||
lines.append(
|
||||
f'{i}e.__notes__ = [*getattr(e, \'__notes__\', []), __c_avn("Structuring typeddict {cl.__qualname__} @ attribute {an}", "{an}", {tn})]'
|
||||
)
|
||||
lines.append(f"{i}errors.append(e)")
|
||||
|
||||
if _cattrs_forbid_extra_keys:
|
||||
post_lines += [
|
||||
" unknown_fields = o.keys() - __c_a",
|
||||
" if unknown_fields:",
|
||||
" errors.append(__c_feke('', __cl, unknown_fields))",
|
||||
]
|
||||
|
||||
post_lines.append(
|
||||
f" if errors: raise __c_cve('While structuring ' + {cl.__name__!r}, errors, __cl)"
|
||||
)
|
||||
else:
|
||||
non_required = []
|
||||
|
||||
# The first loop deals with required args.
|
||||
for ix, a in enumerate(attrs):
|
||||
an = a.name
|
||||
attr_required = an in req_keys
|
||||
override = kwargs.get(an, neutral)
|
||||
if override.omit:
|
||||
continue
|
||||
if not attr_required:
|
||||
non_required.append((ix, a))
|
||||
continue
|
||||
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
t = mapping.get(t.__name__, t)
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if override.struct_hook is not None:
|
||||
handler = override.struct_hook
|
||||
else:
|
||||
# For each attribute, we try resolving the type here and now.
|
||||
# If a type is manually overwritten, this function should be
|
||||
# regenerated.
|
||||
handler = converter.get_structure_hook(t)
|
||||
|
||||
kn = an if override.rename is None else override.rename
|
||||
allowed_fields.add(kn)
|
||||
|
||||
struct_handler_name = f"__c_structure_{ix}"
|
||||
internal_arg_parts[struct_handler_name] = handler
|
||||
if handler == converter._structure_call:
|
||||
internal_arg_parts[struct_handler_name] = t
|
||||
invocation_line = f" res['{an}'] = {struct_handler_name}(o['{kn}'])"
|
||||
else:
|
||||
tn = f"__c_type_{ix}"
|
||||
internal_arg_parts[tn] = t
|
||||
invocation_line = (
|
||||
f" res['{an}'] = {struct_handler_name}(o['{kn}'], {tn})"
|
||||
)
|
||||
|
||||
lines.append(invocation_line)
|
||||
if override.rename is not None:
|
||||
lines.append(f" del res['{override.rename}']")
|
||||
|
||||
# The second loop is for optional args.
|
||||
if non_required:
|
||||
for ix, a in non_required:
|
||||
an = a.name
|
||||
override = kwargs.get(an, neutral)
|
||||
t = a.type
|
||||
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
t = mapping.get(t.__name__, t)
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
if override.struct_hook is not None:
|
||||
handler = override.struct_hook
|
||||
else:
|
||||
# For each attribute, we try resolving the type here and now.
|
||||
# If a type is manually overwritten, this function should be
|
||||
# regenerated.
|
||||
handler = converter.get_structure_hook(t)
|
||||
|
||||
struct_handler_name = f"__c_structure_{ix}"
|
||||
internal_arg_parts[struct_handler_name] = handler
|
||||
|
||||
ian = an
|
||||
kn = an if override.rename is None else override.rename
|
||||
allowed_fields.add(kn)
|
||||
post_lines.append(f" if '{kn}' in o:")
|
||||
if handler == converter._structure_call:
|
||||
internal_arg_parts[struct_handler_name] = t
|
||||
post_lines.append(
|
||||
f" res['{ian}'] = {struct_handler_name}(o['{kn}'])"
|
||||
)
|
||||
else:
|
||||
tn = f"__c_type_{ix}"
|
||||
internal_arg_parts[tn] = t
|
||||
post_lines.append(
|
||||
f" res['{ian}'] = {struct_handler_name}(o['{kn}'], {tn})"
|
||||
)
|
||||
if override.rename is not None:
|
||||
lines.append(f" res.pop('{override.rename}', None)")
|
||||
|
||||
if _cattrs_forbid_extra_keys:
|
||||
post_lines += [
|
||||
" unknown_fields = o.keys() - __c_a",
|
||||
" if unknown_fields:",
|
||||
" raise __c_feke('', __cl, unknown_fields)",
|
||||
]
|
||||
|
||||
# At the end, we create the function header.
|
||||
internal_arg_line = ", ".join([f"{i}={i}" for i in internal_arg_parts])
|
||||
for k, v in internal_arg_parts.items():
|
||||
globs[k] = v
|
||||
|
||||
total_lines = [
|
||||
f"def {fn_name}(o, _, {internal_arg_line}):",
|
||||
*lines,
|
||||
*post_lines,
|
||||
" return res",
|
||||
]
|
||||
|
||||
script = "\n".join(total_lines)
|
||||
fname = generate_unique_filename(
|
||||
cl, "structure", lines=total_lines if _cattrs_use_linecache else []
|
||||
)
|
||||
|
||||
eval(compile(script, fname, "exec"), globs)
|
||||
return globs[fn_name]
|
||||
|
||||
|
||||
def _adapted_fields(cls: Any) -> list[Attribute]:
|
||||
annotations = get_annots(cls)
|
||||
hints = get_full_type_hints(cls)
|
||||
return [
|
||||
Attribute(
|
||||
n,
|
||||
NOTHING,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
type=hints[n] if n in hints else annotations[n],
|
||||
)
|
||||
for n, a in annotations.items()
|
||||
]
|
||||
|
||||
|
||||
def _is_extensions_typeddict(cls) -> bool:
|
||||
return cls.__class__ is _TypedDictMeta or (
|
||||
is_generic(cls) and (cls.__origin__.__class__ is _TypedDictMeta)
|
||||
)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
|
||||
def _required_keys(cls: type) -> set[str]:
|
||||
return cls.__required_keys__
|
||||
|
||||
else:
|
||||
from typing_extensions import Annotated, NotRequired, get_args
|
||||
|
||||
# Note that there is no `typing.Required` on 3.9 and 3.10, only in
|
||||
# `typing_extensions`. Therefore, `typing.TypedDict` will not honor this
|
||||
# annotation, only `typing_extensions.TypedDict`.
|
||||
|
||||
def _required_keys(cls: type) -> set[str]:
|
||||
"""Our own processor for required keys."""
|
||||
if _is_extensions_typeddict(cls):
|
||||
return cls.__required_keys__
|
||||
|
||||
# We vendor a part of the typing_extensions logic for
|
||||
# gathering required keys. *sigh*
|
||||
own_annotations = cls.__dict__.get("__annotations__", {})
|
||||
required_keys = set()
|
||||
# On 3.9 - 3.10, typing.TypedDict doesn't put typeddict superclasses
|
||||
# in the MRO, therefore we cannot handle non-required keys properly
|
||||
# in some situations. Oh well.
|
||||
for key in getattr(cls, "__required_keys__", []):
|
||||
annotation_type = own_annotations[key]
|
||||
annotation_origin = get_origin(annotation_type)
|
||||
if annotation_origin is Annotated:
|
||||
annotation_args = get_args(annotation_type)
|
||||
if annotation_args:
|
||||
annotation_type = annotation_args[0]
|
||||
annotation_origin = get_origin(annotation_type)
|
||||
|
||||
if annotation_origin is NotRequired:
|
||||
pass
|
||||
elif cls.__total__:
|
||||
required_keys.add(key)
|
||||
return required_keys
|
||||
@@ -0,0 +1,11 @@
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from ._compat import is_literal
|
||||
|
||||
__all__ = ["is_literal", "is_literal_containing_enums"]
|
||||
|
||||
|
||||
def is_literal_containing_enums(type: Any) -> bool:
|
||||
"""Is this a literal containing at least one Enum?"""
|
||||
return is_literal(type) and any(isinstance(val, Enum) for val in type.__args__)
|
||||
@@ -0,0 +1,55 @@
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, TypeVar, get_args
|
||||
|
||||
from .._compat import is_subclass
|
||||
from ..converters import Converter, UnstructureHook
|
||||
from ..fns import identity
|
||||
|
||||
if sys.version_info[:2] < (3, 10):
|
||||
from typing_extensions import ParamSpec
|
||||
else:
|
||||
from typing import ParamSpec
|
||||
|
||||
|
||||
def validate_datetime(v, _):
|
||||
if not isinstance(v, datetime):
|
||||
raise Exception(f"Expected datetime, got {v}")
|
||||
return v
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
def wrap(_: Callable[P, Any]) -> Callable[[Callable[..., T]], Callable[P, T]]:
|
||||
"""Wrap a `Converter` `__init__` in a type-safe way."""
|
||||
|
||||
def impl(x: Callable[..., T]) -> Callable[P, T]:
|
||||
return x
|
||||
|
||||
return impl
|
||||
|
||||
|
||||
def is_primitive_enum(type: Any, include_bare_enums: bool = False) -> bool:
|
||||
"""Is this a string or int enum that can be passed through?"""
|
||||
return is_subclass(type, Enum) and (
|
||||
is_subclass(type, (str, int))
|
||||
or (include_bare_enums and type.mro()[1:] == Enum.mro())
|
||||
)
|
||||
|
||||
|
||||
def literals_with_enums_unstructure_factory(
|
||||
typ: Any, converter: Converter
|
||||
) -> UnstructureHook:
|
||||
"""An unstructure hook factory for literals containing enums.
|
||||
|
||||
If all contained enums can be passed through (their unstructure hook is `identity`),
|
||||
the entire literal can also be passed through.
|
||||
"""
|
||||
if all(
|
||||
converter.get_unstructure_hook(type(arg)) == identity for arg in get_args(typ)
|
||||
):
|
||||
return identity
|
||||
return converter.unstructure
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Preconfigured converters for bson."""
|
||||
|
||||
from base64 import b85decode, b85encode
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from bson import DEFAULT_CODEC_OPTIONS, CodecOptions, Int64, ObjectId, decode, encode
|
||||
|
||||
from .._compat import is_mapping, is_subclass
|
||||
from ..cols import mapping_structure_factory
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..dispatch import StructureHook
|
||||
from ..fns import identity
|
||||
from ..literals import is_literal_containing_enums
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import (
|
||||
is_primitive_enum,
|
||||
literals_with_enums_unstructure_factory,
|
||||
validate_datetime,
|
||||
wrap,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Base85Bytes(bytes):
|
||||
"""A subclass to help with binary key encoding/decoding."""
|
||||
|
||||
|
||||
class BsonConverter(Converter):
|
||||
def dumps(
|
||||
self,
|
||||
obj: Any,
|
||||
unstructure_as: Any = None,
|
||||
check_keys: bool = False,
|
||||
codec_options: CodecOptions = DEFAULT_CODEC_OPTIONS,
|
||||
) -> bytes:
|
||||
return encode(
|
||||
self.unstructure(obj, unstructure_as=unstructure_as),
|
||||
check_keys=check_keys,
|
||||
codec_options=codec_options,
|
||||
)
|
||||
|
||||
def loads(
|
||||
self,
|
||||
data: bytes,
|
||||
cl: type[T],
|
||||
codec_options: CodecOptions = DEFAULT_CODEC_OPTIONS,
|
||||
) -> T:
|
||||
return self.structure(decode(data, codec_options=codec_options), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter):
|
||||
"""
|
||||
Configure the converter for use with the bson library.
|
||||
|
||||
* sets are serialized as lists
|
||||
* byte mapping keys are base85-encoded into strings when unstructuring, and reverse
|
||||
* non-string, non-byte mapping keys are coerced into strings when unstructuring
|
||||
* a deserialization hook is registered for bson.ObjectId by default
|
||||
* string and int enums are passed through when unstructuring
|
||||
|
||||
.. versionchanged:: 24.2.0
|
||||
Enums are left to the library to unstructure, speeding them up.
|
||||
"""
|
||||
|
||||
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
|
||||
key_handler = str
|
||||
args = getattr(cl, "__args__", None)
|
||||
if args:
|
||||
if is_subclass(args[0], str):
|
||||
key_handler = None
|
||||
elif is_subclass(args[0], bytes):
|
||||
|
||||
def key_handler(k):
|
||||
return b85encode(k).decode("utf8")
|
||||
|
||||
return converter.gen_unstructure_mapping(
|
||||
cl, unstructure_to=unstructure_to, key_handler=key_handler
|
||||
)
|
||||
|
||||
def gen_structure_mapping(cl: Any) -> StructureHook:
|
||||
args = getattr(cl, "__args__", None)
|
||||
if args and is_subclass(args[0], bytes):
|
||||
h = mapping_structure_factory(cl, converter, key_type=Base85Bytes)
|
||||
else:
|
||||
h = mapping_structure_factory(cl, converter)
|
||||
return h
|
||||
|
||||
converter.register_structure_hook(Base85Bytes, lambda v, _: b85decode(v))
|
||||
converter.register_unstructure_hook_factory(is_mapping, gen_unstructure_mapping)
|
||||
converter.register_structure_hook_factory(is_mapping, gen_structure_mapping)
|
||||
|
||||
converter.register_structure_hook(ObjectId, lambda v, _: ObjectId(v))
|
||||
configure_union_passthrough(
|
||||
Union[str, bool, int, float, None, bytes, datetime, ObjectId, Int64], converter
|
||||
)
|
||||
|
||||
# datetime inherits from date, so identity unstructure hook used
|
||||
# here to prevent the date unstructure hook running.
|
||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
|
||||
@wrap(BsonConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> BsonConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = BsonConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Preconfigured converters for cbor2."""
|
||||
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from cbor2 import dumps, loads
|
||||
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..literals import is_literal_containing_enums
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import is_primitive_enum, literals_with_enums_unstructure_factory, wrap
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Cbor2Converter(Converter):
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> bytes:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: bytes, cl: type[T], **kwargs: Any) -> T:
|
||||
return self.structure(loads(data, **kwargs), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter):
|
||||
"""
|
||||
Configure the converter for use with the cbor2 library.
|
||||
|
||||
* datetimes are serialized as timestamp floats
|
||||
* sets are serialized as lists
|
||||
* string and int enums are passed through when unstructuring
|
||||
"""
|
||||
converter.register_unstructure_hook(datetime, lambda v: v.timestamp())
|
||||
converter.register_structure_hook(
|
||||
datetime, lambda v, _: datetime.fromtimestamp(v, timezone.utc)
|
||||
)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None, bytes], converter)
|
||||
|
||||
|
||||
@wrap(Cbor2Converter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> Cbor2Converter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = Cbor2Converter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Preconfigured converters for the stdlib json."""
|
||||
|
||||
from base64 import b85decode, b85encode
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime
|
||||
from json import dumps, loads
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from .._compat import Counter
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..literals import is_literal_containing_enums
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import is_primitive_enum, literals_with_enums_unstructure_factory, wrap
|
||||
|
||||
__all__ = ["JsonConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class JsonConverter(Converter):
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: Union[bytes, str], cl: type[T], **kwargs: Any) -> T:
|
||||
return self.structure(loads(data, **kwargs), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter) -> None:
|
||||
"""
|
||||
Configure the converter for use with the stdlib json module.
|
||||
|
||||
* bytes are serialized as base85 strings
|
||||
* datetimes are serialized as ISO 8601
|
||||
* counters are serialized as dicts
|
||||
* sets are serialized as lists
|
||||
* string and int enums are passed through when unstructuring
|
||||
* union passthrough is configured for unions of strings, bools, ints,
|
||||
floats and None
|
||||
|
||||
.. versionchanged:: 24.2.0
|
||||
Enums are left to the library to unstructure, speeding them up.
|
||||
"""
|
||||
converter.register_unstructure_hook(
|
||||
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
|
||||
)
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
converter.register_unstructure_hook(datetime, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None], converter)
|
||||
|
||||
|
||||
@wrap(JsonConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> JsonConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
Counter: dict,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = JsonConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Preconfigured converters for msgpack."""
|
||||
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime, time, timezone
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from msgpack import dumps, loads
|
||||
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..literals import is_literal_containing_enums
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import is_primitive_enum, literals_with_enums_unstructure_factory, wrap
|
||||
|
||||
__all__ = ["MsgpackConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MsgpackConverter(Converter):
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> bytes:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: bytes, cl: type[T], **kwargs: Any) -> T:
|
||||
return self.structure(loads(data, **kwargs), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter) -> None:
|
||||
"""
|
||||
Configure the converter for use with the msgpack library.
|
||||
|
||||
* datetimes are serialized as timestamp floats
|
||||
* sets are serialized as lists
|
||||
* string and int enums are passed through when unstructuring
|
||||
|
||||
.. versionchanged:: 24.2.0
|
||||
Enums are left to the library to unstructure, speeding them up.
|
||||
"""
|
||||
converter.register_unstructure_hook(datetime, lambda v: v.timestamp())
|
||||
converter.register_structure_hook(
|
||||
datetime, lambda v, _: datetime.fromtimestamp(v, timezone.utc)
|
||||
)
|
||||
converter.register_unstructure_hook(
|
||||
date, lambda v: datetime.combine(v, time(tzinfo=timezone.utc)).timestamp()
|
||||
)
|
||||
converter.register_structure_hook(
|
||||
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
|
||||
)
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None, bytes], converter)
|
||||
|
||||
|
||||
@wrap(MsgpackConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> MsgpackConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = MsgpackConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Preconfigured converters for msgspec."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from base64 import b64decode
|
||||
from dataclasses import is_dataclass
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import Any, Callable, TypeVar, Union, get_type_hints
|
||||
|
||||
from attrs import has as attrs_has
|
||||
from attrs import resolve_types
|
||||
from msgspec import Struct, convert, to_builtins
|
||||
from msgspec.json import Encoder, decode
|
||||
|
||||
from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
|
||||
from ..cols import is_namedtuple
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..dispatch import UnstructureHook
|
||||
from ..fns import identity
|
||||
from ..gen import make_hetero_tuple_unstructure_fn
|
||||
from ..literals import is_literal_containing_enums
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import literals_with_enums_unstructure_factory, wrap
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
__all__ = ["MsgspecJsonConverter", "configure_converter", "make_converter"]
|
||||
|
||||
|
||||
class MsgspecJsonConverter(Converter):
|
||||
"""A converter specialized for the _msgspec_ library."""
|
||||
|
||||
#: The msgspec encoder for dumping.
|
||||
encoder: Encoder = Encoder()
|
||||
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> bytes:
|
||||
"""Unstructure and encode `obj` into JSON bytes."""
|
||||
return self.encoder.encode(
|
||||
self.unstructure(obj, unstructure_as=unstructure_as), **kwargs
|
||||
)
|
||||
|
||||
def get_dumps_hook(
|
||||
self, unstructure_as: Any, **kwargs: Any
|
||||
) -> Callable[[Any], bytes]:
|
||||
"""Produce a `dumps` hook for the given type."""
|
||||
unstruct_hook = self.get_unstructure_hook(unstructure_as)
|
||||
if unstruct_hook in (identity, to_builtins):
|
||||
return self.encoder.encode
|
||||
return self.dumps
|
||||
|
||||
def loads(self, data: bytes, cl: type[T], **kwargs: Any) -> T:
|
||||
"""Decode and structure `cl` from the provided JSON bytes."""
|
||||
return self.structure(decode(data, **kwargs), cl)
|
||||
|
||||
def get_loads_hook(self, cl: type[T]) -> Callable[[bytes], T]:
|
||||
"""Produce a `loads` hook for the given type."""
|
||||
return partial(self.loads, cl=cl)
|
||||
|
||||
|
||||
def configure_converter(converter: Converter) -> None:
|
||||
"""Configure the converter for the msgspec library.
|
||||
|
||||
* bytes are serialized as base64 strings, directly by msgspec
|
||||
* datetimes and dates are passed through to be serialized as RFC 3339 directly
|
||||
* enums are passed through to msgspec directly
|
||||
* union passthrough configured for str, bool, int, float and None
|
||||
* bare, string and int enums are passed through when unstructuring
|
||||
|
||||
.. versionchanged:: 24.2.0
|
||||
Enums are left to the library to unstructure, speeding them up.
|
||||
"""
|
||||
configure_passthroughs(converter)
|
||||
|
||||
converter.register_unstructure_hook(Struct, to_builtins)
|
||||
converter.register_unstructure_hook(Enum, identity)
|
||||
|
||||
converter.register_structure_hook(Struct, convert)
|
||||
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
|
||||
converter.register_structure_hook(datetime, lambda v, _: convert(v, datetime))
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None], converter)
|
||||
|
||||
|
||||
@wrap(MsgspecJsonConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> MsgspecJsonConverter:
|
||||
res = MsgspecJsonConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
return res
|
||||
|
||||
|
||||
def configure_passthroughs(converter: Converter) -> None:
|
||||
"""Configure optimizing passthroughs.
|
||||
|
||||
A passthrough is when we let msgspec handle something automatically.
|
||||
|
||||
.. versionchanged:: 25.1.0
|
||||
Dataclasses with private attributes are now passed through.
|
||||
"""
|
||||
converter.register_unstructure_hook(bytes, to_builtins)
|
||||
converter.register_unstructure_hook_factory(is_mapping, mapping_unstructure_factory)
|
||||
converter.register_unstructure_hook_factory(is_sequence, seq_unstructure_factory)
|
||||
converter.register_unstructure_hook_factory(
|
||||
attrs_has, msgspec_attrs_unstructure_factory
|
||||
)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_dataclass,
|
||||
partial(msgspec_attrs_unstructure_factory, msgspec_skips_private=False),
|
||||
)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_namedtuple, namedtuple_unstructure_factory
|
||||
)
|
||||
|
||||
|
||||
def seq_unstructure_factory(type, converter: Converter) -> UnstructureHook:
|
||||
"""The msgspec unstructure hook factory for sequences."""
|
||||
if is_bare(type):
|
||||
type_arg = Any
|
||||
else:
|
||||
args = get_args(type)
|
||||
type_arg = args[0]
|
||||
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
|
||||
|
||||
if handler in (identity, to_builtins):
|
||||
return handler
|
||||
return converter.gen_unstructure_iterable(type)
|
||||
|
||||
|
||||
def mapping_unstructure_factory(type, converter: BaseConverter) -> UnstructureHook:
|
||||
"""The msgspec unstructure hook factory for mappings."""
|
||||
if is_bare(type):
|
||||
key_arg = Any
|
||||
val_arg = Any
|
||||
key_handler = converter.get_unstructure_hook(key_arg, cache_result=False)
|
||||
value_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
|
||||
else:
|
||||
args = get_args(type)
|
||||
if len(args) == 2:
|
||||
key_arg, val_arg = args
|
||||
else:
|
||||
# Probably a Counter
|
||||
key_arg, val_arg = args, Any
|
||||
key_handler = converter.get_unstructure_hook(key_arg, cache_result=False)
|
||||
value_handler = converter.get_unstructure_hook(val_arg, cache_result=False)
|
||||
|
||||
if key_handler in (identity, to_builtins) and value_handler in (
|
||||
identity,
|
||||
to_builtins,
|
||||
):
|
||||
return to_builtins
|
||||
return converter.gen_unstructure_mapping(type)
|
||||
|
||||
|
||||
def msgspec_attrs_unstructure_factory(
|
||||
type: Any, converter: Converter, msgspec_skips_private: bool = True
|
||||
) -> UnstructureHook:
|
||||
"""Choose whether to use msgspec handling or our own.
|
||||
|
||||
Args:
|
||||
msgspec_skips_private: Whether the msgspec library skips unstructuring
|
||||
private attributes, making us do the work.
|
||||
"""
|
||||
origin = get_origin(type)
|
||||
attribs = fields(origin or type)
|
||||
if attrs_has(type) and any(isinstance(a.type, str) for a in attribs):
|
||||
resolve_types(type)
|
||||
attribs = fields(origin or type)
|
||||
|
||||
if msgspec_skips_private and any(
|
||||
attr.name.startswith("_")
|
||||
or (
|
||||
converter.get_unstructure_hook(attr.type, cache_result=False)
|
||||
not in (identity, to_builtins)
|
||||
)
|
||||
for attr in attribs
|
||||
):
|
||||
return converter.gen_unstructure_attrs_fromdict(type)
|
||||
|
||||
return to_builtins
|
||||
|
||||
|
||||
def namedtuple_unstructure_factory(
|
||||
type: type[tuple], converter: BaseConverter
|
||||
) -> UnstructureHook:
|
||||
"""A hook factory for unstructuring namedtuples, modified for msgspec."""
|
||||
|
||||
if all(
|
||||
converter.get_unstructure_hook(t) in (identity, to_builtins)
|
||||
for t in get_type_hints(type).values()
|
||||
):
|
||||
return identity
|
||||
|
||||
return make_hetero_tuple_unstructure_fn(
|
||||
type,
|
||||
converter,
|
||||
unstructure_to=tuple,
|
||||
type_args=tuple(get_type_hints(type).values()),
|
||||
)
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Preconfigured converters for orjson."""
|
||||
|
||||
from base64 import b85decode, b85encode
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from orjson import dumps, loads
|
||||
|
||||
from .._compat import is_subclass
|
||||
from ..cols import is_mapping, is_namedtuple, namedtuple_unstructure_factory
|
||||
from ..converters import Converter
|
||||
from ..fns import identity
|
||||
from ..literals import is_literal_containing_enums
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import is_primitive_enum, literals_with_enums_unstructure_factory, wrap
|
||||
|
||||
__all__ = ["OrjsonConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class OrjsonConverter(Converter):
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> bytes:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: Union[bytes, bytearray, memoryview, str], cl: type[T]) -> T:
|
||||
return self.structure(loads(data), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: Converter) -> None:
|
||||
"""
|
||||
Configure the converter for use with the orjson library.
|
||||
|
||||
* bytes are serialized as base85 strings
|
||||
* datetimes and dates are passed through to be serialized as RFC 3339 by orjson
|
||||
* typed namedtuples are serialized as lists
|
||||
* sets are serialized as lists
|
||||
* string enum mapping keys have special handling
|
||||
* mapping keys are coerced into strings when unstructuring
|
||||
* bare, string and int enums are passed through when unstructuring
|
||||
|
||||
.. versionchanged:: 24.1.0
|
||||
Add support for typed namedtuples.
|
||||
.. versionchanged:: 24.2.0
|
||||
Enums are left to the library to unstructure, speeding them up.
|
||||
"""
|
||||
converter.register_unstructure_hook(
|
||||
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
|
||||
)
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
|
||||
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
|
||||
def unstructure_mapping_factory(cl: Any, unstructure_to=None):
|
||||
key_handler = str
|
||||
args = getattr(cl, "__args__", None)
|
||||
if args:
|
||||
if is_subclass(args[0], str) and is_subclass(args[0], Enum):
|
||||
|
||||
def key_handler(v):
|
||||
return v.value
|
||||
|
||||
else:
|
||||
# It's possible the handler for the key type has been overridden.
|
||||
# (For example base85 encoding for bytes.)
|
||||
# In that case, we want to use the override.
|
||||
|
||||
kh = converter.get_unstructure_hook(args[0])
|
||||
if kh != identity:
|
||||
key_handler = kh
|
||||
|
||||
return converter.gen_unstructure_mapping(
|
||||
cl, unstructure_to=unstructure_to, key_handler=key_handler
|
||||
)
|
||||
|
||||
converter._unstructure_func.register_func_list(
|
||||
[
|
||||
(is_mapping, unstructure_mapping_factory, True),
|
||||
(
|
||||
is_namedtuple,
|
||||
partial(namedtuple_unstructure_factory, unstructure_to=tuple),
|
||||
"extended",
|
||||
),
|
||||
]
|
||||
)
|
||||
converter.register_unstructure_hook_func(
|
||||
partial(is_primitive_enum, include_bare_enums=True), identity
|
||||
)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None], converter)
|
||||
|
||||
|
||||
@wrap(OrjsonConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> OrjsonConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = OrjsonConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Preconfigured converters for pyyaml."""
|
||||
|
||||
from datetime import date, datetime
|
||||
from functools import partial
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from yaml import safe_dump, safe_load
|
||||
|
||||
from .._compat import FrozenSetSubscriptable
|
||||
from ..cols import is_namedtuple, namedtuple_unstructure_factory
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import validate_datetime, wrap
|
||||
|
||||
__all__ = ["PyyamlConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def validate_date(v: Any, _):
|
||||
if not isinstance(v, date):
|
||||
raise ValueError(f"Expected date, got {v}")
|
||||
return v
|
||||
|
||||
|
||||
class PyyamlConverter(Converter):
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
|
||||
return safe_dump(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: str, cl: type[T]) -> T:
|
||||
return self.structure(safe_load(data), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter) -> None:
|
||||
"""
|
||||
Configure the converter for use with the pyyaml library.
|
||||
|
||||
* frozensets are serialized as lists
|
||||
* string enums are converted into strings explicitly
|
||||
* datetimes and dates are validated
|
||||
* typed namedtuples are serialized as lists
|
||||
|
||||
.. versionchanged:: 24.1.0
|
||||
Add support for typed namedtuples.
|
||||
"""
|
||||
converter.register_unstructure_hook(
|
||||
str, lambda v: v if v.__class__ is str else v.value
|
||||
)
|
||||
|
||||
# datetime inherits from date, so identity unstructure hook used
|
||||
# here to prevent the date unstructure hook running.
|
||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_structure_hook(date, validate_date)
|
||||
|
||||
converter.register_unstructure_hook_factory(is_namedtuple)(
|
||||
partial(namedtuple_unstructure_factory, unstructure_to=tuple)
|
||||
)
|
||||
|
||||
configure_union_passthrough(
|
||||
Union[str, bool, int, float, None, bytes, datetime, date], converter
|
||||
)
|
||||
|
||||
|
||||
@wrap(PyyamlConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> PyyamlConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
FrozenSetSubscriptable: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = PyyamlConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Preconfigured converters for tomlkit."""
|
||||
|
||||
from base64 import b85decode, b85encode
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from operator import attrgetter
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from tomlkit import dumps, loads
|
||||
from tomlkit.items import Float, Integer, String
|
||||
|
||||
from .._compat import is_mapping, is_subclass
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import validate_datetime, wrap
|
||||
|
||||
__all__ = ["TomlkitConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
_enum_value_getter = attrgetter("_value_")
|
||||
|
||||
|
||||
class TomlkitConverter(Converter):
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: str, cl: type[T]) -> T:
|
||||
return self.structure(loads(data), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter):
|
||||
"""
|
||||
Configure the converter for use with the tomlkit library.
|
||||
|
||||
* bytes are serialized as base85 strings
|
||||
* sets are serialized as lists
|
||||
* tuples are serializas as lists
|
||||
* mapping keys are coerced into strings when unstructuring
|
||||
"""
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
converter.register_unstructure_hook(
|
||||
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
|
||||
)
|
||||
|
||||
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
|
||||
key_handler = str
|
||||
args = getattr(cl, "__args__", None)
|
||||
if args:
|
||||
# Currently, tomlkit has inconsistent behavior on 3.11
|
||||
# so we paper over it here.
|
||||
# https://github.com/sdispater/tomlkit/issues/237
|
||||
if is_subclass(args[0], str):
|
||||
key_handler = _enum_value_getter if is_subclass(args[0], Enum) else None
|
||||
elif is_subclass(args[0], bytes):
|
||||
|
||||
def key_handler(k: bytes):
|
||||
return b85encode(k).decode("utf8")
|
||||
|
||||
return converter.gen_unstructure_mapping(
|
||||
cl, unstructure_to=unstructure_to, key_handler=key_handler
|
||||
)
|
||||
|
||||
converter._unstructure_func.register_func_list(
|
||||
[(is_mapping, gen_unstructure_mapping, True)]
|
||||
)
|
||||
|
||||
# datetime inherits from date, so identity unstructure hook used
|
||||
# here to prevent the date unstructure hook running.
|
||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
configure_union_passthrough(
|
||||
Union[str, String, bool, int, Integer, float, Float], converter
|
||||
)
|
||||
|
||||
|
||||
@wrap(TomlkitConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> TomlkitConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
tuple: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = TomlkitConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Preconfigured converters for ujson."""
|
||||
|
||||
from base64 import b85decode, b85encode
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime
|
||||
from typing import Any, AnyStr, TypeVar, Union
|
||||
|
||||
from ujson import dumps, loads
|
||||
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..literals import is_literal_containing_enums
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import is_primitive_enum, literals_with_enums_unstructure_factory, wrap
|
||||
|
||||
__all__ = ["UjsonConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class UjsonConverter(Converter):
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: AnyStr, cl: type[T], **kwargs: Any) -> T:
|
||||
return self.structure(loads(data, **kwargs), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter):
|
||||
"""
|
||||
Configure the converter for use with the ujson library.
|
||||
|
||||
* bytes are serialized as base64 strings
|
||||
* datetimes are serialized as ISO 8601
|
||||
* sets are serialized as lists
|
||||
* string and int enums are passed through when unstructuring
|
||||
|
||||
.. versionchanged:: 24.2.0
|
||||
Enums are left to the library to unstructure, speeding them up.
|
||||
"""
|
||||
converter.register_unstructure_hook(
|
||||
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
|
||||
)
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
|
||||
converter.register_unstructure_hook(datetime, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None], converter)
|
||||
|
||||
|
||||
@wrap(UjsonConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> UjsonConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = UjsonConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,12 @@
|
||||
"""High level strategies for converters."""
|
||||
|
||||
from ._class_methods import use_class_methods
|
||||
from ._subclasses import include_subclasses
|
||||
from ._unions import configure_tagged_union, configure_union_passthrough
|
||||
|
||||
__all__ = [
|
||||
"configure_tagged_union",
|
||||
"configure_union_passthrough",
|
||||
"include_subclasses",
|
||||
"use_class_methods",
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Strategy for using class-specific (un)structuring methods."""
|
||||
|
||||
from inspect import signature
|
||||
from typing import Any, Callable, Optional, TypeVar
|
||||
|
||||
from .. import BaseConverter
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def use_class_methods(
|
||||
converter: BaseConverter,
|
||||
structure_method_name: Optional[str] = None,
|
||||
unstructure_method_name: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Configure the converter such that dedicated methods are used for (un)structuring
|
||||
the instance of a class if such methods are available. The default (un)structuring
|
||||
will be applied if such an (un)structuring methods cannot be found.
|
||||
|
||||
:param converter: The `Converter` on which this strategy is applied. You can use
|
||||
:class:`cattrs.BaseConverter` or any other derived class.
|
||||
:param structure_method_name: Optional string with the name of the class method
|
||||
which should be used for structuring. If not provided, no class method will be
|
||||
used for structuring.
|
||||
:param unstructure_method_name: Optional string with the name of the class method
|
||||
which should be used for unstructuring. If not provided, no class method will
|
||||
be used for unstructuring.
|
||||
|
||||
If you want to (un)structured nested objects, just append a converter parameter
|
||||
to your (un)structuring methods and you will receive the converter there.
|
||||
|
||||
.. versionadded:: 23.2.0
|
||||
"""
|
||||
|
||||
if structure_method_name:
|
||||
|
||||
def make_class_method_structure(cl: type[T]) -> Callable[[Any, type[T]], T]:
|
||||
fn = getattr(cl, structure_method_name)
|
||||
n_parameters = len(signature(fn).parameters)
|
||||
if n_parameters == 1:
|
||||
return lambda v, _: fn(v)
|
||||
if n_parameters == 2:
|
||||
return lambda v, _: fn(v, converter)
|
||||
raise TypeError("Provide a class method with one or two arguments.")
|
||||
|
||||
converter.register_structure_hook_factory(
|
||||
lambda t: hasattr(t, structure_method_name), make_class_method_structure
|
||||
)
|
||||
|
||||
if unstructure_method_name:
|
||||
|
||||
def make_class_method_unstructure(cl: type[T]) -> Callable[[T], T]:
|
||||
fn = getattr(cl, unstructure_method_name)
|
||||
n_parameters = len(signature(fn).parameters)
|
||||
if n_parameters == 1:
|
||||
return fn
|
||||
if n_parameters == 2:
|
||||
return lambda self_: fn(self_, converter)
|
||||
raise TypeError("Provide a method with no or one argument.")
|
||||
|
||||
converter.register_unstructure_hook_factory(
|
||||
lambda t: hasattr(t, unstructure_method_name), make_class_method_unstructure
|
||||
)
|
||||
@@ -0,0 +1,243 @@
|
||||
"""Strategies for customizing subclass behaviors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from gc import collect
|
||||
from typing import Any, Callable, TypeVar, Union
|
||||
|
||||
from ..converters import BaseConverter
|
||||
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
|
||||
from ..gen._consts import already_generating
|
||||
|
||||
|
||||
def _make_subclasses_tree(cl: type) -> list[type]:
|
||||
# get class origin for accessing subclasses (see #648 for more info)
|
||||
cls_origin = typing.get_origin(cl) or cl
|
||||
return [cl] + [
|
||||
sscl
|
||||
for scl in cls_origin.__subclasses__()
|
||||
for sscl in _make_subclasses_tree(scl)
|
||||
]
|
||||
|
||||
|
||||
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
|
||||
"""Whether the given class has subclasses from `given_subclasses`."""
|
||||
actual = set(cl.__subclasses__())
|
||||
given = set(given_subclasses)
|
||||
return bool(actual & given)
|
||||
|
||||
|
||||
def _get_union_type(cl: type, given_subclasses_tree: tuple[type]) -> type | None:
|
||||
actual_subclass_tree = tuple(_make_subclasses_tree(cl))
|
||||
class_tree = tuple(set(actual_subclass_tree) & set(given_subclasses_tree))
|
||||
return Union[class_tree] if len(class_tree) >= 2 else None
|
||||
|
||||
|
||||
C = TypeVar("C", bound=BaseConverter)
|
||||
|
||||
|
||||
def include_subclasses(
|
||||
cl: type,
|
||||
converter: C,
|
||||
subclasses: tuple[type, ...] | None = None,
|
||||
union_strategy: Callable[[Any, C], Any] | None = None,
|
||||
overrides: dict[str, AttributeOverride] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Configure the converter so that the attrs/dataclass `cl` is un/structured as if it
|
||||
was a union of itself and all its subclasses that are defined at the time when this
|
||||
strategy is applied.
|
||||
|
||||
:param cl: A base `attrs` or `dataclass` class.
|
||||
:param converter: The `Converter` on which this strategy is applied. Do note that
|
||||
the strategy does not work for a :class:`cattrs.BaseConverter`.
|
||||
:param subclasses: A tuple of sublcasses whose ancestor is `cl`. If left as `None`,
|
||||
subclasses are detected using recursively the `__subclasses__` method of `cl`
|
||||
and its descendents.
|
||||
:param union_strategy: A callable of two arguments passed by position
|
||||
(`subclass_union`, `converter`) that defines the union strategy to use to
|
||||
disambiguate the subclasses union. If `None` (the default), the automatic unique
|
||||
field disambiguation is used which means that every single subclass
|
||||
participating in the union must have an attribute name that does not exist in
|
||||
any other sibling class.
|
||||
:param overrides: a mapping of `cl` attribute names to overrides (instantiated with
|
||||
:func:`cattrs.gen.override`) to customize un/structuring.
|
||||
|
||||
.. versionadded:: 23.1.0
|
||||
.. versionchanged:: 24.1.0
|
||||
When overrides are not provided, hooks for individual classes are retrieved from
|
||||
the converter instead of generated with no overrides, using converter defaults.
|
||||
"""
|
||||
# Due to https://github.com/python-attrs/attrs/issues/1047
|
||||
collect()
|
||||
if subclasses is not None:
|
||||
parent_subclass_tree = (cl, *subclasses)
|
||||
else:
|
||||
parent_subclass_tree = tuple(_make_subclasses_tree(cl))
|
||||
|
||||
if union_strategy is None:
|
||||
_include_subclasses_without_union_strategy(
|
||||
cl, converter, parent_subclass_tree, overrides
|
||||
)
|
||||
else:
|
||||
_include_subclasses_with_union_strategy(
|
||||
converter, parent_subclass_tree, union_strategy, overrides
|
||||
)
|
||||
|
||||
|
||||
def _include_subclasses_without_union_strategy(
|
||||
cl,
|
||||
converter: BaseConverter,
|
||||
parent_subclass_tree: tuple[type, ...],
|
||||
overrides: dict[str, AttributeOverride] | None,
|
||||
):
|
||||
# The iteration approach is required if subclasses are more than one level deep:
|
||||
for cl in parent_subclass_tree:
|
||||
# We re-create a reduced union type to handle the following case:
|
||||
#
|
||||
# converter.structure(d, as=Child)
|
||||
#
|
||||
# In the above, the `as=Child` argument will be transformed to a union type of
|
||||
# itself and its subtypes, that way we guarantee that the returned object will
|
||||
# not be the parent.
|
||||
subclass_union = _get_union_type(cl, parent_subclass_tree)
|
||||
|
||||
def cls_is_cl(cls, _cl=cl):
|
||||
return cls is _cl
|
||||
|
||||
if overrides is not None:
|
||||
base_struct_hook = make_dict_structure_fn(cl, converter, **overrides)
|
||||
base_unstruct_hook = make_dict_unstructure_fn(cl, converter, **overrides)
|
||||
else:
|
||||
base_struct_hook = converter.get_structure_hook(cl)
|
||||
base_unstruct_hook = converter.get_unstructure_hook(cl)
|
||||
|
||||
if subclass_union is None:
|
||||
|
||||
def struct_hook(val: dict, _, _cl=cl, _base_hook=base_struct_hook) -> cl:
|
||||
return _base_hook(val, _cl)
|
||||
|
||||
else:
|
||||
dis_fn = converter._get_dis_func(subclass_union, overrides=overrides)
|
||||
|
||||
def struct_hook(
|
||||
val: dict,
|
||||
_,
|
||||
_c=converter,
|
||||
_cl=cl,
|
||||
_base_hook=base_struct_hook,
|
||||
_dis_fn=dis_fn,
|
||||
) -> cl:
|
||||
"""
|
||||
If val is disambiguated to the class `cl`, use its base hook.
|
||||
|
||||
If val is disambiguated to a subclass, dispatch on its exact runtime
|
||||
type.
|
||||
"""
|
||||
dis_cl = _dis_fn(val)
|
||||
if dis_cl is _cl:
|
||||
return _base_hook(val, _cl)
|
||||
return _c.structure(val, dis_cl)
|
||||
|
||||
def unstruct_hook(
|
||||
val: parent_subclass_tree[0],
|
||||
_c=converter,
|
||||
_cl=cl,
|
||||
_base_hook=base_unstruct_hook,
|
||||
) -> dict:
|
||||
"""
|
||||
If val is an instance of the class `cl`, use the hook.
|
||||
|
||||
If val is an instance of a subclass, dispatch on its exact runtime type.
|
||||
"""
|
||||
if val.__class__ is _cl:
|
||||
return _base_hook(val)
|
||||
return _c.unstructure(val, unstructure_as=val.__class__)
|
||||
|
||||
# This needs to use function dispatch, using singledispatch will again
|
||||
# match A and all subclasses, which is not what we want.
|
||||
converter.register_structure_hook_func(cls_is_cl, struct_hook)
|
||||
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
|
||||
|
||||
|
||||
def _include_subclasses_with_union_strategy(
|
||||
converter: C,
|
||||
union_classes: tuple[type, ...],
|
||||
union_strategy: Callable[[Any, C], Any],
|
||||
overrides: dict[str, AttributeOverride] | None,
|
||||
):
|
||||
"""
|
||||
This function is tricky because we're dealing with what is essentially a circular
|
||||
reference.
|
||||
|
||||
We need to generate a structure hook for a class that is both:
|
||||
* specific for that particular class and its own fields
|
||||
* but should handle specific functions for all its descendants too
|
||||
|
||||
Hence the dance with registering below.
|
||||
"""
|
||||
|
||||
parent_classes = [cl for cl in union_classes if _has_subclasses(cl, union_classes)]
|
||||
if not parent_classes:
|
||||
return
|
||||
|
||||
original_unstruct_hooks = {}
|
||||
original_struct_hooks = {}
|
||||
for cl in union_classes:
|
||||
# In the first pass, every class gets its own unstructure function according to
|
||||
# the overrides.
|
||||
# We just generate the hooks, and do not register them. This allows us to
|
||||
# manipulate the _already_generating set to force runtime dispatch.
|
||||
already_generating.working_set = set(union_classes) - {cl}
|
||||
try:
|
||||
if overrides is not None:
|
||||
unstruct_hook = make_dict_unstructure_fn(cl, converter, **overrides)
|
||||
struct_hook = make_dict_structure_fn(cl, converter, **overrides)
|
||||
else:
|
||||
unstruct_hook = converter.get_unstructure_hook(cl, cache_result=False)
|
||||
struct_hook = converter.get_structure_hook(cl, cache_result=False)
|
||||
finally:
|
||||
already_generating.working_set = set()
|
||||
original_unstruct_hooks[cl] = unstruct_hook
|
||||
original_struct_hooks[cl] = struct_hook
|
||||
|
||||
# Now that's done, we can register all the hooks and generate the
|
||||
# union handler. The union handler needs them.
|
||||
final_union = Union[union_classes] # type: ignore
|
||||
|
||||
for cl, hook in original_unstruct_hooks.items():
|
||||
|
||||
def cls_is_cl(cls, _cl=cl):
|
||||
return cls is _cl
|
||||
|
||||
converter.register_unstructure_hook_func(cls_is_cl, hook)
|
||||
|
||||
for cl, hook in original_struct_hooks.items():
|
||||
|
||||
def cls_is_cl(cls, _cl=cl):
|
||||
return cls is _cl
|
||||
|
||||
converter.register_structure_hook_func(cls_is_cl, hook)
|
||||
|
||||
union_strategy(final_union, converter)
|
||||
unstruct_hook = converter.get_unstructure_hook(final_union)
|
||||
struct_hook = converter.get_structure_hook(final_union)
|
||||
|
||||
for cl in union_classes:
|
||||
# In the second pass, we overwrite the hooks with the union hook.
|
||||
|
||||
def cls_is_cl(cls, _cl=cl):
|
||||
return cls is _cl
|
||||
|
||||
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
|
||||
subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
|
||||
if len(subclasses) > 1:
|
||||
u = Union[subclasses] # type: ignore
|
||||
union_strategy(u, converter)
|
||||
struct_hook = converter.get_structure_hook(u)
|
||||
|
||||
def sh(payload: dict, _, _u=u, _s=struct_hook) -> cl:
|
||||
return _s(payload, _u)
|
||||
|
||||
converter.register_structure_hook_func(cls_is_cl, sh)
|
||||
@@ -0,0 +1,264 @@
|
||||
from collections import defaultdict
|
||||
from typing import Any, Callable, Union
|
||||
|
||||
from attrs import NOTHING, NothingType
|
||||
|
||||
from .. import BaseConverter
|
||||
from .._compat import get_newtype_base, is_literal, is_subclass, is_union_type
|
||||
from ..typealiases import is_type_alias
|
||||
|
||||
__all__ = [
|
||||
"configure_tagged_union",
|
||||
"configure_union_passthrough",
|
||||
"default_tag_generator",
|
||||
]
|
||||
|
||||
|
||||
def default_tag_generator(typ: type) -> str:
|
||||
"""Return the class name."""
|
||||
return typ.__name__
|
||||
|
||||
|
||||
def configure_tagged_union(
|
||||
union: Any,
|
||||
converter: BaseConverter,
|
||||
tag_generator: Callable[[type], str] = default_tag_generator,
|
||||
tag_name: str = "_type",
|
||||
default: Union[type, NothingType] = NOTHING,
|
||||
) -> None:
|
||||
"""
|
||||
Configure the converter so that `union` (which should be a union, or a type alias
|
||||
of one) is un/structured with the help of an additional piece of data in the
|
||||
unstructured payload, the tag.
|
||||
|
||||
:param converter: The converter to apply the strategy to.
|
||||
:param tag_generator: A `tag_generator` function is used to map each
|
||||
member of the union to a tag, which is then included in the
|
||||
unstructured payload. The default tag generator returns the name of
|
||||
the class.
|
||||
:param tag_name: The key under which the tag will be set in the
|
||||
unstructured payload. By default, `'_type'`.
|
||||
:param default: An optional class to be used if the tag information
|
||||
is not present when structuring.
|
||||
|
||||
The tagged union strategy currently only works with the dict
|
||||
un/structuring base strategy.
|
||||
|
||||
.. versionadded:: 23.1.0
|
||||
|
||||
.. versionchanged:: 25.1
|
||||
Type aliases of unions are now also supported.
|
||||
"""
|
||||
if is_type_alias(union):
|
||||
union = union.__value__
|
||||
args = union.__args__
|
||||
tag_to_hook = {}
|
||||
exact_cl_unstruct_hooks = {}
|
||||
for cl in args:
|
||||
tag = tag_generator(cl)
|
||||
struct_handler = converter.get_structure_hook(cl)
|
||||
unstruct_handler = converter.get_unstructure_hook(cl)
|
||||
|
||||
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
|
||||
return _h(val, _cl)
|
||||
|
||||
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
|
||||
return _h(val)
|
||||
|
||||
tag_to_hook[tag] = structure_union_member
|
||||
exact_cl_unstruct_hooks[cl] = unstructure_union_member
|
||||
|
||||
cl_to_tag = {cl: tag_generator(cl) for cl in args}
|
||||
|
||||
if default is not NOTHING:
|
||||
default_handler = converter.get_structure_hook(default)
|
||||
|
||||
def structure_default(val: dict, _cl=default, _h=default_handler):
|
||||
return _h(val, _cl)
|
||||
|
||||
tag_to_hook = defaultdict(lambda: structure_default, tag_to_hook)
|
||||
cl_to_tag = defaultdict(lambda: default, cl_to_tag)
|
||||
|
||||
def unstructure_tagged_union(
|
||||
val: union,
|
||||
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
|
||||
_cl_to_tag=cl_to_tag,
|
||||
_tag_name=tag_name,
|
||||
) -> dict:
|
||||
res = _exact_cl_unstruct_hooks[val.__class__](val)
|
||||
res[_tag_name] = _cl_to_tag[val.__class__]
|
||||
return res
|
||||
|
||||
if default is NOTHING:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
val = val.copy()
|
||||
return _tag_to_cl[val.pop(_tag_name)](val)
|
||||
|
||||
else:
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
return _tag_to_cl[val[_tag_name]](val)
|
||||
|
||||
else:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict,
|
||||
_,
|
||||
_tag_to_hook=tag_to_hook,
|
||||
_tag_name=tag_name,
|
||||
_dh=default_handler,
|
||||
_default=default,
|
||||
) -> union:
|
||||
if _tag_name in val:
|
||||
val = val.copy()
|
||||
return _tag_to_hook[val.pop(_tag_name)](val)
|
||||
return _dh(val, _default)
|
||||
|
||||
else:
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict,
|
||||
_,
|
||||
_tag_to_hook=tag_to_hook,
|
||||
_tag_name=tag_name,
|
||||
_dh=default_handler,
|
||||
_default=default,
|
||||
) -> union:
|
||||
if _tag_name in val:
|
||||
return _tag_to_hook[val[_tag_name]](val)
|
||||
return _dh(val, _default)
|
||||
|
||||
converter.register_unstructure_hook(union, unstructure_tagged_union)
|
||||
converter.register_structure_hook(union, structure_tagged_union)
|
||||
|
||||
|
||||
def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
||||
"""
|
||||
Configure the converter to support validating and passing through unions of the
|
||||
provided types and their subsets.
|
||||
|
||||
For example, all mature JSON libraries natively support producing unions of ints,
|
||||
floats, Nones, and strings. Using this strategy, a converter can be configured
|
||||
to efficiently validate and pass through unions containing these types.
|
||||
|
||||
The most important point is that another library (in this example the JSON
|
||||
library) handles producing the union, and the converter is configured to just
|
||||
validate it.
|
||||
|
||||
Literals of provided types are also supported, and are checked by value.
|
||||
|
||||
NewTypes of provided types are also supported.
|
||||
|
||||
The strategy is designed to be O(1) in execution time, and independent of the
|
||||
ordering of types in the union.
|
||||
|
||||
If the union contains a class and one or more of its subclasses, the subclasses
|
||||
will also be included when validating the superclass.
|
||||
|
||||
.. versionadded:: 23.2.0
|
||||
"""
|
||||
args = set(union.__args__)
|
||||
|
||||
def make_structure_native_union(exact_type: Any) -> Callable:
|
||||
# `exact_type` is likely to be a subset of the entire configured union (`args`).
|
||||
literal_values = {
|
||||
v for t in exact_type.__args__ if is_literal(t) for v in t.__args__
|
||||
}
|
||||
|
||||
# We have no idea what the actual type of `val` will be, so we can't
|
||||
# use it blindly with an `in` check since it might not be hashable.
|
||||
# So we do an additional check when handling literals.
|
||||
# Note: do no use `literal_values` here, since {0, False} gets reduced to {0}
|
||||
literal_classes = {
|
||||
v.__class__
|
||||
for t in exact_type.__args__
|
||||
if is_literal(t)
|
||||
for v in t.__args__
|
||||
}
|
||||
|
||||
non_literal_classes = {
|
||||
get_newtype_base(t) or t
|
||||
for t in exact_type.__args__
|
||||
if not is_literal(t) and ((get_newtype_base(t) or t) in args)
|
||||
}
|
||||
|
||||
# We augment the set of allowed classes with any configured subclasses of
|
||||
# the exact subclasses.
|
||||
non_literal_classes |= {
|
||||
a for a in args if any(is_subclass(a, c) for c in non_literal_classes)
|
||||
}
|
||||
|
||||
# We check for spillover - union types not handled by the strategy.
|
||||
# If spillover exists and we fail to validate our types, we call
|
||||
# further into the converter with the rest.
|
||||
spillover = {
|
||||
a
|
||||
for a in exact_type.__args__
|
||||
if (get_newtype_base(a) or a) not in non_literal_classes
|
||||
and not is_literal(a)
|
||||
}
|
||||
|
||||
if spillover:
|
||||
spillover_type = (
|
||||
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
|
||||
)
|
||||
|
||||
def structure_native_union(
|
||||
val: Any,
|
||||
_: Any,
|
||||
classes=non_literal_classes,
|
||||
vals=literal_values,
|
||||
converter=converter,
|
||||
spillover=spillover_type,
|
||||
) -> exact_type:
|
||||
if val.__class__ in literal_classes and val in vals:
|
||||
return val
|
||||
if val.__class__ in classes:
|
||||
return val
|
||||
return converter.structure(val, spillover)
|
||||
|
||||
else:
|
||||
|
||||
def structure_native_union(
|
||||
val: Any, _: Any, classes=non_literal_classes, vals=literal_values
|
||||
) -> exact_type:
|
||||
if val.__class__ in literal_classes and val in vals:
|
||||
return val
|
||||
if val.__class__ in classes:
|
||||
return val
|
||||
raise TypeError(f"{val} ({val.__class__}) not part of {_}")
|
||||
|
||||
return structure_native_union
|
||||
|
||||
def contains_native_union(exact_type: Any) -> bool:
|
||||
"""Can we handle this type?"""
|
||||
if is_union_type(exact_type):
|
||||
type_args = set(exact_type.__args__)
|
||||
# We special case optionals, since they are very common
|
||||
# and are handled a little more efficiently by default.
|
||||
if len(type_args) == 2 and type(None) in type_args:
|
||||
return False
|
||||
|
||||
literal_classes = {
|
||||
lit_arg.__class__
|
||||
for t in type_args
|
||||
if is_literal(t)
|
||||
for lit_arg in t.__args__
|
||||
}
|
||||
non_literal_types = {
|
||||
get_newtype_base(t) or t for t in type_args if not is_literal(t)
|
||||
}
|
||||
|
||||
return (literal_classes | non_literal_types) & args
|
||||
return False
|
||||
|
||||
converter.register_structure_hook_factory(
|
||||
contains_native_union, make_structure_native_union
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Utilities for type aliases."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ._compat import is_generic
|
||||
from ._generics import deep_copy_with
|
||||
from .dispatch import StructureHook
|
||||
from .gen._generics import generate_mapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .converters import BaseConverter
|
||||
|
||||
__all__ = ["get_type_alias_base", "is_type_alias", "type_alias_structure_factory"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from types import GenericAlias
|
||||
from typing import TypeAliasType
|
||||
|
||||
def is_type_alias(type: Any) -> bool:
|
||||
"""Is this a PEP 695 type alias?"""
|
||||
return isinstance(
|
||||
type.__origin__ if type.__class__ is GenericAlias else type, TypeAliasType
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
def is_type_alias(type: Any) -> bool:
|
||||
"""Is this a PEP 695 type alias?"""
|
||||
return False
|
||||
|
||||
|
||||
def get_type_alias_base(type: Any) -> Any:
|
||||
"""
|
||||
What is this a type alias of?
|
||||
|
||||
Works only on 3.12+.
|
||||
"""
|
||||
return type.__value__
|
||||
|
||||
|
||||
def type_alias_structure_factory(type: Any, converter: BaseConverter) -> StructureHook:
|
||||
base = get_type_alias_base(type)
|
||||
if is_generic(type):
|
||||
mapping = generate_mapping(type)
|
||||
if base.__name__ in mapping:
|
||||
# Probably just type T = T
|
||||
base = mapping[base.__name__]
|
||||
else:
|
||||
base = deep_copy_with(base, mapping)
|
||||
res = converter.get_structure_hook(base)
|
||||
if res == converter._structure_call:
|
||||
# we need to replace the type arg of `structure_call`
|
||||
return lambda v, _, __base=base: __base(v)
|
||||
return lambda v, _, __base=base: res(v, __base)
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
__all__ = ["SimpleStructureHook"]
|
||||
|
||||
In = TypeVar("In")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class SimpleStructureHook(Protocol[In, T]):
|
||||
"""A structure hook with an optional (ignored) second argument."""
|
||||
|
||||
def __call__(self, _: In, /, cl=...) -> T: ...
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Cattrs validation."""
|
||||
|
||||
from typing import Callable, Union
|
||||
|
||||
from .errors import (
|
||||
ClassValidationError,
|
||||
ForbiddenExtraKeysError,
|
||||
IterableValidationError,
|
||||
)
|
||||
|
||||
__all__ = ["format_exception", "transform_error"]
|
||||
|
||||
|
||||
def format_exception(exc: BaseException, type: Union[type, None]) -> str:
|
||||
"""The default exception formatter, handling the most common exceptions.
|
||||
|
||||
The following exceptions are handled specially:
|
||||
|
||||
* `KeyErrors` (`required field missing`)
|
||||
* `ValueErrors` (`invalid value for type, expected <type>` or just `invalid value`)
|
||||
* `TypeErrors` (`invalid value for type, expected <type>` and a couple special
|
||||
cases for iterables)
|
||||
* `cattrs.ForbiddenExtraKeysError`
|
||||
* some `AttributeErrors` (special cased for structing mappings)
|
||||
"""
|
||||
if isinstance(exc, KeyError):
|
||||
res = "required field missing"
|
||||
elif isinstance(exc, ValueError):
|
||||
if type is not None:
|
||||
tn = type.__name__ if hasattr(type, "__name__") else repr(type)
|
||||
res = f"invalid value for type, expected {tn}"
|
||||
else:
|
||||
res = "invalid value"
|
||||
elif isinstance(exc, TypeError):
|
||||
if type is None:
|
||||
if exc.args[0].endswith("object is not iterable"):
|
||||
res = "invalid value for type, expected an iterable"
|
||||
else:
|
||||
res = f"invalid type ({exc})"
|
||||
else:
|
||||
tn = type.__name__ if hasattr(type, "__name__") else repr(type)
|
||||
res = f"invalid value for type, expected {tn}"
|
||||
elif isinstance(exc, ForbiddenExtraKeysError):
|
||||
res = f"extra fields found ({', '.join(exc.extra_fields)})"
|
||||
elif isinstance(exc, AttributeError) and exc.args[0].endswith(
|
||||
"object has no attribute 'items'"
|
||||
):
|
||||
# This was supposed to be a mapping (and have .items()) but it something else.
|
||||
res = "expected a mapping"
|
||||
else:
|
||||
res = f"unknown error ({exc})"
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def transform_error(
|
||||
exc: Union[ClassValidationError, IterableValidationError, BaseException],
|
||||
path: str = "$",
|
||||
format_exception: Callable[
|
||||
[BaseException, Union[type, None]], str
|
||||
] = format_exception,
|
||||
) -> list[str]:
|
||||
"""Transform an exception into a list of error messages.
|
||||
|
||||
To get detailed error messages, the exception should be produced by a converter
|
||||
with `detailed_validation` set.
|
||||
|
||||
By default, the error messages are in the form of `{description} @ {path}`.
|
||||
|
||||
While traversing the exception and subexceptions, the path is formed:
|
||||
|
||||
* by appending `.{field_name}` for fields in classes
|
||||
* by appending `[{int}]` for indices in iterables, like lists
|
||||
* by appending `[{str}]` for keys in mappings, like dictionaries
|
||||
|
||||
:param exc: The exception to transform into error messages.
|
||||
:param path: The root path to use.
|
||||
:param format_exception: A callable to use to transform `Exceptions` into
|
||||
string descriptions of errors.
|
||||
|
||||
.. versionadded:: 23.1.0
|
||||
"""
|
||||
errors = []
|
||||
if isinstance(exc, IterableValidationError):
|
||||
with_notes, without = exc.group_exceptions()
|
||||
for exc, note in with_notes:
|
||||
p = f"{path}[{note.index!r}]"
|
||||
if isinstance(exc, (ClassValidationError, IterableValidationError)):
|
||||
errors.extend(transform_error(exc, p, format_exception))
|
||||
else:
|
||||
errors.append(f"{format_exception(exc, note.type)} @ {p}")
|
||||
for exc in without:
|
||||
errors.append(f"{format_exception(exc, None)} @ {path}")
|
||||
elif isinstance(exc, ClassValidationError):
|
||||
with_notes, without = exc.group_exceptions()
|
||||
for exc, note in with_notes:
|
||||
p = f"{path}.{note.name}"
|
||||
if isinstance(exc, (ClassValidationError, IterableValidationError)):
|
||||
errors.extend(transform_error(exc, p, format_exception))
|
||||
else:
|
||||
errors.append(f"{format_exception(exc, note.type)} @ {p}")
|
||||
for exc in without:
|
||||
errors.append(f"{format_exception(exc, None)} @ {path}")
|
||||
else:
|
||||
errors.append(f"{format_exception(exc, None)} @ {path}")
|
||||
return errors
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user