subschema 0.0.2__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {subschema-0.0.2 → subschema-0.0.4}/PKG-INFO +23 -4
- {subschema-0.0.2 → subschema-0.0.4}/README.md +22 -3
- {subschema-0.0.2 → subschema-0.0.4}/pyproject.toml +2 -1
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/api.py +7 -3
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/cli.py +12 -1
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/exceptions.py +22 -2
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/applicators.py +0 -41
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/context.py +19 -7
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/contracts.py +15 -0
- subschema-0.0.4/src/subschema/kernel/disjointness.py +703 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/driver.py +2 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/evaluation.py +17 -2
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/finite.py +11 -15
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/ir.py +20 -0
- subschema-0.0.4/src/subschema/kernel/normalization.py +274 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/regex.py +331 -75
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/sat.py +182 -57
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/schemas.py +1 -0
- subschema-0.0.4/src/subschema/kernel/tagged_unions.py +124 -0
- {subschema-0.0.2 → subschema-0.0.4}/uv.lock +15 -1
- subschema-0.0.2/src/subschema/kernel/disjointness.py +0 -147
- subschema-0.0.2/src/subschema/kernel/normalization.py +0 -65
- {subschema-0.0.2 → subschema-0.0.4}/.gitignore +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/LICENSE +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/__init__.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/dialects.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/__init__.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/certificates.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/composition.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/constraints.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/difference.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/__init__.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/arrays.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/numbers.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/objects.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/strings.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/types.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/engine.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/formulas.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/json_data.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/overlaps.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/projection.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/references.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/scalars.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/semantic.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/symbolic.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/validation.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/values.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/witnesses.py +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/py.typed +0 -0
- {subschema-0.0.2 → subschema-0.0.4}/src/subschema/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subschema
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: A conservative JSON Schema subschema prover
|
|
5
5
|
Project-URL: Homepage, https://github.com/ArakawaHenri/subschema
|
|
6
6
|
Project-URL: Repository, https://github.com/ArakawaHenri/subschema
|
|
@@ -82,7 +82,26 @@ Available public entrypoints:
|
|
|
82
82
|
- `canonicalize_schema(schema, *, dialect=None)`
|
|
83
83
|
- `SchemaError`, `SubschemaError`, and `UnsupportedProofError` as stable catch points.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
## Proof Behavior
|
|
86
|
+
|
|
87
|
+
`subschema` proves sound results when it can. If a query is outside the current
|
|
88
|
+
proof model, public boolean helpers raise `UnsupportedProofError` instead of
|
|
89
|
+
guessing.
|
|
90
|
+
|
|
91
|
+
Use `endeavor=True` or `--endeavor` for finite but potentially expensive proof
|
|
92
|
+
products. In endeavor mode, `max_work` limits proof frontier expansion and
|
|
93
|
+
`timeout_ms` limits solver calls. These controls are accepted only when endeavor
|
|
94
|
+
is enabled.
|
|
95
|
+
|
|
96
|
+
Current intentional boundaries:
|
|
97
|
+
|
|
98
|
+
- external references are not fetched from the network;
|
|
99
|
+
- recursive `$ref` and recursive dynamic-reference proofs are not modeled;
|
|
100
|
+
- `format` is treated as an annotation unless a future assertion backend is
|
|
101
|
+
provided;
|
|
102
|
+
- non-regular ECMAScript regex features such as backreferences and lookaround
|
|
103
|
+
are reported as unsupported;
|
|
104
|
+
- `unsupported` means “not proven by this model,” not “the schema is invalid.”
|
|
86
105
|
|
|
87
106
|
## Dialects
|
|
88
107
|
|
|
@@ -98,8 +117,8 @@ Supported dialects:
|
|
|
98
117
|
- Draft 2019-09
|
|
99
118
|
- Draft 2020-12
|
|
100
119
|
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
Resource exhaustion is reported separately from unsupported proof fragments when
|
|
121
|
+
an endeavor proof exceeds its configured work or timeout limit.
|
|
103
122
|
|
|
104
123
|
## Acknowledgement
|
|
105
124
|
|
|
@@ -65,7 +65,26 @@ Available public entrypoints:
|
|
|
65
65
|
- `canonicalize_schema(schema, *, dialect=None)`
|
|
66
66
|
- `SchemaError`, `SubschemaError`, and `UnsupportedProofError` as stable catch points.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
## Proof Behavior
|
|
69
|
+
|
|
70
|
+
`subschema` proves sound results when it can. If a query is outside the current
|
|
71
|
+
proof model, public boolean helpers raise `UnsupportedProofError` instead of
|
|
72
|
+
guessing.
|
|
73
|
+
|
|
74
|
+
Use `endeavor=True` or `--endeavor` for finite but potentially expensive proof
|
|
75
|
+
products. In endeavor mode, `max_work` limits proof frontier expansion and
|
|
76
|
+
`timeout_ms` limits solver calls. These controls are accepted only when endeavor
|
|
77
|
+
is enabled.
|
|
78
|
+
|
|
79
|
+
Current intentional boundaries:
|
|
80
|
+
|
|
81
|
+
- external references are not fetched from the network;
|
|
82
|
+
- recursive `$ref` and recursive dynamic-reference proofs are not modeled;
|
|
83
|
+
- `format` is treated as an annotation unless a future assertion backend is
|
|
84
|
+
provided;
|
|
85
|
+
- non-regular ECMAScript regex features such as backreferences and lookaround
|
|
86
|
+
are reported as unsupported;
|
|
87
|
+
- `unsupported` means “not proven by this model,” not “the schema is invalid.”
|
|
69
88
|
|
|
70
89
|
## Dialects
|
|
71
90
|
|
|
@@ -81,8 +100,8 @@ Supported dialects:
|
|
|
81
100
|
- Draft 2019-09
|
|
82
101
|
- Draft 2020-12
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
Resource exhaustion is reported separately from unsupported proof fragments when
|
|
104
|
+
an endeavor proof exceeds its configured work or timeout limit.
|
|
86
105
|
|
|
87
106
|
## Acknowledgement
|
|
88
107
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "subschema"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4"
|
|
8
8
|
description = "A conservative JSON Schema subschema prover"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -44,6 +44,7 @@ include = [
|
|
|
44
44
|
[dependency-groups]
|
|
45
45
|
dev = [
|
|
46
46
|
"coverage[toml]",
|
|
47
|
+
"hypothesis>=6.155.1",
|
|
47
48
|
"mypy>=2.1.0",
|
|
48
49
|
"pytest",
|
|
49
50
|
"ruff",
|
|
@@ -15,7 +15,7 @@ from subschema.dialects import (
|
|
|
15
15
|
validate_supported_keywords,
|
|
16
16
|
)
|
|
17
17
|
from subschema.kernel.contracts import ProofBudgets, ProofOptions
|
|
18
|
-
from subschema.kernel.disjointness import schemas_are_disjoint
|
|
18
|
+
from subschema.kernel.disjointness import schema_is_empty_exact, schemas_are_disjoint
|
|
19
19
|
from subschema.kernel.engine import ProofEngine
|
|
20
20
|
from subschema.kernel.json_data import ensure_json_value
|
|
21
21
|
from subschema.kernel.normalization import normalize_boolean_schemas
|
|
@@ -202,12 +202,16 @@ def is_empty(
|
|
|
202
202
|
timeout_ms=timeout_ms,
|
|
203
203
|
)
|
|
204
204
|
empty_schema = empty_schema_for_dialect(resolved_dialect)
|
|
205
|
-
|
|
205
|
+
engine = ProofEngine.for_schemas(
|
|
206
206
|
schema,
|
|
207
207
|
empty_schema,
|
|
208
208
|
dialect=resolved_dialect,
|
|
209
209
|
options=options,
|
|
210
|
-
)
|
|
210
|
+
)
|
|
211
|
+
exact_empty = schema_is_empty_exact(schema, engine.context)
|
|
212
|
+
if exact_empty.status != "unsupported":
|
|
213
|
+
return exact_empty.as_bool(resolved_dialect)
|
|
214
|
+
return engine.is_subschema_bool(schema, empty_schema)
|
|
211
215
|
|
|
212
216
|
|
|
213
217
|
def is_disjoint(
|
|
@@ -2,6 +2,7 @@ import argparse
|
|
|
2
2
|
from typing import TextIO, cast
|
|
3
3
|
|
|
4
4
|
from subschema.api import is_subschema
|
|
5
|
+
from subschema.exceptions import UnsupportedProofError
|
|
5
6
|
from subschema.kernel import ProofBudgets, ProofOptions
|
|
6
7
|
from subschema.kernel.json_data import strict_json_load
|
|
7
8
|
from subschema.types import JSONSchema
|
|
@@ -25,6 +26,10 @@ def load_json_file(path: str, label: str) -> JSONSchema:
|
|
|
25
26
|
raise SystemExit(f"{label} {err}") from err
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
def format_unsupported_proof_error(error: UnsupportedProofError) -> str:
|
|
30
|
+
return error.format()
|
|
31
|
+
|
|
32
|
+
|
|
28
33
|
def main() -> None:
|
|
29
34
|
"""CLI entry point for subschema"""
|
|
30
35
|
|
|
@@ -70,7 +75,13 @@ def main() -> None:
|
|
|
70
75
|
),
|
|
71
76
|
)
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
try:
|
|
79
|
+
result = is_subschema(s1, s2, proof_options=proof_options)
|
|
80
|
+
except UnsupportedProofError as err:
|
|
81
|
+
message = format_unsupported_proof_error(err)
|
|
82
|
+
raise SystemExit(f"unsupported proof: {message}") from err
|
|
83
|
+
|
|
84
|
+
print("LHS <: RHS", result)
|
|
74
85
|
|
|
75
86
|
|
|
76
87
|
if __name__ == "__main__":
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from jsonschema.exceptions import SchemaError
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from subschema.kernel.contracts import UnsupportedDiagnostic
|
|
10
|
+
|
|
8
11
|
__all__ = [
|
|
9
12
|
"ConflictingDialectError",
|
|
10
13
|
"SchemaError",
|
|
@@ -26,13 +29,30 @@ class SubschemaError(Exception):
|
|
|
26
29
|
class UnsupportedProofError(SubschemaError):
|
|
27
30
|
"""Raised when the prover cannot decide a supported public query."""
|
|
28
31
|
|
|
29
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
reason: str,
|
|
35
|
+
*,
|
|
36
|
+
status: str | None = None,
|
|
37
|
+
diagnostics: tuple[UnsupportedDiagnostic, ...] = (),
|
|
38
|
+
):
|
|
30
39
|
self.reason = reason
|
|
31
40
|
self.status = status
|
|
41
|
+
self.diagnostics = diagnostics
|
|
32
42
|
|
|
33
43
|
def __str__(self) -> str:
|
|
34
44
|
return self.reason
|
|
35
45
|
|
|
46
|
+
def formatted_diagnostics(self) -> tuple[str, ...]:
|
|
47
|
+
return tuple(diagnostic.format() for diagnostic in self.diagnostics)
|
|
48
|
+
|
|
49
|
+
def format(self) -> str:
|
|
50
|
+
diagnostics = self.formatted_diagnostics()
|
|
51
|
+
if not diagnostics:
|
|
52
|
+
return self.reason
|
|
53
|
+
diagnostic_lines = "\n".join(f"- {diagnostic}" for diagnostic in diagnostics)
|
|
54
|
+
return f"{self.reason}\ndiagnostics:\n{diagnostic_lines}"
|
|
55
|
+
|
|
36
56
|
|
|
37
57
|
class _UnsupportedCaseError(SubschemaError):
|
|
38
58
|
pass
|
|
@@ -153,9 +153,6 @@ __all__ = [
|
|
|
153
153
|
"right_not_intersection_witness_plan",
|
|
154
154
|
"right_not_resolved_rhs_schema",
|
|
155
155
|
"right_not_subproof_choice",
|
|
156
|
-
"right_applicator_base_first_result_choice",
|
|
157
|
-
"right_applicator_branch_first_pre_base_choice",
|
|
158
|
-
"right_applicator_branch_first_result_choice",
|
|
159
156
|
"right_negative_all_of_branch_product_plan",
|
|
160
157
|
"right_negative_all_of_branch_proof_choice",
|
|
161
158
|
"right_negative_any_of_branch_product_plan",
|
|
@@ -801,44 +798,6 @@ def applicator_base_pre_branch_choice(
|
|
|
801
798
|
return "continue"
|
|
802
799
|
|
|
803
800
|
|
|
804
|
-
def right_applicator_base_first_result_choice(
|
|
805
|
-
base_status: ProofStatus,
|
|
806
|
-
branch_status: ProofStatus,
|
|
807
|
-
) -> ApplicatorProofChoice:
|
|
808
|
-
if base_status == "proved_false":
|
|
809
|
-
return "base_false"
|
|
810
|
-
if branch_status == "proved_false":
|
|
811
|
-
return "branch"
|
|
812
|
-
if base_status == "resource_exhausted":
|
|
813
|
-
return "base"
|
|
814
|
-
if branch_status == "proved_true" and base_status == "proved_true":
|
|
815
|
-
return "proved_true"
|
|
816
|
-
if branch_status == "proved_true":
|
|
817
|
-
return "base"
|
|
818
|
-
return "branch"
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
def right_applicator_branch_first_pre_base_choice(
|
|
822
|
-
branch_status: ProofStatus,
|
|
823
|
-
) -> ApplicatorProofChoice:
|
|
824
|
-
if branch_status in {"proved_false", "resource_exhausted"}:
|
|
825
|
-
return "branch"
|
|
826
|
-
return "continue"
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
def right_applicator_branch_first_result_choice(
|
|
830
|
-
base_status: ProofStatus,
|
|
831
|
-
branch_status: ProofStatus,
|
|
832
|
-
) -> ApplicatorProofChoice:
|
|
833
|
-
if base_status == "proved_false":
|
|
834
|
-
return "base_false"
|
|
835
|
-
if base_status in {"unsupported", "resource_exhausted"}:
|
|
836
|
-
return "base"
|
|
837
|
-
if branch_status == "proved_true":
|
|
838
|
-
return "proved_true"
|
|
839
|
-
return "branch"
|
|
840
|
-
|
|
841
|
-
|
|
842
801
|
def right_negative_any_of_branch_proof_choice(
|
|
843
802
|
status: ProofStatus,
|
|
844
803
|
) -> ApplicatorBranchProofChoice:
|
|
@@ -5,7 +5,7 @@ Proof context, policy, and budget state for the kernel.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
import subschema.kernel.driver as proof_driver
|
|
11
11
|
from subschema.dialects import Dialect
|
|
@@ -17,9 +17,6 @@ from subschema.kernel.contracts import (
|
|
|
17
17
|
)
|
|
18
18
|
from subschema.kernel.values import stable_key
|
|
19
19
|
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from subschema.kernel.evaluation import EvaluationExpression
|
|
22
|
-
|
|
23
20
|
_EXPENSIVE_PROOF_WORK_LABELS: dict[ExpensiveProofKind, str] = {
|
|
24
21
|
"array_product": "array product",
|
|
25
22
|
"branch_product": "branch expansion",
|
|
@@ -36,9 +33,7 @@ class ProofContext:
|
|
|
36
33
|
dialect: Dialect
|
|
37
34
|
options: ProofOptions = field(default_factory=ProofOptions)
|
|
38
35
|
subproof_cache: dict[tuple[Any, ...], ProofResult] = field(default_factory=dict)
|
|
39
|
-
|
|
40
|
-
default_factory=dict
|
|
41
|
-
)
|
|
36
|
+
cache: dict[tuple[object, ...], object] = field(default_factory=dict)
|
|
42
37
|
work_meter: ProofWorkMeter = field(init=False)
|
|
43
38
|
|
|
44
39
|
def __post_init__(self) -> None:
|
|
@@ -72,6 +67,15 @@ class ProofContext:
|
|
|
72
67
|
stable_key(rhs),
|
|
73
68
|
)
|
|
74
69
|
|
|
70
|
+
def cache_get(self, namespace: str, key: tuple[Any, ...]) -> object | None:
|
|
71
|
+
return self.cache.get(self._cache_key(namespace, key))
|
|
72
|
+
|
|
73
|
+
def cache_set(self, namespace: str, key: tuple[Any, ...], value: object) -> None:
|
|
74
|
+
self.cache[self._cache_key(namespace, key)] = value
|
|
75
|
+
|
|
76
|
+
def _cache_key(self, namespace: str, key: tuple[Any, ...]) -> tuple[object, ...]:
|
|
77
|
+
return (namespace, *(_cache_key_part(part) for part in key))
|
|
78
|
+
|
|
75
79
|
def consume_branch_expansion(self, reason: str) -> ProofResult | None:
|
|
76
80
|
return self.spend_work(1, "branch expansion", reason)
|
|
77
81
|
|
|
@@ -132,3 +136,11 @@ class ProofContext:
|
|
|
132
136
|
from subschema.kernel.projection import ProjectionEngine
|
|
133
137
|
|
|
134
138
|
return ProjectionEngine(self).finite_join_projection(lhs, rhs)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _cache_key_part(part: Any) -> object:
|
|
142
|
+
try:
|
|
143
|
+
hash(part)
|
|
144
|
+
except TypeError:
|
|
145
|
+
return stable_key(part)
|
|
146
|
+
return part
|
|
@@ -106,6 +106,20 @@ class ProofResult:
|
|
|
106
106
|
error: Exception | None = None
|
|
107
107
|
diagnostics: tuple[UnsupportedDiagnostic, ...] = ()
|
|
108
108
|
|
|
109
|
+
def __repr__(self) -> str:
|
|
110
|
+
parts = [f"status={self.status!r}"]
|
|
111
|
+
if self.reason is not None:
|
|
112
|
+
parts.append(f"reason={self.reason!r}")
|
|
113
|
+
if self.diagnostics:
|
|
114
|
+
parts.append(f"diagnostics={len(self.diagnostics)}")
|
|
115
|
+
if self.certificate is not None:
|
|
116
|
+
parts.append(f"certificate={self.certificate.kind!r}")
|
|
117
|
+
if self.witness is not None:
|
|
118
|
+
parts.append(f"witness_type={type(self.witness).__name__}")
|
|
119
|
+
if self.error is not None:
|
|
120
|
+
parts.append(f"error={type(self.error).__name__}")
|
|
121
|
+
return f"{type(self).__name__}({', '.join(parts)})"
|
|
122
|
+
|
|
109
123
|
@classmethod
|
|
110
124
|
def true(cls) -> ProofResult:
|
|
111
125
|
return cls("proved_true")
|
|
@@ -155,6 +169,7 @@ class ProofResult:
|
|
|
155
169
|
raise UnsupportedProofError(
|
|
156
170
|
self.reason or "schema proof could not be proven",
|
|
157
171
|
status=self.status,
|
|
172
|
+
diagnostics=self.diagnostics,
|
|
158
173
|
)
|
|
159
174
|
|
|
160
175
|
|