ghandic/jsf

Add support for exclusive minimum and maximum

nilreml opened this issue · 1 comments

Somewhat related to #101 - perhaps this helps someone in pinpointing the issue.

Results of running the code below
Annotated[int, Gt(gt=0)]
Annotated[int, Gt(gt=0), Le(le=1)]
Annotated[int, Gt(gt=0), Lt(lt=1)]          <- skip: invalid constraints for int
Annotated[int, Gt(gt=0), Le(le=2)]
Annotated[int, Gt(gt=0), Lt(lt=2)]
Annotated[int, Ge(ge=0)]
Annotated[int, Ge(ge=0), Le(le=1)]
Annotated[int, Ge(ge=0), Lt(lt=1)]
Annotated[int, Ge(ge=0), Le(le=2)]
Annotated[int, Ge(ge=0), Lt(lt=2)]
Annotated[int, Lt(lt=0)]                    <- fail: empty range for randrange() (0, 0, 0)
Annotated[int, Lt(lt=0), Ge(ge=-1)]
Annotated[int, Lt(lt=0), Gt(gt=-1)]         <- skip: invalid constraints for int
Annotated[int, Lt(lt=0), Ge(ge=-2)]
Annotated[int, Lt(lt=0), Gt(gt=-2)]
Annotated[int, Le(le=0)]
Annotated[int, Le(le=0), Ge(ge=-1)]
Annotated[int, Le(le=0), Gt(gt=-1)]
Annotated[int, Le(le=0), Ge(ge=-2)]
Annotated[int, Le(le=0), Gt(gt=-2)]
Annotated[float, Gt(gt=0)]
Annotated[float, Gt(gt=0), Le(le=1)]
Annotated[float, Gt(gt=0), Lt(lt=1)]        <- fail: empty range for randrange() (1, 1, 0)
Annotated[float, Gt(gt=0), Le(le=2)]
Annotated[float, Gt(gt=0), Lt(lt=2)]
Annotated[float, Ge(ge=0)]
Annotated[float, Ge(ge=0.0), Le(le=1.0)]
Annotated[float, Ge(ge=0), Lt(lt=1)]
Annotated[float, Ge(ge=0), Le(le=2)]
Annotated[float, Ge(ge=0), Lt(lt=2)]
Annotated[float, Lt(lt=0)]                  <- fail: empty range for randrange() (0, 0, 0)
Annotated[float, Lt(lt=0), Ge(ge=-1)]
Annotated[float, Lt(lt=0), Gt(gt=-1)]       <- fail: empty range for randrange() (0, 0, 0)
Annotated[float, Lt(lt=0), Ge(ge=-2)]
Annotated[float, Lt(lt=0), Gt(gt=-2)]
Annotated[float, Le(le=0)]
Annotated[float, Le(le=0), Ge(ge=-1)]
Annotated[float, Le(le=0), Gt(gt=-1)]
Annotated[float, Le(le=0), Ge(ge=-2)]
Annotated[float, Le(le=0), Gt(gt=-2)]
Annotated[float, Gt(gt=0.1)]
Annotated[float, Gt(gt=0.1), Le(le=0.9)]    <- fail: empty range for randrange() (2, 1, -1)
Annotated[float, Gt(gt=0.1), Lt(lt=0.9)]    <- fail: empty range for randrange() (2, 0, -2)
Annotated[float, Gt(gt=0.1), Le(le=2.1)]
Annotated[float, Gt(gt=0.1), Lt(lt=2.1)]    <- fail: empty range for randrange() (2, 2, 0)
Annotated[float, Ge(ge=0.1)]
Annotated[float, Ge(ge=0.1), Le(le=0.9)]    <- fail: empty range for randrange() (1, 1, 0)
Annotated[float, Ge(ge=0.1), Lt(lt=0.9)]    <- fail: empty range for randrange() (1, 0, -1)
Annotated[float, Ge(ge=0.1), Le(le=2.1)]
Annotated[float, Ge(ge=0.1), Lt(lt=2.1)]
Annotated[float, Lt(lt=-0.1)]               <- fail: empty range for randrange() (0, -1, -1)
Annotated[float, Lt(lt=-0.1), Ge(ge=-0.9)]  <- fail: empty range for randrange() (0, -1, -1)
Annotated[float, Lt(lt=-0.1), Gt(gt=-0.9)]  <- fail: empty range for randrange() (1, -1, -2)
Annotated[float, Lt(lt=-0.1), Ge(ge=-2.1)]
Annotated[float, Lt(lt=-0.1), Gt(gt=-2.1)]  <- fail: empty range for randrange() (-1, -1, 0)
Annotated[float, Le(le=-0.1)]               <- fail: empty range for randrange() (0, 0, 0)
Annotated[float, Le(le=-0.1), Ge(ge=-0.9)]  <- fail: empty range for randrange() (0, 0, 0)
Annotated[float, Le(le=-0.1), Gt(gt=-0.9)]  <- fail: empty range for randrange() (1, 0, -1)
Annotated[float, Le(le=-0.1), Ge(ge=-2.1)]
Annotated[float, Le(le=-0.1), Gt(gt=-2.1)]
Some json schemata for which generation fails

