refactor: move the reference implementation to src/paperspec
The oracle had two import names, `tools.oracle` for the tools and `paperspec` for other repositories, which could load the same code twice in one process and needed a gate keyed on the module name. It now lives in the standard src layout under its one name. - git mv tools/oracle -> src/paperspec; the 22 rule modules are unchanged. - The tools import `paperspec`. `tools/__init__.py` now holds the Python 3.14 and cryptography/jsonschema check and puts src/ first on sys.path, so the tools run this checkout's code with or without pixi. The package's own __init__ checks nothing and exposes __version__. - pyproject.toml: version from git tags via setuptools-scm (guess-next-dev, node-and-timestamp, src/paperspec/_version.py, gitignored). The packages stay listed rather than found because schemas/, sql/ and vectors/ stay at the root: the spec links to them and the site serves the schemas at their $id URLs. - pixi.toml: a `dev` feature and environment with setuptools, setuptools-scm and python-build, and `pixi run -e dev wheel`. - check.py scans src/paperspec for rule IDs; README, CONTRIBUTING, the FAQ, the overview, the vectors and tools READMEs and the changelog point at the new path. The FAQ no longer says the oracle is not a library. pixi.lock is not re-solved in this commit: with a dynamic version, pixi builds the package metadata to lock, which needs the conda environment installed. Run `pixi lock` and commit the result before pushing; the site workflow installs with --locked. pixi run check passes; no computed value, vector, fixture, schema or SQL file changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
40 files changed
+134
-84
No files matched your search
@@ -1,91 +0,0 @@
|
||||
"""JSON Schema (draft 2020-12) validation with the `jsonschema` package, and the lint of the keyword subset.
|
||||
|
||||
Non-normative.
|
||||
The spec's schemas are restricted to the keywords below (spec/22-conformance.md) so that every 2020-12
|
||||
validator reads them alike; `lint` refuses any schema that leaves the subset.
|
||||
`jsonschema` and `referencing` are imported on first use, so `Store.load`, `deref` and `lint` work without them.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
KEYWORDS = {
|
||||
"$schema", "$id", "$ref", "$defs", "$comment", "title", "description", "examples", "default", "deprecated",
|
||||
"type", "enum", "const", "properties", "required", "additionalProperties", "patternProperties",
|
||||
"propertyNames", "minProperties", "maxProperties", "dependentRequired", "items", "prefixItems", "minItems",
|
||||
"maxItems", "uniqueItems", "contains", "minLength", "maxLength", "pattern", "format", "minimum", "maximum",
|
||||
"exclusiveMinimum", "exclusiveMaximum", "oneOf", "anyOf", "allOf", "not", "if", "then", "else",
|
||||
}
|
||||
FORBIDDEN_PATTERN_TOKENS = ("\\d", "\\w", "\\s", "\\b", "\\D", "\\W", "\\S", "\\B", "(?<", "(?P")
|
||||
|
||||
|
||||
class SchemaError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Store:
|
||||
"""The schemas of one folder, linted on load, with a compiled validator per schema."""
|
||||
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.docs = {}
|
||||
self._registry = None
|
||||
self._validators = {}
|
||||
|
||||
def registry(self):
|
||||
if self._registry is None:
|
||||
from referencing import Registry, Resource
|
||||
|
||||
names = sorted(f for f in os.listdir(self.root) if f.endswith(".schema.json"))
|
||||
self._registry = Registry().with_resources(
|
||||
[(self.load(f)["$id"], Resource.from_contents(self.load(f))) for f in names])
|
||||
return self._registry
|
||||
|
||||
def validator(self, name):
|
||||
if name not in self._validators:
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
self._validators[name] = Draft202012Validator(self.load(name), registry=self.registry())
|
||||
return self._validators[name]
|
||||
|
||||
def load(self, name):
|
||||
if name not in self.docs:
|
||||
with open(os.path.join(self.root, name), encoding="utf-8") as f:
|
||||
self.docs[name] = json.load(f)
|
||||
lint(self.docs[name], name)
|
||||
return self.docs[name]
|
||||
|
||||
def deref(self, base, ref):
|
||||
name, _, frag = ref.partition("#")
|
||||
name = name or base
|
||||
node = self.load(name)
|
||||
if frag:
|
||||
for part in frag.lstrip("/").split("/"):
|
||||
node = node[part.replace("~1", "/").replace("~0", "~")]
|
||||
return name, node
|
||||
|
||||
|
||||
def lint(schema, where):
|
||||
"""Refuse keywords outside the subset, and regex syntax that differs between dialects."""
|
||||
if isinstance(schema, dict):
|
||||
for k, v in schema.items():
|
||||
if k not in KEYWORDS:
|
||||
raise SchemaError("%s: keyword %s is outside the subset" % (where, k))
|
||||
if k == "pattern" and any(t in v for t in FORBIDDEN_PATTERN_TOKENS):
|
||||
raise SchemaError("%s: pattern %r uses dialect-dependent syntax" % (where, v))
|
||||
if k in ("properties", "patternProperties", "$defs"):
|
||||
for kk, vv in v.items():
|
||||
if k == "patternProperties" and any(t in kk for t in FORBIDDEN_PATTERN_TOKENS):
|
||||
raise SchemaError("%s: pattern %r uses dialect-dependent syntax" % (where, kk))
|
||||
lint(vv, where + "/" + kk)
|
||||
elif k in ("items", "additionalProperties", "propertyNames", "not", "if", "then", "else", "contains"):
|
||||
lint(v, where + "/" + k)
|
||||
elif k in ("oneOf", "anyOf", "allOf", "prefixItems"):
|
||||
for i, s in enumerate(v):
|
||||
lint(s, "%s/%s/%d" % (where, k, i))
|
||||
|
||||
|
||||
def validate(store, name, instance):
|
||||
"""Return a list of error strings, `path: message`, in path order; empty when the instance is valid."""
|
||||
errors = sorted(store.validator(name).iter_errors(instance), key=lambda e: [str(x) for x in e.absolute_path])
|
||||
return ["%s: %s" % ("/" + "/".join(str(x) for x in e.absolute_path), e.message) for e in errors]
|
||||
Reference in new issue
Block a user