Skip to content

Commit 448e36a

Browse files
committed
Pass resolver where it is needed
1 parent f03d92a commit 448e36a

File tree

4 files changed

+102
-78
lines changed

4 files changed

+102
-78
lines changed

src/hypothesis_jsonschema/_canonicalise.py

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
7178
def _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]:
552566
FALSEY = 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-
562569
def 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

Comments
 (0)