voluptuous-openapi 0.4.0__tar.gz → 0.4.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.4
2
2
  Name: voluptuous-openapi
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Convert voluptuous schemas to OpenAPI Schema object
5
5
  Author-email: Denis Shulyaka <Shulyaka@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"]
4
4
 
5
5
  [project]
6
6
  name = "voluptuous-openapi"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  license = "Apache-2.0"
9
9
  description = "Convert voluptuous schemas to OpenAPI Schema object"
10
10
  readme = "README.md"
@@ -1315,3 +1315,133 @@ def test_anonymized_ghp_home_automation_invalid() -> None:
1315
1315
  }
1316
1316
  with pytest.raises(vol.Invalid):
1317
1317
  validator(invalid_automation)
1318
+
1319
+
1320
+ def test_convert_schema_with_reference() -> None:
1321
+ """Test that converting a voluptuous schema containing a reference back to OpenAPI works."""
1322
+ schema = {
1323
+ "$defs": {
1324
+ "PositiveInteger": {
1325
+ "type": "integer",
1326
+ "minimum": 0,
1327
+ }
1328
+ },
1329
+ "type": "object",
1330
+ "properties": {
1331
+ "value": {"$ref": "#/$defs/PositiveInteger"},
1332
+ },
1333
+ }
1334
+ vol_schema = convert_to_voluptuous(schema)
1335
+
1336
+ # Verify that the returned vol_schema validates correctly
1337
+ assert vol_schema({"value": 42}) == {"value": 42}
1338
+ with pytest.raises(vol.Invalid):
1339
+ vol_schema({"value": -5})
1340
+
1341
+ # Verify that convert() successfully serializes the voluptuous schema
1342
+ # and denormalizes the reference.
1343
+ res = convert(vol_schema)
1344
+ assert res == {
1345
+ "type": "object",
1346
+ "properties": {
1347
+ "value": {"type": "integer", "minimum": 0},
1348
+ },
1349
+ "required": [],
1350
+ }
1351
+
1352
+
1353
+ def test_convert_schema_with_nested_reference() -> None:
1354
+ """Test that converting a voluptuous schema containing a nested reference back to OpenAPI works."""
1355
+ schema = {
1356
+ "$defs": {
1357
+ "PositiveInteger": {
1358
+ "type": "integer",
1359
+ "minimum": 0,
1360
+ }
1361
+ },
1362
+ "type": "object",
1363
+ "properties": {
1364
+ "nested": {
1365
+ "type": "object",
1366
+ "properties": {
1367
+ "value": {"$ref": "#/$defs/PositiveInteger"},
1368
+ },
1369
+ }
1370
+ },
1371
+ }
1372
+ vol_schema = convert_to_voluptuous(schema)
1373
+
1374
+ # Verify that the returned vol_schema validates correctly
1375
+ assert vol_schema({"nested": {"value": 42}}) == {"nested": {"value": 42}}
1376
+ with pytest.raises(vol.Invalid):
1377
+ vol_schema({"nested": {"value": -5}})
1378
+
1379
+ # Verify that convert() successfully serializes the voluptuous schema and denormalizes the nested reference.
1380
+ res = convert(vol_schema)
1381
+ assert res == {
1382
+ "type": "object",
1383
+ "properties": {
1384
+ "nested": {
1385
+ "type": "object",
1386
+ "properties": {
1387
+ "value": {"type": "integer", "minimum": 0},
1388
+ },
1389
+ "required": [],
1390
+ }
1391
+ },
1392
+ "required": [],
1393
+ }
1394
+
1395
+
1396
+ def test_convert_recursive_schema() -> None:
1397
+ """Test that converting a voluptuous schema containing a recursive reference back to OpenAPI works."""
1398
+ schema = {
1399
+ "$defs": {
1400
+ "Node": {
1401
+ "type": "object",
1402
+ "properties": {
1403
+ "value": {"type": "string"},
1404
+ "child": {"$ref": "#/$defs/Node"},
1405
+ },
1406
+ }
1407
+ },
1408
+ "$ref": "#/$defs/Node",
1409
+ }
1410
+ vol_schema = convert_to_voluptuous(schema)
1411
+
1412
+ # Verify that the returned vol_schema validates correctly
1413
+ valid_data = {"value": "root", "child": {"value": "child"}}
1414
+ assert vol_schema(valid_data) == valid_data
1415
+
1416
+ # Verify that convert() successfully serializes the voluptuous schema,
1417
+ # denormalizes the reference, and breaks the cycle at "child".
1418
+ res = convert(vol_schema)
1419
+ assert res == {
1420
+ "type": "object",
1421
+ "properties": {
1422
+ "value": {"type": "string"},
1423
+ "child": {"type": "string"},
1424
+ },
1425
+ "required": [],
1426
+ }
1427
+
1428
+
1429
+ def test_convert_custom_callable_class_instance() -> None:
1430
+ """Test that convert() handles custom callable validator instances."""
1431
+
1432
+ class CustomValidator:
1433
+ def __call__(self, value: int) -> int:
1434
+ if value < 0:
1435
+ raise vol.Invalid("Must be positive")
1436
+ return value
1437
+
1438
+ vol_schema = vol.Schema(CustomValidator())
1439
+
1440
+ # Verify native validation works
1441
+ assert vol_schema(42) == 42
1442
+ with pytest.raises(vol.Invalid):
1443
+ vol_schema(-5)
1444
+
1445
+ # Verify serialization extracts the type hint from __call__ parameter
1446
+ res = convert(vol_schema)
1447
+ assert res == {"type": "integer"}
@@ -1,7 +1,7 @@
1
1
  """Module to convert voluptuous schemas to dictionaries."""
