Skip to content

Commit

Permalink
version 1.18.0, fixes #74
Browse files Browse the repository at this point in the history
  • Loading branch information
lnoor committed Apr 20, 2022
1 parent ccd4887 commit bf3a295
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 33 deletions.
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ This software is made available under the GPL v3.
Changelog
=========

Version 1.18.0
--------------

Expanding on the work of `Pavel Odvody <https://github.com/shaded-enmity>`_ with JSON Pointer
the ``:pass_unmodified:`` option is included.
This option prevents escaping the string pointed at.

Version 1.17.2
--------------

Expand Down
23 changes: 23 additions & 0 deletions docs/directive.rst
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,26 @@ It is also possible to hide a key if their value is empty using ``:hide_key_if_e
.. jsonschema::
:hide_key_if_empty: /**/defaults
Prevent escaping of strings
+++++++++++++++++++++++++++
Strings are sometimes subject to multiple evaluation passes when rendering.
This happens because `sphinx-jsonschema` renders a schema by transforming in into a table
and then recursively call on Sphinx to render the table.
To prevent unintended modifications due to this second pass some characters (such as '_'
and '*' are escaped before the second pass.

Sometimes that doesn't work out well and you don't want to escape those characters.
The option ``:pass_unmodified:`` accepts one or more JSON pointers and prevents the strings
pointed at to be escaped.

.. code-block:: rst
.. jsonschema::
:pass_unmodified: /examples/0
{
"examples": [
"unescaped under_score",
"escaped under_score"
]
}
9 changes: 8 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ Indices and tables
Changelog
=========

Version 1.18.0
--------------

Expanding on the work of `Pavel Odvody <https://github.com/shaded-enmity>`_ with JSON Pointer
the ``:pass_unmodified:`` option is included.
This option prevents escaping the string pointed at.

Version 1.17.2
--------------

Expand All @@ -43,7 +50,7 @@ Version 1.17.0
--------------

`Pavel Odvody <https://github.com/shaded-enmity>`_ contributed the ``:hide_key:`` directive option.
This option allows you to hide certain keys, specified by a JSON Path specification, to be excluded
This option allows you to hide certain keys, specified by a JSON Pointer specification, to be excluded
from rendering.

Version 1.16.11
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name='sphinx-jsonschema',
version='1.17.2', # don't forget: must match __init__.py::setup() return value
version='1.18.0', # don't forget: must match __init__.py::setup() return value

description='Sphinx extension to display JSON Schema',
long_description=long_description,
Expand Down
89 changes: 58 additions & 31 deletions sphinx-jsonschema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from docutils.parsers.rst import directives
from docutils.utils import SystemMessagePropagation
from docutils.utils.error_reporting import SafeString
from .wide_format import WideFormat
from .wide_format import WideFormat, NOESC


def pairwise(seq):
Expand All @@ -44,37 +44,45 @@ def maybe_int(val):

def json_path_validate(path):
""" Check that json path doesn't contain consecutive or trailing wildcards """
if path[-1] in ('*', '**'):
return False

forbidden = {
('**', '**'),
('**', '*'),
('*', '**'),
('*', '*')
}

return set(pairwise(path)) & forbidden != set()
return set(pairwise(path)) & forbidden == set()


def _json_fan_out(doc, path, transform, deep=False):
""" Process */** wildcards in the path by fanning out the processing to multiple keys """
if not isinstance(doc, dict) or not path:
if not path:
if isinstance(doc, list):
for r in range(len(doc)):
transform(doc, r)
return

targets = []

for (k,v) in doc.items():
if k == path[0]:
targets.append(k)
def deeper(v):
if deep:
_json_fan_out(v, path, transform, deep)
else:
if isinstance(v, dict):
if deep:
_json_fan_out(v, path, transform, deep=True)
else:
sub_path = path[1:]
if sub_path:
_json_bind(v, sub_path, transform)
sub_path = path[1:]
if len(sub_path) :
_json_bind(v, sub_path, transform)

def iterate(iterator):
for k, v in iterator:
if k == path[0]:
targets.append(k)
else:
deeper(v)

if isinstance(doc, dict):
iterate(doc.items())
if isinstance(doc, list):
iterate(enumerate(doc))

# Since transformers can mutate the original document
# we need to process them once we're done iterating
Expand Down Expand Up @@ -114,22 +122,33 @@ def json_path_transform(document, path, transformer):
# trying to index into a list with a string '0'
parts = [maybe_int(p) for p in path.split('/')[1:]]

if not parts or not json_path_validate(path):
if not parts or not json_path_validate(parts):
raise ValueError('Supplied JSON path is invalid')

_json_bind(document, parts, transformer)


def remove(doc, key):
if key in ('*', '**'):
raise ValueError('Supplied JSON path is invalid')
del doc[key]


def remove_empty(doc, key):
if key in ('*', '**'):
raise ValueError('Supplied JSON path is invalid')
if not doc[key]:
del doc[key]


def hide_key(item):
def tag_noescape(doc, key):
if isinstance(doc[key], str):
doc[key] = NOESC + doc[key]
else:
raise ValueError('"%s" does not refer to a string' % key)


def jsonpath_list(item):
if item:
return list(csv.reader([item])).pop()
raise ValueError('Invalid JSON path: "%s"' % item)
Expand All @@ -144,7 +163,9 @@ def flag(argument):
return True
if value in ['off', 'false']:
return False
raise ValueError('"%s" unknown, choose from "On", "True", "Off" or "False"' % argument)
raise ValueError(
'"%s" unknown, choose from "On", "True", "Off" or "False"' % argument)


class JsonSchema(Directive):
optional_arguments = 1
Expand All @@ -156,8 +177,9 @@ class JsonSchema(Directive):
'auto_target': flag,
'timeout': float,
'encoding': directives.encoding,
'hide_key': hide_key,
'hide_key_if_empty': hide_key}
'hide_key': jsonpath_list,
'hide_key_if_empty': jsonpath_list,
'pass_unmodified': jsonpath_list}

