Skip to content

Commit 7c87131

Browse files
committed
Improved handling of several 'enum' corner cases
1 parent 52bc9cb commit 7c87131

File tree

5 files changed

+190
-19
lines changed

5 files changed

+190
-19
lines changed

jsonsubschema/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from jsonsubschema import api
88
from jsonsubschema import config
9+
from jsonsubschema import exceptions
910
from jsonsubschema import _canonicalization
1011

1112
isSubschema = api.isSubschema

jsonsubschema/_canonicalization.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ def rewrite_enum(d):
239239
ret = {"anyOf": []}
240240
for i in enum:
241241
ret["anyOf"].append(
242+
# {"type": "number", "minimum": i, "maximum": i, "multipleOf": 1}) # check test_numeric/test_join_mulof10
242243
{"type": "integer", "minimum": i, "maximum": i})
243244

244245
if t == "number":
@@ -247,6 +248,8 @@ def rewrite_enum(d):
247248
if utils.is_int_equiv(i):
248249
ret["anyOf"].append(
249250
{"type": "integer", "minimum": i, "maximum": i})
251+
elif numpy.isnan(i):
252+
ret["anyOf"].append({"type": "number", "enum":[numpy.NaN]})
250253
else:
251254
ret["anyOf"].append(
252255
{"type": "number", "minimum": i, "maximum": i})
@@ -257,12 +260,18 @@ def rewrite_enum(d):
257260
return d
258261

259262
if t == "null":
263+
# null schema should be rewritten without enum
264+
# it is a single value anyways.
260265
return {"type": "null"}
261266

262267
if ret:
263-
return canonicalize_dict(ret)
264-
else:
265-
return d
268+
ret["enum"] = enum
269+
return ret
270+
# return canonicalize_dict(ret)
271+
272+
# Unsupported cases of rewriting enums
273+
elif t == 'array' or t == 'object':
274+
raise UnexpectedCanonicalization(msg='Rewriting the following enum is not supported.', tau=t, schema=d)
266275

267276

268277
def simplify_schema_and_embed_checkers(s):

jsonsubschema/_checkers.py

+23-16
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,25 @@ def meet(self, s):
8080
#
8181
ret = self._meet(s)
8282
#
83-
if self.hasEnum() or s.hasEnum():
84-
enum = JSONschema.meet_enum(self, s)
85-
if enum:
86-
ret.enum = ret["enum"] = enum
87-
# ret["enum"] = list(enum)
88-
# ret.enum = ret["enum"]
89-
# instead of returning uninhabited type, return bot
90-
else:
91-
return JSONbot()
83+
# if self.hasEnum() or s.hasEnum():
84+
# enum = JSONschema.meet_enum(self, s)
85+
# if enum:
86+
# ret.enum = ret["enum"] = enum
87+
# # ret["enum"] = list(enum)
88+
# # ret.enum = ret["enum"]
89+
# # instead of returning uninhabited type, return bot
90+
# else:
91+
# return JSONbot()
9292
#
9393
return ret
9494

95-
@staticmethod
96-
def meet_enum(s1, s2):
97-
enum = set(s1.get("enum", [])) | set(s2.get("enum", []))
98-
valid_enum1 = utils.get_valid_enum_vals(enum, s1)
99-
valid_enum2 = utils.get_valid_enum_vals(enum, s2)
95+
# @staticmethod
96+
# def meet_enum(s1, s2):
97+
# enum = set(s1.get("enum", [])) | set(s2.get("enum", []))
98+
# valid_enum1 = utils.get_valid_enum_vals(enum, s1)
99+
# valid_enum2 = utils.get_valid_enum_vals(enum, s2)
100100
# return set(valid_enum1) & set(valid_enum2)
101-
return list(valid_enum1) + list(valid_enum2)
101+
# return list(valid_enum1) + list(valid_enum2)
102102

103103
def meet_handle_rhs(self, s, meet_cb):
104104

@@ -369,6 +369,8 @@ def _isStringSubtype(s1, s2):
369369
#
370370
if s1.pattern == s2.pattern:
371371
return True
372+
elif s1.hasEnum():
373+
return super(JSONTypeString, s1).subtype_enum(s2)
372374
else:
373375
if s1.minLength == 0 and s1.maxLength == I.inf:
374376
pattern1 = s1.pattern
@@ -541,6 +543,8 @@ def _isSubtype(self, s):
541543

542544
def _isIntegerSubtype(s1, s2):
543545
if s2.type in definitions.Jnumeric:
546+
if s1.hasEnum():
547+
return super(JSONTypeInteger, s1).subtype_enum(s2)
544548
#
545549
is_sub_interval = s1.interval in s2.interval
546550
if not is_sub_interval:
@@ -675,6 +679,8 @@ def _isSubtype(self, s):
675679

