Skip to content

Commit

Permalink
Defer evaluation of a UserObject schema entirely to resolve #55
Browse files Browse the repository at this point in the history
  • Loading branch information
jtv8 committed Jan 16, 2021
1 parent 4248f63 commit 79b92fa
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 52 deletions.
41 changes: 41 additions & 0 deletions features/examples/modules/late_subclass_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import List

from abc import ABC, abstractmethod

from wysdom import UserObject, UserProperty, SchemaArray, SchemaConst
from wysdom.mixins import RegistersSubclasses


class Pet(UserObject, RegistersSubclasses, ABC):
pet_type: str = UserProperty(str)
name: str = UserProperty(str)

@abstractmethod
def speak(self):
pass


class Person(UserObject):
first_name: str = UserProperty(str)
last_name: str = UserProperty(str)
pets: List[Pet] = UserProperty(SchemaArray(Pet))


class Dog(Pet):

def speak(self):
return f"{self.name} says Woof!"


class Greyhound(Dog):
pet_type: str = UserProperty(SchemaConst("greyhound"))

def speak(self):
return f"{self.name}, the greyhound, says Woof!"


class Cat(Pet):
pet_type: str = UserProperty(SchemaConst("cat"))

def speak(self):
return f"{self.name} says Miaow!"
28 changes: 17 additions & 11 deletions features/subclass.feature
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Feature: Test subclassed DOM objects

Scenario: Test good input string
Scenario Outline: Test good input string

Given the Python module subclass_module.py
Given the Python module <module>.py
When we execute the following python code:
"""
example_dict_input = {
Expand All @@ -19,12 +19,12 @@ Feature: Test subclassed DOM objects
}
]
}
example = subclass_module.Person(example_dict_input)
example = <module>.Person(example_dict_input)
expected_schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/subclass_module.Person",
"$ref": "#/definitions/<module>.Person",
"definitions": {
"subclass_module.Greyhound": {
"<module>.Greyhound": {
"type": "object",
"properties": {
"pet_type": {"const": "greyhound"},
Expand All @@ -33,7 +33,7 @@ Feature: Test subclassed DOM objects
"required": ["name", "pet_type"],
"additionalProperties": False
},
"subclass_module.Cat": {
"<module>.Cat": {
"type": "object",
"properties": {
"pet_type": {"const": "cat"},
Expand All @@ -42,20 +42,20 @@ Feature: Test subclassed DOM objects
"required": ["name", "pet_type"],
"additionalProperties": False
},
"subclass_module.Pet": {
"<module>.Pet": {
"anyOf": [
{"$ref": "#/definitions/subclass_module.Greyhound"},
{"$ref": "#/definitions/subclass_module.Cat"}
{"$ref": "#/definitions/<module>.Greyhound"},
{"$ref": "#/definitions/<module>.Cat"}
]
},
"subclass_module.Person": {
"<module>.Person": {
"type": "object",
"properties": {
"first_name": {"type": "string"},
"last_name": {"type": "string"},
"pets": {
"array": {
"items": {"$ref": "#/definitions/subclass_module.Pet"}
"items": {"$ref": "#/definitions/<module>.Pet"}
}
}
},
Expand All @@ -81,3 +81,9 @@ Feature: Test subclassed DOM objects
copy.copy(example).to_builtin() == example_dict_input
copy.deepcopy(example).to_builtin() == example_dict_input
"""

Examples:
| module |
| subclass_module |
| late_subclass_module |