def run(self):
try:
Expand All @@ -169,8 +191,12 @@ def run(self):
if self.options.get('hide_key_if_empty'):
for hide_path in self.options['hide_key_if_empty']:
json_path_transform(schema, hide_path, remove_empty)
if self.options.get('pass_unmodified'):
for path in self.options['pass_unmodified']:
json_path_transform(schema, path, tag_noescape)

format = WideFormat(self.state, self.lineno, source, self.options, self.state.document.settings.env.app)
format = WideFormat(self.state, self.lineno, source,
self.options, self.state.document.settings.env.app)
return format.run(schema, pointer)
except SystemMessagePropagation as detail:
return [detail.args[0]]
Expand All @@ -179,10 +205,11 @@ def run(self):
except Exception as error:
tb = error.__traceback__
# loop through all traceback points to only return the last traceback
while tb.tb_next:
while tb and tb.tb_next:
tb = tb.tb_next

raise self.error(''.join(format_exception(type(error), error, tb, chain=False)))
raise self.error(''.join(format_exception(
type(error), error, tb, chain=False)))

def get_json_data(self):
"""
Expand All @@ -209,9 +236,9 @@ def get_json_data(self):
schema = self.ordered_load(schema)
except Exception as error:
error = self.state_machine.reporter.error(
'"%s" directive encountered a the following error while parsing the data.\n %s'
% (self.name, SafeString("".join(format_exception_only(type(error), error)))),
nodes.literal_block(schema, schema), line=self.lineno)
'"%s" directive encountered a the following error while parsing the data.\n %s'
% (self.name, SafeString("".join(format_exception_only(type(error), error)))),
nodes.literal_block(schema, schema), line=self.lineno)
raise SystemMessagePropagation(error)

if pointer:
Expand Down Expand Up @@ -255,14 +282,14 @@ def from_url(self, url):
response = requests.get(url, timeout=timeout)
except requests.exceptions.RequestException as e:
raise self.error(u'"%s" directive recieved an "%s" when loading from url: %s.'
% (self.name, type(e), url))
% (self.name, type(e), url))

if response.status_code != 200:
# When making a connection to the url a status code will be returned
# Normally a OK (200) response would we be returned all other responses
# an error will be raised could be separated futher
raise self.error(u'"%s" directive received an "%s" when loading from url: %s.'
% (self.name, response.reason, url))
% (self.name, response.reason, url))

# response content always binary converting with decode() no specific format defined
data = response.content.decode()
Expand All @@ -281,7 +308,7 @@ def from_file(self, filename):
data = file.read()
except IOError as error:
raise self.error(u'"%s" directive encountered an IOError while loading file: %s\n%s'
% (self.name, source, error))
% (self.name, source, error))

# Simplifing source path and to the document a new dependency
source = utils.relative_path(document_source, source)
Expand Down Expand Up @@ -326,5 +353,5 @@ def setup(app):
app.add_config_value('jsonschema_options', {}, 'env')
return {
'parallel_read_safe': True,
'version': '1.17.2'
'version': '1.18.0'
}
3 changes: 3 additions & 0 deletions sphinx-jsonschema/wide_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
else:
str_unicode = str

NOESC = ':noesc:' # prefix marker to indicate string must not be escaped.

class WideFormat(object):
KV_SIMPLE = [
Expand Down Expand Up @@ -631,6 +632,8 @@ def _get_filename(self, path, include_pointer=False):
return Path(path).name

def _escape(self, text):
if text.startswith(NOESC):
return text[len(NOESC):]
text = text.replace('\\', '\\\\')
text = text.replace('_', '\\_')
text = text.replace('*', '\\*')
Expand Down

0 comments on commit bf3a295

Please sign in to comment.