vtjson 2.1.9__tar.gz → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vtjson
3
- Version: 2.1.9
3
+ Version: 2.2.1
4
4
  Summary: A lightweight package for validating JSON like Python objects
5
5
  Author-email: Michel Van den Bergh <michel.vandenbergh@uhasselt.be>
6
6
  Project-URL: Homepage, https://github.com/vdbergh/vtjson
@@ -130,4 +130,4 @@ class book_schema(TypedDict):
130
130
  year: Annotated[int, ge(1900)]
131
131
  ```
132
132
 
133
- For comprehensive documentation about `vtjson` see [https://www.cantate.be/vtjson](https://www.cantate.be/vtjson).
133
+ For comprehensive documentation about `vtjson` see [https://www.cantate.be/vtjson](https://www.cantate.be/vtjson) (canonical reference) or [https://vtjson.readthedocs.io](https://vtjson.readthedocs.io).
@@ -110,4 +110,4 @@ class book_schema(TypedDict):
110
110
  year: Annotated[int, ge(1900)]
111
111
  ```
112
112
 
113
- For comprehensive documentation about `vtjson` see [https://www.cantate.be/vtjson](https://www.cantate.be/vtjson).
113
+ For comprehensive documentation about `vtjson` see [https://www.cantate.be/vtjson](https://www.cantate.be/vtjson) (canonical reference) or [https://vtjson.readthedocs.io](https://vtjson.readthedocs.io).
@@ -22,6 +22,7 @@ from typing import (
22
22
  TypeVar,
23
23
  Union,
24
24
  cast,
25
+ overload,
25
26
  )
26
27
 
27
28
  try:
@@ -85,8 +86,6 @@ import dns.resolver
85
86
  import email_validator
86
87
  import idna
87
88
 
88
- T = TypeVar("T")
89
-
90
89
 
91
90
  def safe_cast(schema: Type[T], obj: Any) -> T:
92
91
  """
@@ -205,7 +204,7 @@ class SchemaError(Exception):
205
204
  pass
206
205
 
207
206
 
208
- __version__ = "2.1.9"
207
+ __version__ = "2.2.1"
209
208
 
210
209
 
211
210
  @dataclass
@@ -273,13 +272,13 @@ def _get_type_hints(schema: object) -> dict[str, object]:
273
272
 
274
273
  def _to_dict(
275
274
  type_hints: Mapping[str, object], total: bool = True
276
- ) -> dict[str | optional_key[str], object]:
277
- d: dict[str | optional_key[str], object] = {}
275
+ ) -> dict[optional_key[str], object]:
276
+ d: dict[optional_key[str], object] = {}
278
277
  if not supports_Generics:
279
278
  raise SchemaError("Generic types are not supported")
280
279
  for k, v in type_hints.items():
281
280
  v_ = v
282
- k_: str | optional_key[str] = k
281
+ k_ = optional_key(k, _optional=False)
283
282
  value_type = typing.get_origin(v)
284
283
  if supports_NotRequired and value_type in (Required, NotRequired):
285
284
  v_ = typing.get_args(v)[0]
@@ -320,6 +319,29 @@ def _c(s: object) -> str:
320
319
  return ret
321
320
 
322
321
 
322
+ T = TypeVar("T")
323
+
324
+
325
+ @overload
326
+ def _canonize_key(key: str | optional_key[str]) -> optional_key[str]: ...
327
+
328
+
329
+ @overload
330
+ def _canonize_key(key: object) -> object: ...
331
+
332
+
333
+ def _canonize_key(key: object) -> object:
334
+ if isinstance(key, str):
335
+ if len(key) > 0 and key[-1] == "?":
336
+ if not (len(key) > 2 and key[-2] == "\\"):
337
+ return optional_key(key[:-1])
338
+ else:
339
+ return optional_key(key[:-2] + "?", _optional=False)
340
+ else:
341
+ return optional_key(key, _optional=False)
342
+ return key
343
+
344
+
323
345
  def _wrong_type_message(
324
346
  obj: object,
325
347
  name: str,
@@ -395,22 +417,29 @@ K = TypeVar("K")
395
417
  class optional_key(Generic[K]):
396
418
  """
397
419
  Make a key in a Mapping schema optional. In the common case that the key
398
- is a string, the same effect can be achieved by appending `?`.
420
+ is a string, the same effect can be achieved by appending `?`. If you
421
+ absolutely positively need a non-optional string key that ends in `?`
422
+ you can quote the `?` by preceding it with a backslash.
399
423
  """
400
424
 
401
425
  key: K
426
+ optional: bool
402
427
 
403
- def __init__(self, key: K) -> None:
428
+ def __init__(self, key: K, _optional: bool = True) -> None:
404
429
  """
405
430
  :param key: the key to be made optional
431
+ :param _optional: if `False` create a mandatory key; this is normally
432
+ for internal use only
406
433
  """
407
434
  self.key = key
435
+ self.optional = _optional
408
436
 
409
437
  def __eq__(self, key: object) -> bool:
410
438
  if not isinstance(key, optional_key):
411
439
  return False
412
440
  k: object = key.key
413
- return self.key == k
441
+ o: object = key.optional
442
+ return (self.key, key.optional) == (k, o)
414
443
 
415
444
  def __hash__(self) -> int:
416
445
  return hash(self.key)
@@ -1614,6 +1643,24 @@ class number(compiled_schema):
1614
1643
  return _wrong_type_message(obj, name, "number")
1615
1644
 
1616
1645
 
1646
+ class float_(compiled_schema):
1647
+ """
1648
+ Schema that only matches floats. Not ints.
1649
+ """
1650
+
1651
+ def __validate__(
1652
+ self,
1653
+ obj: object,
1654
+ name: str = "object",
1655
+ strict: bool = True,
1656
+ subs: Mapping[str, object] = {},
1657
+ ) -> str:
1658
+ if isinstance(obj, float):
1659
+ return ""
1660
+ else:
1661
+ return _wrong_type_message(obj, name, "float_")
1662
+
1663
+
1617
1664
  class email(compiled_schema):
1618
1665
  """
1619
1666
  Checks if the object is a valid email address. This uses the package
@@ -2148,11 +2195,11 @@ class cond(wrapper):
2148
2195
 
2149
2196
 
2150
2197
  class _fields(compiled_schema):
2151
- d: dict[str | optional_key[str], compiled_schema]
2198
+ d: dict[optional_key[str], compiled_schema]
2152
2199
 
2153
2200
  def __init__(
2154
2201
  self,
2155
- d: Mapping[str | optional_key[str], object],
2202
+ d: Mapping[optional_key[str], object],
2156
2203
  _deferred_compiles: _mapping | None = None,
2157
2204
  ) -> None:
2158
2205
  self.d = {}
@@ -2167,9 +2214,14 @@ class _fields(compiled_schema):
2167
2214
  subs: Mapping[str, object] = {},
2168
2215
  ) -> str:
2169
2216
  for k, v in self.d.items():
2170
- name_ = f"{name}.{k}"
2171
- if not isinstance(k, optional_key) and not hasattr(obj, k):
2172
- return f"{name_} is missing"
2217
+ name_ = f"{name}.{k.key}"
2218
+ if not isinstance(k, optional_key):
2219
+ if not hasattr(obj, k):
2220
+ return f"{name_} is missing"
2221
+ elif not k.optional:
2222
+ if not hasattr(obj, k.key):
2223
+ return f"{name_} is missing"
2224
+
2173
2225
  if isinstance(k, optional_key):
2174
2226
  k_ = k.key
2175
2227
  else:
@@ -2189,7 +2241,7 @@ class fields(wrapper):
2189
2241
  schemaN` respectively
2190
2242
  """
2191
2243
 
2192
- d: dict[str | optional_key[str], object]
2244
+ d: dict[optional_key[str], object]
2193
2245
 
2194
2246
  def __init__(self, d: Mapping[str | optional_key[str], object]) -> None:
2195
2247
  """
@@ -2207,10 +2259,8 @@ class fields(wrapper):
2207
2259
  f"key {repr(k)} in {repr(d)} is not an instance of"
2208
2260
  " optional_key and not a string"
2209
2261
  )
2210
- if isinstance(k, str) and k[-1] == "?":
2211
- self.d[optional_key(k[:-1])] = v
2212
- else:
2213
- self.d[k] = v
2262
+ key_ = _canonize_key(k)
2263
+ self.d[key_] = v
2214
2264
 
2215
2265
  def __compile__(self, _deferred_compiles: _mapping | None = None) -> _fields:
2216
2266
  return _fields(self.d, _deferred_compiles=_deferred_compiles)
@@ -2307,8 +2357,6 @@ class _type(compiled_schema):
2307
2357
  if math_numbers:
2308
2358
  if schema == float:
2309
2359
  setattr(self, "__validate__", self.__validate_float__)
2310
- if schema == complex:
2311
- setattr(self, "__validate__", self.__validate_complex__)
2312
2360
  self.schema = schema
2313
2361
 
2314
2362
  def __validate__(
@@ -2341,19 +2389,6 @@ class _type(compiled_schema):
2341
2389
  else:
2342
2390
  return _wrong_type_message(obj, name, "float")
2343
2391
 
2344
- def __validate_complex__(
2345
- self,
2346
- obj: object,
2347
- name: str = "object",
2348
- strict: bool = True,
2349
- subs: Mapping[str, object] = {},
2350
- ) -> str:
2351
- # consider int, float as subtypes of complex
2352
- if isinstance(obj, (int, float, complex)):
2353
- return ""
2354
- else:
2355
- return _wrong_type_message(obj, name, "complex")
2356
-
2357
2392
  def __str__(self) -> str:
2358
2393
  return self.schema.__name__
2359
2394
 
@@ -2512,13 +2547,13 @@ class _dict(compiled_schema):
2512
2547
  for k in schema:
2513
2548
  compiled_schema = _compile(schema[k], _deferred_compiles=_deferred_compiles)
2514
2549
  optional = True
2515
- if isinstance(k, optional_key):
2516
- key = k.key
2517
- elif isinstance(k, str) and len(k) > 0 and k[-1] == "?":
2518
- key = k[:-1]
2550
+ key_ = _canonize_key(k)
2551
+ if isinstance(key_, optional_key):
2552
+ key = key_.key
2553
+ optional = key_.optional
2519
2554
  else:
2555
+ key = key_
2520
2556
  optional = False
2521
- key = k
2522
2557
  c = _compile(key, _deferred_compiles=_deferred_compiles)
2523
2558
  if isinstance(c, _const):
2524
2559
  if not optional:
@@ -2654,7 +2689,7 @@ class protocol(wrapper):
2654
2689
  which validate the corresponding fields in the object.
2655
2690
  """
2656
2691
 
2657
- type_dict: dict[str | optional_key[str], object]
2692
+ type_dict: dict[optional_key[str], object]
2658
2693
  dict: bool
2659
2694
  __name__: str
2660
2695
 
@@ -2685,7 +2720,7 @@ class protocol(wrapper):
2685
2720
  ) -> compiled_schema:
2686
2721
  if not self.dict:
2687
2722
  return _set_name(
2688
- fields(self.type_dict),
2723
+ _fields(self.type_dict),
2689
2724
  self.__name__,
2690
2725
  reason=True,
2691
2726
  _deferred_compiles=_deferred_compiles,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vtjson
3
- Version: 2.1.9
3
+ Version: 2.2.1
4
4
  Summary: A lightweight package for validating JSON like Python objects
5
5
  Author-email: Michel Van den Bergh <michel.vandenbergh@uhasselt.be>
6
6
  Project-URL: Homepage, https://github.com/vdbergh/vtjson
@@ -130,4 +130,4 @@ class book_schema(TypedDict):
130
130
  year: Annotated[int, ge(1900)]
131
131
  ```
132
132
 
133
- For comprehensive documentation about `vtjson` see [https://www.cantate.be/vtjson](https://www.cantate.be/vtjson).
133
+ For comprehensive documentation about `vtjson` see [https://www.cantate.be/vtjson](https://www.cantate.be/vtjson) (canonical reference) or [https://vtjson.readthedocs.io](https://vtjson.readthedocs.io).
@@ -75,6 +75,7 @@ from vtjson import (
75
75
  email,
76
76
  fields,
77
77
  filter,
78
+ float_,
78
79
  ge,
79
80
  glob,
80
81
  gt,
@@ -303,6 +304,17 @@ class TestValidation(unittest.TestCase):
303
304
  object_ = {"a": 3}
304
305
  validate(schema, object_)
305
306
  show(mc)
307
+ schema = {r"a\?": str, "b": str}
308
+ with self.assertRaises(ValidationError) as mc:
309
+ object_ = {"b": "c"}
310
+ validate(schema, object_)
311
+ show(mc)
312
+ with self.assertRaises(ValidationError) as mc:
313
+ object_ = {"a": "c", "b": "d"}
314
+ validate(schema, object_)
315
+ show(mc)
316
+ object_ = {"a?": "c", "b": "d"}
317
+ validate(schema, object_)
306
318
 
307
319
  class fake_string:
308
320
  def __init__(self, s: str) -> None:
@@ -1872,7 +1884,7 @@ class TestValidation(unittest.TestCase):
1872
1884
  self.assertTrue("...}" in valid)
1873
1885
  self.assertTrue("TRUNCATED" in valid)
1874
1886
 
1875
- def test_int_float_complex(self) -> None:
1887
+ def test_int_float(self) -> None:
1876
1888
  schema: object
1877
1889
  schema = int
1878
1890
  validate(schema, 1)
@@ -1885,10 +1897,11 @@ class TestValidation(unittest.TestCase):
1885
1897
  with self.assertRaises(ValidationError) as mc:
1886
1898
  validate(schema, 1.0 + 1.0j)
1887
1899
  show(mc)
1888
- schema = complex
1889
- validate(schema, 1)
1900
+ schema = float_
1901
+ with self.assertRaises(ValidationError) as mc:
1902
+ validate(schema, 1)
1903
+ show(mc)
1890
1904
  validate(schema, 1.0)
1891
- validate(schema, 1.0 + 1.0j)
1892
1905
 
1893
1906
  def test_float_equal(self) -> None:
1894
1907
  schema: object
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes