@@ -68,6 +68,13 @@ def next_down(val: float) -> float:
68
68
return out
69
69
70
70
71
+ class LocalResolver (jsonschema .RefResolver ):
72
+ def resolve_remote (self , uri : str ) -> NoReturn :
73
+ raise HypothesisRefResolutionError (
74
+ f"hypothesis-jsonschema does not fetch remote references (uri={ uri !r} )"
75
+ )
76
+
77
+
71
78
def _get_validator_class (schema : Schema ) -> JSONSchemaValidator :
72
79
try :
73
80
validator = jsonschema .validators .validator_for (schema )
@@ -202,7 +209,9 @@ def get_integer_bounds(schema: Schema) -> Tuple[Optional[int], Optional[int]]:
202
209
return lower , upper
203
210
204
211
205
- def canonicalish (schema : JSONType ) -> Dict [str , Any ]:
212
+ def canonicalish (
213
+ schema : JSONType , resolver : Optional [LocalResolver ] = None
214
+ ) -> Dict [str , Any ]:
206
215
"""Convert a schema into a more-canonical form.
207
216
208
217
This is obviously incomplete, but improves best-effort recognition of
@@ -224,12 +233,15 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
224
233
"but expected a dict."
225
234
)
226
235
236
+ if resolver is None :
237
+ resolver = LocalResolver .from_schema (deepcopy (schema ))
238
+
227
239
if "const" in schema :
228
- if not make_validator (schema ).is_valid (schema ["const" ]):
240
+ if not make_validator (schema , resolver = resolver ).is_valid (schema ["const" ]):
229
241
return FALSEY
230
242
return {"const" : schema ["const" ]}
231
243
if "enum" in schema :
232
- validator = make_validator (schema )
244
+ validator = make_validator (schema , resolver = resolver )
233
245
enum_ = sorted (
234
246
(v for v in schema ["enum" ] if validator .is_valid (v )), key = sort_key
235
247
)
@@ -253,15 +265,15 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
253
265
# Recurse into the value of each keyword with a schema (or list of them) as a value
254
266
for key in SCHEMA_KEYS :
255
267
if isinstance (schema .get (key ), list ):
256
- schema [key ] = [canonicalish (v ) for v in schema [key ]]
268
+ schema [key ] = [canonicalish (v , resolver = resolver ) for v in schema [key ]]
257
269
elif isinstance (schema .get (key ), (bool , dict )):
258
- schema [key ] = canonicalish (schema [key ])
270
+ schema [key ] = canonicalish (schema [key ], resolver = resolver )
259
271
else :
260
272
assert key not in schema , (key , schema [key ])
261
273
for key in SCHEMA_OBJECT_KEYS :
262
274
if key in schema :
263
275
schema [key ] = {
264
- k : v if isinstance (v , list ) else canonicalish (v )
276
+ k : v if isinstance (v , list ) else canonicalish (v , resolver = resolver )
265
277
for k , v in schema [key ].items ()
266
278
}
267
279
@@ -307,7 +319,9 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
307
319
308
320
if "array" in type_ and "contains" in schema :
309
321
if isinstance (schema .get ("items" ), dict ):
310
- contains_items = merged ([schema ["contains" ], schema ["items" ]])
322
+ contains_items = merged (
323
+ [schema ["contains" ], schema ["items" ]], resolver = resolver
324
+ )
311
325
if contains_items is not None :
312
326
schema ["contains" ] = contains_items
313
327
@@ -432,7 +446,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
432
446
type_ .remove ("object" )
433
447
else :
434
448
propnames = schema .get ("propertyNames" , {})
435
- validator = make_validator (propnames )
449
+ validator = make_validator (propnames , resolver = resolver )
436
450
if not all (validator .is_valid (name ) for name in schema ["required" ]):
437
451
type_ .remove ("object" )
438
452
@@ -461,9 +475,9 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
461
475
type_ .remove (t )
462
476
if t not in ("integer" , "number" ):
463
477
not_ ["type" ].remove (t )
464
- not_ = canonicalish (not_ )
478
+ not_ = canonicalish (not_ , resolver = resolver )
465
479
466
- m = merged ([not_ , {** schema , "type" : type_ }])
480
+ m = merged ([not_ , {** schema , "type" : type_ }], resolver = resolver )
467
481
if m is not None :
468
482
not_ = m
469
483
if not_ != FALSEY :
@@ -525,7 +539,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
525
539
else :
526
540
tmp = schema .copy ()
527
541
ao = tmp .pop ("allOf" )
528
- out = merged ([tmp ] + ao )
542
+ out = merged ([tmp ] + ao , resolver = resolver )
529
543
if isinstance (out , dict ): # pragma: no branch
530
544
schema = out
531
545
# TODO: this assertion is soley because mypy 0.750 doesn't know
@@ -537,7 +551,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
537
551
one_of = sorted (one_of , key = encode_canonical_json )
538
552
one_of = [s for s in one_of if s != FALSEY ]
539
553
if len (one_of ) == 1 :
540
- m = merged ([schema , one_of [0 ]])
554
+ m = merged ([schema , one_of [0 ]], resolver = resolver )
541
555
if m is not None : # pragma: no branch
542
556
return m
543
557
if (not one_of ) or one_of .count (TRUTHY ) > 1 :
@@ -552,13 +566,6 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
552
566
FALSEY = canonicalish (False )
553
567
554
568
555
- class LocalResolver (jsonschema .RefResolver ):
556
- def resolve_remote (self , uri : str ) -> NoReturn :
557
- raise HypothesisRefResolutionError (
558
- f"hypothesis-jsonschema does not fetch remote references (uri={ uri !r} )"
559
- )
560
-
561
-
562
569
def resolve_all_refs (
563
570
schema : Union [bool , Schema ], * , resolver : LocalResolver = None
564
571
) -> Schema :
@@ -590,7 +597,7 @@ def is_recursive(reference: str) -> bool:
590
597
with resolver .resolving (ref ) as got :
591
598
if s == {}:
592
599
return resolve_all_refs (got , resolver = resolver )
593
- m = merged ([s , got ])
600
+ m = merged ([s , got ], resolver = resolver )
594
601
if m is None : # pragma: no cover
595
602
msg = f"$ref:{ ref !r} had incompatible base schema { s !r} "
596
603
raise HypothesisRefResolutionError (msg )
@@ -600,7 +607,9 @@ def is_recursive(reference: str) -> bool:
600
607
val = schema .get (key , False )
601
608
if isinstance (val , list ):
602
609
schema [key ] = [
603
- resolve_all_refs (deepcopy (v ), resolver = resolver ) if isinstance (v , dict ) else v
610
+ resolve_all_refs (deepcopy (v ), resolver = resolver )
611
+ if isinstance (v , dict )
612
+ else v
604
613
for v in val
605
614
]
606
615
elif isinstance (val , dict ):
@@ -621,7 +630,9 @@ def is_recursive(reference: str) -> bool:
621
630
return schema
622
631
623
632
624
- def merged (schemas : List [Any ]) -> Optional [Schema ]:
633
+ def merged (
634
+ schemas : List [Any ], resolver : Optional [LocalResolver ] = None
635
+ ) -> Optional [Schema ]:
625
636
"""Merge *n* schemas into a single schema, or None if result is invalid.
626
637
627
638
Takes the logical intersection, so any object that validates against the returned
@@ -634,7 +645,9 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
634
645
It's currently also used for keys that could be merged but aren't yet.
635
646
"""
636
647
assert schemas , "internal error: must pass at least one schema to merge"
637
- schemas = sorted ((canonicalish (s ) for s in schemas ), key = upper_bound_instances )
648
+ schemas = sorted (
649
+ (canonicalish (s , resolver = resolver ) for s in schemas ), key = upper_bound_instances
650
+ )
638
651
if any (s == FALSEY for s in schemas ):
639
652
return FALSEY
640
653
out = schemas [0 ]
@@ -643,11 +656,11 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
643
656
continue
644
657
# If we have a const or enum, this is fairly easy by filtering:
645
658
if "const" in out :
646
- if make_validator (s ).is_valid (out ["const" ]):
659
+ if make_validator (s , resolver = resolver ).is_valid (out ["const" ]):
647
660
continue
648
661
return FALSEY
649
662
if "enum" in out :
650
- validator = make_validator (s )
663
+ validator = make_validator (s , resolver = resolver )
651
664
enum_ = [v for v in out ["enum" ] if validator .is_valid (v )]
652
665
if not enum_ :
653
666
return FALSEY
@@ -698,36 +711,41 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
698
711
else :
699
712
out_combined = merged (
700
713
[s for p , s in out_pat .items () if re .search (p , prop_name )]
701
- or [out_add ]
714
+ or [out_add ],
715
+ resolver = resolver ,
702
716
)
703
717
if prop_name in s_props :
704
718
s_combined = s_props [prop_name ]
705
719
else :
706
720
s_combined = merged (
707
721
[s for p , s in s_pat .items () if re .search (p , prop_name )]
708
- or [s_add ]
722
+ or [s_add ],
723
+ resolver = resolver ,
709
724
)
710
725
if out_combined is None or s_combined is None : # pragma: no cover
711
726
# Note that this can only be the case if we were actually going to
712
727
# use the schema which we attempted to merge, i.e. prop_name was
713
728
# not in the schema and there were unmergable pattern schemas.
714
729
return None
715
- m = merged ([out_combined , s_combined ])
730
+ m = merged ([out_combined , s_combined ], resolver = resolver )
716
731
if m is None :
717
732
return None
718
733
out_props [prop_name ] = m
719
734
# With all the property names done, it's time to handle the patterns. This is
720
735
# simpler as we merge with either an identical pattern, or additionalProperties.
721
736
if out_pat or s_pat :
722
737
for pattern in set (out_pat ) | set (s_pat ):
723
- m = merged ([out_pat .get (pattern , out_add ), s_pat .get (pattern , s_add )])
738
+ m = merged (
739
+ [out_pat .get (pattern , out_add ), s_pat .get (pattern , s_add )],
740
+ resolver = resolver ,
741
+ )
724
742
if m is None : # pragma: no cover
725
743
return None
726
744
out_pat [pattern ] = m
727
745
out ["patternProperties" ] = out_pat
728
746
# Finally, we merge togther the additionalProperties schemas.
729
747
if out_add or s_add :
730
- m = merged ([out_add , s_add ])
748
+ m = merged ([out_add , s_add ], resolver = resolver )
731
749
if m is None : # pragma: no cover
732
750
return None
733
751
out ["additionalProperties" ] = m
@@ -761,7 +779,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
761
779
return None
762
780
if "contains" in out and "contains" in s and out ["contains" ] != s ["contains" ]:
763
781
# If one `contains` schema is a subset of the other, we can discard it.
764
- m = merged ([out ["contains" ], s ["contains" ]])
782
+ m = merged ([out ["contains" ], s ["contains" ]], resolver = resolver )
765
783
if m == out ["contains" ] or m == s ["contains" ]:
766
784
out ["contains" ] = m
767
785
s .pop ("contains" )
@@ -791,7 +809,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
791
809
v = {"required" : v }
792
810
elif isinstance (sval , list ):
793
811
sval = {"required" : sval }
794
- m = merged ([v , sval ])
812
+ m = merged ([v , sval ], resolver = resolver )
795
813
if m is None :
796
814
return None
797
815
odeps [k ] = m
@@ -805,26 +823,27 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
805
823
[
806
824
out .get ("additionalItems" , TRUTHY ),
807
825
s .get ("additionalItems" , TRUTHY ),
808
- ]
826
+ ],
827
+ resolver = resolver ,
809
828
)
810
829
for a , b in itertools .zip_longest (oitems , sitems ):
811
830
if a is None :
812
831
a = out .get ("additionalItems" , TRUTHY )
813
832
elif b is None :
814
833
b = s .get ("additionalItems" , TRUTHY )
815
- out ["items" ].append (merged ([a , b ]))
834
+ out ["items" ].append (merged ([a , b ], resolver = resolver ))
816
835
elif isinstance (oitems , list ):
817
- out ["items" ] = [merged ([x , sitems ]) for x in oitems ]
836
+ out ["items" ] = [merged ([x , sitems ], resolver = resolver ) for x in oitems ]
818
837
out ["additionalItems" ] = merged (
819
- [out .get ("additionalItems" , TRUTHY ), sitems ]
838
+ [out .get ("additionalItems" , TRUTHY ), sitems ], resolver = resolver
820
839
)
821
840
elif isinstance (sitems , list ):
822
- out ["items" ] = [merged ([x , oitems ]) for x in sitems ]
841
+ out ["items" ] = [merged ([x , oitems ], resolver = resolver ) for x in sitems ]
823
842
out ["additionalItems" ] = merged (
824
- [s .get ("additionalItems" , TRUTHY ), oitems ]
843
+ [s .get ("additionalItems" , TRUTHY ), oitems ], resolver = resolver
825
844
)
826
845
else :
827
- out ["items" ] = merged ([oitems , sitems ])
846
+ out ["items" ] = merged ([oitems , sitems ], resolver = resolver )
828
847
if out ["items" ] is None :
829
848
return None
830
849
if isinstance (out ["items" ], list ) and None in out ["items" ]:
@@ -848,7 +867,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
848
867
# If non-validation keys like `title` or `description` don't match,
849
868
# that doesn't really matter and we'll just go with first we saw.
850
869
return None
851
- out = canonicalish (out )
870
+ out = canonicalish (out , resolver = resolver )
852
871
if out == FALSEY :
853
872
return FALSEY
854
873
assert isinstance (out , dict )
0 commit comments