Repository: cwacek/python-jsonschema-objects

Issue:
Chained references are not resolved correctly
**Describe the bug**
I only recently tried to switch to version 0.5.5 (from 0.4.6) and noticed that apparently relative `$ref`s in a referenced document are not resolved correctly anymore with the new `referencing` lib.

So, when I reference a definition *d1* within a a schema *schema-a* from a *schema-b*, and *d1* references another definition *d2* within the very same *schema-a* using a local JSON pointer, the resolution of *d2* fails as it searches in *schema-b*. With version 0.4.6 this exact use case worked using the legacy `RefResolver`.

**Example Schema and code**
```
import python_jsonschema_objects as pjs
import referencing

SCHEMA_A = {
        '$id': 'schema-a',
        '$schema': 'http://json-schema.org/draft-04/schema#',
        'title': "Schema A",
        'definitions': {
                'myint': {
                        'type': 'integer',
                        'minimum': 42
                    },
                'myintarray': {
                        'type': 'array',
                        'items': {
                                '$ref': '#/definitions/myint', # using 'schema-a#/definitions/myint' would work
                            },
                    },
                'myintref': {
                        '$ref': '#/definitions/myint', # using 'schema-a#/definitions/myint' would work
                    },
            },
        'type': 'object',
        'properties': {
                'theint': {
                        '$ref': '#/definitions/myint', # using 'schema-a#/definitions/myint' would work
                    },
            },
    }

SCHEMA_B = {
        '$id': 'schema-b',
        '$schema': 'http://json-schema.org/draft-04/schema#',
        'title': "Schema B",
        'type': 'object',
        'definitions': {
            'myintref': {
                    '$ref': 'schema-a#/definitions/myintref',
                },
        },
        'properties': {
                # all three properties cause the failure
                'obja': {
                        '$ref': 'schema-a',
                    },
                'theintarray': {
                        '$ref': 'schema-a#/definitions/myintarray',
                    },
                'theintref': {
                        '$ref': '#/definitions/myintref',
                    },
            },
    }

registry = referencing.Registry().with_resources([
        ('schema-a', referencing.Resource.from_contents(SCHEMA_A)),
        ('schema-b', referencing.Resource.from_contents(SCHEMA_B)),
    ])

# works fine
builder_a = pjs.ObjectBuilder(SCHEMA_A, registry=registry)
namespace_a = builder_a.build_classes(named_only=False)

# fails
builder_b = pjs.ObjectBuilder(SCHEMA_B, registry=registry)
namespace_b = builder_b.build_classes(named_only=False) # referencing.exceptions.PointerToNowhere: '/definitions/myint' does not exist within SCHEMA_B

b = namespace_b.SchemaB()
b.obja = {'theint': 42}
b.theintarray = [42, 43, 44]
b.theintref = 42
print(b.for_json())
```

The exception throws by `build_classes()` actually references `SCHEMA_B`, although the given pointer `/definitions/myint` is only ever used in `SCHEMA_A`.

**Expected behavior**
I would expect that local JSON pointers are resolved within the current document not within any referencing document.

**Workaround**
Make all `$ref`s absolute.

**Fix**
I believe that there needs to be a new `ClassBuilder` instance with an adapted resolver when recursing in `ClassBuilder.resolve_type()`. My quick and dirty patch seems to work and the above code prints `{'obja': {'theint': 42}, 'theintarray': [42, 43, 44], 'theintref': 42}`:

```
--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -492,7 +492,8 @@
                 )
             )
             resolved = self.resolver.lookup(ref)
-            self.resolved[uri] = self.construct(uri, resolved.contents, (ProtocolBase,))
+            sub_cb = ClassBuilder(referencing._core.Resolver(base_uri=uri.split('#')[0], registry=self.resolver._registry))
+            self.resolved[uri] = sub_cb.construct(uri, resolved.contents, (ProtocolBase,))
             return self.resolved[uri]
 
     def construct(self, uri, *args, **kw):
```

Discussion:

Patch:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 7517d34..d58172d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -24,7 +24,7 @@ jobs:
           python -m pip install -U pip wheel
           python setup.py bdist_wheel sdist
       - name: pypi-publish
-        uses: pypa/gh-action-pypi-publish@v1.12.2
+        uses: pypa/gh-action-pypi-publish@v1.9.0
         with:
           user: __token__
           password: ${{ secrets.PYPI_PJO_TOKEN }}
diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py
index bb01230..dbb8116 100644
--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -1,4 +1,5 @@
 import collections.abc
+from urllib.parse import urldefrag, urljoin
 import copy
 import itertools
 import logging
@@ -491,8 +492,17 @@ class ClassBuilder(object):
                     "Resolving direct reference object {0} -> {1}", source, uri
                 )
             )
-            resolved = self.resolver.lookup(ref)
-            self.resolved[uri] = self.construct(uri, resolved.contents, (ProtocolBase,))
+            resolved = self.resolver.lookup(uri)
+            if resolved.resolver != self.resolver:
+                sub_cb = ClassBuilder(resolved.resolver)
+                self.resolved[uri] = sub_cb.construct(
+                    uri, resolved.contents, (ProtocolBase,)
+                )
+            else:
+                self.resolved[uri] = self.construct(
+                    uri, resolved.contents, (ProtocolBase,)
+                )
+
             return self.resolved[uri]
 
     def construct(self, uri, *args, **kw):


Test patch:
diff --git a/conftest.py b/conftest.py
index a8ea1c6..492427f 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,4 +1,5 @@
 import importlib.resources
+import importlib.resources
 import json
 
 import pytest
