Skip to content

Commit af37707

Browse files
committed
non GPL format option
1 parent 9df513c commit af37707

File tree

4 files changed

+179
-17
lines changed

4 files changed

+179
-17
lines changed

jsonschema/_format.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,24 @@ def is_idn_host_name(instance):
248248
try:
249249
import rfc3987
250250
except ImportError:
251-
pass
251+
from jsonschema._format_validators import validate_rfc3986
252+
253+
@_checks_drafts(name="uri",)
254+
def is_uri(instance):
255+
if not isinstance(instance, str_types):
256+
return True
257+
return validate_rfc3986(instance, rule="URI")
258+
259+
@_checks_drafts(
260+
draft6="uri-reference",
261+
draft7="uri-reference",
262+
raises=ValueError,
263+
)
264+
def is_uri_reference(instance):
265+
if not isinstance(instance, str_types):
266+
return True
267+
return validate_rfc3986(instance, rule="URI_reference")
268+
252269
else:
253270
@_checks_drafts(draft7="iri", raises=ValueError)
254271
def is_iri(instance):
@@ -280,21 +297,23 @@ def is_uri_reference(instance):
280297

281298

282299
try:
283-
import strict_rfc3339
300+
from strict_rfc3339 import validate_rfc3339
284301
except ImportError:
285-
pass
286-
else:
287-
@_checks_drafts(name="date-time")
288-
def is_datetime(instance):
289-
if not isinstance(instance, str_types):
290-
return True
291-
return strict_rfc3339.validate_rfc3339(instance)
302+
from jsonschema._format_validators import validate_rfc3339
292303

293-
@_checks_drafts(draft7="time")
294-
def is_time(instance):
295-
if not isinstance(instance, str_types):
296-
return True
297-
return is_datetime("1970-01-01T" + instance)
304+
305+
@_checks_drafts(name="date-time")
306+
def is_datetime(instance):
307+
if not isinstance(instance, str_types):
308+
return True
309+
return validate_rfc3339(instance)
310+
311+
312+
@_checks_drafts(draft7="time")
313+
def is_time(instance):
314+
if not isinstance(instance, str_types):
315+
return True
316+
return is_datetime("1970-01-01T" + instance)
298317

299318

300319
@_checks_drafts(name="regex", raises=re.error)

