1import math
2import types
3from collections.abc import Callable, Iterator
4from dataclasses import dataclass
5from datetime import tzinfo
6from types import EllipsisType
7from typing import (
8 TYPE_CHECKING,
9 Annotated,
10 Any,
11 Literal,
12 Protocol,
13 SupportsFloat,
14 SupportsIndex,
15 TypeVar,
16 Union,
17 runtime_checkable,
18)
19
20__all__ = (
21 'BaseMetadata',
22 'GroupedMetadata',
23 'Gt',
24 'Ge',
25 'Lt',
26 'Le',
27 'Interval',
28 'MultipleOf',
29 'MinLen',
30 'MaxLen',
31 'Len',
32 'Timezone',
33 'Predicate',
34 'LowerCase',
35 'UpperCase',
36 'IsDigits',
37 'IsFinite',
38 'IsNotFinite',
39 'IsNan',
40 'IsNotNan',
41 'IsInfinite',
42 'IsNotInfinite',
43 'doc',
44 'DocInfo',
45 '__version__',
46)
47
48__version__ = '0.8.0'
49
50
51T = TypeVar('T')
52
53
54# arguments that start with __ are considered
55# positional only
56# see https://peps.python.org/pep-0484/#positional-only-arguments
57
58
59class SupportsGt(Protocol):
60 def __gt__(self: T, __other: T) -> bool:
61 ...
62
63
64class SupportsGe(Protocol):
65 def __ge__(self: T, __other: T) -> bool:
66 ...
67
68
69class SupportsLt(Protocol):
70 def __lt__(self: T, __other: T) -> bool:
71 ...
72
73
74class SupportsLe(Protocol):
75 def __le__(self: T, __other: T) -> bool:
76 ...
77
78
79class SupportsMod(Protocol):
80 def __mod__(self: T, __other: T) -> T:
81 ...
82
83
84class SupportsDiv(Protocol):
85 def __div__(self: T, __other: T) -> T:
86 ...
87
88
89class BaseMetadata:
90 """Base class for all metadata.
91
92 This exists mainly so that implementers
93 can do `isinstance(..., BaseMetadata)` while traversing field annotations.
94 """
95
96 __slots__ = ()
97
98
99@dataclass(frozen=True, slots=True)
100class Gt(BaseMetadata):
101 """Gt(gt=x) implies that the value must be greater than x.
102
103 It can be used with any type that supports the ``>`` operator,
104 including numbers, dates and times, strings, sets, and so on.
105 """
106
107 gt: SupportsGt
108
109
110@dataclass(frozen=True, slots=True)
111class Ge(BaseMetadata):
112 """Ge(ge=x) implies that the value must be greater than or equal to x.
113
114 It can be used with any type that supports the ``>=`` operator,
115 including numbers, dates and times, strings, sets, and so on.
116 """
117
118 ge: SupportsGe
119
120
121@dataclass(frozen=True, slots=True)
122class Lt(BaseMetadata):
123 """Lt(lt=x) implies that the value must be less than x.
124
125 It can be used with any type that supports the ``<`` operator,
126 including numbers, dates and times, strings, sets, and so on.
127 """
128
129 lt: SupportsLt
130
131
132@dataclass(frozen=True, slots=True)
133class Le(BaseMetadata):
134 """Le(le=x) implies that the value must be less than or equal to x.
135
136 It can be used with any type that supports the ``<=`` operator,
137 including numbers, dates and times, strings, sets, and so on.
138 """
139
140 le: SupportsLe
141
142
143@runtime_checkable
144class GroupedMetadata(Protocol):
145 """A grouping of multiple objects, like typing.Unpack.
146
147 `GroupedMetadata` on its own is not metadata and has no meaning.
148 All of the constraints and metadata should be fully expressable
149 in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
150
151 Concrete implementations should override `GroupedMetadata.__iter__()`
152 to add their own metadata.
153 For example:
154
155 >>> @dataclass
156 >>> class Field(GroupedMetadata):
157 >>> gt: float | None = None
158 >>> description: str | None = None
159 ...
160 >>> def __iter__(self) -> Iterable[object]:
161 >>> if self.gt is not None:
162 >>> yield Gt(self.gt)
163 >>> if self.description is not None:
164 >>> yield Description(self.gt)
165
166 Also see the implementation of `Interval` below for an example.
167
168 Parsers should recognize this and unpack it so that it can be used
169 both with and without unpacking:
170
171 - `Annotated[int, Field(...)]` (parser must unpack Field)
172 - `Annotated[int, *Field(...)]` (PEP-646)
173 """ # noqa: trailing-whitespace
174
175 @property
176 def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
177 return True
178
179 def __iter__(self) -> Iterator[object]:
180 ...
181
182 if not TYPE_CHECKING:
183 __slots__ = () # allow subclasses to use slots
184
185 def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
186 # Basic ABC like functionality without the complexity of an ABC
187 super().__init_subclass__(*args, **kwargs)
188 if cls.__iter__ is GroupedMetadata.__iter__:
189 raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
190
191 def __iter__(self) -> Iterator[object]: # noqa: F811
192 raise NotImplementedError # more helpful than "None has no attribute..." type errors
193
194
195@dataclass(frozen=True, kw_only=True, slots=True)
196class Interval(GroupedMetadata):
197 """Interval can express inclusive or exclusive bounds with a single object.
198
199 It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
200 are interpreted the same way as the single-bound constraints.
201 """
202
203 gt: SupportsGt | None = None
204 ge: SupportsGe | None = None
205 lt: SupportsLt | None = None
206 le: SupportsLe | None = None
207
208 def __iter__(self) -> Iterator[BaseMetadata]:
209 """Unpack an Interval into zero or more single-bounds."""
210 if self.gt is not None:
211 yield Gt(self.gt)
212 if self.ge is not None:
213 yield Ge(self.ge)
214 if self.lt is not None:
215 yield Lt(self.lt)
216 if self.le is not None:
217 yield Le(self.le)
218
219
220@dataclass(frozen=True, slots=True)
221class MultipleOf(BaseMetadata):
222 """MultipleOf(multiple_of=x) might be interpreted in two ways:
223
224 1. Python semantics, implying ``value % multiple_of == 0``, or
225 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
226
227 We encourage users to be aware of these two common interpretations,
228 and libraries to carefully document which they implement.
229 """
230
231 multiple_of: SupportsDiv | SupportsMod
232
233
234@dataclass(frozen=True, slots=True)
235class MinLen(BaseMetadata):
236 """
237 MinLen() implies minimum inclusive length,
238 e.g. ``len(value) >= min_length``.
239 """
240
241 min_length: Annotated[int, Ge(0)]
242
243
244@dataclass(frozen=True, slots=True)
245class MaxLen(BaseMetadata):
246 """
247 MaxLen() implies maximum inclusive length,
248 e.g. ``len(value) <= max_length``.
249 """
250
251 max_length: Annotated[int, Ge(0)]
252
253
254@dataclass(frozen=True, slots=True)
255class Len(GroupedMetadata):
256 """
257 Len() implies that ``min_length <= len(value) <= max_length``.
258
259 Upper bound may be omitted or ``None`` to indicate no upper length bound.
260 """
261
262 min_length: Annotated[int, Ge(0)] = 0
263 max_length: Annotated[int, Ge(0)] | None = None
264
265 def __iter__(self) -> Iterator[BaseMetadata]:
266 """Unpack a Len into zero or more single-bounds."""
267 if self.min_length > 0:
268 yield MinLen(self.min_length)
269 if self.max_length is not None:
270 yield MaxLen(self.max_length)
271
272
273@dataclass(frozen=True, slots=True)
274class Timezone(BaseMetadata):
275 """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
276
277 ``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
278 ``Timezone(...)`` (the ellipsis literal) expresses that the datetime must be
279 tz-aware but any timezone is allowed.
280
281 You may also pass a specific timezone string or tzinfo object such as
282 ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
283 you only allow a specific timezone, though we note that this is often
284 a symptom of poor design.
285 """
286
287 tz: str | tzinfo | EllipsisType | None
288
289
290@dataclass(frozen=True, slots=True)
291class Unit(BaseMetadata):
292 """Indicates that the value is a physical quantity with the specified unit.
293
294 It is intended for usage with numeric types, where the value represents the
295 magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]``
296 or ``speed: Annotated[float, Unit('m/s')]``.
297
298 Interpretation of the unit string is left to the discretion of the consumer.
299 It is suggested to follow conventions established by python libraries that work
300 with physical quantities, such as
301
302 - ``pint`` : <https://pint.readthedocs.io/en/stable/>
303 - ``astropy.units``: <https://docs.astropy.org/en/stable/units/>
304
305 For indicating a quantity with a certain dimensionality but without a specific unit
306 it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`.
307 Note, however, ``annotated_types`` itself makes no use of the unit string.
308 """
309
310 unit: str
311
312
313@dataclass(frozen=True, slots=True)
314class Predicate(BaseMetadata):
315 """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
316
317 Users should prefer statically inspectable metadata, but if you need the full
318 power and flexibility of arbitrary runtime predicates... here it is.
319
320 We provide a few predefined predicates for common string constraints:
321 ``LowerCase = Predicate(str.islower)``, ``UpperCase = Predicate(str.isupper)``, and
322 ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which
323 can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
324
325 Some libraries might have special logic to handle certain predicates, e.g. by
326 checking for `str.isdigit` and using its presence to both call custom logic to
327 enforce digit-only strings, and customise some generated external schema.
328
329 We do not specify what behaviour should be expected for predicates that raise
330 an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
331 skip invalid constraints, or statically raise an error; or it might try calling it
332 and then propagate or discard the resulting exception.
333 """
334
335 func: Callable[[Any], bool]
336
337 def __repr__(self) -> str:
338 if getattr(self.func, "__name__", "<lambda>") == "<lambda>":
339 return f"{self.__class__.__name__}({self.func!r})"
340 if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and (
341 namespace := getattr(self.func.__self__, "__name__", None)
342 ):
343 return f"{self.__class__.__name__}({namespace}.{self.func.__name__})"
344 if isinstance(self.func, type(str.isascii)): # method descriptor
345 return f"{self.__class__.__name__}({self.func.__qualname__})"
346 return f"{self.__class__.__name__}({self.func.__name__})"
347
348
349@dataclass
350class Not:
351 func: Callable[[Any], bool]
352
353 def __call__(self, __v: Any) -> bool:
354 return not self.func(__v)
355
356
357_StrType = TypeVar("_StrType", bound=str)
358
359LowerCase = Annotated[_StrType, Predicate(str.islower)]
360"""
361Return True if the string is a lowercase string, False otherwise.
362
363A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
364""" # noqa: E501
365UpperCase = Annotated[_StrType, Predicate(str.isupper)]
366"""
367Return True if the string is an uppercase string, False otherwise.
368
369A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
370""" # noqa: E501
371IsDigit = Annotated[_StrType, Predicate(str.isdigit)]
372IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63
373"""
374Return True if the string is a digit string, False otherwise.
375
376A string is a digit string if all characters in the string are digits and there is at least one character in the string.
377""" # noqa: E501
378IsAscii = Annotated[_StrType, Predicate(str.isascii)]
379"""
380Return True if all characters in the string are ASCII, False otherwise.
381
382ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
383"""
384
385_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
386IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
387"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
388IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
389"""Return True if x is one of infinity or NaN, and False otherwise"""
390IsNan = Annotated[_NumericType, Predicate(math.isnan)]
391"""Return True if x is a NaN (not a number), and False otherwise."""
392IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
393"""Return True if x is anything but NaN (not a number), and False otherwise."""
394IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
395"""Return True if x is a positive or negative infinity, and False otherwise."""
396IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
397"""Return True if x is neither a positive or negative infinity, and False otherwise."""
398
399try:
400 # PEP 727 – Documentation in Annotated Metadata
401 from typing_extensions import Doc # type: ignore[attr-defined]
402except ImportError:
403
404 @dataclass(frozen=True, slots=True)
405 class Doc: # type: ignore [no-redef]
406 """ "
407 The return value of doc(), mainly to be used by tools that want to extract the
408 Annotated documentation at runtime.
409 """
410
411 documentation: str
412 """The documentation string passed to doc()."""
413
414
415DocInfo = Doc # backwards compatibility
416doc = Doc