@@ -8,9 +9,16 @@ import python_jsonschema_objects as pjs
 
 @pytest.fixture
 def markdown_examples():
-    with importlib.resources.path(
-        "python_jsonschema_objects.examples", "README.md"
-    ) as md:
+    if hasattr(importlib.resources, "as_file"):
+        filehandle = importlib.resources.as_file(
+            importlib.resources.files("python_jsonschema_objects.examples")
+            / "README.md"
+        )
+    else:
+        filehandle = importlib.resources.path(
+            "python_jsonschema_objects.examples", "README.md"
+        )
+    with filehandle as md:
         examples = pjs.markdown_support.extract_code_blocks(md)
 
     return {json.loads(v)["title"]: json.loads(v) for v in examples["schema"]}
diff --git a/test/test_292.py b/test/test_292.py
new file mode 100644
index 0000000..7ec0766
--- /dev/null
+++ b/test/test_292.py
@@ -0,0 +1,158 @@
+import logging
+
+import pytest
+
+import python_jsonschema_objects as pjs
+import referencing
+
+SCHEMA_A = {
+    "$id": "schema-a",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "Schema A",
+    "definitions": {
+        "myint": {"type": "integer", "minimum": 42},
+        "myintarray": {
+            "type": "array",
+            "items": {
+                "$ref": "#/definitions/myint",  # using 'schema-a#/definitions/myint' would work
+            },
+        },
+        "myintref": {
+            "$ref": "#/definitions/myint",  # using 'schema-a#/definitions/myint' would work
+        },
+    },
+    "type": "object",
+    "properties": {
+        "theint": {
+            "$ref": "#/definitions/myint",  # using 'schema-a#/definitions/myint' would work
+        },
+    },
+}
+
+
+def test_referenced_schema_works_indirectly():
+    registry = referencing.Registry().with_resources(
+        [
+            ("schema-a", referencing.Resource.from_contents(SCHEMA_A)),
+        ]
+    )
+
+    # works fine
+    builder_a = pjs.ObjectBuilder(SCHEMA_A, registry=registry)
+    namespace_a = builder_a.build_classes(named_only=False)
+
+    b = namespace_a.SchemaA()
+    b.obja = {"theint": 42}
+    b.theintarray = [42, 43, 44]
+    b.theintref = 42
+    print(b.for_json())
+
+
+@pytest.mark.parametrize(
+    "ref",
+    [
+        "schema-a#/definitions/myint",
+        "schema-a#/definitions/myintref",  # This is an interesting variation, because this ref is itself a ref.
+    ],
+)
+def test_referenced_schema_works_directly(ref):
+    registry = referencing.Registry().with_resources(
+        [
+            ("schema-a", referencing.Resource.from_contents(SCHEMA_A)),
+        ]
+    )
+
+    # WE make a dumb schema that references this
+    schema = {
+        "$schema": "http://json-schema.org/draft-04/schema#",
+        "$id": "test",
+        "title": "Test",
+        "type": "object",
+        "properties": {"name": {"$ref": ref}},
+    }
+
+    # works fine
+    builder_a = pjs.ObjectBuilder(schema, registry=registry)
+    namespace_a = builder_a.build_classes(named_only=False)
+
+    b = namespace_a.Test()
+    b.name = 42
+    print(b.for_json())
+
+
+def test_you_do_actually_need_a_reference():
+    logging.basicConfig(level=logging.DEBUG)
+
+    registry = referencing.Registry().with_resources(
+        [
+            ("schema-a", referencing.Resource.from_contents(SCHEMA_A)),
+        ]
+    )
+
+    # WE make a dumb schema that references
+    schema = {
+        "$schema": "http://json-schema.org/draft-04/schema#",
+        "$id": "test",
+        "title": "Test",
+        "type": "object",
+        "properties": {
+            "name": {
+                "$ref": "#/definitions/myint",
+            }
+        },
+    }
+
+    # works fine
+    builder_a = pjs.ObjectBuilder(schema, registry=registry)
+    with pytest.raises(Exception):
+        # WE would expect this to fail because this isn't actually
+        # a good reference. Our schema doesn't have a definitions block,
+        # so schema-a should be a required URI
+        builder_a.build_classes(named_only=False)
+
+
+def test_regression_292():
+    SCHEMA_B = {
+        "$id": "schema-b",
+        "$schema": "http://json-schema.org/draft-04/schema#",
+        "title": "Schema B",
+        "type": "object",
+        "definitions": {
+            "myintref": {
+                "$ref": "schema-a#/definitions/myint",
+            },
+        },
+        "properties": {
+            # all three properties cause the failure
+            "obja": {
+                "$ref": "schema-a",
+            },
+            "theintarray": {
+                "$ref": "schema-a#/definitions/myintarray",
+            },
+            "thedirectintref": {
+                "$ref": "schema-a#/definitions/myint",
+            },
+            "theintref": {
+                "$ref": "#/definitions/myintref",
+            },
+        },
+    }
+
+    registry = referencing.Registry().with_resources(
+        [
+            ("schema-a", referencing.Resource.from_contents(SCHEMA_A)),
+        ]
+    )
+
+    # fails
+    builder_b = pjs.ObjectBuilder(SCHEMA_B, registry=registry)
+    namespace_b = builder_b.build_classes(
+        named_only=False
+    )  # referencing.exceptions.PointerToNowhere: '/definitions/myint' does not exist within SCHEMA_B
+
+    b = namespace_b.SchemaB()
+    b.obja = {"theint": 42}
+    b.theintarray = [42, 43, 44]
+    b.theintref = 42
+    print(b.for_json())