676680
def _isNumberSubtype(s1, s2):
677681
if s2.type == "number":
682+
if s1.hasEnum():
683+
return super(JSONTypeNumber, s1).subtype_enum(s2)
678684
is_sub_interval = s1.interval in s2.interval
679685
if not is_sub_interval:
680686
print_db("num__00")
@@ -1255,7 +1261,8 @@ def _isObjectSubtype(s1, s2):
12551261
'''
12561262
if s2.type != "object":
12571263
return False
1258-
1264+
if s1.hasEnum():
1265+
return super(JSONTypeObject, s1).subtype_enum(s2)
12591266
# Check properties range
12601267
is_sub_interval = s1.interval in s2.interval
12611268
if not is_sub_interval:

jsonsubschema/exceptions.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'''
2+
Created on May 11, 2020
3+
@author: Andrew Habib
4+
'''
5+
6+
7+
class _Error(Exception):
8+
pass
9+
10+
11+
class UnexpectedCanonicalization(_Error):
12+
13+
def __init__(self, msg, tau, schema):
14+
self.msg = msg
15+
self.tau = tau
16+
self.schema = schema
17+
18+
def __str__(self):
19+
return '{}\n"type": {} \n"schema": {}'.format(self.msg, self.tau, self.schema)
20+
21+
22+
# class UnsupportedSchemaType(_Error):
23+
# '''
24+
# Probably this is not required since custom types are not
25+
# supported by jsonschema validation anyways; so we will not reat
26+
# a case that uses this exception.'''
27+
28+
# def __init__(self, schema, tau):
29+
# self.schema = schema
30+
# self.tau = tau
31+
32+
# def __str__(self):
33+
# return '{} is unsupported jsonschema type in schema: {}'.format(self.tau, self.schema)
34+
35+
36+
# class UnsupportedSubtypeChecker(_Error):
37+
38+
# def __init__(self, schema, desc):
39+
# self.schema = schema
40+
# self.desc = desc
41+
42+
# def __str__(self):
43+
# return '{} is unsupported. Schema: {}'.format(self.desc, self.schema)

test/test_enum.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'''
2+
Created on June 3, 2019
3+
@author: Andrew Habib
4+
'''
5+
6+
import unittest
7+
8+
from jsonsubschema import isSubschema
9+
from jsonsubschema.exceptions import UnexpectedCanonicalization
10+
11+
12+
class TestEnum(unittest.TestCase):
13+
14+
def test_enum_simple1(self):
15+
s1 = {'enum': [1]}
16+
s2 = {'enum': [1, 2]}
17+
18+
with self.subTest('LHS < RHS'):
19+
self.assertTrue(isSubschema(s1, s2))
20+
with self.subTest('LHS > RHS'):
21+
self.assertFalse(isSubschema(s2, s1))
22+
23+
def test_enum_simple2(self):
24+
s1 = {'enum': [True]}
25+
s2 = {'enum': [1, 2]}
26+
27+
with self.subTest('LHS < RHS'):
28+
self.assertFalse(isSubschema(s1, s2))
29+
with self.subTest('LHS > RHS'):
30+
self.assertFalse(isSubschema(s2, s1))
31+
32+
def test_enum_simple3(self):
33+
s1 = {'type': 'integer', 'enum': [1, 2]}
34+
s2 = {'type': 'boolean', 'enum': [True]}
35+
36+
with self.subTest('LHS < RHS'):
37+
self.assertFalse(isSubschema(s1, s2))
38+
with self.subTest('LHS > RHS'):
39+
self.assertFalse(isSubschema(s2, s1))
40+
41+
def test_enum_simple4(self):
42+
s1 = {'enum': ['1', 2]}
43+
s2 = {'enum': [1, '2']}
44+
45+
with self.subTest('LHS < RHS'):
46+
self.assertFalse(isSubschema(s1, s2))
47+
with self.subTest('LHS > RHS'):
48+
self.assertFalse(isSubschema(s2, s1))
49+
50+
def test_enum_uninhabited1(self):
51+
s1 = {'type': 'string', 'enum': [1, 2]}
52+
s2 = {'type': 'string'}
53+
54+
with self.subTest('LHS < RHS'):
55+
self.assertTrue(isSubschema(s1, s2))
56+
with self.subTest('LHS > RHS'):
57+
self.assertFalse(isSubschema(s2, s1))
58+
59+
def test_enum_uninhabited2(self):
60+
s1 = {'type': 'string', 'enum': [0, 1]}
61+
s2 = {'type': 'boolean', 'enum': [0]}
62+
63+
with self.subTest('LHS < RHS'):
64+
self.assertTrue(isSubschema(s1, s2))
65+
with self.subTest('LHS > RHS'):
66+
self.assertTrue(isSubschema(s2, s1))
67+
68+
def test_enum_uninhabited3(self):
69+
s1 = {'enum': []}
70+
s2 = {'type': 'boolean'}
71+
72+
with self.subTest('LHS < RHS'):
73+
self.assertTrue(isSubschema(s1, s2))
74+
with self.subTest('LHS > RHS'):
75+
self.assertFalse(isSubschema(s2, s1))
76+
77+
def test_enum_uninhabited4(self):
78+
s1 = {'enum': []}
79+
s2 = {'not': {}}
80+
81+
with self.subTest('LHS < RHS'):
82+
self.assertTrue(isSubschema(s1, s2))
83+
with self.subTest('LHS > RHS'):
84+
self.assertTrue(isSubschema(s2, s1))
85+
86+
87+
class TestEnumNotSupported(unittest.TestCase):
88+
89+
def test_array(self):
90+
s1 = {'enum': [[]]}
91+
s2 = {'type': 'array'}
92+
93+
with self.subTest():
94+
self.assertRaises(UnexpectedCanonicalization, isSubschema, s1, s2)
95+
96+
with self.subTest(): # To test prining the exception msg
97+
with self.assertRaises(UnexpectedCanonicalization) as ctxt:
98+
isSubschema(s2, s1)
99+
print(ctxt.exception)
100+
101+
def test_object(self):
102+
s1 = {'enum': [{'foo': 1}]}
103+
s2 = {'type': 'object'}
104+
105+
with self.subTest():
106+
self.assertRaises(UnexpectedCanonicalization, isSubschema, s1, s2)
107+
108+
with self.subTest(): # To test prining the exception msg
109+
with self.assertRaises(UnexpectedCanonicalization) as ctxt:
110+
isSubschema(s2, s1)
111+
print(ctxt.exception)

0 commit comments

Comments
 (0)