Skip to content

Commit

Permalink
feature: Add support for the const keyword.
Browse files Browse the repository at this point in the history
Fixes #229
  • Loading branch information
cwacek committed Mar 5, 2024
1 parent fd28c9c commit dd1a825
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 5 deletions.
30 changes: 27 additions & 3 deletions python_jsonschema_objects/classbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import sys

import jsonschema.exceptions
import referencing._core
import six

Expand Down Expand Up @@ -184,11 +185,23 @@ def __init__(self, **props):
# but only for the ones that have defaults set.
for name in self.__has_default__:
if name not in props:
default_value = copy.deepcopy(self.__propinfo__[name]["default"])
# "defaults" could come from either the 'default' keyword or the 'const' keyword
try:
default_value = self.__propinfo__[name]["default"]
except KeyError:
try:
default_value = self.__propinfo__[name]["const"]
except KeyError:
raise jsonschema.exceptions.SchemaError(
"Schema parsing error. Expected {0} to have default or const value".format(
name
)
)

logger.debug(
util.lazy_format("Initializing '{0}' to '{1}'", name, default_value)
)
setattr(self, name, default_value)
setattr(self, name, copy.deepcopy(default_value))

for prop in props:
try:
Expand Down Expand Up @@ -626,7 +639,7 @@ def _build_literal(self, nm, clsdata):
"__propinfo__": {
"__literal__": clsdata,
"__title__": clsdata.get("title"),
"__default__": clsdata.get("default"),
"__default__": clsdata.get("default") or clsdata.get("const"),
}
},
)
Expand Down Expand Up @@ -670,6 +683,17 @@ def _build_object(self, nm, clsdata, parents, **kw):
)
defaults.add(prop)

if "const" in detail:
logger.debug(
util.lazy_format(
"Setting const for {0}.{1} to: {2}",
nm,
prop,
detail["const"],
)
)
defaults.add(prop)

if detail.get("type", None) == "object":
uri = "{0}/{1}_{2}".format(nm, prop, "<anonymous>")
self.resolved[uri] = self.construct(uri, detail, (ProtocolBase,), **kw)
Expand Down
8 changes: 8 additions & 0 deletions python_jsonschema_objects/literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def __init__(self, value, typ=None):

self.validate()

constval = self.const()
if constval is not None:
self._value = constval

def as_dict(self):
return self.for_json()

Expand All @@ -54,6 +58,10 @@ def for_json(self):
def default(cls):
return cls.__propinfo__.get("__default__")

@classmethod
def const(cls):
return cls.__propinfo__.get("__literal__", {}).get("const", None)

@classmethod
def propinfo(cls, propname):
if propname not in cls.__propinfo__:
Expand Down
6 changes: 6 additions & 0 deletions python_jsonschema_objects/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ def enum(param, value, _):
raise ValidationError("{0} is not one of {1}".format(value, param))


@registry.register()
def const(param, value, _):
if value != param:
raise ValidationError("{0} is not constant {1}".format(value, param))


@registry.register()
def minimum(param, value, type_data):
exclusive = type_data.get("exclusiveMinimum")
Expand Down
51 changes: 51 additions & 0 deletions test/test_229.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest

import python_jsonschema_objects as pjo


def test_const_properties():
schema = {
"title": "Example",
"type": "object",
"properties": {
"url": {
"type": "string",
"default": "https://example.com/your-username/my-project",
},
"type": {"type": "string", "const": "git"},
},
}

ob = pjo.ObjectBuilder(schema)
ns1 = ob.build_classes()
ex = ns1.Example()
ex.url = "can be anything"

# we expect the value to be set already for const values
assert ex.type == "git"
with pytest.raises(pjo.ValidationError):
# Trying to set the value to something else should throw validation errors
ex.type = "mercurial"

# setting the value to the const value is a no-op, but permitted
ex.type = "git"


def test_const_bare_type():
schema = {
"title": "Example",
"type": "string",
"const": "I stand alone",
}

ob = pjo.ObjectBuilder(schema)
ns1 = ob.build_classes()
ex = ns1.Example("I stand alone")
# we expect the value to be set already for const values
assert ex == "I stand alone"
with pytest.raises(pjo.ValidationError):
# Trying to set the value to something else should throw validation errors
ex = ns1.Example("mercurial")

# setting the value to the const value is a no-op, but permitted
ex = ns1.Example("I stand alone")
6 changes: 4 additions & 2 deletions test/test_default_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ def test_nullable_types_are_still_nullable(ns):

thing1.p1 = None
thing1.validate()
assert thing1.as_dict() == {"p1": 0, "p2": None}
assert thing1.as_dict() == {"p1": None, "p2": None}


def test_null_types_without_defaults_do_not_serialize(ns):
thing1 = ns.DefaultTest()

assert thing1.as_dict() == {"p1": 0, "p2": None}

thing1.p3 = 10
thing1.validate()
thing1.p1 = None
thing1.validate()

assert thing1.as_dict() == {"p1": 0, "p2": None, "p3": 10}
assert thing1.as_dict() == {"p1": None, "p2": None, "p3": 10}

0 comments on commit dd1a825

Please sign in to comment.