def test_floats_in_tiny_interval_within_bounds(data, center): assume(not (math.isinf(next_down(center)) or math.isinf(next_up(center)))) lo = Decimal.from_float(next_down(center)).next_plus() hi = Decimal.from_float(next_up(center)).next_minus() assert float(lo) < lo < center < hi < float(hi) val = data.draw(st.floats(lo, hi)) assert lo < val < hi
def test_floats_in_tiny_interval_within_bounds(data, center): assume(not (math.isinf(next_down(center)) or math.isinf(next_up(center)))) lo = Decimal.from_float(next_down(center)).next_plus() hi = Decimal.from_float(next_up(center)).next_minus() assert float(lo) < lo < center < hi < float(hi) val = data.draw(st.floats(lo, hi)) assert lo < val < hi
def test_fuzz_floats_bounds(data): width = data.draw(sampled_from([64, 32, 16])) bound = none() | floats(allow_nan=False, width=width) low, high = data.draw(tuples(bound, bound), label="low, high") if low is not None and high is not None and low > high: low, high = high, low if low is not None and high is not None and low > high: low, high = high, low exmin = low is not None and low != inf and data.draw(booleans(), label="ex_min") exmax = high is not None and high != -inf and data.draw(booleans(), label="ex_max") if low is not None and high is not None: lo = next_up(low, width) if exmin else low hi = next_down(high, width) if exmax else high # There must actually be floats between these bounds assume(lo <= hi) if lo == hi == 0: assume(not exmin and not exmax and copysign(1.0, lo) <= copysign(1.0, hi)) s = floats(low, high, exclude_min=exmin, exclude_max=exmax, width=width) val = data.draw(s, label="value") assume(val) # positive/negative zero is an issue if low is not None: assert low <= val if high is not None: assert val <= high if exmin: assert low != val if exmax: assert high != val
def test_up_means_greater(x): hi = next_up(x) if not x < hi: assert ( (math.isnan(x) and math.isnan(hi)) or (x > 0 and math.isinf(x)) or (x == hi == 0 and is_negative(x) and not is_negative(hi)) )
def test_up_means_greater(x): hi = next_up(x) if not x < hi: assert ( (math.isnan(x) and math.isnan(hi)) or (x > 0 and math.isinf(x)) or (x == hi == 0 and is_negative(x) and not is_negative(hi)) )
def flushes_to_zero(xp, width: int) -> bool: """Infer whether build of array module has its float dtype of the specified width flush subnormals to zero We do this per-width because compilers might FTZ for one dtype but allow subnormals in the other. """ if width not in [32, 64]: raise ValueError(f"{width=}, but should be either 32 or 64") dtype = getattr(xp, f"float{width}") return bool(xp.asarray(next_up(0.0, width=width), dtype=dtype) == 0)
def get_number_bounds( schema: Schema, ) -> Tuple[Optional[float], Optional[float], bool, bool]: """Get the min and max allowed floats, and whether they are exclusive.""" lower, upper, exmin, exmax = _get_numeric_bounds(schema) if lower is not None: lo = float(lower) if lo < lower: lo = next_up(lo) exmin = False lower = lo if upper is not None: hi = float(upper) if hi > upper: hi = next_down(hi) exmax = False upper = hi return lower, upper, exmin, exmax
def get_number_bounds( schema: Schema, *, _for_integer: bool = False, ) -> Tuple[Optional[float], Optional[float], bool, bool]: """Get the min and max allowed floats, and whether they are exclusive.""" assert "number" in get_type(schema) or _for_integer lower = schema.get("minimum") upper = schema.get("maximum") exmin = schema.get("exclusiveMinimum", False) exmax = schema.get("exclusiveMaximum", False) assert lower is None or isinstance(lower, (int, float)) assert upper is None or isinstance(upper, (int, float)) assert isinstance(exmin, (bool, int, float)) assert isinstance(exmax, (bool, int, float)) # Canonicalise to number-and-boolean representation if exmin is not True and exmin is not False: if lower is None or exmin >= lower: lower, exmin = exmin, True else: exmin = False if exmax is not True and exmax is not False: if upper is None or exmax <= upper: upper, exmax = exmax, True else: exmax = False assert isinstance(exmin, bool) assert isinstance(exmax, bool) # Adjust bounds and cast to float if lower is not None and not _for_integer: lo = float(lower) if lo < lower: lo = next_up(lo) exmin = False lower = lo if upper is not None and not _for_integer: hi = float(upper) if hi > upper: hi = next_down(hi) exmax = False upper = hi return lower, upper, exmin, exmax
def test_up_means_greater(x): hi = next_up(x) if not x < hi: assert (math.isnan(x) and math.isnan(hi)) or (x > 0 and math.isinf(x))
def canonicalish(schema: JSONType) -> Dict[str, Any]: """Convert a schema into a more-canonical form. This is obviously incomplete, but improves best-effort recognition of equivalent schemas and makes conversion logic simpler. """ if schema is True: return {} elif schema is False: return {"not": {}} # Make a copy, so we don't mutate the existing schema in place. # Using the canonical encoding makes all integer-valued floats into ints. schema = json.loads(encode_canonical_json(schema)) # Otherwise, we're dealing with "objects", i.e. dicts. if not isinstance(schema, dict): raise InvalidArgument( f"Got schema={schema} of type {type(schema).__name__}, " "but expected a dict.") if "const" in schema: if not make_validator(schema).is_valid(schema["const"]): return FALSEY return {"const": schema["const"]} if "enum" in schema: validator = make_validator(schema) enum_ = sorted((v for v in schema["enum"] if validator.is_valid(v)), key=sort_key) if not enum_: return FALSEY elif len(enum_) == 1: return {"const": enum_[0]} return {"enum": enum_} # Recurse into the value of each keyword with a schema (or list of them) as a value for key in SCHEMA_KEYS: if isinstance(schema.get(key), list): schema[key] = [canonicalish(v) for v in schema[key]] elif isinstance(schema.get(key), (bool, dict)): schema[key] = canonicalish(schema[key]) else: assert key not in schema for key in SCHEMA_OBJECT_KEYS: if key in schema: schema[key] = { k: v if isinstance(v, list) else canonicalish(v) for k, v in schema[key].items() } type_ = get_type(schema) if "number" in type_: if schema.get("exclusiveMinimum") is False: del schema["exclusiveMinimum"] if schema.get("exclusiveMaximum") is False: del schema["exclusiveMaximum"] lo, hi, exmin, exmax = get_number_bounds(schema) mul = schema.get("multipleOf") if isinstance(mul, int): # Numbers which are a multiple of an integer? That's the integer type. type_.remove("number") type_ = [t for t in TYPE_STRINGS if t in type_ or t == "integer"] elif lo is not None and hi is not None: lobound = next_up(lo) if exmin else lo hibound = next_down(hi) if exmax else hi if (mul and not has_divisibles(lo, hi, mul, exmin, exmax) ) or lobound > hibound: type_.remove("number") elif type_ == ["number"] and lobound == hibound: return {"const": lobound} if "integer" in type_: lo, hi = get_integer_bounds(schema) mul = schema.get("multipleOf") if lo is not None and isinstance(mul, int) and mul > 1 and (lo % mul): lo += mul - (lo % mul) if hi is not None and isinstance(mul, int) and mul > 1 and (hi % mul): hi -= hi % mul if lo is not None: schema["minimum"] = lo schema.pop("exclusiveMinimum", None) if hi is not None: schema["maximum"] = hi schema.pop("exclusiveMaximum", None) if lo is not None and hi is not None and lo > hi: type_.remove("integer") if "array" in type_ and "contains" in schema: if isinstance(schema.get("items"), dict): contains_items = merged([schema["contains"], schema["items"]]) if contains_items is not None: schema["contains"] = contains_items if schema["contains"] == FALSEY: type_.remove("array") else: schema["minItems"] = max(schema.get("minItems", 0), 1) if schema["contains"] == TRUTHY: schema.pop("contains") schema["minItems"] = max(schema.get("minItems", 1), 1) if "array" in type_ and schema.get("minItems", 0) > schema.get( "maxItems", math.inf): type_.remove("array") if ("array" in type_ and "minItems" in schema and isinstance(schema.get("items", []), dict)): count = upper_bound_instances(schema["items"]) if (count == 0 and schema["minItems"] > 0) or (schema.get( "uniqueItems", False) and count < schema["minItems"]): type_.remove("array") if "array" in type_ and isinstance(schema.get("items"), list): schema["items"] = schema["items"][:schema.get("maxItems")] for idx, s in enumerate(schema["items"]): if s == FALSEY: schema["items"] = schema["items"][:idx] schema["maxItems"] = idx schema.pop("additionalItems", None) break if schema.get("minItems", 0) > min( len(schema["items"]) + upper_bound_instances(schema.get("additionalItems", TRUTHY)), schema.get("maxItems", math.inf), ): type_.remove("array") if ("array" in type_ and isinstance(schema.get("items"), list) and schema.get("additionalItems") == FALSEY): schema.pop("maxItems", None) if "array" in type_ and (schema.get("items") == FALSEY or schema.get("maxItems", 1) == 0): schema["maxItems"] = 0 schema.pop("items", None) schema.pop("uniqueItems", None) schema.pop("additionalItems", None) if "array" in type_ and schema.get("items", TRUTHY) == TRUTHY: schema.pop("items", None) if ("properties" in schema and not schema.get("patternProperties") and schema.get("additionalProperties") == FALSEY): schema["maxProperties"] = min(schema.get("maxProperties", math.inf), len(schema["properties"])) if "object" in type_ and schema.get("minProperties", 0) > schema.get( "maxProperties", math.inf): type_.remove("object") # Remove no-op requires if "required" in schema and not schema["required"]: schema.pop("required") # Canonicalise "required" schemas to remove redundancy if "object" in type_ and "required" in schema: assert isinstance(schema["required"], list) reqs = set(schema["required"]) if schema.get("dependencies"): # When the presence of a required property requires other properties via # dependencies, those properties can be moved to the base required keys. dep_names = { k: sorted(v) for k, v in schema["dependencies"].items() if isinstance(v, list) } while reqs.intersection(dep_names): for r in reqs.intersection(dep_names): reqs.update(dep_names.pop(r)) for k, v in list(schema["dependencies"].items()): if isinstance(v, list) and k not in dep_names: schema["dependencies"].pop(k) schema["required"] = sorted(reqs) max_ = schema.get("maxProperties", float("inf")) assert isinstance(max_, (int, float)) if len(schema["required"]) > max_: type_.remove("object") else: propnames = schema.get("propertyNames", {}) validator = make_validator(propnames) if not all( validator.is_valid(name) for name in schema["required"]): type_.remove("object") for t, kw in TYPE_SPECIFIC_KEYS: numeric = {"number", "integer"} if t in type_ or (t in numeric and numeric.intersection(type_)): continue for k in kw.split(): schema.pop(k, None) # Canonicalise "not" subschemas if "not" in schema: not_ = schema.pop("not") if not_ == TRUTHY or not_ == schema: # If everything is rejected, discard all other (irrelevant) keys # TODO: more sensitive detection of cases where the not-clause # excludes everything in the schema. return FALSEY type_keys = {k: set(v.split()) for k, v in TYPE_SPECIFIC_KEYS} type_constraints = {"type"} for v in type_keys.values(): type_constraints |= v if set(not_).issubset(type_constraints): not_["type"] = get_type(not_) for t in set(type_).intersection(not_["type"]): if not type_keys.get(t, set()).intersection(not_): type_.remove(t) if t not in ("integer", "number"): not_["type"].remove(t) not_ = canonicalish(not_) if not_ != FALSEY: # If the "not" key rejects nothing, discard it schema["not"] = not_ assert isinstance(type_, list), type_ if not type_: assert type_ == [] return FALSEY if type_ == ["null"]: return {"const": None} if type_ == ["boolean"]: return {"enum": [False, True]} if type_ == ["null", "boolean"]: return {"enum": [None, False, True]} if len(type_) == 1: schema["type"] = type_[0] elif type_ == get_type({}): schema.pop("type", None) else: schema["type"] = type_ # Canonicalise "xxxOf" lists; in each case canonicalising and sorting the # sub-schemas then handling any key-specific logic. if TRUTHY in schema.get("anyOf", ()): schema.pop("anyOf", None) if "anyOf" in schema: schema["anyOf"] = sorted(schema["anyOf"], key=encode_canonical_json) schema["anyOf"] = [s for s in schema["anyOf"] if s != FALSEY] if not schema["anyOf"]: return FALSEY if len(schema) == len(schema["anyOf"]) == 1: return schema["anyOf"][0] # type: ignore if "allOf" in schema: schema["allOf"] = [ json.loads(enc) for enc in sorted(set(map(encode_canonical_json, schema["allOf"]))) ] if any(s == FALSEY for s in schema["allOf"]): return FALSEY if all(s == TRUTHY for s in schema["allOf"]): schema.pop("allOf") elif len(schema) == len(schema["allOf"]) == 1: return schema["allOf"][0] # type: ignore else: tmp = schema.copy() ao = tmp.pop("allOf") out = merged([tmp] + ao) if isinstance(out, dict): # pragma: no branch schema = out # TODO: this assertion is soley because mypy 0.750 doesn't know # that `schema` is a dict otherwise. Needs minimal report upstream. assert isinstance(schema, dict) if "oneOf" in schema: one_of = schema.pop("oneOf") assert isinstance(one_of, list) one_of = sorted(one_of, key=encode_canonical_json) one_of = [s for s in one_of if s != FALSEY] if len(one_of) == 1: m = merged([schema, one_of[0]]) if m is not None: # pragma: no branch return m if (not one_of) or one_of.count(TRUTHY) > 1: return FALSEY schema["oneOf"] = one_of # if/then/else schemas are ignored unless if and another are present if "if" not in schema: schema.pop("then", None) schema.pop("else", None) if "then" not in schema and "else" not in schema: schema.pop("if", None) if schema.get("uniqueItems") is False: del schema["uniqueItems"] return schema
except TypeError: # The select interface for entry_points was introduced in py3.10, # supplanting its dict interface. We fallback to the dict interface so # we can still find entry points in py3.8 and py3.9. eps = entry_points().get("array_api", []) return {ep.name: ep for ep in eps} # We try importing the Array API namespace from NumPy first, which modern # versions should include. If not available we default to our own mocked module, # which should allow our test suite to still work. A constant is set accordingly # to inform our test suite of whether the array module here is a mock or not. modules = installed_array_modules() try: with catch_warnings(): # NumPy currently warns on import xp = modules["numpy"].load() except KeyError: xp = mock_xp with pytest.warns(HypothesisWarning): xps = make_strategies_namespace(xp) COMPLIANT_XP = False else: xps = make_strategies_namespace(xp) COMPLIANT_XP = True # Infer whether build of array module has its float flush subnormals to zero WIDTHS_FTZ = { 32: bool(xp.asarray(next_up(0.0, width=32), dtype=xp.float32) == 0), 64: bool(xp.asarray(next_up(0.0, width=64), dtype=xp.float64) == 0), }
def test_updown_roundtrip(val): assert val == next_up(next_down(val)) assert val == next_down(next_up(val))
def test_up_means_greater(x): hi = next_up(x) if not x < hi: assert (math.isnan(x) and math.isnan(hi)) or (x > 0 and math.isinf(x))
def canonicalish(schema: JSONType) -> Dict[str, Any]: """Convert a schema into a more-canonical form. This is obviously incomplete, but improves best-effort recognition of equivalent schemas and makes conversion logic simpler. """ if schema is True: return {} elif schema is False: return {"not": {}} # Make a copy, so we don't mutate the existing schema in place. # Using the canonical encoding makes all integer-valued floats into ints. schema = json.loads(encode_canonical_json(schema)) # Otherwise, we're dealing with "objects", i.e. dicts. if not isinstance(schema, dict): raise InvalidArgument( f"Got schema={schema!r} of type {type(schema).__name__}, " "but expected a dict.") if "const" in schema: if not make_validator(schema).is_valid(schema["const"]): return FALSEY return {"const": schema["const"]} if "enum" in schema: validator = make_validator(schema) enum_ = sorted((v for v in schema["enum"] if validator.is_valid(v)), key=sort_key) if not enum_: return FALSEY elif len(enum_) == 1: return {"const": enum_[0]} return {"enum": enum_} # if/then/else schemas are ignored unless if and another are present if_ = schema.pop("if", None) then = schema.pop("then", schema) else_ = schema.pop("else", schema) if if_ is not None and (then is not schema or else_ is not schema): if then not in (if_, TRUTHY) or else_ != TRUTHY: alternatives = [ { "allOf": [if_, then, schema] }, { "allOf": [{ "not": if_ }, else_, schema] }, ] schema = canonicalish({"anyOf": alternatives}) assert isinstance(schema, dict) # Recurse into the value of each keyword with a schema (or list of them) as a value for key in SCHEMA_KEYS: if isinstance(schema.get(key), list): schema[key] = [canonicalish(v) for v in schema[key]] elif isinstance(schema.get(key), (bool, dict)): schema[key] = canonicalish(schema[key]) else: assert key not in schema, (key, schema[key]) for key in SCHEMA_OBJECT_KEYS: if key in schema: schema[key] = { k: v if isinstance(v, list) else canonicalish(v) for k, v in schema[key].items() } type_ = get_type(schema) if "number" in type_: if schema.get("exclusiveMinimum") is False: del schema["exclusiveMinimum"] if schema.get("exclusiveMaximum") is False: del schema["exclusiveMaximum"] lo, hi, exmin, exmax = get_number_bounds(schema) mul = schema.get("multipleOf") if isinstance(mul, int): # Numbers which are a multiple of an integer? That's the integer type. type_.remove("number") type_ = [t for t in TYPE_STRINGS if t in type_ or t == "integer"] elif lo is not None and hi is not None: lobound = next_up(lo) if exmin else lo hibound = next_down(hi) if exmax else hi if (mul and not has_divisibles(lo, hi, mul, exmin, exmax) ) or lobound > hibound: type_.remove("number") elif type_ == ["number"] and lobound == hibound: return {"const": lobound} if "integer" in type_: lo, hi = get_integer_bounds(schema) mul = schema.get("multipleOf") if lo is not None and isinstance(mul, int) and mul > 1 and (lo % mul): lo += mul - (lo % mul) if hi is not None and isinstance(mul, int) and mul > 1 and (hi % mul): hi -= hi % mul if lo is not None: schema["minimum"] = lo schema.pop("exclusiveMinimum", None) if hi is not None: schema["maximum"] = hi schema.pop("exclusiveMaximum", None) if lo is not None and hi is not None and lo > hi: type_.remove("integer") if "array" in type_ and "contains" in schema: if isinstance(schema.get("items"), dict): contains_items = merged([schema["contains"], schema["items"]]) if contains_items is not None: schema["contains"] = contains_items if schema["contains"] == FALSEY: type_.remove("array") else: schema["minItems"] = max(schema.get("minItems", 0), 1) if schema["contains"] == TRUTHY: schema.pop("contains") schema["minItems"] = max(schema.get("minItems", 1), 1) if ("array" in type_ and "uniqueItems" in schema and isinstance(schema.get("items", []), dict)): item_count = upper_bound_instances(schema["items"]) if math.isfinite(item_count): schema["maxItems"] = min(item_count, schema.get("maxItems", math.inf)) if "array" in type_ and schema.get("minItems", 0) > schema.get( "maxItems", math.inf): type_.remove("array") if ("array" in type_ and "minItems" in schema and isinstance(schema.get("items", []), dict)): count = upper_bound_instances(schema["items"]) if (count == 0 and schema["minItems"] > 0) or (schema.get( "uniqueItems", False) and count < schema["minItems"]): type_.remove("array") if "array" in type_ and isinstance(schema.get("items"), list): schema["items"] = schema["items"][:schema.get("maxItems")] for idx, s in enumerate(schema["items"]): if s == FALSEY: schema["items"] = schema["items"][:idx] schema["maxItems"] = idx schema.pop("additionalItems", None) break if schema.get("minItems", 0) > min( len(schema["items"]) + upper_bound_instances(schema.get("additionalItems", TRUTHY)), schema.get("maxItems", math.inf), ): type_.remove("array") if ("array" in type_ and isinstance(schema.get("items"), list) and schema.get("additionalItems") == FALSEY): schema.pop("maxItems", None) if "array" in type_ and (schema.get("items") == FALSEY or schema.get("maxItems", 1) == 0): schema["maxItems"] = 0 schema.pop("items", None) schema.pop("uniqueItems", None) schema.pop("additionalItems", None) if "array" in type_ and schema.get("items", TRUTHY) == TRUTHY: schema.pop("items", None) if ("properties" in schema and not schema.get("patternProperties") and schema.get("additionalProperties") == FALSEY): max_props = schema.get("maxProperties", math.inf) assert isinstance(max_props, (int, float)) schema["maxProperties"] = min(max_props, len(schema["properties"])) if "object" in type_ and schema.get("minProperties", 0) > schema.get( "maxProperties", math.inf): type_.remove("object") # Discard dependencies values that don't restrict anything for k, v in schema.get("dependencies", {}).copy().items(): if v == [] or v == TRUTHY: schema["dependencies"].pop(k) # Remove no-op keywords for kw, identity in { "minItems": 0, "items": {}, "additionalItems": {}, "dependencies": {}, "minProperties": 0, "properties": {}, "propertyNames": {}, "patternProperties": {}, "additionalProperties": {}, "required": [], }.items(): if kw in schema and schema[kw] == identity: schema.pop(kw) # Canonicalise "required" schemas to remove redundancy if "object" in type_ and "required" in schema: assert isinstance(schema["required"], list) reqs = set(schema["required"]) if schema.get("dependencies"): # When the presence of a required property requires other properties via # dependencies, those properties can be moved to the base required keys. dep_names = { k: sorted(set(v)) for k, v in schema["dependencies"].items() if isinstance(v, list) } schema["dependencies"].update(dep_names) while reqs.intersection(dep_names): for r in reqs.intersection(dep_names): reqs.update(dep_names.pop(r)) schema["dependencies"].pop(r) # TODO: else merge schema-dependencies of required properties # into the base schema after adding required back in and being # careful to avoid an infinite loop... if not schema["dependencies"]: schema.pop("dependencies") schema["required"] = sorted(reqs) max_ = schema.get("maxProperties", float("inf")) assert isinstance(max_, (int, float)) properties = schema.get("properties", {}) if len(schema["required"]) > max_: type_.remove("object") elif any( properties.get(name, {}) == FALSEY for name in schema["required"]): type_.remove("object") else: propnames = schema.get("propertyNames", {}) validator = make_validator(propnames) if not all( validator.is_valid(name) for name in schema["required"]): type_.remove("object") for t, kw in TYPE_SPECIFIC_KEYS: numeric = {"number", "integer"} if t in type_ or (t in numeric and numeric.intersection(type_)): continue for k in kw.split(): schema.pop(k, None) # Canonicalise "not" subschemas if "not" in schema: not_ = schema.pop("not") negated = [] to_negate = not_["anyOf"] if set(not_) == {"anyOf"} else [not_] for not_ in to_negate: type_keys = {k: set(v.split()) for k, v in TYPE_SPECIFIC_KEYS} type_constraints = {"type"} for v in type_keys.values(): type_constraints |= v if set(not_).issubset(type_constraints): not_["type"] = get_type(not_) for t in set(type_).intersection(not_["type"]): if not type_keys.get(t, set()).intersection(not_): type_.remove(t) if t not in ("integer", "number"): not_["type"].remove(t) not_ = canonicalish(not_) m = merged([not_, {**schema, "type": type_}]) if m is not None: not_ = m if not_ != FALSEY: negated.append(not_) if len(negated) > 1: schema["not"] = {"anyOf": negated} elif negated: schema["not"] = negated[0] assert isinstance(type_, list), type_ if not type_: assert type_ == [] return FALSEY if type_ == ["null"]: return {"const": None} if type_ == ["boolean"]: return {"enum": [False, True]} if type_ == ["null", "boolean"]: return {"enum": [None, False, True]} if len(type_) == 1: schema["type"] = type_[0] elif type_ == get_type({}): schema.pop("type", None) else: schema["type"] = type_ # Canonicalise "xxxOf" lists; in each case canonicalising and sorting the # sub-schemas then handling any key-specific logic. if TRUTHY in schema.get("anyOf", ()): schema.pop("anyOf", None) if "anyOf" in schema: i = 0 while i < len(schema["anyOf"]): s = schema["anyOf"][i] if set(s) == {"anyOf"}: schema["anyOf"][i:i + 1] = s["anyOf"] continue i += 1 schema["anyOf"] = [ json.loads(s) for s in sorted({ encode_canonical_json(a) for a in schema["anyOf"] if a != FALSEY }) ] if not schema["anyOf"]: return FALSEY if len(schema) == len(schema["anyOf"]) == 1: return schema["anyOf"][0] # type: ignore types = [] # Turn # {"anyOf": [{"type": "string"}, {"type": "null"}]} # into # {"type": ["string", "null"]} for subschema in schema["anyOf"]: if "type" in subschema and len(subschema) == 1: types.extend(get_type(subschema)) else: break else: # All subschemas have only the "type" keyword, then we merge all types # into the parent schema del schema["anyOf"] new_types = canonicalish({"type": types}) schema = merged([schema, new_types]) assert isinstance(schema, dict) # merging was certainly valid if "allOf" in schema: schema["allOf"] = [ json.loads(enc) for enc in sorted(set(map(encode_canonical_json, schema["allOf"]))) ] if any(s == FALSEY for s in schema["allOf"]): return FALSEY if all(s == TRUTHY for s in schema["allOf"]): schema.pop("allOf") elif len(schema) == len(schema["allOf"]) == 1: return schema["allOf"][0] # type: ignore else: tmp = schema.copy() ao = tmp.pop("allOf") out = merged([tmp] + ao) if isinstance(out, dict): # pragma: no branch schema = out # TODO: this assertion is soley because mypy 0.750 doesn't know # that `schema` is a dict otherwise. Needs minimal report upstream. assert isinstance(schema, dict) if "oneOf" in schema: one_of = schema.pop("oneOf") assert isinstance(one_of, list) one_of = sorted(one_of, key=encode_canonical_json) one_of = [s for s in one_of if s != FALSEY] if len(one_of) == 1: m = merged([schema, one_of[0]]) if m is not None: # pragma: no branch return m if (not one_of) or one_of.count(TRUTHY) > 1: return FALSEY schema["oneOf"] = one_of if schema.get("uniqueItems") is False: del schema["uniqueItems"] return schema
"""Reject the last example (i.e. don't count it towards our budget of elements because it's not going to go in the final collection).""" assert self.count > 0 self.count -= 1 self.rejections += 1 self.rejected = True # We set a minimum number of rejections before we give up to avoid # failing too fast when we reject the first draw. if self.rejections > max(3, 2 * self.count): if self.count < self.min_size: self.data.mark_invalid() else: self.force_stop = True SMALLEST_POSITIVE_FLOAT = next_up(0.0) or sys.float_info.min @lru_cache() def _calc_p_continue(desired_avg, max_size): """Return the p_continue which will generate the desired average size.""" assert desired_avg <= max_size, (desired_avg, max_size) if desired_avg == max_size: return 1.0 p_continue = 1 - 1.0 / (1 + desired_avg) if p_continue == 0 or max_size == float("inf"): assert 0 <= p_continue < 1, p_continue return p_continue assert 0 < p_continue < 1, p_continue # For small max_size, the infinite-series p_continue is a poor approximation, # and while we can't solve the polynomial a few rounds of iteration quickly
def test_updown_roundtrip(val): assert val == next_up(next_down(val)) assert val == next_down(next_up(val))
def kw(marks=(), **kwargs): id_ = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) return pytest.param(kwargs, id=id_, marks=marks) @pytest.mark.parametrize( "kwargs", [ kw(min_value=1), kw(min_value=1), kw(max_value=-1), kw(min_value=float_info.min), kw(min_value=next_down(float_info.min), exclude_min=True), kw(max_value=-float_info.min), kw(min_value=next_up(-float_info.min), exclude_max=True), ], ) def test_subnormal_validation(kwargs): strat = floats(**kwargs, allow_subnormal=True) with pytest.raises(InvalidArgument): strat.example() @pytest.mark.parametrize( "kwargs", [ # min value kw(allow_subnormal=False, min_value=1), kw(allow_subnormal=False, min_value=float_info.min), kw(allow_subnormal=True, min_value=-1),
def numeric_schema(schema: dict) -> st.SearchStrategy[float]: """Handle numeric schemata.""" lower = schema.get("minimum") upper = schema.get("maximum") type_ = get_type(schema) or ["integer", "number"] exmin = schema.get("exclusiveMinimum") if exmin is True and "integer" in type_: assert lower is not None, "boolean exclusiveMinimum implies numeric minimum" lower += 1 exmin = False elif exmin is not False and exmin is not None: lo = exmin + 1 if int(exmin) == exmin else math.ceil(exmin) if lower is None: lower = lo if "integer" in type_ else exmin else: lower = max(lower, lo if "integer" in type_ else exmin) exmin = False exmax = schema.get("exclusiveMaximum") if exmax is True and "integer" in type_: assert upper is not None, "boolean exclusiveMaximum implies numeric maximum" upper -= 1 exmax = False elif exmax is not False and exmax is not None: hi = exmax - 1 if int(exmax) == exmax else math.floor(exmax) if upper is None: upper = hi if "integer" in type_ else exmax else: upper = min(upper, hi if "integer" in type_ else exmax) exmax = False if "multipleOf" in schema: multiple_of = schema["multipleOf"] assert isinstance(multiple_of, (int, float)) if lower is not None: lo = math.ceil(lower / multiple_of) assert lo * multiple_of >= lower, (lower, lo) lower = lo if upper is not None: hi = math.floor(upper / multiple_of) assert hi * multiple_of <= upper, (upper, hi) upper = hi strat = st.integers(lower, upper).map(partial(operator.mul, multiple_of)) # check for and filter out float bounds, inexact multiplication, etc. return strat.filter(partial(is_valid, schema=schema)) strat = st.nothing() if "integer" in type_: lo = lower if lower is None else math.ceil(lower) hi = upper if upper is None else math.floor(upper) if lo is None or hi is None or lo <= hi: strat = st.integers(lo, hi) if "number" in type_: # Filter out negative-zero as it does not exist in JSON lo = exmin if lower is None else lower if lo is not None: lower = float(lo) if lower < lo: lower = next_up(lower) # type: ignore # scary floats magic assert lower >= lo hi = exmax if upper is None else upper if hi is not None: upper = float(hi) if upper > hi: upper = next_down(upper) # type: ignore # scary floats magic assert upper <= hi strat |= st.floats( min_value=lower, max_value=upper, allow_nan=False, allow_infinity=False, exclude_min=exmin is not None, exclude_max=exmax is not None, ).filter(lambda n: n != 0 or math.copysign(1, n) == 1) return strat
def floats( min_value: Optional[Real] = None, max_value: Optional[Real] = None, *, allow_nan: Optional[bool] = None, allow_infinity: Optional[bool] = None, allow_subnormal: Optional[bool] = None, width: int = 64, exclude_min: bool = False, exclude_max: bool = False, ) -> SearchStrategy[float]: """Returns a strategy which generates floats. - If min_value is not None, all values will be ``>= min_value`` (or ``> min_value`` if ``exclude_min``). - If max_value is not None, all values will be ``<= max_value`` (or ``< max_value`` if ``exclude_max``). - If min_value or max_value is not None, it is an error to enable allow_nan. - If both min_value and max_value are not None, it is an error to enable allow_infinity. - If inferred values range does not include subnormal values, it is an error to enable allow_subnormal. Where not explicitly ruled out by the bounds, :wikipedia:`subnormals <Subnormal_number>`, infinities, and NaNs are possible values generated by this strategy. The width argument specifies the maximum number of bits of precision required to represent the generated float. Valid values are 16, 32, or 64. Passing ``width=32`` will still use the builtin 64-bit ``float`` class, but always for values which can be exactly represented as a 32-bit float. The exclude_min and exclude_max argument can be used to generate numbers from open or half-open intervals, by excluding the respective endpoints. Excluding either signed zero will also exclude the other. Attempting to exclude an endpoint which is None will raise an error; use ``allow_infinity=False`` to generate finite floats. You can however use e.g. ``min_value=-math.inf, exclude_min=True`` to exclude only one infinite endpoint. Examples from this strategy have a complicated and hard to explain shrinking behaviour, but it tries to improve "human readability". Finite numbers will be preferred to infinity and infinity will be preferred to NaN. """ check_type(bool, exclude_min, "exclude_min") check_type(bool, exclude_max, "exclude_max") if allow_nan is None: allow_nan = bool(min_value is None and max_value is None) elif allow_nan and (min_value is not None or max_value is not None): raise InvalidArgument( f"Cannot have allow_nan={allow_nan!r}, with min_value or max_value" ) if width not in (16, 32, 64): raise InvalidArgument( f"Got width={width!r}, but the only valid values " "are the integers 16, 32, and 64.") check_valid_bound(min_value, "min_value") check_valid_bound(max_value, "max_value") if math.copysign(1.0, -0.0) == 1.0: # pragma: no cover raise FloatingPointError( "You Python install can't represent -0.0, which is required by the " "IEEE-754 floating-point specification. This is probably because it was " "compiled with an unsafe option like -ffast-math; for a more detailed " "explanation see https://simonbyrne.github.io/notes/fastmath/") if allow_subnormal and next_up(0.0, width=width) == 0: # pragma: no cover # Not worth having separate CI envs and dependencies just to cover this branch; # discussion in https://github.com/HypothesisWorks/hypothesis/issues/3092 # # Erroring out here ensures that the database contents are interpreted # consistently - which matters for such a foundational strategy, even if it's # not always true for all user-composed strategies further up the stack. raise FloatingPointError( f"Got allow_subnormal={allow_subnormal!r}, but we can't represent " f"subnormal floats right now, in violation of the IEEE-754 floating-point " f"specification. This is usually because something was compiled with " f"-ffast-math or a similar option, which sets global processor state. " f"See https://simonbyrne.github.io/notes/fastmath/ for a more detailed " f"writeup - and good luck!") min_arg, max_arg = min_value, max_value if min_value is not None: min_value = float_of(min_value, width) assert isinstance(min_value, float) if max_value is not None: max_value = float_of(max_value, width) assert isinstance(max_value, float) if min_value != min_arg: raise InvalidArgument( f"min_value={min_arg!r} cannot be exactly represented as a float " f"of width {width} - use min_value={min_value!r} instead.") if max_value != max_arg: raise InvalidArgument( f"max_value={max_arg!r} cannot be exactly represented as a float " f"of width {width} - use max_value={max_value!r} instead.") if exclude_min and (min_value is None or min_value == math.inf): raise InvalidArgument(f"Cannot exclude min_value={min_value!r}") if exclude_max and (max_value is None or max_value == -math.inf): raise InvalidArgument(f"Cannot exclude max_value={max_value!r}") assumed_allow_subnormal = allow_subnormal is None or allow_subnormal if min_value is not None and (exclude_min or (min_arg is not None and min_value < min_arg)): min_value = next_up_normal(min_value, width, assumed_allow_subnormal) if min_value == min_arg: assert min_value == min_arg == 0 assert is_negative(min_arg) and not is_negative(min_value) min_value = next_up_normal(min_value, width, assumed_allow_subnormal) assert min_value > min_arg # type: ignore if max_value is not None and (exclude_max or (max_arg is not None and max_value > max_arg)): max_value = next_down_normal(max_value, width, assumed_allow_subnormal) if max_value == max_arg: assert max_value == max_arg == 0 assert is_negative(max_value) and not is_negative(max_arg) max_value = next_down_normal(max_value, width, assumed_allow_subnormal) assert max_value < max_arg # type: ignore if min_value == -math.inf: min_value = None if max_value == math.inf: max_value = None bad_zero_bounds = (min_value == max_value == 0 and is_negative(max_value) and not is_negative(min_value)) if (min_value is not None and max_value is not None and (min_value > max_value or bad_zero_bounds)): # This is a custom alternative to check_valid_interval, because we want # to include the bit-width and exclusion information in the message. msg = ( "There are no %s-bit floating-point values between min_value=%r " "and max_value=%r" % (width, min_arg, max_arg)) if exclude_min or exclude_max: msg += f", exclude_min={exclude_min!r} and exclude_max={exclude_max!r}" raise InvalidArgument(msg) if allow_infinity is None: allow_infinity = bool(min_value is None or max_value is None) elif allow_infinity: if min_value is not None and max_value is not None: raise InvalidArgument( f"Cannot have allow_infinity={allow_infinity!r}, " "with both min_value and max_value") elif min_value == math.inf: if min_arg == math.inf: raise InvalidArgument( "allow_infinity=False excludes min_value=inf") raise InvalidArgument( f"exclude_min=True turns min_value={min_arg!r} into inf, " "but allow_infinity=False") elif max_value == -math.inf: if max_arg == -math.inf: raise InvalidArgument( "allow_infinity=False excludes max_value=-inf") raise InvalidArgument( f"exclude_max=True turns max_value={max_arg!r} into -inf, " "but allow_infinity=False") smallest_normal = width_smallest_normals[width] if allow_subnormal is None: if min_value is not None and max_value is not None: if min_value == max_value: allow_subnormal = -smallest_normal < min_value < smallest_normal else: allow_subnormal = (min_value < smallest_normal and max_value > -smallest_normal) elif min_value is not None: allow_subnormal = min_value < smallest_normal elif max_value is not None: allow_subnormal = max_value > -smallest_normal else: allow_subnormal = True if allow_subnormal: if min_value is not None and min_value >= smallest_normal: raise InvalidArgument( f"allow_subnormal=True, but minimum value {min_value} " f"excludes values below float{width}'s " f"smallest positive normal {smallest_normal}") if max_value is not None and max_value <= -smallest_normal: raise InvalidArgument( f"allow_subnormal=True, but maximum value {max_value} " f"excludes values above float{width}'s " f"smallest negative normal {-smallest_normal}") # Any type hint silences mypy when we unpack these parameters kw: Any = {"allow_subnormal": allow_subnormal, "width": width} unbounded_floats = FloatStrategy(allow_infinity=allow_infinity, allow_nan=allow_nan, **kw) if min_value is None and max_value is None: return unbounded_floats elif min_value is not None and max_value is not None: if min_value == max_value: assert isinstance(min_value, float) result = just(min_value) elif is_negative(min_value): if is_negative(max_value): return floats(min_value=-max_value, max_value=-min_value, **kw).map(operator.neg) else: return floats( min_value=0.0, max_value=max_value, **kw) | floats( min_value=0.0, max_value=-min_value, **kw).map( operator.neg # type: ignore ) elif (count_between_floats(min_value, max_value, width) > 1000 or not allow_subnormal): return FixedBoundedFloatStrategy(lower_bound=min_value, upper_bound=max_value, **kw) else: ub_int = float_to_int(max_value, width) lb_int = float_to_int(min_value, width) assert lb_int <= ub_int result = integers( min_value=lb_int, max_value=ub_int).map(lambda x: int_to_float(x, width)) elif min_value is not None: assert isinstance(min_value, float) if is_negative(min_value): # Ignore known bug https://github.com/python/mypy/issues/6697 return unbounded_floats.map(abs) | floats( # type: ignore min_value=min_value, max_value=-0.0, **kw) else: result = unbounded_floats.map(lambda x: min_value + abs(x)) else: assert isinstance(max_value, float) if not is_negative(max_value): return floats(min_value=0.0, max_value=max_value, ** kw) | unbounded_floats.map(lambda x: -abs(x)) else: result = unbounded_floats.map(lambda x: max_value - abs(x)) if width < 64: def downcast(x): try: return float_of(x, width) except OverflowError: # pragma: no cover reject() result = result.map(downcast) if not allow_infinity: result = result.filter(lambda x: not math.isinf(x)) return result
SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa assert math.isnan(SIGNALING_NAN) and math.copysign(1, SIGNALING_NAN) == 1 NASTY_FLOATS = sorted( [ 0.0, 0.5, 1.1, 1.5, 1.9, 1.0 / 3, 10e6, 10e-6, 1.175494351e-38, next_up(0.0), float_info.min, float_info.max, 3.402823466e38, 9007199254740992, 1 - 10e-6, 2 + 10e-6, 1.192092896e-07, 2.2204460492503131e-016, ] + [2.0**-n for n in (24, 14, 149, 126)] # minimum (sub)normals for float16,32 + [float_info.min / n for n in (2, 10, 1000, 100_000)] # subnormal in float64 + [math.inf, math.nan] * 5 + [SIGNALING_NAN], key=flt.float_to_lex, )
def floats( min_value: Optional[Real] = None, max_value: Optional[Real] = None, *, allow_nan: Optional[bool] = None, allow_infinity: Optional[bool] = None, width: int = 64, exclude_min: bool = False, exclude_max: bool = False, ) -> SearchStrategy[float]: """Returns a strategy which generates floats. - If min_value is not None, all values will be ``>= min_value`` (or ``> min_value`` if ``exclude_min``). - If max_value is not None, all values will be ``<= max_value`` (or ``< max_value`` if ``exclude_max``). - If min_value or max_value is not None, it is an error to enable allow_nan. - If both min_value and max_value are not None, it is an error to enable allow_infinity. Where not explicitly ruled out by the bounds, all of infinity, -infinity and NaN are possible values generated by this strategy. The width argument specifies the maximum number of bits of precision required to represent the generated float. Valid values are 16, 32, or 64. Passing ``width=32`` will still use the builtin 64-bit ``float`` class, but always for values which can be exactly represented as a 32-bit float. The exclude_min and exclude_max argument can be used to generate numbers from open or half-open intervals, by excluding the respective endpoints. Excluding either signed zero will also exclude the other. Attempting to exclude an endpoint which is None will raise an error; use ``allow_infinity=False`` to generate finite floats. You can however use e.g. ``min_value=-math.inf, exclude_min=True`` to exclude only one infinite endpoint. Examples from this strategy have a complicated and hard to explain shrinking behaviour, but it tries to improve "human readability". Finite numbers will be preferred to infinity and infinity will be preferred to NaN. """ check_type(bool, exclude_min, "exclude_min") check_type(bool, exclude_max, "exclude_max") if allow_nan is None: allow_nan = bool(min_value is None and max_value is None) elif allow_nan and (min_value is not None or max_value is not None): raise InvalidArgument( f"Cannot have allow_nan={allow_nan!r}, with min_value or max_value" ) if width not in (16, 32, 64): raise InvalidArgument( f"Got width={width!r}, but the only valid values " "are the integers 16, 32, and 64.") check_valid_bound(min_value, "min_value") check_valid_bound(max_value, "max_value") min_arg, max_arg = min_value, max_value if min_value is not None: min_value = float_of(min_value, width) assert isinstance(min_value, float) if max_value is not None: max_value = float_of(max_value, width) assert isinstance(max_value, float) if min_value != min_arg: raise InvalidArgument( f"min_value={min_arg!r} cannot be exactly represented as a float " f"of width {width} - use min_value={min_value!r} instead.") if max_value != max_arg: raise InvalidArgument( f"max_value={max_arg!r} cannot be exactly represented as a float " f"of width {width} - use max_value={max_value!r} instead.") if exclude_min and (min_value is None or min_value == math.inf): raise InvalidArgument(f"Cannot exclude min_value={min_value!r}") if exclude_max and (max_value is None or max_value == -math.inf): raise InvalidArgument(f"Cannot exclude max_value={max_value!r}") if min_value is not None and (exclude_min or (min_arg is not None and min_value < min_arg)): min_value = next_up(min_value, width) if min_value == min_arg: assert min_value == min_arg == 0 assert is_negative(min_arg) and not is_negative(min_value) min_value = next_up(min_value, width) assert min_value > min_arg # type: ignore if max_value is not None and (exclude_max or (max_arg is not None and max_value > max_arg)): max_value = next_down(max_value, width) if max_value == max_arg: assert max_value == max_arg == 0 assert is_negative(max_value) and not is_negative(max_arg) max_value = next_down(max_value, width) assert max_value < max_arg # type: ignore if min_value == -math.inf: min_value = None if max_value == math.inf: max_value = None bad_zero_bounds = (min_value == max_value == 0 and is_negative(max_value) and not is_negative(min_value)) if (min_value is not None and max_value is not None and (min_value > max_value or bad_zero_bounds)): # This is a custom alternative to check_valid_interval, because we want # to include the bit-width and exclusion information in the message. msg = ( "There are no %s-bit floating-point values between min_value=%r " "and max_value=%r" % (width, min_arg, max_arg)) if exclude_min or exclude_max: msg += f", exclude_min={exclude_min!r} and exclude_max={exclude_max!r}" raise InvalidArgument(msg) if allow_infinity is None: allow_infinity = bool(min_value is None or max_value is None) elif allow_infinity: if min_value is not None and max_value is not None: raise InvalidArgument( f"Cannot have allow_infinity={allow_infinity!r}, " "with both min_value and max_value") elif min_value == math.inf: raise InvalidArgument("allow_infinity=False excludes min_value=inf") elif max_value == -math.inf: raise InvalidArgument("allow_infinity=False excludes max_value=-inf") unbounded_floats = FloatStrategy(allow_infinity=allow_infinity, allow_nan=allow_nan, width=width) if min_value is None and max_value is None: return unbounded_floats elif min_value is not None and max_value is not None: if min_value == max_value: assert isinstance(min_value, float) result = just(min_value) elif is_negative(min_value): if is_negative(max_value): return floats(min_value=-max_value, max_value=-min_value, width=width).map(operator.neg) else: return floats( min_value=0.0, max_value=max_value, width=width) | floats( min_value=0.0, max_value=-min_value, width=width).map( operator.neg) elif count_between_floats(min_value, max_value) > 1000: return FixedBoundedFloatStrategy(lower_bound=min_value, upper_bound=max_value, width=width) else: ub_int = float_to_int(max_value, width) lb_int = float_to_int(min_value, width) assert lb_int <= ub_int result = integers( min_value=lb_int, max_value=ub_int).map(lambda x: int_to_float(x, width)) elif min_value is not None: assert isinstance(min_value, float) if is_negative(min_value): # Ignore known bug https://github.com/python/mypy/issues/6697 return unbounded_floats.map(abs) | floats( # type: ignore min_value=min_value, max_value=-0.0, width=width) else: result = unbounded_floats.map(lambda x: min_value + abs(x)) else: assert isinstance(max_value, float) if not is_negative(max_value): return floats( min_value=0.0, max_value=max_value, width=width) | unbounded_floats.map(lambda x: -abs(x)) else: result = unbounded_floats.map(lambda x: max_value - abs(x)) if width < 64: def downcast(x): try: return float_of(x, width) except OverflowError: # pragma: no cover reject() result = result.map(downcast) if not allow_infinity: result = result.filter(lambda x: not math.isinf(x)) return result
@pytest.mark.parametrize( "s, msg", [ ( floats(min_value=inf, allow_infinity=False), "allow_infinity=False excludes min_value=inf", ), ( floats(min_value=next_down(inf), exclude_min=True, allow_infinity=False), "exclude_min=True turns min_value=.+? into inf, but allow_infinity=False", ), ( floats(max_value=-inf, allow_infinity=False), "allow_infinity=False excludes max_value=-inf", ), ( floats(max_value=next_up(-inf), exclude_max=True, allow_infinity=False), "exclude_max=True turns max_value=.+? into -inf, but allow_infinity=False", ), ], ) def test_floats_message(s, msg): # https://github.com/HypothesisWorks/hypothesis/issues/3207 with pytest.raises(InvalidArgument, match=msg): s.validate()