2
2
 
3
3
  from collections.abc import Callable, Mapping, Sequence
4
- from inspect import signature
4
+ from inspect import isroutine, signature
5
5
  from enum import Enum, StrEnum
6
6
  import itertools
7
7
  import re
@@ -47,6 +47,7 @@ def convert(
47
47
  *,
48
48
  custom_serializer: Callable | None = None,
49
49
  openapi_version: OpenApiVersion = OpenApiVersion.V3,
50
+ _seen_refs: set[str] | None = None,
50
51
  ) -> dict:
51
52
  """Convert a voluptuous schema to a OpenAPI Schema object."""
52
53
  # pylint: disable=too-many-return-statements,too-many-branches
@@ -54,9 +55,28 @@ def convert(
54
55
  def convert_with_args(schema: Any) -> dict:
55
56
  """Convert schema for recusing and propagating arguments."""
56
57
  return convert(
57
- schema, custom_serializer=custom_serializer, openapi_version=openapi_version
58
+ schema,
59
+ custom_serializer=custom_serializer,
60
+ openapi_version=openapi_version,
61
+ _seen_refs=_seen_refs,
58
62
  )
59
63
 
64
+ if isinstance(schema, LazySchema):
65
+ # Recursively resolve and denormalize references. We track visited references
66
+ # in _seen_refs to detect cycle/recursion loops. If a cycle is detected, we
67
+ # break it by returning {} (representing Any).
68
+ if _seen_refs is None:
69
+ _seen_refs = set()
70
+ if schema.ref in _seen_refs:
71
+ return {}
72
+ _seen_refs.add(schema.ref)
73
+ try:
74
+ target_schema = resolve_ref(schema.ref, schema.root_schema)
75
+ vol_target = convert_to_voluptuous(target_schema, schema.root_schema)
76
+ return convert_with_args(vol_target)
77
+ finally:
78
+ _seen_refs.remove(schema.ref)
79
+
60
80
  def ensure_default(value: dict[str:Any]):
61
81
  """Make sure that type is set."""
62
82
  if all(x not in value for x in ("type", "anyOf", "oneOf", "allOf", "not")):
@@ -433,9 +453,16 @@ def convert(
433
453
  return {"type": "object", "additionalProperties": True}
434
454
 
435
455
  if callable(schema):
436
- schema = get_type_hints(schema).get(
437
- list(signature(schema).parameters.keys())[0], Any
438
- )
456
+ if isinstance(schema, type) or isroutine(schema):
457
+ hints = get_type_hints(schema)
458
+ else:
459
+ # For custom callable class instances (such as LazySchema), we inspect
460
+ # the __call__ method instead. We cannot fully serialize arbitrary Python
461
+ # callables back to OpenAPI, but we can try to extract the type annotation
462
+ # of their first parameter to preserve type information in the generated schema.
463
+ hints = get_type_hints(schema.__call__)
464
+ params = list(signature(schema).parameters.keys())
465
+ schema = hints.get(params[0], Any) if params else Any
439
466
  if schema is Any or isinstance(schema, TypeVar):
440
467
  return {}
441
468
  if isinstance(schema, UnionType) or get_origin(schema) is Union:
@@ -499,6 +526,14 @@ class LazySchema:
499
526
  By returning a LazySchema instance during compilation, we avoid infinite
500
527
  recursion/loops for circular or self-referential schemas. The schema is
501
528
  resolved and compiled only on its first invocation, and cached for future runs.
529
+
530
+ Serialization Round-Trip Caveats:
531
+ When converting a voluptuous schema containing a LazySchema back to an OpenAPI
532
+ schema:
533
+ 1. Non-recursive references are fully denormalized (resolved and expanded).
534
+ 2. Recursive/circular references are broken at the recursion boundary and return
535
+ an empty schema {} (representing Any, which is defaulted to a string type by
536
+ the library's ensure_default helper) to prevent infinite recursion/loops.
502
537
  """
503
538
 
504
539
  def __init__(self, ref: str, root_schema: dict):
@@ -525,6 +560,9 @@ class LazySchema:
525
560
  return False
526
561
  return self.ref == other.ref and self.root_schema == other.root_schema
527
562
 
563
+ def __hash__(self) -> int:
564
+ return hash(self.ref)
565
+
528
566
  def __repr__(self) -> str:
529
567
  return f"LazySchema({self.ref!r})"
530
568
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voluptuous-openapi
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Convert voluptuous schemas to OpenAPI Schema object
5
5
  Author-email: Denis Shulyaka <Shulyaka@gmail.com>
6
6
  License-Expression: Apache-2.0