Skip to content

Commit 865073b

Browse files
committed
Prefer 'keyword' over 'validator' in docs.
In newer JSON Schema specifications we've standardized more on this language rather than calling things validators (and such a thing already has plenty of overloaded meaning here). This commit doesn't do any deprecation, so there's still some awkwardness in that ValidationError.validator is the keyword which failed validation, and Validator.VALIDATORS is a mapping of keywords to callables. We may choose to do so later, but for now will save some API churn in case something else changes.
1 parent 9f34627 commit 865073b

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed

sphinx_json_schema_spec/__init__.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from contextlib import suppress
2+
from datetime import datetime
3+
from importlib import metadata
4+
from urllib.parse import urljoin
5+
import os
6+
import urllib.request
7+
8+
from docutils import nodes
9+
from lxml import html
10+
import certifi
11+
12+
__version__ = "1.2.0"
13+
14+
BASE_URL = "https://json-schema.org/draft-07/"
15+
VALIDATION_SPEC = urljoin(BASE_URL, "json-schema-validation.html")
16+
REF_URL = urljoin(BASE_URL, "json-schema-core.html#rfc.section.8.3")
17+
SCHEMA_URL = urljoin(BASE_URL, "json-schema-core.html#rfc.section.7")
18+
19+
20+
def setup(app):
21+
"""
22+
Install the plugin.
23+
24+
Arguments:
25+
26+
app (sphinx.application.Sphinx):
27+
28+
the Sphinx application context
29+
"""
30+
31+
app.add_config_value("cache_path", "_cache", "")
32+
33+
os.makedirs(app.config.cache_path, exist_ok=True)
34+
35+
path = os.path.join(app.config.cache_path, "spec.html")
36+
spec = fetch_or_load(path)
37+
app.add_role("kw", docutils_does_not_allow_using_classes(spec))
38+
39+
return dict(version=__version__, parallel_read_safe=True)
40+
41+
42+
def fetch_or_load(spec_path):
43+
"""
44+
Fetch a new specification or use the cache if it's current.
45+
46+
Arguments:
47+
48+
cache_path:
49+
50+
the path to a cached specification
51+
"""
52+
53+
headers = {
54+
"User-Agent": "python-jsonschema v{} - documentation build v{}".format(
55+
metadata.version("jsonschema"),
56+
__version__,
57+
),
58+
}
59+
60+
with suppress(FileNotFoundError):
61+
modified = datetime.utcfromtimestamp(os.path.getmtime(spec_path))
62+
date = modified.strftime("%a, %d %b %Y %I:%M:%S UTC")
63+
headers["If-Modified-Since"] = date
64+
65+
request = urllib.request.Request(VALIDATION_SPEC, headers=headers)
66+
response = urllib.request.urlopen(request, cafile=certifi.where())
67+
68+
if response.code == 200:
69+
with open(spec_path, "w+b") as spec:
70+
spec.writelines(response)
71+
spec.seek(0)
72+
return html.parse(spec)
73+
74+
with open(spec_path) as spec:
75+
return html.parse(spec)
76+
77+
78+
def docutils_does_not_allow_using_classes(spec):
79+
"""
80+
Yeah.
81+
82+
It doesn't allow using a class because it does annoying stuff like
83+
try to set attributes on the callable object rather than just
84+
keeping a dict.
85+
"""
86+
87+
def keyword(name, raw_text, text, lineno, inliner):
88+
"""
89+
Link to the JSON Schema documentation for a keyword.
90+
91+
Arguments:
92+
93+
name (str):
94+
95+
the name of the role in the document
96+
97+
raw_source (str):
98+
99+
the raw text (role with argument)
100+
101+
text (str):
102+
103+
the argument given to the role
104+
105+
lineno (int):
106+
107+
the line number
108+
109+
inliner (docutils.parsers.rst.states.Inliner):
110+
111+
the inliner
112+
113+
Returns:
114+
115+
tuple:
116+
117+
a 2-tuple of nodes to insert into the document and an
118+
iterable of system messages, both possibly empty
119+
"""
120+
121+
if text == "$ref":
122+
return [nodes.reference(raw_text, text, refuri=REF_URL)], []
123+
elif text == "$schema":
124+
return [nodes.reference(raw_text, text, refuri=SCHEMA_URL)], []
125+
126+
# find the header in the validation spec containing matching text
127+
header = spec.xpath("//h1[contains(text(), '{0}')]".format(text))
128+
129+
if len(header) == 0:
130+
inliner.reporter.warning(
131+
"Didn't find a target for {0}".format(text),
132+
)
133+
uri = VALIDATION_SPEC
134+
else:
135+
if len(header) > 1:
136+
inliner.reporter.info(
137+
"Found multiple targets for {0}".format(text),
138+
)
139+
140+
# get the href from link in the header
141+
uri = urljoin(VALIDATION_SPEC, header[0].find("a").attrib["href"])
142+
143+
reference = nodes.reference(raw_text, text, refuri=uri)
144+
return [reference], []
145+
146+
return keyword

0 commit comments

Comments
 (0)