Skip to content

Commit

Permalink
Feature/clean up object interface (#62)
Browse files Browse the repository at this point in the history
* Move default to class args

* Simplify object python repr

* Update changelog
  • Loading branch information
Jack Smith authored Apr 5, 2020
1 parent 40cdbd7 commit 77225d0
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Types of changes are:

## [Unreleased]
### Changed
* `default` is now a class argument on subclasses of `ObjectMeta`.
* Bump `json-ref-dict` to version `0.5.3`.
* Improve implementation of official JSON Schema test suite.

Expand Down
80 changes: 38 additions & 42 deletions statham/dsl/elements/meta.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
import keyword
from typing import Any, Dict, List, Tuple, Type, Union

Expand All @@ -19,9 +20,7 @@
)


RESERVED_PROPERTIES = (
dir(object) + list(keyword.kwlist) + ["default", "properties", "_dict"]
)
RESERVED_PROPERTIES = dir(object) + list(keyword.kwlist) + ["_dict"]


class ObjectClassDict(dict):
Expand All @@ -30,19 +29,15 @@ class ObjectClassDict(dict):
Collects schema properties and default value if present.
"""

default: Any
properties: Dict[str, _Property]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.properties = {}
self.default = self.pop("default", NotPassed())

def __setitem__(self, key, value):
if key in RESERVED_PROPERTIES and isinstance(value, _Property):
raise SchemaDefinitionError.reserved_attribute(key)
if key == "default":
self.default = value
if isinstance(value, _Property):
value.bind_name(key)
return self.properties.__setitem__(key, value)
Expand All @@ -58,7 +53,7 @@ class ObjectMeta(type, Element):

properties: Dict[str, _Property]
additionalProperties: Union[Element, bool]
patternProperties: Dict[str, Element]
patternProperties: Maybe[Dict[str, Element]]
minProperties: Maybe[int]
maxProperties: Maybe[int]
propertyNames: Maybe[Element]
Expand All @@ -78,22 +73,36 @@ def __subclasses__():
def __prepare__(mcs, _name, _bases, **_kwargs):
return ObjectClassDict()

# pylint: disable=too-many-locals
def __new__(
mcs, name: str, bases: Tuple[Type], classdict: ObjectClassDict, **kwargs
mcs,
name: str,
bases: Tuple[Type],
classdict: ObjectClassDict,
*,
default: Maybe[Any] = NotPassed(),
const: Maybe[Any] = NotPassed(),
enum: Maybe[List[Any]] = NotPassed(),
minProperties: Maybe[int] = NotPassed(),
maxProperties: Maybe[int] = NotPassed(),
patternProperties: Maybe[Dict[str, Element]] = NotPassed(),
additionalProperties: Union[Element, bool] = True,
propertyNames: Maybe[Element] = NotPassed(),
dependencies: Maybe[Dict[str, Union[List[str], Element]]] = NotPassed(),
):
cls: Type[ObjectMeta] = type.__new__(mcs, name, bases, dict(classdict))
cls.properties = classdict.properties
for property_ in cls.properties.values():
property_.bind_class(cls)
cls.default = classdict.default # TODO: Make this a class arg.
cls.additionalProperties = kwargs.get("additionalProperties", True)
cls.patternProperties = kwargs.get("patternProperties", {})
cls.minProperties = kwargs.get("minProperties", NotPassed())
cls.maxProperties = kwargs.get("maxProperties", NotPassed())
cls.propertyNames = kwargs.get("propertyNames", NotPassed())
cls.dependencies = kwargs.get("dependencies", NotPassed())
cls.const = kwargs.get("const", NotPassed())
cls.enum = kwargs.get("enum", NotPassed())
cls.default = default
cls.const = const
cls.enum = enum
cls.minProperties = minProperties
cls.maxProperties = maxProperties
cls.patternProperties = patternProperties
cls.additionalProperties = additionalProperties
cls.propertyNames = propertyNames
cls.dependencies = dependencies
return cls

def __hash__(cls):
Expand Down Expand Up @@ -134,36 +143,23 @@ def validators(cls) -> List[Validator]:
def python(cls) -> str:
super_cls = next(iter(cls.mro()[1:]))
cls_args = [super_cls.__name__]
if not isinstance(cls.const, NotPassed):
cls_args.append(f"const={cls.const}")
if not isinstance(cls.enum, NotPassed):
cls_args.append(f"enum={cls.enum}")
if cls.minProperties:
cls_args.append(f"minProperties={cls.minProperties}")
if not isinstance(cls.maxProperties, NotPassed):
cls_args.append(f"maxProperties={cls.maxProperties}")
if cls.patternProperties:
cls_args.append(f"patternProperties={cls.patternProperties}")
if cls.additionalProperties not in (True, Element()):
cls_args.append(f"additionalProperties={cls.additionalProperties}")
if not isinstance(cls.propertyNames, NotPassed):
cls_args.append(f"propertyNames={cls.propertyNames}")
if not isinstance(cls.dependencies, NotPassed):
cls_args.append(f"dependencies={cls.dependencies}")
parameters = list(
inspect.signature(type(cls).__new__).parameters.values()
)
for param in parameters:
if param.kind != param.KEYWORD_ONLY:
continue
value = getattr(cls, param.name, NotPassed())
if value == param.default:
continue
cls_args.append(f"{param.name}={repr(value)}")
class_def = f"""class {repr(cls)}({', '.join(cls_args)}):
"""
if not cls.properties and isinstance(cls.default, NotPassed):
if not cls.properties:
class_def = (
class_def
+ """
pass
"""
)
if not isinstance(cls.default, NotPassed):
class_def = (
class_def
+ f"""
default = {repr(cls.default)}
"""
)
for property_ in cls.properties.values():
Expand Down
4 changes: 2 additions & 2 deletions statham/dsl/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ def parse_object(
if not title:
raise SchemaParseError.missing_title(schema)
title = _title_format(title)
default = schema.get("default", NotPassed())
properties = schema.get("properties", {})
properties.update(
{
Expand All @@ -283,7 +282,7 @@ def parse_object(
if parse_attribute_name(key) not in properties
}
)
class_dict = ObjectClassDict(default=default)
class_dict = ObjectClassDict()
for key, value in properties.items():
class_dict[key] = value
cls_args = dict(additionalProperties=schema["additionalProperties"])
Expand All @@ -295,6 +294,7 @@ def parse_object(
"dependencies",
"const",
"enum",
"default",
]:
if key in schema:
cls_args[key] = schema[key]
Expand Down
9 changes: 3 additions & 6 deletions statham/dsl/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,12 @@ def Property(element: "Element", *, required: bool = False, source: str = None):
:param source: The source name of this property. Only necessary if it must
differ from that of the attribute, for example when the property name
conflicts with a reserved keyword. For example, to express a property
called `default`, one could do the following:
called `class`, one could do the following:
```python
class MyObject(Object):
# Default for the whole object
default = {"default": "string"}
# Property called default
_default: str = Property(String(), source="default")
# Property called class
_class: str = Property(String(), source="class")
```
"""
return _Property(element, required=required, source=source)
3 changes: 2 additions & 1 deletion statham/orderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def _iter_object_deps(object_type: ObjectMeta) -> Iterator[Element]:
yield object_type.additionalProperties
if isinstance(object_type.propertyNames, Element):
yield object_type.propertyNames
yield from object_type.patternProperties.values()
if isinstance(object_type.patternProperties, dict):
yield from object_type.patternProperties.values()


