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

104 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-27 07:38 +0000

1import sys 

2from dataclasses import dataclass 

3from datetime import timezone 

4from typing import Any, Callable, Iterator, Optional, TypeVar, Union 

5 

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

7 from typing_extensions import Protocol 

8else: 

9 from typing import Protocol 

10 

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

12 from typing_extensions import Annotated 

13else: 

14 from typing import Annotated 

15 

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

17 EllipsisType = type(Ellipsis) 

18 KW_ONLY = {} 

19 SLOTS = {} 

20else: 

21 from types import EllipsisType 

22 

23 KW_ONLY = {"kw_only": True} 

24 SLOTS = {"slots": True} 

25 

26 

27__all__ = ( 

28 'BaseMetadata', 

29 'GroupedMetadata', 

30 'Gt', 

31 'Ge', 

32 'Lt', 

33 'Le', 

34 'Interval', 

35 'MultipleOf', 

36 'MinLen', 

37 'MaxLen', 

38 'Len', 

39 'Timezone', 

40 'Predicate', 

41 'LowerCase', 

42 'UpperCase', 

43 'IsDigits', 

44 '__version__', 

45) 

46 

47__version__ = '0.4.0' 

48 

49 

50T = TypeVar('T') 

51 

52 

53# arguments that start with __ are considered 

54# positional only 

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

56 

57 

58class SupportsGt(Protocol): 

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

60 ... 

61 

62 

63class SupportsGe(Protocol): 

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

65 ... 

66 

67 

68class SupportsLt(Protocol): 

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

70 ... 

71 

72 

73class SupportsLe(Protocol): 

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

75 ... 

76 

77 

78class SupportsMod(Protocol): 

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

80 ... 

81 

82 

83class SupportsDiv(Protocol): 

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

85 ... 

86 

87 

88class BaseMetadata: 

89 """Base class for all metadata. 

90 

91 This exists mainly so that implementers 

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

93 """ 

94 

95 __slots__ = () 

96 

97 

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

99class Gt(BaseMetadata): 

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

101 

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

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

104 """ 

105 

106 gt: SupportsGt 

107 

108 

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

110class Ge(BaseMetadata): 

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

112 

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

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

115 """ 

116 

117 ge: SupportsGe 

118 

119 

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

121class Lt(BaseMetadata): 

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

123 

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

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

126 """ 

127 

128 lt: SupportsLt 

129 

130 

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

132class Le(BaseMetadata): 

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

134 

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

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

137 """ 

138 

139 le: SupportsLe 

140 

141 

142class GroupedMetadata: 

143 """A grouping of multiple BaseMetadata objects. 

144 

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

146 All it the the constraint and metadata should be fully expressable 

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

148 

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

150 to add their own metadata. 

151 For example: 

152 

153 >>> @dataclass 

154 >>> class Field(GroupedMetadata): 

155 >>> gt: float | None = None 

156 >>> description: str | None = None 

157 ... 

158 >>> def __iter__(self) -> Iterable[BaseMetadata]: 

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

160 >>> yield Gt(self.gt) 

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

162 >>> yield Description(self.gt) 

163 

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

165 

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

167 both with and without unpacking: 

168 

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

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

171 """ # noqa: trailing-whitespace 

172 

173 __slots__ = () 

174 

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

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

177 if cls.__iter__ is GroupedMetadata.__iter__: 

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

179 

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

181 raise NotImplementedError 

182 

183 

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

185class Interval(GroupedMetadata): 

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

187 

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

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

190 """ 

191 

192 gt: Union[SupportsGt, None] = None 

193 ge: Union[SupportsGe, None] = None 

194 lt: Union[SupportsLt, None] = None 

195 le: Union[SupportsLe, None] = None 

196 

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

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

199 if self.gt is not None: 

200 yield Gt(self.gt) 

201 if self.ge is not None: 

202 yield Ge(self.ge) 

203 if self.lt is not None: 

204 yield Lt(self.lt) 

205 if self.le is not None: 

206 yield Le(self.le) 

207 

208 

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

210class MultipleOf(BaseMetadata): 

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

212 

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

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

215 

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

217 and libraries to carefully document which they implement. 

218 """ 

219 

220 multiple_of: Union[SupportsDiv, SupportsMod] 

221 

222 

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

224class MinLen(BaseMetadata): 

225 """ 

226 MinLen() implies minimum inclusive length, 

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

228 """ 

229 

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

231 

232 

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

234class MaxLen(BaseMetadata): 

235 """ 

236 MaxLen() implies maximum inclusive length, 

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

238 """ 

239 

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

241 

242 

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

244class Len(GroupedMetadata): 

245 """ 

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

247 

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

249 """ 

250 

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

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

253 

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

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

256 if self.min_length > 0: 

257 yield MinLen(self.min_length) 

258 if self.max_length is not None: 

259 yield MaxLen(self.max_length) 

260 

261 

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

263class Timezone(BaseMetadata): 

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

265 

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

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

268 tz-aware bug any timezone is allowed. 

269 

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

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

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

273 a symptom of poor design. 

274 """ 

275 

276 tz: Union[str, timezone, EllipsisType, None] 

277 

278 

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

280class Predicate(BaseMetadata): 

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

282 

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

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

285 

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

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

288 ``IsDigit = Predicate(str.isdigit)``. Users are encouraged to use methods which 

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

290 

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

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

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

294 

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

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

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

298 and then propogate or discard the resulting exception. 

299 """ 

300 

301 func: Callable[[Any], bool] 

302 

303 

304StrType = TypeVar("StrType", bound=str) 

305 

306LowerCase = Annotated[StrType, Predicate(str.islower)] 

307UpperCase = Annotated[StrType, Predicate(str.isupper)] 

308IsDigits = Annotated[StrType, Predicate(str.isdigit)] 

309IsAscii = Annotated[StrType, Predicate(str.isascii)]