Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/annotated_types/__init__.py: 76%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

144 statements  

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