Skip to content

Add support for exclusive minimum and maximum  #116

Description

@nilreml

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()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions