Skip to content

Commit

Permalink
Merge pull request #301 from cwacek/fix/297
Browse files Browse the repository at this point in the history
Attempt to resolve #297
  • Loading branch information
cwacek authored Nov 13, 2024
2 parents 3fdb280 + f2bca60 commit 77a1086
Show file tree
Hide file tree
Showing 4 changed files with 3,368 additions and 26 deletions.
8 changes: 4 additions & 4 deletions python_jsonschema_objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,14 @@ def build_classes(
A namespace containing all the generated classes
"""
kw = {"strict": strict, "any_of": any_of}
builder = classbuilder.ClassBuilder(self.resolver)
opts = {"strict": strict, "any_of": any_of}
builder = classbuilder.ClassBuilder(self.resolver, opts)
for nm, defn in self.schema.get("definitions", {}).items():
resolved = self.resolver.lookup("#/definitions/" + nm)
uri = python_jsonschema_objects.util.resolve_ref_uri(
self.resolver._base_uri, "#/definitions/" + nm
)
builder.construct(uri, resolved.contents, **kw)
builder.construct(uri, resolved.contents)

if standardize_names:
name_transform = lambda t: inflection.camelize(
Expand All @@ -236,7 +236,7 @@ def build_classes(
nm = self.schema["title"] if "title" in self.schema else self.schema["$id"]
nm = inflection.parameterize(str(nm), "_")

builder.construct(nm, self.schema, **kw)
builder.construct(nm, self.schema)
self._resolved = builder.resolved

classes = {}
Expand Down
56 changes: 34 additions & 22 deletions python_jsonschema_objects/classbuilder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import collections.abc
from urllib.parse import urldefrag, urljoin
import typing
import copy
import itertools
import logging
Expand Down Expand Up @@ -452,11 +452,19 @@ def __call__(self, *a, **kw):
)


class ClassBuilderOptions(typing.TypedDict):
strict: bool
any_of: str


class ClassBuilder(object):
def __init__(self, resolver: referencing._core.Resolver):
def __init__(
self, resolver: referencing._core.Resolver, options: ClassBuilderOptions
):
self.resolver = resolver
self.resolved = {}
self.under_construction = set()
self.options = options

def expand_references(self, source_uri, iterable):
"""Give an iterable of jsonschema descriptors, expands any
Expand Down Expand Up @@ -494,7 +502,7 @@ def resolve_type(self, ref, source):
)
resolved = self.resolver.lookup(uri)
if resolved.resolver != self.resolver:
sub_cb = ClassBuilder(resolved.resolver)
sub_cb = ClassBuilder(resolved.resolver, self.options)
self.resolved[uri] = sub_cb.construct(
uri, resolved.contents, (ProtocolBase,)
)
Expand All @@ -505,34 +513,38 @@ def resolve_type(self, ref, source):

return self.resolved[uri]

def construct(self, uri, *args, **kw):
def construct(
self, uri: str, clsdata: typing.Mapping[str, any], parent=(ProtocolBase,)
):
"""Wrapper to debug things"""
logger.debug(util.lazy_format("Constructing {0}", uri))
if ("override" not in kw or kw["override"] is False) and uri in self.resolved:
if uri in self.resolved:
logger.debug(util.lazy_format("Using existing {0}", uri))
assert self.resolved[uri] is not None
return self.resolved[uri]
else:
ret = self._construct(uri, *args, **kw)
ret = self._construct(uri, clsdata, parent=parent)
logger.debug(util.lazy_format("Constructed {0}", ret))

return ret

def _construct(self, uri, clsdata, parent=(ProtocolBase,), **kw):
def _construct(
self, uri: str, clsdata: typing.Mapping[str, any], parent=(ProtocolBase,)
):
if "anyOf" in clsdata:
if kw.get("any_of", None) is None:
if self.options.get("any_of", None) is None:
raise NotImplementedError(
"anyOf is not supported as bare property (workarounds available by setting any_of flag)"
)
if kw["any_of"] == "use-first":
if self.options["any_of"] == "use-first":
# Patch so the first anyOf becomes a single oneOf
clsdata["oneOf"] = [
clsdata["anyOf"].pop(0),
]
del clsdata["anyOf"]
else:
raise NotImplementedError(
f"anyOf workaround is not a recognized type (any_of = {kw['any_of']})"
f"anyOf workaround is not a recognized type (any_of = {self.options['any_of']})"
)

if "oneOf" in clsdata:
Expand All @@ -557,7 +569,7 @@ def _construct(self, uri, clsdata, parent=(ProtocolBase,), **kw):
elif util.safe_issubclass(p, ProtocolBase):
parents.append(p)

self.resolved[uri] = self._build_object(uri, clsdata, parents, **kw)
self.resolved[uri] = self._build_object(uri, clsdata, parents)
return self.resolved[uri]

elif "$ref" in clsdata:
Expand Down Expand Up @@ -611,7 +623,7 @@ def _construct(self, uri, clsdata, parent=(ProtocolBase,), **kw):
or clsdata.get("properties", None) is not None
or clsdata.get("additionalProperties", False)
):
self.resolved[uri] = self._build_object(uri, clsdata, parent, **kw)
self.resolved[uri] = self._build_object(uri, clsdata, parent)
return self.resolved[uri]
elif clsdata.get("type") in ("integer", "number", "string", "boolean", "null"):
self.resolved[uri] = self._build_literal(uri, clsdata)
Expand Down Expand Up @@ -654,7 +666,7 @@ def _build_literal(self, nm, clsdata):
"default",
"asldkfn24olkjalskdfn e;laishd;1loj;flkansd;iow;naosdinfe;lkamjsdfj",
)
is not "asldkfn24olkjalskdfn e;laishd;1loj;flkansd;iow;naosdinfe;lkamjsdfj"
!= "asldkfn24olkjalskdfn e;laishd;1loj;flkansd;iow;naosdinfe;lkamjsdfj"
else clsdata.get("const")
),
}
Expand All @@ -663,7 +675,7 @@ def _build_literal(self, nm, clsdata):

return cls

def _build_object(self, nm, clsdata, parents, **kw):
def _build_object(self, nm, clsdata, parents):
logger.debug(util.lazy_format("Building object {0}", nm))

# To support circular references, we tag objects that we're
Expand Down Expand Up @@ -713,7 +725,7 @@ def _build_object(self, nm, clsdata, parents, **kw):

if detail.get("type", None) == "object":
uri = "{0}/{1}_{2}".format(nm, prop, "<anonymous>")
self.resolved[uri] = self.construct(uri, detail, (ProtocolBase,), **kw)
self.resolved[uri] = self.construct(uri, detail, (ProtocolBase,))

props[prop] = make_property(
prop, {"type": self.resolved[uri]}, self.resolved[uri].__doc__
Expand Down Expand Up @@ -741,7 +753,7 @@ def _build_object(self, nm, clsdata, parents, **kw):
if "$ref" in detail["items"]:
typ = self.resolve_type(detail["items"]["$ref"], nm)
constraints = copy.copy(detail)
constraints["strict"] = kw.get("strict")
constraints["strict"] = self.options.get("strict")
propdata = {
"type": "array",
"validator": wrapper_types.ArrayWrapper.create(
Expand All @@ -760,10 +772,10 @@ def _build_object(self, nm, clsdata, parents, **kw):
)
)
else:
typ = self.construct(uri, detail["items"], **kw)
typ = self.construct(uri, detail["items"])

constraints = copy.copy(detail)
constraints["strict"] = kw.get("strict")
constraints["strict"] = self.options.get("strict")
propdata = {
"type": "array",
"validator": wrapper_types.ArrayWrapper.create(
Expand All @@ -774,7 +786,7 @@ def _build_object(self, nm, clsdata, parents, **kw):
except NotImplementedError:
typ = detail["items"]
constraints = copy.copy(detail)
constraints["strict"] = kw.get("strict")
constraints["strict"] = self.options.get("strict")
propdata = {
"type": "array",
"validator": wrapper_types.ArrayWrapper.create(
Expand All @@ -787,15 +799,15 @@ def _build_object(self, nm, clsdata, parents, **kw):
typs = []
for i, elem in enumerate(detail["items"]):
uri = "{0}/{1}/<anonymous_{2}>".format(nm, prop, i)
typ = self.construct(uri, elem, **kw)
typ = self.construct(uri, elem)
typs.append(typ)

props[prop] = make_property(prop, {"type": typs})

else:
desc = detail["description"] if "description" in detail else ""
uri = "{0}/{1}".format(nm, prop)
typ = self.construct(uri, detail, **kw)
typ = self.construct(uri, detail)

props[prop] = make_property(prop, {"type": typ}, desc)

Expand All @@ -821,7 +833,7 @@ def _build_object(self, nm, clsdata, parents, **kw):

props["__required__"] = required
props["__has_default__"] = defaults
if required and kw.get("strict"):
if required and self.options.get("strict"):
props["__strict__"] = True

props["__title__"] = clsdata.get("title")
Expand Down
Loading

0 comments on commit 77a1086

Please sign in to comment.