class Orderer:
Expand Down
18 changes: 7 additions & 11 deletions tests/dsl/elements/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ def test_object_instance_reprs(instance, expected):


class TestSchemaWithDefault:
class DefaultStringWrapper(Object):

default = dict(value="bar")
class DefaultStringWrapper(Object, default=dict(value="bar")):

value: str = Property(String(minLength=3), required=True)

Expand All @@ -114,8 +112,7 @@ def test_that_it_accepts_an_arg(self):
class TestSchemaNestedDefault:
@staticmethod
def test_default_object_match():
class DefaultStringWrapper(Object):
default = dict(value="bar")
class DefaultStringWrapper(Object, default=dict(value="bar")):
value: str = Property(String(), required=True)

class WrapDefaultObject(Object):
Expand All @@ -127,8 +124,7 @@ class WrapDefaultObject(Object):

@staticmethod
def test_default_object_no_match():
class DefaultStringWrapper(Object):
default = dict(other="bar")
class DefaultStringWrapper(Object, default=dict(other="bar")):
value: str = Property(String(), required=True)

class WrapDefaultObject(Object):
Expand Down Expand Up @@ -160,9 +156,9 @@ def test_object_annotation():


class TestRenamedProperties:
class PropertyRename(Object, additionalProperties=False):

default = {"default": "string"}
class PropertyRename(
Object, default={"default": "string"}, additionalProperties=False
):