jsonschema/_format_validators.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import re
2+
import calendar
3+
import six
4+
5+
RFC3339_REGEX_FLAGS = 0
6+
if six.PY3:
7+
RFC3339_REGEX_FLAGS |= re.ASCII
8+
9+
RFC3339_REGEX = re.compile(r"""
10+
^
11+
(\d{4}) # Year
12+
-
13+
(0[1-9]|1[0-2]) # Month
14+
-
15+
(\d{2}) # Day
16+
T
17+
(?:[01]\d|2[0123]) # Hours
18+
:
19+
(?:[0-5]\d) # Minutes
20+
:
21+
(?:[0-5]\d) # Seconds
22+
(?:\.\d+)? # Secfrac
23+
(?: Z # UTC
24+
| [+-](?:[01]\d|2[0123]):[0-5]\d # Offset
25+
)
26+
$
27+
""", re.VERBOSE | RFC3339_REGEX_FLAGS)
28+
29+
30+
def validate_rfc3339(date_string):
31+
"""
32+
Validates dates against RFC3339 datetime format
33+
Leap seconds are no supported.
34+
"""
35+
m = RFC3339_REGEX.match(date_string)
36+
if m is None:
37+
return False
38+
year, month, day = map(int, m.groups())
39+
if not year:
40+
# Year 0 is not valid a valid date
41+
return False
42+
(_, max_day) = calendar.monthrange(year, month)
43+
if not 1 <= day <= max_day:
44+
return False
45+
return True
46+
47+
48+
# Following regex rules references the ABNF terminology from
49+
# [RFC3986](https://tools.ietf.org/html/rfc3986#appendix-A)
50+
51+
# IPv6 validation rule
52+
IPv6_RE = (
53+
r"(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]["
54+
r"0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,"
55+
r"4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]["
56+
r"0-9]?))|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2["
57+
r"0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,"
58+
r"4}:)?[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4]["
59+
r"0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){,"
60+
r"2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4]["
61+
r"0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){,"
62+
r"3}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|["
63+
r"01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){,4}[0-9A-Fa-f]{1,"
64+
r"4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2["
65+
r"0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:["
66+
r"0-9A-Fa-f]{1,4}:){,6}[0-9A-Fa-f]{1,4})?::)"
67+
)
68+
69+
70+
# An authority is defined as: [ userinfo "@" ] host [ ":" port ]
71+
AUTHORITY_RE = r"""
72+
(?:(?:[a-zA-Z0-9_.~\-!$&'()*+,;=:]|%[0-9A-Fa-f]{{2}})*@)? # user info
73+
(?:
74+
\[(?:{ip_v6}|v[0-9A-Fa-f]+\.[a-zA-Z0-9_.~\-!$&'()*+,;=:]+)\] # IP-literal
75+
| (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){{3}}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) # IPv4
76+
| (?:[a-zA-Z0-9_.~\-!$&'()*+,;=]|%[0-9A-Fa-f]{{2}})* # reg-name
77+
) # host
78+
(?::[0-9]*)? # port
79+
""".format(ip_v6=IPv6_RE,)
80+
# Path char regex rule
81+
PCHAR_RE = r"(?:[a-zA-Z0-9_.~\-!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})"
82+
# Query and Fragment rules are exactly the same
83+
QUERY_RE = r"(?:[a-zA-Z0-9_.~\-!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*"
84+
# An URI is defined as: scheme ":" hier-part [ "?" query ] [ "#" fragment ]
85+
URI_RE = r"""
86+
[a-zA-Z][a-zA-Z0-9+.-]* #scheme
87+
:
88+
(?:
89+
//
90+
{authority}
91+
(?:/{pchar}*)* # path-abempty
92+
| /(?:{pchar}+ (?:/{pchar}*)*)? # path-absolute
93+
| {pchar}+ (?:/{pchar}*)* # path-rootless
94+
| # or nothing
95+
) # hier-part
96+
(?:\?{query})? # Query
97+
(?:\#{fragment})? # Fragment
98+
""".format(
99+
authority=AUTHORITY_RE,
100+
query=QUERY_RE,
101+
fragment=QUERY_RE,
102+
pchar=PCHAR_RE
103+
)
104+
105+
# A relative-ref is defined as: relative-part [ "?" query ] [ "#" fragment ]
106+
RELATIVE_REF_RE = r"""
107+
(?:
108+
//
109+
{authority}
110+
(?:/{pchar}*)* # path-abempty
111+
| /(?:{pchar}+ (?:/{pchar}*)*)? # path-absolute
112+
| (?:[a-zA-Z0-9_.~\-!$&'()*+,;=@]|%[0-9A-Fa-f]{{2}})+ (?:/{pchar}*)* # path-noscheme
113+
| # or nothing
114+
) # relative-part
115+
(?:\?{query})? # Query
116+
(?:\#{fragment})? # Fragment
117+
""".format(
118+
authority=AUTHORITY_RE,
119+
query=QUERY_RE,
120+
fragment=QUERY_RE,
121+
pchar=PCHAR_RE
122+
)
123+
# Compiled URI regex rule
124+
URI_RE_COMP = re.compile(r"^{uri_re}$".format(uri_re=URI_RE), re.VERBOSE)
125+
# Compiled URI-reference regex rule. URI-reference is defined as: URI / relative-ref
126+
URI_REF_RE_COMP = re.compile(r"^(?:{uri_re}|{relative_ref})$".format(
127+
uri_re=URI_RE,
128+
relative_ref=RELATIVE_REF_RE,
129+
), re.VERBOSE)
130+
131+
132+
def validate_rfc3986(url, rule='URI'):
133+
if rule == 'URI':
134+
return URI_RE_COMP.match(url)
135+
elif rule == 'URI_reference':
136+
return URI_REF_RE_COMP.match(url)
137+
else:
138+
raise ValueError('Invalid rule')

setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ format =
4040
rfc3987
4141
strict-rfc3339
4242
webcolors
43+
format_nongpl =
44+
idna
45+
jsonpointer>1.13
46+
webcolors
4347

4448
[options.entry_points]
4549
console_scripts =

tox.ini

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tox]
22
envlist =
3-
py{35,36,37,py,py3}-{build,tests}
3+
py{35,36,37,py,py3}-{build,tests,tests_nongpl},
44
demo
55
readme
66
safety
@@ -22,8 +22,9 @@ whitelist_externals =
2222
virtualenv
2323
commands =
2424
perf,tests: {envbindir}/python -m pip install '{toxinidir}[format]'
25+
tests_nongpl: {envbindir}/python -m pip install '{toxinidir}[format_nongpl]'
2526

26-
tests: {envbindir}/trial {posargs:jsonschema}
27+
tests,tests_nongpl: {envbindir}/trial {posargs:jsonschema}
2728
tests: {envpython} -m doctest {toxinidir}/README.rst
2829

2930
perf: mkdir {envtmpdir}/benchmarks/
@@ -53,7 +54,7 @@ deps =
5354

5455
perf: pyperf
5556

56-
tests,coverage,codecov: -r{toxinidir}/test-requirements.txt
57+
tests,tests_nongpl,coverage,codecov: -r{toxinidir}/test-requirements.txt
5758

5859
coverage,codecov: coverage
5960
codecov: codecov

0 commit comments

Comments
 (0)