12 changes: 12 additions & 0 deletions wysdom/mixins/RegistersSubclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,15 @@ def registered_subclass_instance(


RegisteredSubclassList = List[Type[RegistersSubclasses]]


def has_registered_subclasses(cls: type) -> bool:
"""
Tests whether any class is a subclass of RegistersSubclasses and whether
it has any registered subclasses. If either is false, returns false.
"""
has_subclasses = False
if issubclass(cls, RegistersSubclasses):
if cls.registered_subclasses():
has_subclasses = True
return has_subclasses
2 changes: 1 addition & 1 deletion wysdom/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .ReadsJSON import ReadsJSON
from .ReadsYAML import ReadsYAML
from .RegistersSubclasses import RegistersSubclasses
from .RegistersSubclasses import RegistersSubclasses, has_registered_subclasses
8 changes: 2 additions & 6 deletions wysdom/object_schema/SchemaAnyOf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ class SchemaAnyOf(Schema):
is referred to by other schemas.
"""

_allowed_schemas: Tuple[Schema] = None
allowed_schemas: Tuple[Schema] = None
schema_ref_name: Optional[str] = None

def __init__(
self,
allowed_schemas: Iterable[Schema],
schema_ref_name: Optional[str] = None
) -> None:
self._allowed_schemas = tuple(allowed_schemas)
self.allowed_schemas = tuple(allowed_schemas)
self.schema_ref_name = schema_ref_name

def __call__(
Expand All @@ -47,10 +47,6 @@ def __call__(
)
return valid_schemas[0](value, dom_info)

@property
def allowed_schemas(self) -> Tuple[Schema]:
return self._allowed_schemas

@property
def referenced_schemas(self) -> Dict[str, Schema]:
referenced_schemas = {}
Expand Down
86 changes: 52 additions & 34 deletions wysdom/user_objects/UserObject.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations

from typing import Any, Type, Iterator, Union, Mapping, List
from typing import Any, Optional, Type, Iterator, Union, Mapping, Dict

import inspect

from ..mixins import RegistersSubclasses
from ..mixins import RegistersSubclasses, has_registered_subclasses
from ..base_schema import Schema
from ..object_schema import SchemaObject, SchemaAnyOf
from ..dom import (
Expand Down Expand Up @@ -57,12 +57,16 @@ def _schema_superclasses(self) -> Iterator[Type[UserObject]]:
yield superclass


class SchemaAnyRegisteredSubclass(SchemaAnyOf):
class UserObjectSchema(Schema):
"""
A SchemaAnyOf that allows any registered subclass of a UserObject.
Exists so that allowed_schemas can be populated dynamically and is not fixed
based on the object's current subclasses, but will also include any
future subclasses that are defined after this object's creation.
A Schema for a given UserObject, which may behave like a SchemaAnyOf if
the UserObject has registered subclasses, or a SchemaObject if it
does not.
The exact behavior (determined by UserObjectSchema.inner_schema) is
determined dynamically to ensure that the schema will include any
future registered subclasses that are defined after the UserObject's
creation.
:param object_type: The UserObject subclass.
"""
Expand All @@ -71,22 +75,49 @@ def __init__(
self,
object_type: Type[UserObject]
):
super().__init__(
allowed_schemas=[],
schema_ref_name=f"{object_type.__module__}.{object_type.__name__}"
)
assert issubclass(object_type, RegistersSubclasses)
self.object_type = object_type

@property
def allowed_schemas(self) -> List[Schema]:
return [
subclass.__json_schema__()
for subclass_list in self.object_type.registered_subclasses().values()
for subclass in subclass_list
if issubclass(subclass, UserObject)
and not isinstance(subclass.__json_schema__(), SchemaAnyOf)
]
def inner_schema(self) -> Schema:
if has_registered_subclasses(self.object_type):
assert issubclass(self.object_type, RegistersSubclasses)
return SchemaAnyOf(
allowed_schemas=[
subclass.__json_schema__()
for subclass_list in self.object_type.registered_subclasses().values()
for subclass in subclass_list
if issubclass(subclass, UserObject)
and not has_registered_subclasses(subclass)
],
schema_ref_name=f"{self.object_type.__module__}.{self.object_type.__name__}"
)
else:
return SchemaObject(
properties=self.object_type.__json_schema_properties__.properties,
required=self.object_type.__json_schema_properties__.required,
additional_properties=self.object_type.__json_schema_properties__.additional_properties,
object_type=self.object_type,
schema_ref_name=f"{self.object_type.__module__}.{self.object_type.__name__}"
)

def __call__(
self,
value: Any,
dom_info: DOMInfo = None
) -> Any:
return self.inner_schema.__call__(value, dom_info)

@property
def referenced_schemas(self) -> Dict[str, Schema]:
return self.inner_schema.referenced_schemas

@property
def jsonschema_definition(self) -> Dict[str, Any]:
return self.inner_schema.jsonschema_definition

@property
def schema_ref_name(self) -> Optional[str]:
return self.inner_schema.schema_ref_name


class UserObject(DOMObject):
Expand Down Expand Up @@ -137,19 +168,6 @@ def __init__(

@classmethod
def __json_schema__(cls) -> Schema:
has_subclasses = False
if issubclass(cls, RegistersSubclasses):
if cls.registered_subclasses():
has_subclasses = True
if has_subclasses:
return SchemaAnyRegisteredSubclass(cls)
else:
return SchemaObject(
properties=cls.__json_schema_properties__.properties,
required=cls.__json_schema_properties__.required,
additional_properties=cls.__json_schema_properties__.additional_properties,
object_type=cls,
schema_ref_name=f"{cls.__module__}.{cls.__name__}"
)
return UserObjectSchema(cls)


0 comments on commit 79b92fa

Please sign in to comment.