bagof-validators 0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bagof_validators-0.1/LICENSE +21 -0
- bagof_validators-0.1/PKG-INFO +52 -0
- bagof_validators-0.1/README.md +18 -0
- bagof_validators-0.1/pyproject.toml +110 -0
- bagof_validators-0.1/setup.cfg +4 -0
- bagof_validators-0.1/src/bagof/validators/__init__.py +75 -0
- bagof_validators-0.1/src/bagof/validators/_compat.py +35 -0
- bagof_validators-0.1/src/bagof/validators/_version.py +1 -0
- bagof_validators-0.1/src/bagof/validators/base.py +335 -0
- bagof_validators-0.1/src/bagof/validators/collections.py +378 -0
- bagof_validators-0.1/src/bagof/validators/common.py +224 -0
- bagof_validators-0.1/src/bagof/validators/exceptions.py +33 -0
- bagof_validators-0.1/src/bagof/validators/misc.py +78 -0
- bagof_validators-0.1/src/bagof/validators/numbers.py +246 -0
- bagof_validators-0.1/src/bagof/validators/numpy.py +28 -0
- bagof_validators-0.1/src/bagof/validators/py.typed +0 -0
- bagof_validators-0.1/src/bagof/validators/strings.py +53 -0
- bagof_validators-0.1/src/bagof_validators.egg-info/PKG-INFO +52 -0
- bagof_validators-0.1/src/bagof_validators.egg-info/SOURCES.txt +31 -0
- bagof_validators-0.1/src/bagof_validators.egg-info/dependency_links.txt +1 -0
- bagof_validators-0.1/src/bagof_validators.egg-info/requires.txt +8 -0
- bagof_validators-0.1/src/bagof_validators.egg-info/top_level.txt +1 -0
- bagof_validators-0.1/tests/test_base.py +378 -0
- bagof_validators-0.1/tests/test_collections.py +618 -0
- bagof_validators-0.1/tests/test_common.py +613 -0
- bagof_validators-0.1/tests/test_compat.py +58 -0
- bagof_validators-0.1/tests/test_exceptions.py +66 -0
- bagof_validators-0.1/tests/test_import.py +9 -0
- bagof_validators-0.1/tests/test_misc.py +105 -0
- bagof_validators-0.1/tests/test_numbers.py +308 -0
- bagof_validators-0.1/tests/test_numpy.py +66 -0
- bagof_validators-0.1/tests/test_strings.py +110 -0
- bagof_validators-0.1/tests/test_typevars.py +187 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bagofseeds
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bagof-validators
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Hint-based runtime validators.
|
|
5
|
+
Author-email: Yael Balbastre <yael.balbastre@gmail.com>
|
|
6
|
+
Maintainer-email: Yael Balbastre <yael.balbastre@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/bagofseeds/bagof-validators
|
|
9
|
+
Project-URL: Documentation, https://github.com/bagofseeds/bagof-validators#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/bagofseeds/bagof-validators/issues
|
|
11
|
+
Project-URL: Repository, https://github.com/bagofseeds/bagof-validators
|
|
12
|
+
Keywords: bagof,validators,python
|
|
13
|
+
Classifier: Development Status :: 1 - Planning
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: typing_extensions>=4.13
|
|
27
|
+
Requires-Dist: bagof-hints~=0.1
|
|
28
|
+
Requires-Dist: bagof-core-magic~=0.1
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
31
|
+
Requires-Dist: pytest-cov>=4; extra == "test"
|
|
32
|
+
Requires-Dist: numpy; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# bagof-validators
|
|
36
|
+
|
|
37
|
+
Hint-based validators that operate at runtime.
|
|
38
|
+
|
|
39
|
+
## Example
|
|
40
|
+
|
|
41
|
+
```pycon
|
|
42
|
+
>>> from collections import abc
|
|
43
|
+
>>> from bagof.validators import get_validator
|
|
44
|
+
>>> validate = get_validator(abc.Sequence[int])
|
|
45
|
+
>>> validate([1, 2, 3])
|
|
46
|
+
>>> validate(1)
|
|
47
|
+
TypeValidationError: IsSequence(collections.abc.Sequence[int]): Not a valid instance.
|
|
48
|
+
|> value = 1
|
|
49
|
+
>>> validate(["a", "b", "c"])
|
|
50
|
+
ValueValidationError: IsSequence(collections.abc.Sequence[int]): Iterable's 0th element is not valid.
|
|
51
|
+
|> value = ['a', 'b', 'c']
|
|
52
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# bagof-validators
|
|
2
|
+
|
|
3
|
+
Hint-based validators that operate at runtime.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```pycon
|
|
8
|
+
>>> from collections import abc
|
|
9
|
+
>>> from bagof.validators import get_validator
|
|
10
|
+
>>> validate = get_validator(abc.Sequence[int])
|
|
11
|
+
>>> validate([1, 2, 3])
|
|
12
|
+
>>> validate(1)
|
|
13
|
+
TypeValidationError: IsSequence(collections.abc.Sequence[int]): Not a valid instance.
|
|
14
|
+
|> value = 1
|
|
15
|
+
>>> validate(["a", "b", "c"])
|
|
16
|
+
ValueValidationError: IsSequence(collections.abc.Sequence[int]): Iterable's 0th element is not valid.
|
|
17
|
+
|> value = ['a', 'b', 'c']
|
|
18
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bagof-validators"
|
|
3
|
+
authors = [
|
|
4
|
+
{name = "Yael Balbastre", email = "yael.balbastre@gmail.com"},
|
|
5
|
+
]
|
|
6
|
+
maintainers = [
|
|
7
|
+
{name = "Yael Balbastre", email = "yael.balbastre@gmail.com"},
|
|
8
|
+
]
|
|
9
|
+
description = "Hint-based runtime validators."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
keywords = ["bagof", "validators", "python"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 1 - Planning",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.8",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dynamic = ["version"]
|
|
26
|
+
requires-python = ">=3.8"
|
|
27
|
+
dependencies = [
|
|
28
|
+
"typing_extensions >= 4.13",
|
|
29
|
+
"bagof-hints ~= 0.1",
|
|
30
|
+
"bagof-core-magic ~= 0.1"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/bagofseeds/bagof-validators"
|
|
35
|
+
Documentation = "https://github.com/bagofseeds/bagof-validators#readme"
|
|
36
|
+
Issues = "https://github.com/bagofseeds/bagof-validators/issues"
|
|
37
|
+
Repository = "https://github.com/bagofseeds/bagof-validators"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
test = [
|
|
41
|
+
"pytest >= 7",
|
|
42
|
+
"pytest-cov >= 4",
|
|
43
|
+
# Optional runtime dependency: the numpy validators are only tested
|
|
44
|
+
# when it is installed (see tests/test_numpy.py).
|
|
45
|
+
"numpy",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = [
|
|
50
|
+
"setuptools >= 59.0",
|
|
51
|
+
"wheel >= 0.45.1",
|
|
52
|
+
"versioningit >= 1.0",
|
|
53
|
+
]
|
|
54
|
+
build-backend = "setuptools.build_meta"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools]
|
|
57
|
+
package-dir = {"" = "src"}
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
61
|
+
include = ["bagof*"]
|
|
62
|
+
namespaces = true
|
|
63
|
+
|
|
64
|
+
[tool.setuptools.package-data]
|
|
65
|
+
"bagof.validators" = ["py.typed"]
|
|
66
|
+
|
|
67
|
+
[tool.versioningit]
|
|
68
|
+
default-version = "0+unknown"
|
|
69
|
+
|
|
70
|
+
[tool.versioningit.vcs]
|
|
71
|
+
default-tag = "0.1"
|
|
72
|
+
|
|
73
|
+
[tool.versioningit.format]
|
|
74
|
+
distance = "{base_version}+{distance}.{vcs}{rev}"
|
|
75
|
+
dirty = "{base_version}+{distance}.{vcs}{rev}.dirty"
|
|
76
|
+
distance-dirty = "{base_version}+{distance}.{vcs}{rev}.dirty"
|
|
77
|
+
|
|
78
|
+
[tool.versioningit.write]
|
|
79
|
+
file = "src/bagof/validators/_version.py"
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
testpaths = ["tests"]
|
|
83
|
+
|
|
84
|
+
[tool.coverage.run]
|
|
85
|
+
source = ["bagof.validators"]
|
|
86
|
+
|
|
87
|
+
[tool.coverage.report]
|
|
88
|
+
exclude_also = [
|
|
89
|
+
# Type-checking blocks are never executed at runtime.
|
|
90
|
+
"if tx.TYPE_CHECKING:",
|
|
91
|
+
"if tx.TYPE_CHECKING or",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.ruff]
|
|
95
|
+
line-length = 79
|
|
96
|
+
target-version = "py38"
|
|
97
|
+
|
|
98
|
+
[tool.ruff.lint]
|
|
99
|
+
select = ["ANN", "B", "E", "F", "I", "UP", "W"]
|
|
100
|
+
ignore = [
|
|
101
|
+
"ANN002", # Do not type *args
|
|
102
|
+
"ANN003", # Do not type *kwargs
|
|
103
|
+
"ANN401", # Allow typing with Any
|
|
104
|
+
"UP006", # Old-style collections hints (List instead of list)
|
|
105
|
+
"UP035", # Use `typing_extensions` instead of `typing`
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
[tool.codespell]
|
|
109
|
+
check-filenames = true
|
|
110
|
+
skip = ".git,*.egg-info,build,dist,*.pdf,*.svg"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automatic type-based validators.
|
|
3
|
+
|
|
4
|
+
Modules
|
|
5
|
+
-------
|
|
6
|
+
base
|
|
7
|
+
Base class for magic validators.
|
|
8
|
+
collections
|
|
9
|
+
Validators for collection types (list, tuple, dict, etc.).
|
|
10
|
+
common
|
|
11
|
+
Common validators (any, union, etc.).
|
|
12
|
+
exceptions
|
|
13
|
+
Exceptions raised by validators on validation error.
|
|
14
|
+
misc
|
|
15
|
+
Miscellaneous validators (forbidden values, etc.).
|
|
16
|
+
numbers
|
|
17
|
+
Validators for numeric types (int, float, etc.).
|
|
18
|
+
numpy
|
|
19
|
+
Validators for numpy types (dtype, etc.).
|
|
20
|
+
strings
|
|
21
|
+
Validators for string types (regex patterns, etc.).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
"base",
|
|
27
|
+
"collections",
|
|
28
|
+
"common",
|
|
29
|
+
"exceptions",
|
|
30
|
+
"misc",
|
|
31
|
+
"numbers",
|
|
32
|
+
"numpy",
|
|
33
|
+
"strings",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from ._version import __version__
|
|
38
|
+
except ImportError: # pragma: no cover
|
|
39
|
+
__version__ = "0+unknown"
|
|
40
|
+
|
|
41
|
+
from . import (
|
|
42
|
+
base,
|
|
43
|
+
collections,
|
|
44
|
+
common,
|
|
45
|
+
exceptions,
|
|
46
|
+
misc,
|
|
47
|
+
numbers,
|
|
48
|
+
numpy,
|
|
49
|
+
strings,
|
|
50
|
+
)
|
|
51
|
+
from .base import * # noqa: F401, F403
|
|
52
|
+
from .base import __all__ as __all_base
|
|
53
|
+
from .collections import * # noqa: F401, F403
|
|
54
|
+
from .collections import __all__ as __all_collections
|
|
55
|
+
from .common import * # noqa: F401, F403
|
|
56
|
+
from .common import __all__ as __all_common
|
|
57
|
+
from .exceptions import * # noqa: F401, F403
|
|
58
|
+
from .exceptions import __all__ as __all_exceptions
|
|
59
|
+
from .misc import * # noqa: F401, F403
|
|
60
|
+
from .misc import __all__ as __all_misc
|
|
61
|
+
from .numbers import * # noqa: F401, F403
|
|
62
|
+
from .numbers import __all__ as __all_numbers
|
|
63
|
+
from .numpy import * # noqa: F401, F403
|
|
64
|
+
from .numpy import __all__ as __all_numpy
|
|
65
|
+
from .strings import * # noqa: F401, F403
|
|
66
|
+
from .strings import __all__ as __all_strings
|
|
67
|
+
|
|
68
|
+
__all__ += __all_base
|
|
69
|
+
__all__ += __all_collections
|
|
70
|
+
__all__ += __all_common
|
|
71
|
+
__all__ += __all_exceptions
|
|
72
|
+
__all__ += __all_misc
|
|
73
|
+
__all__ += __all_numbers
|
|
74
|
+
__all__ += __all_numpy
|
|
75
|
+
__all__ += __all_strings
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import typing_extensions as tx
|
|
2
|
+
|
|
3
|
+
# optional
|
|
4
|
+
if tx.TYPE_CHECKING:
|
|
5
|
+
from types import NoneType, UnionType
|
|
6
|
+
|
|
7
|
+
UNION_TYPES = (tx.Union, UnionType)
|
|
8
|
+
|
|
9
|
+
else:
|
|
10
|
+
try:
|
|
11
|
+
from types import NoneType
|
|
12
|
+
except ImportError: # pragma: no cover
|
|
13
|
+
NoneType = type(None) # type: ignore[assignment]
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from types import UnionType
|
|
17
|
+
UNION_TYPES = (tx.Union, UnionType)
|
|
18
|
+
except ImportError: # pragma: no cover
|
|
19
|
+
UnionType = tx.Union # type: ignore[assignment]
|
|
20
|
+
UNION_TYPES = (tx.Union,)
|
|
21
|
+
|
|
22
|
+
if tx.TYPE_CHECKING:
|
|
23
|
+
import numpy as np
|
|
24
|
+
import numpy.typing as npt
|
|
25
|
+
|
|
26
|
+
else:
|
|
27
|
+
try:
|
|
28
|
+
import numpy as np
|
|
29
|
+
except ImportError: # pragma: no cover
|
|
30
|
+
np = None # type: ignore[assignment]
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import numpy.typing as npt
|
|
34
|
+
except ImportError: # pragma: no cover
|
|
35
|
+
npt = None # type: ignore[assignment]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1"
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Base class for all validators."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"Validator",
|
|
5
|
+
"register_validator",
|
|
6
|
+
"get_validator",
|
|
7
|
+
"get_validator_class"
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
# dependencies
|
|
11
|
+
import typing_extensions as tx # noqa: I001
|
|
12
|
+
|
|
13
|
+
# bags
|
|
14
|
+
from bagof.core.magic import (
|
|
15
|
+
UNSET,
|
|
16
|
+
MagicHint,
|
|
17
|
+
get_from_registry,
|
|
18
|
+
ishintstance,
|
|
19
|
+
safe_isinstance,
|
|
20
|
+
safe_issubclass,
|
|
21
|
+
)
|
|
22
|
+
from bagof.hints.typevars.co import TYPE, T
|
|
23
|
+
|
|
24
|
+
# locals
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
TypeValidationError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
ValueValidationError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# typing
|
|
32
|
+
|
|
33
|
+
ClassDecorator: tx.TypeAlias = tx.Callable[[TYPE], TYPE]
|
|
34
|
+
"""A class decorator (that takes a class and returns a class)."""
|
|
35
|
+
|
|
36
|
+
ValidatorRegistry = tx.Dict[tx.Hashable, tx.Type["Validator"]]
|
|
37
|
+
"""A registry of validators, mapping type hints to validator classes."""
|
|
38
|
+
|
|
39
|
+
# constants
|
|
40
|
+
VALIDATORS: ValidatorRegistry = {}
|
|
41
|
+
"""The global registry of validators."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ValidatorMetaclass(type(MagicHint)):
|
|
45
|
+
"""Metaclass for all validators."""
|
|
46
|
+
|
|
47
|
+
def __new__(
|
|
48
|
+
metacls,
|
|
49
|
+
name: str,
|
|
50
|
+
bases: tx.Tuple[type, ...],
|
|
51
|
+
namespace: tx.Mapping[str, tx.Any],
|
|
52
|
+
**kwargs
|
|
53
|
+
) -> tx.Self:
|
|
54
|
+
register = kwargs.pop("register", UNSET)
|
|
55
|
+
cls = super().__new__(metacls, name, bases, namespace, **kwargs)
|
|
56
|
+
if register is not UNSET:
|
|
57
|
+
if register is True:
|
|
58
|
+
register = (cls.DEFAULT,)
|
|
59
|
+
if not isinstance(register, tuple):
|
|
60
|
+
register = (register,)
|
|
61
|
+
Validator.register(cls, *register)
|
|
62
|
+
return cls
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Validator(MagicHint[T], metaclass=ValidatorMetaclass):
|
|
66
|
+
"""
|
|
67
|
+
Base class for magic validators.
|
|
68
|
+
|
|
69
|
+
The default validator falls back to a rather crude heuristic to check
|
|
70
|
+
whether the type of the validated object is compatible with the type
|
|
71
|
+
hint.
|
|
72
|
+
|
|
73
|
+
!!! warning
|
|
74
|
+
This heuristic does not work well for generic types, so it is
|
|
75
|
+
recommended to implement a custom validator for generic types.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
DEFAULT = tx.Any
|
|
79
|
+
|
|
80
|
+
def __init__(self, hint: tx.Any = UNSET, compose: bool = False) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
hint : Any, optional
|
|
85
|
+
The type hint to use for this magic object.
|
|
86
|
+
If not provided, the default hint for the class is used.
|
|
87
|
+
compose : bool
|
|
88
|
+
Whether to compose this validator with others, when they are
|
|
89
|
+
found in [`Annotated`][typing.Annotated] metadata.
|
|
90
|
+
"""
|
|
91
|
+
super().__init__(hint)
|
|
92
|
+
self.compose = compose
|
|
93
|
+
|
|
94
|
+
def __call__(self, value: T) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Validate the given value.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
value : T
|
|
101
|
+
The value to validate.
|
|
102
|
+
|
|
103
|
+
Raises
|
|
104
|
+
------
|
|
105
|
+
ValidationError
|
|
106
|
+
If the value is not valid for this validator.
|
|
107
|
+
"""
|
|
108
|
+
if not ishintstance(value, self.hint):
|
|
109
|
+
raise self.type_error(value, "Not a valid instance.")
|
|
110
|
+
|
|
111
|
+
def error(
|
|
112
|
+
self, value: tx.Any, message: tx.Optional[str] = None, **kwargs
|
|
113
|
+
) -> ValidationError:
|
|
114
|
+
"""Return a [`ValidationError`][] with the given value and message."""
|
|
115
|
+
type = kwargs.pop("type", ValidationError)
|
|
116
|
+
type = {
|
|
117
|
+
"value": ValueValidationError,
|
|
118
|
+
"type": TypeValidationError
|
|
119
|
+
}.get(type, type)
|
|
120
|
+
kwargs.setdefault("this", self)
|
|
121
|
+
kwargs.setdefault("value", value)
|
|
122
|
+
if message is None:
|
|
123
|
+
message = "Invalid value."
|
|
124
|
+
return type(message, **kwargs)
|
|
125
|
+
|
|
126
|
+
def type_error(
|
|
127
|
+
self, value: tx.Any, message: tx.Optional[str] = None
|
|
128
|
+
) -> TypeValidationError:
|
|
129
|
+
"""Return a [`TypeValidationError`][] with the given value."""
|
|
130
|
+
if message is None:
|
|
131
|
+
message = f"Invalid value type: {type(value)}"
|
|
132
|
+
return self.error(value, message, type=TypeValidationError)
|
|
133
|
+
|
|
134
|
+
def value_error(
|
|
135
|
+
self, value: tx.Any, message: tx.Optional[str] = None
|
|
136
|
+
) -> ValueValidationError:
|
|
137
|
+
"""Return a [`ValueValidationError`][] with the given value."""
|
|
138
|
+
if message is None:
|
|
139
|
+
message = "Invalid value."
|
|
140
|
+
return self.error(value, message, type=ValueValidationError)
|
|
141
|
+
|
|
142
|
+
def _wrap_validator(self, validator: tx.Callable) -> tx.Callable:
|
|
143
|
+
"""
|
|
144
|
+
Wrap a validator so that a plain [`TypeError`][] or
|
|
145
|
+
[`ValueError`][] raised inside it surfaces as a
|
|
146
|
+
[`ValidationError`][], with the original attached as its cause.
|
|
147
|
+
|
|
148
|
+
A [`ValidationError`][] raised by the wrapped validator passes
|
|
149
|
+
through unchanged.
|
|
150
|
+
"""
|
|
151
|
+
return _trywrap_validator(validator, self.value_error)
|
|
152
|
+
|
|
153
|
+
@tx.overload
|
|
154
|
+
@staticmethod
|
|
155
|
+
def register(
|
|
156
|
+
validator: tx.Type["Validator"],
|
|
157
|
+
*hints: tx.Unpack[tx.Tuple[tx.Any, ...]],
|
|
158
|
+
registry: ValidatorRegistry = ...
|
|
159
|
+
) -> tx.Type["Validator"]:
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
@tx.overload
|
|
163
|
+
@staticmethod
|
|
164
|
+
def register(
|
|
165
|
+
*hints: tx.Unpack[tx.Tuple[tx.Any, ...]],
|
|
166
|
+
registry: ValidatorRegistry = ...
|
|
167
|
+
) -> ClassDecorator:
|
|
168
|
+
...
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def register(*hints, registry=VALIDATORS):
|
|
172
|
+
"""
|
|
173
|
+
Decorator to register a validator class for one or more type hints.
|
|
174
|
+
|
|
175
|
+
Can be used as a bare decorator (its first argument is then the
|
|
176
|
+
validator class itself, as in the example below), or called with
|
|
177
|
+
one or more type hints to obtain a decorator factory instead.
|
|
178
|
+
|
|
179
|
+
!!! example
|
|
180
|
+
```python
|
|
181
|
+
@Validator.register
|
|
182
|
+
class IntValidator(Validator[int]):
|
|
183
|
+
|
|
184
|
+
DEFAULT = int
|
|
185
|
+
|
|
186
|
+
def __call__(self, value: int) -> None:
|
|
187
|
+
try:
|
|
188
|
+
int(value)
|
|
189
|
+
except (TypeError, ValueError) as e:
|
|
190
|
+
raise self.type_error(value) from e
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
*hints
|
|
196
|
+
One or more type hints to register the validator class for.
|
|
197
|
+
Defaults to the validator class's `DEFAULT` hint if none are
|
|
198
|
+
given. When used as a bare decorator, the first "hint" is
|
|
199
|
+
actually the validator class to register.
|
|
200
|
+
registry : ValidatorRegistry
|
|
201
|
+
The registry to register the validator class in.
|
|
202
|
+
Defaults to the global registry.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
tx.Type["Validator"] | ClassDecorator
|
|
207
|
+
When used as a bare decorator, the same validator class, now
|
|
208
|
+
registered. Otherwise, a decorator that registers the
|
|
209
|
+
validator class it is applied to.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
if hints and safe_issubclass(hints[0], Validator):
|
|
213
|
+
validator, *hints = hints
|
|
214
|
+
return Validator.register(*hints, registry=registry)(validator)
|
|
215
|
+
|
|
216
|
+
def decorator(cls: tx.Type[Validator]) -> tx.Type[Validator]:
|
|
217
|
+
hints_ = hints or (cls.DEFAULT,)
|
|
218
|
+
for hint in hints_:
|
|
219
|
+
registry[hint] = cls
|
|
220
|
+
return cls
|
|
221
|
+
|
|
222
|
+
return decorator
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def get(
|
|
226
|
+
hint: tx.Any,
|
|
227
|
+
registry: ValidatorRegistry = VALIDATORS,
|
|
228
|
+
fallback: tx.Optional[tx.Type["Validator"]] = UNSET
|
|
229
|
+
) -> tx.Optional["Validator"]:
|
|
230
|
+
"""
|
|
231
|
+
Get the best-matching conversion function for a given type hint.
|
|
232
|
+
|
|
233
|
+
The returned validator is a callable that returns `None` on success
|
|
234
|
+
and raises a [`ValidationError`][] on failure:
|
|
235
|
+
|
|
236
|
+
!!! example
|
|
237
|
+
```pycon
|
|
238
|
+
>>> from bagof.validators import get_validator
|
|
239
|
+
>>> validate = get_validator(int)
|
|
240
|
+
>>> validate(3)
|
|
241
|
+
>>> validate("x")
|
|
242
|
+
TypeValidationError: IsNumber(<class 'int'>): Not a valid instance.
|
|
243
|
+
|> value = 'x'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
hint
|
|
249
|
+
The type hint for which to get a validator.
|
|
250
|
+
registry : ValidatorRegistry
|
|
251
|
+
The registry to look up the validator in.
|
|
252
|
+
Defaults to the global registry.
|
|
253
|
+
fallback : tx.Optional[Type[Validator]]
|
|
254
|
+
The fallback validator class to use if no matching validator
|
|
255
|
+
is found. Defaults to [`Validator`][].
|
|
256
|
+
Pass `None` explicitly to get `None` instead of a fallback.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
tx.Optional[Validator]
|
|
261
|
+
The best-matching validator for the given type hint, or `None`
|
|
262
|
+
if no matching validator is found and no fallback is provided.
|
|
263
|
+
"""
|
|
264
|
+
cls = Validator.get_class(hint, registry, fallback)
|
|
265
|
+
if cls is None:
|
|
266
|
+
return None
|
|
267
|
+
return cls(hint)
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def get_class(
|
|
271
|
+
hint: tx.Any,
|
|
272
|
+
registry: ValidatorRegistry = VALIDATORS,
|
|
273
|
+
fallback: tx.Optional[tx.Type["Validator"]] = UNSET
|
|
274
|
+
) -> tx.Optional[tx.Type["Validator"]]:
|
|
275
|
+
"""
|
|
276
|
+
Get the best-matching conversion class for a given type hint.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
hint
|
|
281
|
+
The type hint for which to get a validator.
|
|
282
|
+
registry : ValidatorRegistry
|
|
283
|
+
The registry to look up the validator in.
|
|
284
|
+
Defaults to the global registry.
|
|
285
|
+
fallback : tx.Optional[Type[Validator]]
|
|
286
|
+
The fallback validator class to use if no matching validator
|
|
287
|
+
is found. Defaults to [`Validator`][].
|
|
288
|
+
Pass `None` explicitly to get `None` instead of a fallback.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
tx.Optional[Type[Validator]]
|
|
293
|
+
The best-matching validator class for the given type hint,
|
|
294
|
+
or `None` if no matching validator is found and no fallback
|
|
295
|
+
is provided.
|
|
296
|
+
"""
|
|
297
|
+
if fallback is UNSET:
|
|
298
|
+
fallback = Validator
|
|
299
|
+
return get_from_registry(hint, registry) or fallback
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
register_validator = Validator.register
|
|
303
|
+
"""Backward-compatible alias for [`Validator.register`][]"""
|
|
304
|
+
|
|
305
|
+
get_validator = Validator.get
|
|
306
|
+
"""Backward-compatible alias for [`Validator.get`][]"""
|
|
307
|
+
|
|
308
|
+
get_validator_class = Validator.get_class
|
|
309
|
+
"""Backward-compatible alias for [`Validator.get_class`][]"""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _trywrap_validator(
|
|
313
|
+
validator: tx.Callable[[T], None],
|
|
314
|
+
error: tx.Union[Exception, tx.Type[Exception], tx.Callable[[T], Exception]]
|
|
315
|
+
) -> tx.Callable[[T], None]:
|
|
316
|
+
"""
|
|
317
|
+
Wrap a validator to catch errors and raise a [`ValidationError`][] instead.
|
|
318
|
+
"""
|
|
319
|
+
def wrapped(value: T) -> None:
|
|
320
|
+
try:
|
|
321
|
+
return validator(value)
|
|
322
|
+
except ValidationError:
|
|
323
|
+
# Already the right kind of error: re-raise it untouched, so
|
|
324
|
+
# its specific type and its `causes` survive. `TypeError` and
|
|
325
|
+
# `ValueError` are the base classes of `TypeValidationError`
|
|
326
|
+
# and `ValueValidationError`, so without this the `except`
|
|
327
|
+
# below would swallow and downgrade every real failure.
|
|
328
|
+
raise
|
|
329
|
+
except (TypeError, ValueError) as e:
|
|
330
|
+
_error = error
|
|
331
|
+
if not safe_isinstance(_error, BaseException):
|
|
332
|
+
# Either an exception class or a factory (e.g. `value_error`).
|
|
333
|
+
_error = _error(value)
|
|
334
|
+
raise _error from e
|
|
335
|
+
return wrapped
|