@@ -68,6 +68,13 @@ def next_down(val: float) -> float:
6868 return out
6969
7070
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+
7178def _get_validator_class (schema : Schema ) -> JSONSchemaValidator :
7279 try :
7380 validator = jsonschema .validators .validator_for (schema )
@@ -202,7 +209,9 @@ def get_integer_bounds(schema: Schema) -> Tuple[Optional[int], Optional[int]]:
202209 return lower , upper
203210
204211
205- def canonicalish (schema : JSONType ) -> Dict [str , Any ]:
212+ def canonicalish (
213+ schema : JSONType , resolver : Optional [LocalResolver ] = None
214+ ) -> Dict [str , Any ]:
206215 """Convert a schema into a more-canonical form.
207216
208217 This is obviously incomplete, but improves best-effort recognition of
@@ -224,12 +233,15 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
224233 "but expected a dict."
225234 )
226235
236+ if resolver is None :
237+ resolver = LocalResolver .from_schema (deepcopy (schema ))
238+
227239 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" ]):
229241 return FALSEY
230242 return {"const" : schema ["const" ]}
231243 if "enum" in schema :
232- validator = make_validator (schema )
244+ validator = make_validator (schema , resolver = resolver )
233245 enum_ = sorted (
234246 (v for v in schema ["enum" ] if validator .is_valid (v )), key = sort_key
235247 )
@@ -253,15 +265,15 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
253265 # Recurse into the value of each keyword with a schema (or list of them) as a value
254266 for key in SCHEMA_KEYS :
255267 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 ]]
257269 elif isinstance (schema .get (key ), (bool , dict )):
258- schema [key ] = canonicalish (schema [key ])
270+ schema [key ] = canonicalish (schema [key ], resolver = resolver )
259271 else :
260272 assert key not in schema , (key , schema [key ])
261273 for key in SCHEMA_OBJECT_KEYS :
262274 if key in schema :
263275 schema [key ] = {
264- k : v if isinstance (v , list ) else canonicalish (v )
276+ k : v if isinstance (v , list ) else canonicalish (v , resolver = resolver )
265277 for k , v in schema [key ].items ()
266278 }
267279
@@ -307,7 +319,9 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
307319
308320 if "array" in type_ and "contains" in schema :
309321 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+ )
311325 if contains_items is not None :
312326 schema ["contains" ] = contains_items
313327
@@ -432,7 +446,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
432446 type_ .remove ("object" )
433447 else :
434448 propnames = schema .get ("propertyNames" , {})
435- validator = make_validator (propnames )
449+ validator = make_validator (propnames , resolver = resolver )
436450 if not all (validator .is_valid (name ) for name in schema ["required" ]):
437451 type_ .remove ("object" )
438452
@@ -461,9 +475,9 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
461475 type_ .remove (t )
462476 if t not in ("integer" , "number" ):
463477 not_ ["type" ].remove (t )
464- not_ = canonicalish (not_ )
478+ not_ = canonicalish (not_ , resolver = resolver )
465479
466- m = merged ([not_ , {** schema , "type" : type_ }])
480+ m = merged ([not_ , {** schema , "type" : type_ }], resolver = resolver )
467481 if m is not None :
468482 not_ = m
469483 if not_ != FALSEY :
@@ -525,7 +539,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
525539 else :
526540 tmp = schema .copy ()
527541 ao = tmp .pop ("allOf" )
528- out = merged ([tmp ] + ao )
542+ out = merged ([tmp ] + ao , resolver = resolver )
529543 if isinstance (out , dict ): # pragma: no branch
530544 schema = out
531545 # TODO: this assertion is soley because mypy 0.750 doesn't know
@@ -537,7 +551,7 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
537551 one_of = sorted (one_of , key = encode_canonical_json )
538552 one_of = [s for s in one_of if s != FALSEY ]
539553 if len (one_of ) == 1 :
540- m = merged ([schema , one_of [0 ]])
554+ m = merged ([schema , one_of [0 ]], resolver = resolver )
541555 if m is not None : # pragma: no branch
542556 return m
543557 if (not one_of ) or one_of .count (TRUTHY ) > 1 :
@@ -552,13 +566,6 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
552566FALSEY = canonicalish (False )
553567
554568
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-
562569def resolve_all_refs (
563570 schema : Union [bool , Schema ], * , resolver : LocalResolver = None
564571) -> Schema :
@@ -590,7 +597,7 @@ def is_recursive(reference: str) -> bool:
590597 with resolver .resolving (ref ) as got :
591598 if s == {}:
592599 return resolve_all_refs (got , resolver = resolver )
593- m = merged ([s , got ])
600+ m = merged ([s , got ], resolver = resolver )
594601 if m is None : # pragma: no cover
595602 msg = f"$ref:{ ref !r} had incompatible base schema { s !r} "
596603 raise HypothesisRefResolutionError (msg )
@@ -600,7 +607,9 @@ def is_recursive(reference: str) -> bool:
600607 val = schema .get (key , False )
601608 if isinstance (val , list ):
602609 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
604613 for v in val
605614 ]
606615 elif isinstance (val , dict ):
@@ -621,7 +630,9 @@ def is_recursive(reference: str) -> bool:
621630 return schema
622631
623632
624- def merged (schemas : List [Any ]) -> Optional [Schema ]:
633+ def merged (
634+ schemas : List [Any ], resolver : Optional [LocalResolver ] = None
635+ ) -> Optional [Schema ]:
625636 """Merge *n* schemas into a single schema, or None if result is invalid.
626637
627638 Takes the logical intersection, so any object that validates against the returned
@@ -634,7 +645,9 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
634645 It's currently also used for keys that could be merged but aren't yet.
635646 """
636647 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+ )
638651 if any (s == FALSEY for s in schemas ):
639652 return FALSEY
640653 out = schemas [0 ]
@@ -643,11 +656,11 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
643656 continue
644657 # If we have a const or enum, this is fairly easy by filtering:
645658 if "const" in out :
646- if make_validator (s ).is_valid (out ["const" ]):
659+ if make_validator (s , resolver = resolver ).is_valid (out ["const" ]):
647660 continue
648661 return FALSEY
649662 if "enum" in out :
650- validator = make_validator (s )
663+ validator = make_validator (s , resolver = resolver )
651664 enum_ = [v for v in out ["enum" ] if validator .is_valid (v )]
652665 if not enum_ :
653666 return FALSEY
@@ -698,36 +711,41 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
698711 else :
699712 out_combined = merged (
700713 [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 ,
702716 )
703717 if prop_name in s_props :
704718 s_combined = s_props [prop_name ]
705719 else :
706720 s_combined = merged (
707721 [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 ,
709724 )
710725 if out_combined is None or s_combined is None : # pragma: no cover
711726 # Note that this can only be the case if we were actually going to
712727 # use the schema which we attempted to merge, i.e. prop_name was
713728 # not in the schema and there were unmergable pattern schemas.
714729 return None
715- m = merged ([out_combined , s_combined ])
730+ m = merged ([out_combined , s_combined ], resolver = resolver )
716731 if m is None :
717732 return None
718733 out_props [prop_name ] = m
719734 # With all the property names done, it's time to handle the patterns. This is
720735 # simpler as we merge with either an identical pattern, or additionalProperties.
721736 if out_pat or s_pat :
722737 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+ )
724742 if m is None : # pragma: no cover
725743 return None
726744 out_pat [pattern ] = m
727745 out ["patternProperties" ] = out_pat
728746 # Finally, we merge togther the additionalProperties schemas.
729747 if out_add or s_add :
730- m = merged ([out_add , s_add ])
748+ m = merged ([out_add , s_add ], resolver = resolver )
731749 if m is None : # pragma: no cover
732750 return None
733751 out ["additionalProperties" ] = m
@@ -761,7 +779,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
761779 return None
762780 if "contains" in out and "contains" in s and out ["contains" ] != s ["contains" ]:
763781 # 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 )
765783 if m == out ["contains" ] or m == s ["contains" ]:
766784 out ["contains" ] = m
767785 s .pop ("contains" )
@@ -791,7 +809,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
791809 v = {"required" : v }
792810 elif isinstance (sval , list ):
793811 sval = {"required" : sval }
794- m = merged ([v , sval ])
812+ m = merged ([v , sval ], resolver = resolver )
795813 if m is None :
796814 return None
797815 odeps [k ] = m
@@ -805,26 +823,27 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
805823 [
806824 out .get ("additionalItems" , TRUTHY ),
807825 s .get ("additionalItems" , TRUTHY ),
808- ]
826+ ],
827+ resolver = resolver ,
809828 )
810829 for a , b in itertools .zip_longest (oitems , sitems ):
811830 if a is None :
812831 a = out .get ("additionalItems" , TRUTHY )
813832 elif b is None :
814833 b = s .get ("additionalItems" , TRUTHY )
815- out ["items" ].append (merged ([a , b ]))
834+ out ["items" ].append (merged ([a , b ], resolver = resolver ))
816835 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 ]
818837 out ["additionalItems" ] = merged (
819- [out .get ("additionalItems" , TRUTHY ), sitems ]
838+ [out .get ("additionalItems" , TRUTHY ), sitems ], resolver = resolver
820839 )
821840 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 ]
823842 out ["additionalItems" ] = merged (
824- [s .get ("additionalItems" , TRUTHY ), oitems ]
843+ [s .get ("additionalItems" , TRUTHY ), oitems ], resolver = resolver
825844 )
826845 else :
827- out ["items" ] = merged ([oitems , sitems ])
846+ out ["items" ] = merged ([oitems , sitems ], resolver = resolver )
828847 if out ["items" ] is None :
829848 return None
830849 if isinstance (out ["items" ], list ) and None in out ["items" ]:
@@ -848,7 +867,7 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
848867 # If non-validation keys like `title` or `description` don't match,
849868 # that doesn't really matter and we'll just go with first we saw.
850869 return None
851- out = canonicalish (out )
870+ out = canonicalish (out , resolver = resolver )
852871 if out == FALSEY :
853872 return FALSEY
854873 assert isinstance (out , dict )
0 commit comments