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

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

156 statements  

1import math 

2import sys 

3import types 

4from dataclasses import dataclass 

5from datetime import tzinfo 

6from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union 

7 

8if sys.version_info < (3, 8): 

9 from typing_extensions import Protocol, runtime_checkable 

10else: 

11 from typing import Protocol, runtime_checkable 

12 

13if sys.version_info < (3, 9): 

14 from typing_extensions import Annotated, Literal 

15else: 

16 from typing import Annotated, Literal 

17 

18if sys.version_info < (3, 10): 

19 EllipsisType = type(Ellipsis) 

20 KW_ONLY = {} 

21 SLOTS = {} 

22else: 

23 from types import EllipsisType 

24 

25 KW_ONLY = {"kw_only": True} 

26 SLOTS = {"slots": True} 

27 

28 

29__all__ = ( 

30 'BaseMetadata', 

31 'GroupedMetadata', 

32 'Gt', 

33 'Ge', 

34 'Lt', 

35 'Le', 

36 'Interval', 

37 'MultipleOf', 

38 'MinLen', 

39 'MaxLen', 

40 'Len', 

41 'Timezone', 

42 'Predicate', 

43 'LowerCase', 

44 'UpperCase', 

45 'IsDigits', 

46 'IsFinite', 

47 'IsNotFinite', 

48 'IsNan', 

49 'IsNotNan', 

50 'IsInfinite', 

51 'IsNotInfinite', 

52 'doc', 

53 'DocInfo', 

54 '__version__', 

55) 

56 

57__version__ = '0.7.0' 

58 

59 

60T = TypeVar('T') 

61 

62 

63# arguments that start with __ are considered 

64# positional only 

65# see https://peps.python.org/pep-0484/#positional-only-arguments 

66 

67 

68class SupportsGt(Protocol): 

69 def __gt__(self: T, __other: T) -> bool: 

70 ... 

71 

72 

73class SupportsGe(Protocol): 

74 def __ge__(self: T, __other: T) -> bool: 

75 ... 

76 

77 

78class SupportsLt(Protocol): 

79 def __lt__(self: T, __other: T) -> bool: 

80 ... 

81 

82 

83class SupportsLe(Protocol): 

84 def __le__(self: T, __other: T) -> bool: 

85 ... 

86 

87 

88class SupportsMod(Protocol): 

89 def __mod__(self: T, __other: T) -> T: 

90 ... 

91 

92 

93class SupportsDiv(Protocol): 

94 def __div__(self: T, __other: T) -> T: 

95 ... 

96 

97 

98class BaseMetadata: 

99 """Base class for all metadata. 

100 

101 This exists mainly so that implementers 

102 can do `isinstance(..., BaseMetadata)` while traversing field annotations. 

103 """ 

104 

105 __slots__ = () 

106 

107 

108@dataclass(frozen=True, **SLOTS) 

109class Gt(BaseMetadata): 

110 """Gt(gt=x) implies that the value must be greater than x. 

111 

112 It can be used with any type that supports the ``>`` operator, 

113 including numbers, dates and times, strings, sets, and so on. 

114 """ 

115 

116 gt: SupportsGt 

117 

118 

119@dataclass(frozen=True, **SLOTS) 

120class Ge(BaseMetadata): 

121 """Ge(ge=x) implies that the value must be greater than or equal to x. 

122 

123 It can be used with any type that supports the ``>=`` operator, 

124 including numbers, dates and times, strings, sets, and so on. 

125 """ 

126 

127 ge: SupportsGe 

128 

129 

130@dataclass(frozen=True, **SLOTS) 

131class Lt(BaseMetadata): 

132 """Lt(lt=x) implies that the value must be less than x. 

133 

134 It can be used with any type that supports the ``<`` operator, 

135 including numbers, dates and times, strings, sets, and so on. 

136 """ 

137 

138 lt: SupportsLt 

139 

140 

141@dataclass(frozen=True, **SLOTS) 

142class Le(BaseMetadata): 

143 """Le(le=x) implies that the value must be less than or equal to x. 

144 

145 It can be used with any type that supports the ``<=`` operator, 

146 including numbers, dates and times, strings, sets, and so on. 

147 """ 

148 

149 le: SupportsLe 

150 

151 

152@runtime_checkable 

153class GroupedMetadata(Protocol): 

154 """A grouping of multiple objects, like typing.Unpack. 

155 

156 `GroupedMetadata` on its own is not metadata and has no meaning. 

157 All of the constraints and metadata should be fully expressable 

158 in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`. 

159 

160 Concrete implementations should override `GroupedMetadata.__iter__()` 

161 to add their own metadata. 

162 For example: 

163 

164 >>> @dataclass 

165 >>> class Field(GroupedMetadata): 

166 >>> gt: float | None = None 

167 >>> description: str | None = None 

168 ... 

169 >>> def __iter__(self) -> Iterable[object]: 

170 >>> if self.gt is not None: 

171 >>> yield Gt(self.gt) 

172 >>> if self.description is not None: 

173 >>> yield Description(self.gt) 

174 

175 Also see the implementation of `Interval` below for an example. 

176 

177 Parsers should recognize this and unpack it so that it can be used 

178 both with and without unpacking: 

179 

180 - `Annotated[int, Field(...)]` (parser must unpack Field) 

181 - `Annotated[int, *Field(...)]` (PEP-646) 

182 """ # noqa: trailing-whitespace 