integer, x < 0:

{
  "exclusiveMaximum": 0,
  "type": "integer",
  "title": "Annotated[int, Lt(lt=0)]:  empty range for randrange() (0, 0, 0)"
}

number, x < 0:

{
  "exclusiveMaximum": 0.0,
  "type": "number",
  "title": "Annotated[float, Lt(lt=0)]:  empty range for randrange() (0, 0, 0)"
}

number, 0 < x < 1:

{
  "exclusiveMaximum": 1.0,
  "exclusiveMinimum": 0.0,
  "type": "number",
  "title": "Annotated[float, Gt(gt=0), Lt(lt=1)]:  empty range for randrange() (1, 1, 0)"
}

number, 0.1 < x < 0.9:

{
  "exclusiveMaximum": 0.9,
  "exclusiveMinimum": 0.1,
  "type": "number",
  "title": "Annotated[float, Gt(gt=0.1), Lt(lt=0.9)]:  empty range for randrange() (2, 0, -2)"
}

number, 0.1 < x <= 0.9:

{
  "exclusiveMinimum": 0.1,
  "maximum": 0.9,
  "type": "number",
  "title": "Annotated[float, Gt(gt=0.1), Le(le=0.9)]:  empty range for randrange() (2, 1, -1)"
}
Schema generation code (3.11+, pydantic):
import json
from functools import partial
from itertools import chain, product
from pathlib import Path
from typing import Annotated

from annotated_types import Ge, Gt, Le, Lt
from jsf import JSF
from pydantic import TypeAdapter

path = Path("test_schemata").resolve()
path.mkdir(parents=True, exist_ok=True)

printf = partial(print, end="")

cmat_both = [
    ([Gt(0), Ge(0)], [None, Le(1), Lt(1), Le(2), Lt(2)]),  # Positive, NonNegative
    ([Lt(0), Le(0)], [None, Ge(-1), Gt(-1), Ge(-2), Gt(-2)]),  # Negative, NonPositive
]
cmat_float = [
    ([Gt(0.1), Ge(0.1)], [None, Le(0.9), Lt(0.9), Le(2.1), Lt(2.1)]),  # Positive
    ([Lt(-0.1), Le(-0.1)], [None, Ge(-0.9), Gt(-0.9), Ge(-2.1), Gt(-2.1)]),  # Negative
]
invalid_int = [
    (Gt(0), Lt(1)),  # 0 < x < 1
    (Lt(0), Gt(-1)),  # -1 < x < 0
]
cons_both = [*chain(*[product(a, b) for a, b in cmat_both])]
cons_float = [*chain(*[product(a, b) for a, b in cmat_float])]

for origin in [int, float]:
    for i, c in enumerate(cons_both + cons_float if origin == float else cons_both):
        # remove None constraints
        constraints = tuple([x for x in c if x is not None])

        typ = Annotated[origin, *constraints]
        title = str(typ).replace("typing.", "")
        printf(f"\n{title:<42}")

        if origin == int and constraints in invalid_int:
            printf("  <- skip: invalid constraints for int")
            continue

        schema = TypeAdapter(typ).json_schema()
        prefix = "pass"

        try:
            JSF(schema).generate()
        except Exception as e:  # noqa: BLE001
            printf(f"  <- fail: {e}")
            prefix = "fail"
            title += f":  {e}"

        # write json schema file
        schema["title"] = title
        (path / f"{prefix}_{origin.__name__}_{i:02}.json").write_text(json.dumps(schema, indent=2))

print()

Thanks! PR's welcome, I don't believe we support exclusiveMaximum at the moment hence why tests won't be working.Should be an easy feature for someone to get stuck into