Skip to content

Commit 82c038b

Browse files
authored
Merge pull request #52 from gazpachoking/recursive_madness
Allow self referential references
2 parents 9faf61e + 9284656 commit 82c038b

File tree

6 files changed

+58
-37
lines changed

6 files changed

+58
-37
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
- "3.8"
1616
- "3.9"
1717
- "3.10"
18+
- "3.11"
1819

1920
steps:
2021
- uses: actions/checkout@v3
@@ -35,4 +36,4 @@ jobs:
3536
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3637
- name: Test with pytest
3738
run: |
38-
pytest tests.py
39+
pytest tests.py

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

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

1212
This library lets you use a data structure with JSON reference objects,
1313
as if the references had been replaced with the referent data.

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jsonref
88

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

1313
.. testcode::
1414

jsonref.py

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from proxytypes import LazyProxy
1919

20-
__version__ = "1.0.1"
20+
__version__ = "1.1.0"
2121

2222

2323
class JsonRefError(Exception):
@@ -124,22 +124,20 @@ def callback(self):
124124
uri, fragment = urlparse.urldefrag(self.full_uri)
125125

126126
# If we already looked this up, return a reference to the same object
127-
if uri in self.store:
128-
result = self.resolve_pointer(self.store[uri], fragment)
129-
else:
127+
if uri not in self.store:
130128
# Remote ref
131129
try:
132130
base_doc = self.loader(uri)
133131
except Exception as e:
134132
raise self._error(
135133
"%s: %s" % (e.__class__.__name__, str(e)), cause=e
136134
) from e
137-
138-
kwargs = self._ref_kwargs
139-
kwargs["base_uri"] = uri
140-
kwargs["recursing"] = False
141-
base_doc = _replace_refs(base_doc, **kwargs)
142-
result = self.resolve_pointer(base_doc, fragment)
135+
base_doc = _replace_refs(
136+
base_doc, **{**self._ref_kwargs, "base_uri": uri, "recursing": False}
137+
)
138+
else:
139+
base_doc = self.store[uri]
140+
result = self.resolve_pointer(base_doc, fragment)
143141
if result is self:
144142
raise self._error("Reference refers directly to itself.")
145143
if hasattr(result, "__subject__"):
@@ -174,6 +172,9 @@ def resolve_pointer(self, document, pointer):
174172
part = int(part)
175173
except ValueError:
176174
pass
175+
# If a reference points inside itself, it must mean inside reference object, not the referent data
176+
if document is self:
177+
document = self.__reference__
177178
try:
178179
document = document[part]
179180
except (TypeError, LookupError) as e:
@@ -362,25 +363,7 @@ def _replace_refs(
362363
base_uri = urlparse.urljoin(base_uri, id_)
363364
store_uri = base_uri
364365

365-
try:
366-
if not isinstance(obj["$ref"], str):
367-
raise TypeError
368-
except (TypeError, LookupError):
369-
pass
370-
else:
371-
return JsonRef(
372-
obj,
373-
base_uri=base_uri,
374-
loader=loader,
375-
jsonschema=jsonschema,
376-
load_on_repr=load_on_repr,
377-
merge_props=merge_props,
378-
_path=path,
379-
_store=store,
380-
)
381-
382-
# If our obj was not a json reference object, iterate through it,
383-
# replacing children with JsonRefs
366+
# First recursively iterate through our object, replacing children with JsonRefs
384367
if isinstance(obj, Mapping):
385368
obj = {
386369
k: _replace_refs(
@@ -411,8 +394,24 @@ def _replace_refs(
411394
)
412395
for i, v in enumerate(obj)
413396
]
397+
398+
# If this object itself was a reference, replace it with a JsonRef
399+
if isinstance(obj, Mapping) and isinstance(obj.get("$ref"), str):
400+
obj = JsonRef(
401+
obj,
402+
base_uri=base_uri,
403+
loader=loader,
404+
jsonschema=jsonschema,
405+
load_on_repr=load_on_repr,
406+
merge_props=merge_props,
407+
_path=path,
408+
_store=store,
409+
)
410+
411+
# Store the document with all references replaced in our cache
414412
if store_uri is not None:
415413
store[store_uri] = obj
414+
416415
return obj
417416

418417

@@ -432,7 +431,7 @@ def load(
432431
proxied to their referent data.
433432
434433
:param fp: File-like object containing JSON document
435-
:param kwargs: This function takes any of the keyword arguments from
434+
:param **kwargs: This function takes any of the keyword arguments from
436435
:func:`replace_refs`. Any other keyword arguments will be passed to
437436
:func:`json.load`
438437
@@ -469,7 +468,7 @@ def loads(
469468
proxied to their referent data.
470469
471470
:param s: String containing JSON document
472-
:param kwargs: This function takes any of the keyword arguments from
471+
:param **kwargs: This function takes any of the keyword arguments from
473472
:func:`replace_refs`. Any other keyword arguments will be passed to
474473
:func:`json.loads`
475474
@@ -505,7 +504,7 @@ def load_uri(
505504
data.
506505
507506
:param uri: URI to fetch the JSON from
508-
:param kwargs: This function takes any of the keyword arguments from
507+
:param **kwargs: This function takes any of the keyword arguments from
509508
:func:`replace_refs`
510509
511510
"""

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = [
77
license = {text = "MIT"}
88
readme = "README.md"
99
dynamic = ["version"]
10-
requires-python = ">=3.3"
10+
requires-python = ">=3.7"
1111
dependencies = []
1212

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

2929
[tool.isort]
3030
profile = "black"
31-

tests.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ def test_extra_ref_attributes(self, parametrized_replace_refs):
113113
}
114114
}
115115

116+
def test_refs_inside_extra_props(self, parametrized_replace_refs):
117+
"""This seems really dubious per the spec... but OpenAPI 3.1 spec does it."""
118+
docs = {
119+
"a.json": {
120+
"file": "a",
121+
"b": {"$ref": "b.json#/ba", "extra": {"$ref": "b.json#/bb"}},
122+
},
123+
"b.json": {"ba": {"a": 1}, "bb": {"b": 2}},
124+
}
125+
result = parametrized_replace_refs(docs["a.json"], loader=docs.get, merge_props=True)
126+
assert result == {"file": "a", "b": {"a": 1, "extra": {"b": 2}}}
127+
116128
def test_recursive_extra(self, parametrized_replace_refs):
117129
json = {"a": {"$ref": "#", "extra": "foo"}}
118130
result = parametrized_replace_refs(json, merge_props=True)
@@ -234,6 +246,16 @@ def test_recursive_data_structures_remote_fragment(self):
234246
result = replace_refs(json1, base_uri="/json1", loader=loader)
235247
assert result["a"].__subject__ is result
236248

249+
def test_self_referent_reference(self, parametrized_replace_refs):
250+
json = {"$ref": "#/sub", "sub": [1, 2]}
251+
result = parametrized_replace_refs(json)
252+
assert result == json["sub"]
253+
254+
def test_self_referent_reference_w_merge(self, parametrized_replace_refs):
255+
json = {"$ref": "#/sub", "extra": "aoeu", "sub": {"main": "aoeu"}}
256+
result = parametrized_replace_refs(json, merge_props=True)
257+
assert result == {"main": "aoeu", "extra": "aoeu", "sub": {"main": "aoeu"}}
258+
237259
def test_custom_loader(self):
238260
data = {"$ref": "foo"}
239261
loader = mock.Mock(return_value=42)

0 commit comments

Comments
 (0)