183 

184 @property 

185 def __is_annotated_types_grouped_metadata__(self) -> Literal[True]: 

186 return True 

187 

188 def __iter__(self) -> Iterator[object]: 

189 ... 

190 

191 if not TYPE_CHECKING: 

192 __slots__ = () # allow subclasses to use slots 

193 

194 def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: 

195 # Basic ABC like functionality without the complexity of an ABC 

196 super().__init_subclass__(*args, **kwargs) 

197 if cls.__iter__ is GroupedMetadata.__iter__: 

198 raise TypeError("Can't subclass GroupedMetadata without implementing __iter__") 

199 

200 def __iter__(self) -> Iterator[object]: # noqa: F811 

201 raise NotImplementedError # more helpful than "None has no attribute..." type errors 

202 

203 

204@dataclass(frozen=True, **KW_ONLY, **SLOTS) 

205class Interval(GroupedMetadata): 

206 """Interval can express inclusive or exclusive bounds with a single object. 

207 

208 It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which 

209 are interpreted the same way as the single-bound constraints. 

210 """ 

211 

212 gt: Union[SupportsGt, None] = None 

213 ge: Union[SupportsGe, None] = None 

214 lt: Union[SupportsLt, None] = None 

215 le: Union[SupportsLe, None] = None 

216 

217 def __iter__(self) -> Iterator[BaseMetadata]: 

218 """Unpack an Interval into zero or more single-bounds.""" 

219 if self.gt is not None: 

220 yield Gt(self.gt) 

221 if self.ge is not None: 

222 yield Ge(self.ge) 

223 if self.lt is not None: 

224 yield Lt(self.lt) 

225 if self.le is not None: 

226 yield Le(self.le) 

227 

228 

229@dataclass(frozen=True, **SLOTS) 

230class MultipleOf(BaseMetadata): 

231 """MultipleOf(multiple_of=x) might be interpreted in two ways: 

232 

233 1. Python semantics, implying ``value % multiple_of == 0``, or 

234 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of`` 

235 

236 We encourage users to be aware of these two common interpretations, 

237 and libraries to carefully document which they implement. 

238 """ 

239 

240 multiple_of: Union[SupportsDiv, SupportsMod] 

241 

242 

243@dataclass(frozen=True, **SLOTS) 

244class MinLen(BaseMetadata): 

245 """ 

246 MinLen() implies minimum inclusive length, 

247 e.g. ``len(value) >= min_length``. 

248 """ 

249 

250 min_length: Annotated[int, Ge(0)] 

251 

252 

253@dataclass(frozen=True, **SLOTS) 

254class MaxLen(BaseMetadata): 

255 """ 

256 MaxLen() implies maximum inclusive length, 

257 e.g. ``len(value) <= max_length``. 

258 """ 

259 

260 max_length: Annotated[int, Ge(0)] 

261 

262 

263@dataclass(frozen=True, **SLOTS) 

264class Len(GroupedMetadata): 

265 """ 

266 Len() implies that ``min_length <= len(value) <= max_length``. 

267 

268 Upper bound may be omitted or ``None`` to indicate no upper length bound. 

269 """ 

270 

271 min_length: Annotated[int, Ge(0)] = 0 

272 max_length: Optional[Annotated[int, Ge(0)]] = None 

273 

274 def __iter__(self) -> Iterator[BaseMetadata]: 

275 """Unpack a Len into zone or more single-bounds.""" 

276 if self.min_length > 0: 

277 yield MinLen(self.min_length) 

278 if self.max_length is not None: 

279 yield MaxLen(self.max_length) 

280 

281 

282@dataclass(frozen=True, **SLOTS) 

283class Timezone(BaseMetadata): 

284 """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive). 

285 

286 ``Annotated[datetime, Timezone(None)]`` must be a naive datetime. 

287 ``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be 

288 tz-aware but any timezone is allowed. 

289 

290 You may also pass a specific timezone string or tzinfo object such as 

291 ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that 

292 you only allow a specific timezone, though we note that this is often 

293 a symptom of poor design. 

294 """ 

295 

296 tz: Union[str, tzinfo, EllipsisType, None] 

297 

298 

299@dataclass(frozen=True, **SLOTS) 

300class Unit(BaseMetadata): 