_default = Property(String(), source="default")

Expand All @@ -186,7 +182,7 @@ def test_using_a_bad_property_raises_specific_error(self):
with pytest.raises(SchemaDefinitionError):

class MyObject(Object):
default = Property(String())
__init__ = Property(String())


class TestAdditionalPropertiesAsElement:
Expand Down
4 changes: 2 additions & 2 deletions tests/dsl/parser/test_parse_attribute_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"name,expected",
[
("name", "name"),
("default", "_default"),
("properties", "_properties"),
("default", "default"),
("properties", "properties"),
("options", "options"),
("additional_properties", "additional_properties"),
("_dict", "__dict"),
Expand Down
15 changes: 7 additions & 8 deletions tests/dsl/parser/test_parse_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ class ObjectWrapper(Object):
value = Property(StringWrapper, required=True)


class ObjectWithDefaultProp(Object):
default = {"default": "a string"}
class ObjectWithDefaultProp(Object, default={"default": "a string"}):

_default = Property(String(), source="default")
value = Property(String())


class ObjectWithAdditionalPropElement(Object, additionalProperties=String()):
Expand Down Expand Up @@ -120,10 +119,10 @@ class ObjectWithDependencies(
"type": "object",
"title": "ObjectWithDefaultProp",
"default": {"default": "a string"},
"properties": {"default": {"type": "string"}},
"properties": {"value": {"type": "string"}},
},
ObjectWithDefaultProp,
id="with-property-named-default",
id="with-default-default",
),
pytest.param(
{
Expand Down Expand Up @@ -238,7 +237,7 @@ class TestParseState:
@pytest.fixture()
def base_type():
return ObjectMeta(
"Foo", (Object,), ObjectClassDict(default={"foo": "bar"})
"Foo", (Object,), ObjectClassDict(), default={"foo": "bar"}
)

@staticmethod
Expand All @@ -251,7 +250,7 @@ def state(base_type):
@staticmethod
def test_that_duplicate_type_is_replaced(state, base_type):
duplicate = ObjectMeta(
"Foo", (Object,), ObjectClassDict(default={"foo": "bar"})
"Foo", (Object,), ObjectClassDict(), default={"foo": "bar"}
)
deduped = state.dedupe(duplicate)
assert deduped is base_type
Expand All @@ -261,7 +260,7 @@ def test_that_duplicate_type_is_replaced(state, base_type):
@staticmethod
def test_that_distinct_type_is_not_replaced(state, base_type):
distinct = ObjectMeta(
"Foo", (Object,), ObjectClassDict(default={"bar": "baz"})
"Foo", (Object,), ObjectClassDict(), default={"bar": "baz"}
)
deduped = state.dedupe(distinct)
assert deduped is distinct
Expand Down
6 changes: 2 additions & 4 deletions tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@

def test_schema_reserializes_to_expected_python_string():
assert serialize_python(*parse(SCHEMA)) == _IMPORT_STATEMENTS + (
"""class Category(Object):
default = {'value': 'none'}
"""class Category(Object, default={'value': 'none'}):
value: Maybe[str] = Property(String())
Expand All @@ -48,7 +46,7 @@ class Parent(Object):
category: Category = Property(Category, required=True)
_default: Maybe[str] = Property(String(), source='default')
default: Maybe[str] = Property(String())
class Other(Object):
Expand Down

0 comments on commit 77225d0

Please sign in to comment.