@@ -79,9 +79,20 @@ def _get_validator_class(schema: Schema) -> JSONSchemaValidator:
79
79
return validator
80
80
81
81
82
- def make_validator (schema : Schema ) -> JSONSchemaValidator :
82
+ class LocalResolver (jsonschema .RefResolver ):
83
+ def resolve_remote (self , uri : str ) -> NoReturn :
84
+ raise HypothesisRefResolutionError (
85
+ f"hypothesis-jsonschema does not fetch remote references (uri={ uri !r} )"
86
+ )
87
+
88
+
89
+ def make_validator (
90
+ schema : Schema , resolver : LocalResolver = None
91
+ ) -> JSONSchemaValidator :
92
+ if resolver is None :
93
+ resolver = LocalResolver .from_schema (schema )
83
94
validator = _get_validator_class (schema )
84
- return validator (schema )
95
+ return validator (schema , resolver = resolver )
85
96
86
97
87
98
class HypothesisRefResolutionError (jsonschema .exceptions .RefResolutionError ):
@@ -203,7 +214,7 @@ def get_integer_bounds(schema: Schema) -> Tuple[Optional[int], Optional[int]]:
203
214
return lower , upper
204
215
205
216
206
- def canonicalish (schema : JSONType ) -> Dict [str , Any ]:
217
+ def canonicalish (schema : JSONType , resolver : LocalResolver = None ) -> Dict [str , Any ]:
207
218
"""Convert a schema into a more-canonical form.
208
219
209
220
This is obviously incomplete, but improves best-effort recognition of
@@ -225,12 +236,15 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
225
236
"but expected a dict."
226
237
)
227
238
239
+ if resolver is None :
240
+ resolver = LocalResolver .from_schema (schema )
241
+
228
242
if "const" in schema :
229
- if not make_validator (schema ).is_valid (schema ["const" ]):
243
+ if not make_validator (schema , resolver = resolver ).is_valid (schema ["const" ]):
230
244
return FALSEY
231
245
return {"const" : schema ["const" ]}
232
246
if "enum" in schema :
233
- validator = make_validator (schema )
247
+ validator = make_validator (schema , resolver = resolver )
234
248
enum_ = sorted (
235
249
(v for v in schema ["enum" ] if validator .is_valid (v )), key = sort_key
236
250
)
@@ -254,15 +268,15 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
254
268
# Recurse into the value of each keyword with a schema (or list of them) as a value
255
269
for key in SCHEMA_KEYS :
256
270
if isinstance (schema .get (key ), list ):
257
- schema [key ] = [canonicalish (v ) for v in schema [key ]]
271
+ schema [key ] = [canonicalish (v , resolver = resolver ) for v in schema [key ]]
258
272
elif isinstance (schema .get (key ), (bool , dict )):
259
- schema [key ] = canonicalish (schema [key ])
273
+ schema [key ] = canonicalish (schema [key ], resolver = resolver )
260
274
else :
261
275
assert key not in schema , (key , schema [key ])
262
276
for key in SCHEMA_OBJECT_KEYS :
263
277
if key in schema :
264
278
schema [key ] = {
265
- k : v if isinstance (v , list ) else canonicalish (v )
279
+ k : v if isinstance (v , list ) else canonicalish (v , resolver = resolver )
266
280
for k , v in schema [key ].items ()
267
281
}
268
282
@@ -308,7 +322,9 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
308
322
309
323
if "array" in type_ and "contains" in schema :
310
324
if isinstance (schema .get ("items" ), dict ):
311
- contains_items = merged ([schema ["contains" ], schema ["items" ]])
325
+ contains_items = merged (
326
+ [schema ["contains" ], schema ["items" ]], resolver = resolver
327
+ )
312
328
if contains_items is not None :
313
329
schema ["contains" ] = contains_items
314
330
@@ -462,9 +478,9 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
462
478
type_ .remove (t )
463
479
if t not in ("integer" , "number" ):
464
480
not_ ["type" ].remove (t )
465
- not_ = canonicalish (not_ )
481
+ not_ = canonicalish (not_ , resolver = resolver )
466
482
467
- m = merged ([not_ , {** schema , "type" : type_ }])
483
+ m = merged ([not_ , {** schema , "type" : type_ }], resolver = resolver )
468
484
if m is not None :
469
485
not_ = m
470
486
if not_ != FALSEY :
@@ -543,7 +559,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
543
559
else :
544
560
tmp = schema .copy ()
545
561
ao = tmp .pop ("allOf" )
546
- out = merged ([tmp ] + ao )
562
+ out = merged ([tmp ] + ao , resolver = resolver )
547
563
if isinstance (out , dict ): # pragma: no branch
548
564
schema = out
549
565
# TODO: this assertion is soley because mypy 0.750 doesn't know
@@ -555,7 +571,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
555
571
one_of = sorted (one_of , key = encode_canonical_json )
556
572
one_of = [s for s in one_of if s != FALSEY ]
557
573
if len (one_of ) == 1 :
558
- m = merged ([schema , one_of [0 ]])
574
+ m = merged ([schema , one_of [0 ]], resolver = resolver )
559
575
if m is not None : # pragma: no branch
560
576
return m
561
577
if (not one_of ) or one_of .count (TRUTHY ) > 1 :
@@ -570,13 +586,6 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
570
586
FALSEY = canonicalish (False )
571
587
572
588
573
- class LocalResolver (jsonschema .RefResolver ):
574
- def resolve_remote (self , uri : str ) -> NoReturn :
575
- raise HypothesisRefResolutionError (
576
- f"hypothesis-jsonschema does not fetch remote references (uri={ uri !r} )"
577
- )
578
-
579
-
580
589
def is_recursive_reference (reference : str , resolver : LocalResolver ) -> bool :
581
590
"""Detect if the given reference is recursive."""
582
591
# Special case: a reference to the schema's root is always recursive
@@ -593,7 +602,7 @@ def is_recursive_reference(reference: str, resolver: LocalResolver) -> bool:
593
602
594
603
595
604
def resolve_all_refs (
596
- schema : Union [bool , Schema ], * , resolver : LocalResolver = None
605
+ schema : Union [bool , Schema ], * , resolver : LocalResolver
597
606
) -> Tuple [Schema , bool ]:
598
607
"""Resolve all non-recursive references in the given schema.
599
608
@@ -602,8 +611,6 @@ def resolve_all_refs(
602
611
if isinstance (schema , bool ):
603
612
return canonicalish (schema ), False
604
613
assert isinstance (schema , dict ), schema
605
- if resolver is None :
606
- resolver = LocalResolver .from_schema (deepcopy (schema ))
607
614
if not isinstance (resolver , jsonschema .RefResolver ):
608
615
raise InvalidArgument (
609
616
f"resolver={ resolver } (type { type (resolver ).__name__ } ) is not a RefResolver"
@@ -617,7 +624,7 @@ def resolve_all_refs(
617
624
with resolver .resolving (ref ) as got :
618
625
if s == {}:
619
626
return resolve_all_refs (deepcopy (got ), resolver = resolver )
620
- m = merged ([s , got ])
627
+ m = merged ([s , got ], resolver = resolver )
621
628
if m is None : # pragma: no cover
622
629
msg = f"$ref:{ ref !r} had incompatible base schema { s !r} "
623
630
raise HypothesisRefResolutionError (msg )
@@ -671,7 +678,7 @@ def resolve_all_refs(
671
678
return schema , False
672
679
673
680
674
- def merged (schemas : List [Any ]) -> Optional [Schema ]:
681
+ def merged (schemas : List [Any ], resolver : LocalResolver = None ) -> Optional [Schema ]:
675
682
"""Merge *n* schemas into a single schema, or None if result is invalid.
676
683
677
684
Takes the logical intersection, so any object that validates against the returned
@@ -684,7 +691,9 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
684
691
It's currently also used for keys that could be merged but aren't yet.
685
692
"""
686
693
assert schemas , "internal error: must pass at least one schema to merge"
687
- schemas = sorted ((canonicalish (s ) for s in schemas ), key = upper_bound_instances )
694
+ schemas = sorted (
695
+ (canonicalish (s , resolver = resolver ) for s in schemas ), key = upper_bound_instances
696
+ )
688
697
if any (s == FALSEY for s in schemas ):
689
698
return FALSEY
690
699
out = schemas [0 ]
@@ -693,11 +702,11 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
693
702
continue
694
703
# If we have a const or enum, this is fairly easy by filtering:
695
704
if "const" in out :
696
- if make_validator (s ).is_valid (out ["const" ]):
705
+ if make_validator (s , resolver = resolver ).is_valid (out ["const" ]):
697
706
continue
698
707
return FALSEY
699
708
if "enum" in out :
700
- validator = make_validator (s )
709
+ validator = make_validator (s , resolver = resolver )
701
710
enum_ = [v for v in out ["enum" ] if validator .is_valid (v )]
702
711
if not enum_ :
703
712
return FALSEY
@@ -748,36 +757,41 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
748
757
else :
749
758
out_combined = merged (
750
759
[s for p , s in out_pat .items () if re .search (p , prop_name )]
751
- or [out_add ]
760
+ or [out_add ],
761
+ resolver = resolver ,
752
762
)
753
763
if prop_name in s_props :
754
764
s_combined = s_props [prop_name ]
755
765
else :
756
766
s_combined = merged (
757
767
[s for p , s in s_pat .items () if re .search (p , prop_name )]
758
- or [s_add ]
768
+ or [s_add ],
769
+ resolver = resolver ,
759
770
)
760
771
if out_combined is None or s_combined is None : # pragma: no cover
761
772
# Note that this can only be the case if we were actually going to
762
773
# use the schema which we attempted to merge, i.e. prop_name was
763
774
# not in the schema and there were unmergable pattern schemas.
764
775
return None
765
- m = merged ([out_combined , s_combined ])
776
+ m = merged ([out_combined , s_combined ], resolver = resolver )
766
777
if m is None :
767
778
return None
768
779
out_props [prop_name ] = m
769
780
# With all the property names done, it's time to handle the patterns. This is
770
781
# simpler as we merge with either an identical pattern, or additionalProperties.
771
782
if out_pat or s_pat :
772
783
for pattern in set (out_pat ) | set (s_pat ):
773
- m = merged ([out_pat .get (pattern , out_add ), s_pat .get (pattern , s_add )])
784
+ m = merged (
785
+ [out_pat .get (pattern , out_add ), s_pat .get (pattern , s_add )],
786
+ resolver = resolver ,
787
+ )
774
788
if m is None : # pragma: no cover
775
789
return None
776
790
out_pat [pattern ] = m
777
791
out ["patternProperties" ] = out_pat
778
792
# Finally, we merge togther the additionalProperties schemas.
779
793
if out_add or s_add :
780
- m = merged ([out_add , s_add ])
794
+ m = merged ([out_add , s_add ], resolver = resolver )
781
795
if m is None : # pragma: no cover
782
796
return None
783
797
out ["additionalProperties" ] = m
@@ -811,7 +825,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
811
825
return None
812
826
if "contains" in out and "contains" in s and out ["contains" ] != s ["contains" ]:
813
827
# If one `contains` schema is a subset of the other, we can discard it.
814
- m = merged ([out ["contains" ], s ["contains" ]])
828
+ m = merged ([out ["contains" ], s ["contains" ]], resolver = resolver )
815
829
if m == out ["contains" ] or m == s ["contains" ]:
816
830
out ["contains" ] = m
817
831
s .pop ("contains" )
@@ -841,7 +855,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
841
855
v = {"required" : v }
842
856
elif isinstance (sval , list ):
843
857
sval = {"required" : sval }
844
- m = merged ([v , sval ])
858
+ m = merged ([v , sval ], resolver = resolver )
845
859
if m is None :
846
860
return None
847
861
odeps [k ] = m
@@ -855,26 +869,27 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
855
869
[
856
870
out .get ("additionalItems" , TRUTHY ),
857
871
s .get ("additionalItems" , TRUTHY ),
858
- ]
872
+ ],
873
+ resolver = resolver ,
859
874
)
860
875
for a , b in itertools .zip_longest (oitems , sitems ):
861
876
if a is None :
862
877
a = out .get ("additionalItems" , TRUTHY )
863
878
elif b is None :
864
879
b = s .get ("additionalItems" , TRUTHY )
865
- out ["items" ].append (merged ([a , b ]))
880
+ out ["items" ].append (merged ([a , b ], resolver = resolver ))
866
881
elif isinstance (oitems , list ):
867
- out ["items" ] = [merged ([x , sitems ]) for x in oitems ]
882
+ out ["items" ] = [merged ([x , sitems ], resolver = resolver ) for x in oitems ]
868
883
out ["additionalItems" ] = merged (
869
- [out .get ("additionalItems" , TRUTHY ), sitems ]
884
+ [out .get ("additionalItems" , TRUTHY ), sitems ], resolver = resolver
870
885
)
871
886
elif isinstance (sitems , list ):
872
- out ["items" ] = [merged ([x , oitems ]) for x in sitems ]
887
+ out ["items" ] = [merged ([x , oitems ], resolver = resolver ) for x in sitems ]
873
888
out ["additionalItems" ] = merged (
874
- [s .get ("additionalItems" , TRUTHY ), oitems ]
889
+ [s .get ("additionalItems" , TRUTHY ), oitems ], resolver = resolver
875
890
)
876
891
else :
877
- out ["items" ] = merged ([oitems , sitems ])
892
+ out ["items" ] = merged ([oitems , sitems ], resolver = resolver )
878
893
if out ["items" ] is None :
879
894
return None
880
895
if isinstance (out ["items" ], list ) and None in out ["items" ]:
@@ -898,7 +913,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
898
913
# If non-validation keys like `title` or `description` don't match,
899
914
# that doesn't really matter and we'll just go with first we saw.
900
915
return None
901
- out = canonicalish (out )
916
+ out = canonicalish (out , resolver = resolver )
902
917
if out == FALSEY :
903
918
return FALSEY
904
919
assert isinstance (out , dict )
0 commit comments