Skip to content

Commit

Permalink
Merge pull request #52 from gazpachoking/recursive_madness
Browse files Browse the repository at this point in the history
Allow self referential references
  • Loading branch information
gazpachoking authored Jan 16, 2023
2 parents 9faf61e + 9284656 commit 82c038b
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 37 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11"

steps:
- uses: actions/checkout@v3
Expand All @@ -35,4 +36,4 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest tests.py
pytest tests.py
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

`jsonref` is a library for automatic dereferencing of [JSON
Reference](https://datatracker.ietf.org/doc/html/draft-pbryan-zyp-json-ref-03)
objects for Python (supporting Python 3.3+).
objects for Python (supporting Python 3.7+).

This library lets you use a data structure with JSON reference objects,
as if the references had been replaced with the referent data.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jsonref

``jsonref`` is a library for automatic dereferencing of
`JSON Reference <https://tools.ietf.org/id/draft-pbryan-zyp-json-ref-03.html>`_
objects for Python (supporting Python 3.3+).
objects for Python (supporting Python 3.7+).

.. testcode::

Expand Down
63 changes: 31 additions & 32 deletions jsonref.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from proxytypes import LazyProxy

__version__ = "1.0.1"
__version__ = "1.1.0"


class JsonRefError(Exception):
Expand Down Expand Up @@ -124,22 +124,20 @@ def callback(self):
uri, fragment = urlparse.urldefrag(self.full_uri)

# If we already looked this up, return a reference to the same object
if uri in self.store:
result = self.resolve_pointer(self.store[uri], fragment)
else:
if uri not in self.store:
# Remote ref
try:
base_doc = self.loader(uri)
except Exception as e:
raise self._error(
"%s: %s" % (e.__class__.__name__, str(e)), cause=e
) from e

kwargs = self._ref_kwargs
kwargs["base_uri"] = uri
kwargs["recursing"] = False
base_doc = _replace_refs(base_doc, **kwargs)
result = self.resolve_pointer(base_doc, fragment)
base_doc = _replace_refs(
base_doc, **{**self._ref_kwargs, "base_uri": uri, "recursing": False}
)
else:
base_doc = self.store[uri]
result = self.resolve_pointer(base_doc, fragment)
if result is self:
raise self._error("Reference refers directly to itself.")
if hasattr(result, "__subject__"):
Expand Down Expand Up @@ -174,6 +172,9 @@ def resolve_pointer(self, document, pointer):
part = int(part)
except ValueError:
pass
# If a reference points inside itself, it must mean inside reference object, not the referent data
if document is self:
document = self.__reference__
try:
document = document[part]
except (TypeError, LookupError) as e:
Expand Down Expand Up @@ -362,25 +363,7 @@ def _replace_refs(
base_uri = urlparse.urljoin(base_uri, id_)
store_uri = base_uri

try:
if not isinstance(obj["$ref"], str):
raise TypeError
except (TypeError, LookupError):
pass
else:
return JsonRef(
obj,
base_uri=base_uri,
loader=loader,
jsonschema=jsonschema,
load_on_repr=load_on_repr,
merge_props=merge_props,
_path=path,
_store=store,
)

# If our obj was not a json reference object, iterate through it,
# replacing children with JsonRefs
# First recursively iterate through our object, replacing children with JsonRefs
if isinstance(obj, Mapping):
obj = {
k: _replace_refs(
Expand Down Expand Up @@ -411,8 +394,24 @@ def _replace_refs(
)
for i, v in enumerate(obj)
]

# If this object itself was a reference, replace it with a JsonRef
if isinstance(obj, Mapping) and isinstance(obj.get("$ref"), str):
obj = JsonRef(
obj,
base_uri=base_uri,
loader=loader,
jsonschema=jsonschema,
load_on_repr=load_on_repr,
merge_props=merge_props,
_path=path,
_store=store,
)

# Store the document with all references replaced in our cache
if store_uri is not None:
store[store_uri] = obj

return obj


Expand All @@ -432,7 +431,7 @@ def load(
proxied to their referent data.
:param fp: File-like object containing JSON document
:param kwargs: This function takes any of the keyword arguments from
:param **kwargs: This function takes any of the keyword arguments from
:func:`replace_refs`. Any other keyword arguments will be passed to
:func:`json.load`
Expand Down Expand Up @@ -469,7 +468,7 @@ def loads(
proxied to their referent data.
:param s: String containing JSON document
:param kwargs: This function takes any of the keyword arguments from
:param **kwargs: This function takes any of the keyword arguments from
:func:`replace_refs`. Any other keyword arguments will be passed to
:func:`json.loads`
Expand Down Expand Up @@ -505,7 +504,7 @@ def load_uri(
data.
:param uri: URI to fetch the JSON from
:param kwargs: This function takes any of the keyword arguments from
:param **kwargs: This function takes any of the keyword arguments from
:func:`replace_refs`
"""
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = [
license = {text = "MIT"}
readme = "README.md"
dynamic = ["version"]
requires-python = ">=3.3"
requires-python = ">=3.7"
dependencies = []

[project.urls]
Expand All @@ -28,4 +28,3 @@ build-backend = "pdm.pep517.api"

[tool.isort]
profile = "black"

22 changes: 22 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ def test_extra_ref_attributes(self, parametrized_replace_refs):
}
}

def test_refs_inside_extra_props(self, parametrized_replace_refs):
"""This seems really dubious per the spec... but OpenAPI 3.1 spec does it."""
docs = {
"a.json": {
"file": "a",
"b": {"$ref": "b.json#/ba", "extra": {"$ref": "b.json#/bb"}},
},
"b.json": {"ba": {"a": 1}, "bb": {"b": 2}},
}
result = parametrized_replace_refs(docs["a.json"], loader=docs.get, merge_props=True)
assert result == {"file": "a", "b": {"a": 1, "extra": {"b": 2}}}

def test_recursive_extra(self, parametrized_replace_refs):
json = {"a": {"$ref": "#", "extra": "foo"}}
result = parametrized_replace_refs(json, merge_props=True)
Expand Down Expand Up @@ -234,6 +246,16 @@ def test_recursive_data_structures_remote_fragment(self):
result = replace_refs(json1, base_uri="/json1", loader=loader)
assert result["a"].__subject__ is result

def test_self_referent_reference(self, parametrized_replace_refs):
json = {"$ref": "#/sub", "sub": [1, 2]}
result = parametrized_replace_refs(json)
assert result == json["sub"]

def test_self_referent_reference_w_merge(self, parametrized_replace_refs):
json = {"$ref": "#/sub", "extra": "aoeu", "sub": {"main": "aoeu"}}
result = parametrized_replace_refs(json, merge_props=True)
assert result == {"main": "aoeu", "extra": "aoeu", "sub": {"main": "aoeu"}}

def test_custom_loader(self):
data = {"$ref": "foo"}
loader = mock.Mock(return_value=42)
Expand Down

0 comments on commit 82c038b

Please sign in to comment.