Skip to content

Commit cf548f2

Browse files
committed
Fix #57 dependencies merging
1 parent fc8c98b commit cf548f2

File tree

4 files changed

+95
-9
lines changed

4 files changed

+95
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
#### 0.17.2 - 2020-07-16
4+
- improved handling of overlapping `dependencies` keywords (#57)
5+
36
#### 0.17.1 - 2020-07-16
47
- fixed an internal bug where results incorrectly depended on iteration order (#59)
58

src/hypothesis_jsonschema/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
The only public API is `from_schema`; check the docstring for details.
44
"""
55

6-
__version__ = "0.17.1"
6+
__version__ = "0.17.2"
77
__all__ = ["from_schema"]
88

99
from ._from_schema import from_schema

src/hypothesis_jsonschema/_canonicalise.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,25 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
422422
"maxProperties", math.inf
423423
):
424424
type_.remove("object")
425-
# Remove no-op requires
426-
if "required" in schema and not schema["required"]:
427-
schema.pop("required")
425+
# Discard dependencies values that don't restrict anything
426+
for k, v in schema.get("dependencies", {}).copy().items():
427+
if v == [] or v == TRUTHY:
428+
schema["dependencies"].pop(k)
429+
# Remove no-op keywords
430+
for kw, identity in {
431+
"minItems": 0,
432+
"items": {},
433+
"additionalItems": {},
434+
"dependencies": {},
435+
"minProperties": 0,
436+
"properties": {},
437+
"propertyNames": {},
438+
"patternProperties": {},
439+
"additionalProperties": {},
440+
"required": [],
441+
}.items():
442+
if kw in schema and schema[kw] == identity:
443+
schema.pop(kw)
428444
# Canonicalise "required" schemas to remove redundancy
429445
if "object" in type_ and "required" in schema:
430446
assert isinstance(schema["required"], list)
@@ -433,16 +449,18 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
433449
# When the presence of a required property requires other properties via
434450
# dependencies, those properties can be moved to the base required keys.
435451
dep_names = {
436-
k: sorted(v)
452+
k: sorted(set(v))
437453
for k, v in schema["dependencies"].items()
438454
if isinstance(v, list)
439455
}
456+
schema["dependencies"].update(dep_names)
440457
while reqs.intersection(dep_names):
441458
for r in reqs.intersection(dep_names):
442459
reqs.update(dep_names.pop(r))
443-
for k, v in list(schema["dependencies"].items()):
444-
if isinstance(v, list) and k not in dep_names:
445-
schema["dependencies"].pop(k)
460+
schema["dependencies"].pop(r)
461+
# TODO: else merge schema-dependencies of required properties
462+
# into the base schema after adding required back in and being
463+
# careful to avoid an infinite loop...
446464
schema["required"] = sorted(reqs)
447465
max_ = schema.get("maxProperties", float("inf"))
448466
assert isinstance(max_, (int, float))
@@ -782,9 +800,37 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
782800
s.pop("contains")
783801
if "not" in out and "not" in s and out["not"] != s["not"]:
784802
out["not"] = {"anyOf": [out["not"], s.pop("not")]}
803+
if (
804+
"dependencies" in out
805+
and "dependencies" in s
806+
and out["dependencies"] != s["dependencies"]
807+
):
808+
# Note: draft 2019-09 added separate keywords for name-dependencies
809+
# and schema-dependencies, but when we add support for that it will
810+
# be by canonicalising to the existing backwards-compatible keyword.
811+
#
812+
# In each dependencies dict, the keys are property names and the values
813+
# are either a list of required names, or a schema that the whole
814+
# instance must match. To merge a list and a schema, convert the
815+
# former into a `required` key!
816+
odeps = out["dependencies"]
817+
for k, v in odeps.copy().items():
818+
if k in s["dependencies"]:
819+
sval = s["dependencies"].pop(k)
820+
if isinstance(v, list) and isinstance(sval, list):
821+
odeps[k] = v + sval
822+
continue
823+
if isinstance(v, list):
824+
v = {"required": v}
825+
elif isinstance(sval, list):
826+
sval = {"required": sval}
827+
m = merged([v, sval])
828+
if m is None:
829+
return None
830+
odeps[k] = m
831+
odeps.update(s.pop("dependencies"))
785832

786833
# TODO: merge `items` schemas or lists-of-schemas
787-
# TODO: merge dependencies
788834

789835
# This loop handles the remaining cases. Notably, we do not attempt to
790836
# merge distinct values for:

tests/test_canonicalise.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,43 @@ def test_canonicalises_to_expected(schema, expected):
356356
[{"not": {"enum": [1, 2, 3]}}, {"not": {"enum": ["a", "b", "c"]}}],
357357
{"not": {"anyOf": [{"enum": ["a", "b", "c"]}, {"enum": [1, 2, 3]}]}},
358358
),
359+
(
360+
[{"dependencies": {"a": ["b"]}}, {"dependencies": {"a": ["c"]}}],
361+
{"dependencies": {"a": ["b", "c"]}},
362+
),
363+
(
364+
[{"dependencies": {"a": ["b"]}}, {"dependencies": {"b": ["c"]}}],
365+
{"dependencies": {"a": ["b"], "b": ["c"]}},
366+
),
367+
(
368+
[
369+
{"dependencies": {"a": ["b"]}},
370+
{"dependencies": {"a": {"properties": {"b": {"type": "string"}}}}},
371+
],
372+
{
373+
"dependencies": {
374+
"a": {"required": ["b"], "properties": {"b": {"type": "string"}}}
375+
},
376+
},
377+
),
378+
(
379+
[
380+
{"dependencies": {"a": {"properties": {"b": {"type": "string"}}}}},
381+
{"dependencies": {"a": ["b"]}},
382+
],
383+
{
384+
"dependencies": {
385+
"a": {"required": ["b"], "properties": {"b": {"type": "string"}}}
386+
},
387+
},
388+
),
389+
(
390+
[
391+
{"dependencies": {"a": {"pattern": "a"}}},
392+
{"dependencies": {"a": {"pattern": "b"}}},
393+
],
394+
None,
395+
),
359396
]
360397
+ [
361398
([{lo: 0, hi: 9}, {lo: 1, hi: 10}], {lo: 1, hi: 9})

0 commit comments

Comments
 (0)