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.
Files changed (51) hide show
  1. {subschema-0.0.2 → subschema-0.0.4}/PKG-INFO +23 -4
  2. {subschema-0.0.2 → subschema-0.0.4}/README.md +22 -3
  3. {subschema-0.0.2 → subschema-0.0.4}/pyproject.toml +2 -1
  4. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/api.py +7 -3
  5. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/cli.py +12 -1
  6. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/exceptions.py +22 -2
  7. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/applicators.py +0 -41
  8. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/context.py +19 -7
  9. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/contracts.py +15 -0
  10. subschema-0.0.4/src/subschema/kernel/disjointness.py +703 -0
  11. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/driver.py +2 -0
  12. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/evaluation.py +17 -2
  13. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/finite.py +11 -15
  14. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/ir.py +20 -0
  15. subschema-0.0.4/src/subschema/kernel/normalization.py +274 -0
  16. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/regex.py +331 -75
  17. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/sat.py +182 -57
  18. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/schemas.py +1 -0
  19. subschema-0.0.4/src/subschema/kernel/tagged_unions.py +124 -0
  20. {subschema-0.0.2 → subschema-0.0.4}/uv.lock +15 -1
  21. subschema-0.0.2/src/subschema/kernel/disjointness.py +0 -147
  22. subschema-0.0.2/src/subschema/kernel/normalization.py +0 -65
  23. {subschema-0.0.2 → subschema-0.0.4}/.gitignore +0 -0
  24. {subschema-0.0.2 → subschema-0.0.4}/LICENSE +0 -0
  25. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/__init__.py +0 -0
  26. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/dialects.py +0 -0
  27. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/__init__.py +0 -0
  28. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/certificates.py +0 -0
  29. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/composition.py +0 -0
  30. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/constraints.py +0 -0
  31. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/difference.py +0 -0
  32. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/__init__.py +0 -0
  33. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/arrays.py +0 -0
  34. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/numbers.py +0 -0
  35. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/objects.py +0 -0
  36. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/strings.py +0 -0
  37. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/domains/types.py +0 -0
  38. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/engine.py +0 -0
  39. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/formulas.py +0 -0
  40. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/json_data.py +0 -0
  41. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/overlaps.py +0 -0
  42. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/projection.py +0 -0
  43. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/references.py +0 -0
  44. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/scalars.py +0 -0
  45. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/semantic.py +0 -0
  46. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/symbolic.py +0 -0
  47. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/validation.py +0 -0
  48. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/values.py +0 -0
  49. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/kernel/witnesses.py +0 -0
  50. {subschema-0.0.2 → subschema-0.0.4}/src/subschema/py.typed +0 -0
  51. {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.2
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
- The wheel includes `py.typed` for typed-package consumers.
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
- The prover is intentionally conservative. Unsupported, unbounded, recursive, or
102
- exhausted proof fragments are reported instead of being silently approximated.
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
- The wheel includes `py.typed` for typed-package consumers.
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
- The prover is intentionally conservative. Unsupported, unbounded, recursive, or
85
- exhausted proof fragments are reported instead of being silently approximated.
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.2"
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
- return ProofEngine.for_schemas(
205
+ engine = ProofEngine.for_schemas(
206
206
  schema,
207
207
  empty_schema,
208
208
  dialect=resolved_dialect,
209
209
  options=options,
210
- ).is_subschema_bool(schema, empty_schema)
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
- print("LHS <: RHS", is_subschema(s1, s2, proof_options=proof_options))
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__(self, reason: str, *, status: str | None = None):
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 TYPE_CHECKING, Any
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
- evaluation_expression_cache: dict[tuple[Any, ...], EvaluationExpression] = field(
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