1616import json
1717import math
1818import re
19+ from fractions import Fraction
1920from typing import Any , Dict , List , Optional , Tuple , Union
2021
2122import jsonschema
@@ -263,6 +264,9 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
263264 k : v if isinstance (v , list ) else canonicalish (v )
264265 for k , v in schema [key ].items ()
265266 }
267+ # multipleOf is semantically unaffected by the sign, so ensure it's positive
268+ if "multipleOf" in schema :
269+ schema ["multipleOf" ] = abs (schema ["multipleOf" ])
266270
267271 type_ = get_type (schema )
268272 if "number" in type_ :
@@ -289,6 +293,10 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
289293 if "integer" in type_ :
290294 lo , hi = get_integer_bounds (schema )
291295 mul = schema .get ("multipleOf" )
296+ if mul is not None and "number" not in type_ and Fraction (mul ).numerator == 1 :
297+ # Every integer is a multiple of 1/n for all natural numbers n.
298+ schema .pop ("multipleOf" )
299+ mul = None
292300 if lo is not None and isinstance (mul , int ) and mul > 1 and (lo % mul ):
293301 lo += mul - (lo % mul )
294302 if hi is not None and isinstance (mul , int ) and mul > 1 and (hi % mul ):
@@ -303,6 +311,8 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
303311
304312 if lo is not None and hi is not None and lo > hi :
305313 type_ .remove ("integer" )
314+ elif type_ == ["integer" ] and lo == hi and make_validator (schema ).is_valid (lo ):
315+ return {"const" : lo }
306316
307317 if "array" in type_ and "contains" in schema :
308318 if isinstance (schema .get ("items" ), dict ):
@@ -542,11 +552,8 @@ def canonicalish(schema: JSONType) -> Dict[str, Any]:
542552 tmp = schema .copy ()
543553 ao = tmp .pop ("allOf" )
544554 out = merged ([tmp ] + ao )
545- if isinstance ( out , dict ): # pragma: no branch
555+ if out is not None :
546556 schema = out
547- # TODO: this assertion is soley because mypy 0.750 doesn't know
548- # that `schema` is a dict otherwise. Needs minimal report upstream.
549- assert isinstance (schema , dict )
550557 if "oneOf" in schema :
551558 one_of = schema .pop ("oneOf" )
552559 assert isinstance (one_of , list )
@@ -701,8 +708,8 @@ def merged(schemas: List[Any]) -> Optional[Schema]:
701708 if isinstance (x , int ) and isinstance (y , int ):
702709 out ["multipleOf" ] = x * y // math .gcd (x , y )
703710 elif x != y :
704- ratio = max (x , y ) / min (x , y )
705- if ratio == int ( ratio ) : # e.g. x=0.5, y=2
711+ ratio = Fraction ( max (x , y )) / Fraction ( min (x , y ) )
712+ if ratio . denominator == 1 : # e.g. .75, 1.5
706713 out ["multipleOf" ] = max (x , y )
707714 else :
708715 return None
0 commit comments