301 """Indicates that the value is a physical quantity with the specified unit. 

302 

303 It is intended for usage with numeric types, where the value represents the 

304 magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]`` 

305 or ``speed: Annotated[float, Unit('m/s')]``. 

306 

307 Interpretation of the unit string is left to the discretion of the consumer. 

308 It is suggested to follow conventions established by python libraries that work 

309 with physical quantities, such as 

310 

311 - ``pint`` : <https://pint.readthedocs.io/en/stable/> 

312 - ``astropy.units``: <https://docs.astropy.org/en/stable/units/> 

313 

314 For indicating a quantity with a certain dimensionality but without a specific unit 

315 it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`. 

316 Note, however, ``annotated_types`` itself makes no use of the unit string. 

317 """ 

318 

319 unit: str 

320 

321 

322@dataclass(frozen=True, **SLOTS) 

323class Predicate(BaseMetadata): 

324 """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values. 

325 

326 Users should prefer statically inspectable metadata, but if you need the full 

327 power and flexibility of arbitrary runtime predicates... here it is. 

328 

329 We provide a few predefined predicates for common string constraints: 

330 ``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and 

331 ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which 

332 can be given special handling, and avoid indirection like ``lambda s: s.lower()``. 

333 

334 Some libraries might have special logic to handle certain predicates, e.g. by 

335 checking for `str.isdigit` and using its presence to both call custom logic to 

336 enforce digit-only strings, and customise some generated external schema. 

337 

338 We do not specify what behaviour should be expected for predicates that raise 

339 an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently 

340 skip invalid constraints, or statically raise an error; or it might try calling it 

341 and then propagate or discard the resulting exception. 

342 """ 

343 

344 func: Callable[[Any], bool] 

345 

346 def __repr__(self) -> str: 

347 if getattr(self.func, "__name__", "<lambda>") == "<lambda>": 

348 return f"{self.__class__.__name__}({self.func!r})" 

349 if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and ( 

350 namespace := getattr(self.func.__self__, "__name__", None) 

351 ): 

352 return f"{self.__class__.__name__}({namespace}.{self.func.__name__})" 

353 if isinstance(self.func, type(str.isascii)): # method descriptor 

354 return f"{self.__class__.__name__}({self.func.__qualname__})" 

355 return f"{self.__class__.__name__}({self.func.__name__})" 

356 

357 

358@dataclass 

359class Not: 

360 func: Callable[[Any], bool] 

361 

362 def __call__(self, __v: Any) -> bool: 

363 return not self.func(__v) 

364 

365 

366_StrType = TypeVar("_StrType", bound=str) 

367 

368LowerCase = Annotated[_StrType, Predicate(str.islower)] 

369""" 

370Return True if the string is a lowercase string, False otherwise. 

371 

372A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string. 

373""" # noqa: E501 

374UpperCase = Annotated[_StrType, Predicate(str.isupper)] 

375""" 

376Return True if the string is an uppercase string, False otherwise. 

377 

378A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string. 

379""" # noqa: E501 

380IsDigit = Annotated[_StrType, Predicate(str.isdigit)] 

381IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63 

382""" 

383Return True if the string is a digit string, False otherwise. 

384 

385A string is a digit string if all characters in the string are digits and there is at least one character in the string. 

386""" # noqa: E501 

387IsAscii = Annotated[_StrType, Predicate(str.isascii)] 

388""" 

389Return True if all characters in the string are ASCII, False otherwise. 

390 

391ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too. 

392""" 

393 

394_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex]) 

395IsFinite = Annotated[_NumericType, Predicate(math.isfinite)] 

396"""Return True if x is neither an infinity nor a NaN, and False otherwise.""" 

397IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))] 

398"""Return True if x is one of infinity or NaN, and False otherwise""" 

399IsNan = Annotated[_NumericType, Predicate(math.isnan)] 

400"""Return True if x is a NaN (not a number), and False otherwise.""" 

401IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))] 

402"""Return True if x is anything but NaN (not a number), and False otherwise.""" 

403IsInfinite = Annotated[_NumericType, Predicate(math.isinf)] 

404"""Return True if x is a positive or negative infinity, and False otherwise.""" 

405IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))] 

406"""Return True if x is neither a positive or negative infinity, and False otherwise.""" 

407 

408try: 

409 from typing_extensions import DocInfo, doc # type: ignore [attr-defined] 

410except ImportError: 

411 

412 @dataclass(frozen=True, **SLOTS) 

413 class DocInfo: # type: ignore [no-redef] 

414 """ " 

415 The return value of doc(), mainly to be used by tools that want to extract the 

416 Annotated documentation at runtime. 

417 """ 

418 

419 documentation: str 

420 """The documentation string passed to doc().""" 

421 

422 def doc( 

423 documentation: str, 

424 ) -> DocInfo: 

425 """ 

426 Add documentation to a type annotation inside of Annotated. 

427 

428 For example: 

429 

430 >>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ... 

431 """ 

432 return DocInfo(documentation)