diff --git a/docs/conf.py b/docs/conf.py index 398cb07..8439805 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,8 @@ # -- General configuration --------------------------------------------------- +autoclass_content = 'both' + # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. diff --git a/docs/index.rst b/docs/index.rst index 71af0b2..8043656 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,330 +1,19 @@ +Welcome to wysdom! +################## + .. toctree:: :maxdepth: 2 :caption: Contents: -.. include:: ../README.rst - - -User objects API -================= - -To build custom user object definitions in a declarative style, -you do so by creating subclasses of `wysdom.UserObject`. -Instances of your subclass will behave as a `MutableMapping`, -so any code that works on the underlying dict that you use to -populate it should also work on the object instance. - -.. autoclass:: wysdom.UserObject - :members: - :inherited-members: - -There are two ways to add properties to a UserObject. The first -is to add named properties, by using the `wysdom.UserProperty` -data descriptor:: - - class Person(UserObject): - first_name = UserProperty(str) - -The second is to allow dynamically named additional properties:: - - class Person(UserObject, additional_properties=True): - ... - -Any additional properties that are not explicitly defined as named -attributes using the UserProperty descriptor must be accessed -using the subscript style, `object_instance[property_name]`. - -You may also restrict the data types of the additional properties -that you will allow. The type parameter that you pass in to -`additional_properties` can be a primitive Python type, a subclass -of `UserObject`, or an instance of `wysdom.Schema`:: - - class Person(UserObject, additional_properties=str): - ... - - class Person(UserObject, additional_properties=Address): - ... - - class Person(UserObject, additional_properties=SchemaDict[Vehicle]): - ... - -.. autoclass:: wysdom.UserProperty - :members: - -Property Types --------------- - -The type parameter that you pass in to `UserProperty` can be a primitive -Python type, a subclass of `UserObject`, or an instance of `wysdom.Schema`:: - - class Person(UserObject): - first_name = UserProperty(str) - last_name = UserProperty(str) - current_address = UserProperty(Address) - previous_addresses = UserProperty(SchemaArray(Address)) - vehicles = UserProperty(SchemaDict(Vehicle)) - -Property Naming ---------------- - -If a UserProperty is not explicitly given a name, it is populated using -the attribute name that it is given on the parent class. If you want -the name of the attribute in the class to be different from the -key in the underlying data that is supplied to the object, you -may specify it explicitly using the `name` parameter:: - - class Person(UserObject): - last_name = UserProperty(str, name="surname") - -Defaults --------- - -If you need a UserProperty to have a default value, you may give it -a static value using the `default` parameter:: - - class Person(UserObject): - first_name = UserProperty(str, default="") - -Or if you need the default value to have a dynamic value based on other -properties, you may use the `default_function` parameter:: - - class Person(UserObject): - ... - known_as = UserProperty( - str, - default_function=lambda person: person.first_name - ) - -DOM functions -============= - -While the DOM and schema information can be retrieved from a DOMElement -using the `__json_dom_info__` property and `__json_schema__()` method -respectively, the following convenience functions are provided -for code readability. - -.. autofunction:: wysdom.document - -.. autofunction:: wysdom.parent - -.. autofunction:: wysdom.key - -.. autofunction:: wysdom.schema - - -Mixins -====== - -The interface for UserObject has been kept as minimal as possible to -avoid cluttering the interfaces of user subclasses with unnecessary -methods. However, there is some common functionality, such as reading -and writing JSON and YAML - -ReadsJSON ---------- - -Usage: As in the first usage example, but add ReadsJSON to the -bases of Person:: - - class Person(UserObject, ReadsJSON): - first_name = UserProperty(str) - last_name = UserProperty(str) - current_address = UserProperty(Address) - previous_addresses = UserProperty(SchemaArray(Address)) - - person_instance = Person.from_json( - """ - { - "first_name": "Marge", - "last_name": "Simpson", - "current_address": { - "first_line": "123 Fake Street", - "second_line": "", - "city": "Springfield", - "postal_code": 58008 - }, - "previous_addresses": [{ - "first_line": "742 Evergreen Terrace", - "second_line": "", - "city": "Springfield", - "postal_code": 58008 - }], - "vehicles": { - "eabf04": { - "color": "orange", - "description": "Station Wagon" - } - } - } - """ - ) - -.. autoclass:: wysdom.ReadsJSON - :members: - -ReadsYAML ---------- - -Usage: As in the first usage example, but add ReadsYAML to the -bases of Person:: + source/user_docs + source/api_reference - class Person(UserObject, ReadsYAML): - first_name = UserProperty(str) - last_name = UserProperty(str) - current_address = UserProperty(Address) - previous_addresses = UserProperty(SchemaArray(Address)) - person_instance = Person.from_yaml( - """ - first_name: Marge - last_name: Simpson - current_address: - first_line: 123 Fake Street - second_line: '' - city: Springfield - postal_code: 58008 - previous_addresses: - - first_line: 742 Evergreen Terrace - second_line: '' - city: Springfield - postal_code: 58008 - vehicles: - eabf04: - color: orange - description: Station Wagon - """ - ) - -.. autoclass:: wysdom.ReadsYAML - :members: - - -RegistersSubclasses -------------------- - -Use `RegistersSubclasses` as a mixin if you want an abstract base class to -have several more specific subclasses:: - - class Pet(UserObject, RegistersSubclasses, ABC): - pet_type: str = UserProperty(str) - name: str = UserProperty(str) - - @abstractmethod - def speak(self): - pass - - - class Dog(Pet): - pet_type: str = UserProperty(SchemaConst("dog")) - - def speak(self): - return f"{self.name} says Woof!" - - - class Cat(Pet): - pet_type: str = UserProperty(SchemaConst("cat")) - - def speak(self): - return f"{self.name} says Miaow!" - - -If you use RegistersSubclasses, you may refer to the abstract -base class when defining properties and schemas in wysdom. When -the DOM is populated with data, the subclass which matches the -supplied data's schema will automatically be chosen:: - - class Person(UserObject): - pets = UserProperty(SchemaArray(Pet)) - - - person_instance = Person({ - "pets": [{ - "pet_type": "dog", - "name": "Santa's Little Helper" - }] - }) - ->>> type(person_instance.pets[0]) - - - -If you include an abstract base class in an object definition, it will -be represented in the JSON schema using the `SchemaAnyOf` with all of -the defined subclasses as allowed options. - - -Registering classes by name -........................... - -If your application needs to look up registered subclasses by a key, -you may supply the register_as keyword when declaring a subclass:: - - class Elephant(Pet, register_as="elephant"): - pet_type: str = UserProperty(SchemaConst("elephant")) - - def speak(self): - return f"{self.name} says Trumpet!" - -You may then use the class's registered name to look up the class or -create an instance from its parent class:: - - >>> Pet.registered_subclass("elephant") - - - >>> Pet.registered_subclass_instance("elephant", - ... {"pet_type": "elephant", "name": "Stampy"}).speak() - 'Stampy says Trumpet!' - - -.. autoclass:: wysdom.RegistersSubclasses - :members: - - -Internals +A-Z Index ========= -Schema objects --------------- - -.. autoclass:: wysdom.Schema - :members: - -Base schemas -............ - -The following schemas define simple atomic schemas -(defined in the subpackage `wysdom.base_schema`): - -=============== ================================================================== -Name Description -=============== ================================================================== -Schema abstract base class -SchemaType abstract base class for any schema with the "type" directive -SchemaAnything any valid JSON will be accepted -SchemaConst a string constant -SchemaNone a null value -SchemaPrimitive a primitive variable -=============== ================================================================== - -Object schemas -.............. - -The following schemas define complex schemas which reference other schemas -(defined in the subpackage `wysdom.object_schema`): - -=============== ================================================================== -Name Description -=============== ================================================================== -SchemaAnyOf Any of the permitted schemas supplied -SchemaArray An array (corresponding to a Python list) -SchemaObject An object with named properties -SchemaDict An object with dynamic properties (corresponding to a Python dict) -=============== ================================================================== +* :ref:`genindex` -Indices and tables -================== +.. include:: ../README.rst -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst new file mode 100644 index 0000000..d6cd2f0 --- /dev/null +++ b/docs/source/api_reference.rst @@ -0,0 +1,22 @@ +API reference +============= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + wysdom.base_schema + wysdom.dom + wysdom.mixins + wysdom.object_schema + wysdom.user_objects + +Exceptions +---------- + +.. automodule:: wysdom.exceptions + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/user_docs.rst b/docs/source/user_docs.rst new file mode 100644 index 0000000..0fb2530 --- /dev/null +++ b/docs/source/user_docs.rst @@ -0,0 +1,343 @@ +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +User Documentation +################## + + +User objects API +================= + +To build custom user object definitions in a declarative style, +you do so by creating subclasses of :class:`wysdom.UserObject`. +Instances of your subclass will behave as a `MutableMapping`, +so any code that works on the underlying dict that you use to +populate it should also work on the object instance. + +There are two ways to add properties to a UserObject. The first +is to add named properties, by using the :class:`wysdom.UserProperty` +data descriptor:: + + class Person(UserObject): + first_name = UserProperty(str) + +The second is to allow dynamically named additional properties:: + + class Person(UserObject, additional_properties=True): + ... + +Any additional properties that are not explicitly defined as named +attributes using the UserProperty descriptor must be accessed +using the subscript style, `object_instance[property_name]`. + +You may also restrict the data types of the additional properties +that you will allow. The type parameter that you pass in to +`additional_properties` can be a primitive Python type, a subclass +of :class:`~wysdom.UserProperty`, or an instance of :class:`~wysdom.Schema`:: + + class Person(UserObject, additional_properties=str): + ... + + class Person(UserObject, additional_properties=Address): + ... + + class Person(UserObject, additional_properties=SchemaDict[Vehicle]): + ... + + +Property Types +-------------- + +The type parameter that you pass in to :class:`~wysdom.UserProperty` can be a primitive +Python type, a subclass of :class:`~wysdom.UserProperty`, or an instance of :class:`~wysdom.Schema`:: + + class Person(UserObject): + first_name = UserProperty(str) + last_name = UserProperty(str) + current_address = UserProperty(Address) + previous_addresses = UserProperty(SchemaArray(Address)) + vehicles = UserProperty(SchemaDict(Vehicle)) + +Property Naming +--------------- + +If a UserProperty is not explicitly given a name, it is populated using +the attribute name that it is given on the parent class. If you want +the name of the attribute in the class to be different from the +key in the underlying data that is supplied to the object, you +may specify it explicitly using the `name` parameter:: + + class Person(UserObject): + last_name = UserProperty(str, name="surname") + +Defaults +-------- + +If you need a UserProperty to have a default value, you may give it +a static value using the `default` parameter:: + + class Person(UserObject): + first_name = UserProperty(str, default="") + +Or if you need the default value to have a dynamic value based on other +properties, you may use the `default_function` parameter:: + + class Person(UserObject): + ... + known_as = UserProperty( + str, + default_function=lambda person: person.first_name + ) + +Constants +--------- + +Sometimes a property should always have one constant value for a given +schema. A common use case is for properties that identify an object as a +particular object type. + +In this case, use the :class:`wysdom.SchemaConst` class:: + + pet_type = UserProperty(SchemaConst("cat")) + + +Arrays and Dicts +---------------- + +For complex schemas, it is often necessary to declare a property as +being an array or a dictionary or other objects. + +For an array, use the :class:`wysdom.SchemaArray`. Properties of this type +function identically to a Python list (specifically a +:class:`collections.abc.MutableSequence`):: + + related_people = UserProperty(SchemaArray(Person)) + +For an dictionary, use the :class:`wysdom.SchemaDict`. Properties of this type +function identically to a Python dict (specifically a +:class:`collections.abc.MutableMapping` with keys of type :class:`str`):: + + related_people = UserProperty(SchemaDict(Person)) + +A `SchemaDict` is a special case of a :class:`wysdom.SchemaObject` with +no named properties and with additional_properties set to the type +specification that you supply. + +For both SchemaArray and SchemaDict you may pass in any type definition that +you would pass to a UserProperty. + + +DOM functions +============= + +While the DOM and schema information can be retrieved from a DOMElement +using the `__json_dom_info__` property and `__json_schema__()` method +respectively, the following convenience functions are provided +for code readability. + +.. autofunction:: wysdom.document + +.. autofunction:: wysdom.parent + +.. autofunction:: wysdom.key + +.. autofunction:: wysdom.schema + + +Mixins +====== + +The interface for UserObject has been kept as minimal as possible to +avoid cluttering the interfaces of user subclasses with unnecessary +methods. However, there is some common functionality, such as reading +and writing JSON and YAML + +ReadsJSON +--------- + +Usage: As in the first usage example, but add :class:`wysdom.mixins.ReadsJSON` +to the bases of Person:: + + class Person(UserObject, ReadsJSON): + first_name = UserProperty(str) + last_name = UserProperty(str) + current_address = UserProperty(Address) + previous_addresses = UserProperty(SchemaArray(Address)) + + person_instance = Person.from_json( + """ + { + "first_name": "Marge", + "last_name": "Simpson", + "current_address": { + "first_line": "123 Fake Street", + "second_line": "", + "city": "Springfield", + "postal_code": 58008 + }, + "previous_addresses": [{ + "first_line": "742 Evergreen Terrace", + "second_line": "", + "city": "Springfield", + "postal_code": 58008 + }], + "vehicles": { + "eabf04": { + "color": "orange", + "description": "Station Wagon" + } + } + } + """ + ) + + +ReadsYAML +--------- + +Usage: As in the first usage example, but add :class:`wysdom.mixins.ReadsYAML` +to the bases of Person:: + + class Person(UserObject, ReadsYAML): + first_name = UserProperty(str) + last_name = UserProperty(str) + current_address = UserProperty(Address) + previous_addresses = UserProperty(SchemaArray(Address)) + + person_instance = Person.from_yaml( + """ + first_name: Marge + last_name: Simpson + current_address: + first_line: 123 Fake Street + second_line: '' + city: Springfield + postal_code: 58008 + previous_addresses: + - first_line: 742 Evergreen Terrace + second_line: '' + city: Springfield + postal_code: 58008 + vehicles: + eabf04: + color: orange + description: Station Wagon + """ + ) + + +RegistersSubclasses +------------------- + +Use :class:`wysdom.mixins.RegistersSubclasses` as a mixin if you want an abstract base class to +have several more specific subclasses:: + + class Pet(UserObject, RegistersSubclasses, ABC): + pet_type: str = UserProperty(str) + name: str = UserProperty(str) + + @abstractmethod + def speak(self): + pass + + + class Dog(Pet): + pet_type: str = UserProperty(SchemaConst("dog")) + + def speak(self): + return f"{self.name} says Woof!" + + + class Cat(Pet): + pet_type: str = UserProperty(SchemaConst("cat")) + + def speak(self): + return f"{self.name} says Miaow!" + + +If you use RegistersSubclasses, you may refer to the abstract +base class when defining properties and schemas in wysdom. When +the DOM is populated with data, the subclass which matches the +supplied data's schema will automatically be chosen:: + + class Person(UserObject): + pets = UserProperty(SchemaArray(Pet)) + + + person_instance = Person({ + "pets": [{ + "pet_type": "dog", + "name": "Santa's Little Helper" + }] + }) + +>>> type(person_instance.pets[0]) + + + +If you include an abstract base class in an object definition, it will +be represented in the JSON schema using the `SchemaAnyOf` with all of +the defined subclasses as allowed options. + + +Registering classes by name +........................... + +If your application needs to look up registered subclasses by a key, +you may supply the register_as keyword when declaring a subclass:: + + class Elephant(Pet, register_as="elephant"): + pet_type: str = UserProperty(SchemaConst("elephant")) + + def speak(self): + return f"{self.name} says Trumpet!" + +You may then use the class's registered name to look up the class or +create an instance from its parent class:: + + >>> Pet.registered_subclass("elephant") + + + >>> Pet.registered_subclass_instance("elephant", + ... {"pet_type": "elephant", "name": "Stampy"}).speak() + 'Stampy says Trumpet!' + + +Internals +========= + +Schemas +------- + +Base schemas +............ + +The following schemas define simple atomic schemas +(defined in the subpackage `wysdom.base_schema`): + +================================ ================================================================== +Name Description +================================ ================================================================== +:class:`~wysdom.Schema` abstract base class +:class:`~wysdom.SchemaType` abstract base class for any schema with the "type" directive +:class:`~wysdom.SchemaAnything` any valid JSON will be accepted +:class:`~wysdom.SchemaConst` a string constant +:class:`~wysdom.SchemaNone` a null value +:class:`~wysdom.SchemaPrimitive` a primitive variable +================================ ================================================================== + +Object schemas +.............. + +The following schemas define complex schemas which reference other schemas +(defined in the subpackage `wysdom.object_schema`): + +================================ ================================================================== +Name Description +================================ ================================================================== +:class:`~wysdom.SchemaAnyOf` Any of the permitted schemas supplied +:class:`~wysdom.SchemaArray` An array (corresponding to a Python list) +:class:`~wysdom.SchemaObject` An object with named properties +:class:`~wysdom.SchemaDict` An object with dynamic properties (corresponding to a Python dict) +================================ ================================================================== diff --git a/docs/source/wysdom.base_schema.rst b/docs/source/wysdom.base_schema.rst new file mode 100644 index 0000000..354a9d8 --- /dev/null +++ b/docs/source/wysdom.base_schema.rst @@ -0,0 +1,50 @@ +base\_schema +=========================== + +Schema +--------------------------------- + +.. autoclass:: wysdom.Schema + :members: + :undoc-members: + :show-inheritance: + +SchemaAnything +----------------------------------------- + +.. autoclass:: wysdom.SchemaAnything + :members: + :undoc-members: + :show-inheritance: + +SchemaConst +-------------------------------------- + +.. autoclass:: wysdom.SchemaConst + :members: + :undoc-members: + :show-inheritance: + +SchemaNone +------------------------------------- + +.. autoclass:: wysdom.SchemaNone + :members: + :undoc-members: + :show-inheritance: + +SchemaPrimitive +------------------------------------------ + +.. autoclass:: wysdom.SchemaPrimitive + :members: + :undoc-members: + :show-inheritance: + +SchemaType +------------------------------------- + +.. autoclass:: wysdom.SchemaType + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/wysdom.dom.rst b/docs/source/wysdom.dom.rst new file mode 100644 index 0000000..9a1bec7 --- /dev/null +++ b/docs/source/wysdom.dom.rst @@ -0,0 +1,59 @@ +dom +================== + +DOMDict +------------------------- + +.. autoclass:: wysdom.dom.DOMDict + :members: + :undoc-members: + :show-inheritance: + +DOMElement +---------------------------- + +.. autoclass:: wysdom.dom.DOMElement + :members: + :undoc-members: + :show-inheritance: + +DOMInfo +---------------------------- + +.. autoclass:: wysdom.dom.DOMInfo + :members: + :undoc-members: + :show-inheritance: + + +DOMList +------------------------- + +.. autoclass:: wysdom.dom.DOMList + :members: + :undoc-members: + :show-inheritance: + +DOMObject +--------------------------- + +.. autoclass:: wysdom.dom.DOMObject + :members: + :undoc-members: + :show-inheritance: + +DOMProperties +------------------------------- + +.. autoclass:: wysdom.dom.DOMProperties + :members: + :undoc-members: + :show-inheritance: + +functions +--------------------------- + +.. automodule:: wysdom.dom.functions + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/wysdom.mixins.rst b/docs/source/wysdom.mixins.rst new file mode 100644 index 0000000..8bf2694 --- /dev/null +++ b/docs/source/wysdom.mixins.rst @@ -0,0 +1,26 @@ +mixins +===================== + +ReadsJSON +------------------------------ + +.. autoclass:: wysdom.mixins.ReadsJSON + :members: + :undoc-members: + :show-inheritance: + +ReadsYAML +------------------------------ + +.. autoclass:: wysdom.mixins.ReadsYAML + :members: + :undoc-members: + :show-inheritance: + +RegistersSubclasses +---------------------------------------- + +.. autoclass:: wysdom.mixins.RegistersSubclasses + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/wysdom.object_schema.rst b/docs/source/wysdom.object_schema.rst new file mode 100644 index 0000000..ed6999d --- /dev/null +++ b/docs/source/wysdom.object_schema.rst @@ -0,0 +1,42 @@ +object\_schema +============================= + +SchemaAnyOf +---------------------------------------- + +.. autoclass:: wysdom.SchemaAnyOf + :members: + :undoc-members: + :show-inheritance: + +SchemaArray +---------------------------------------- + +.. autoclass:: wysdom.SchemaArray + :members: + :undoc-members: + :show-inheritance: + +SchemaDict +--------------------------------------- + +.. autoclass:: wysdom.SchemaDict + :members: + :undoc-members: + :show-inheritance: + +SchemaObject +----------------------------------------- + +.. autoclass:: wysdom.SchemaObject + :members: + :undoc-members: + :show-inheritance: + +resolve\_arg\_to\_type +--------------------------------------------------- + +.. automodule:: wysdom.object_schema.resolve_arg_to_type + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/wysdom.user_objects.rst b/docs/source/wysdom.user_objects.rst new file mode 100644 index 0000000..4af709c --- /dev/null +++ b/docs/source/wysdom.user_objects.rst @@ -0,0 +1,26 @@ +user\_objects +============================ + +UserObject +-------------------------------------- + +.. autoclass:: wysdom.UserObject + :members: + :undoc-members: + :show-inheritance: + +UserProperties +-------------------------------------- + +.. autoclass:: wysdom.user_objects.UserProperties + :members: + :undoc-members: + :show-inheritance: + +UserProperty +---------------------------------------- + +.. autoclass:: wysdom.UserProperty + :members: + :undoc-members: + :show-inheritance: diff --git a/features/dict.feature b/features/dict.feature index afed65e..d8b2ceb 100644 --- a/features/dict.feature +++ b/features/dict.feature @@ -72,6 +72,7 @@ Feature: Test dictionary DOM objects } } } + walk_elements = list(example.walk_elements()) """ Then the following statements are true: """ @@ -111,6 +112,26 @@ Feature: Test dictionary DOM objects copy.copy(example).to_builtin() == example_dict_input copy.deepcopy(example).to_builtin() == example_dict_input """ + And the list walk_elements contains the following tuples: + | element | document | parent | element_key | + | example | example | None | None | + | "Marge" | example | example | "first_name" | + | "Simpson" | example | example | "last_name" | + | example.current_address | example | example | "current_address" | + | "123 Fake Street" | example | example.current_address | "first_line" | + | "" | example | example.current_address | "second_line" | + | "Springfield" | example | example.current_address | "city" | + | 58008 | example | example.current_address | "postal_code" | + | example.previous_addresses | example | example | "previous_addresses" | + | example.previous_addresses[0] | example | example.previous_addresses | None | + | "742 Evergreen Terrace" | example | example.previous_addresses[0] | "first_line" | + | "" | example | example.previous_addresses[0] | "second_line" | + | "Springfield" | example | example.previous_addresses[0] | "city" | + | 58008 | example | example.previous_addresses[0] | "postal_code" | + | example.vehicles | example | example | "vehicles" | + | example.vehicles["eabf04"] | example | example.vehicles | "eabf04" | + | "orange" | example | example.vehicles["eabf04"] | "color" | + | "Station Wagon" | example | example.vehicles["eabf04"] | "description" | Scenario: Test bad input string diff --git a/features/steps/steps.py b/features/steps/steps.py index 4aade48..53cf832 100644 --- a/features/steps/steps.py +++ b/features/steps/steps.py @@ -54,3 +54,22 @@ def step_impl(context, exception_type): except Exception as e: if e.__class__.__name__ != exception_type: raise + +@then("the list {variable_name} contains the following tuples") +def step_impl(context, variable_name): + tuple_list = eval(variable_name) + + def matches(a, b): + return a is b or a == b + + for x in context.table: + if not any( + matches(y.element, eval(x["element"])) + and matches(y.document, eval(x["document"])) + and matches(y.parent, eval(x["parent"])) + and matches(y.element_key, eval(x["element_key"])) + for y in tuple_list + ): + raise Exception(f"Could not find {x}") + if len(list(context.table)) != len(tuple_list): + raise Exception("Lengths of lists do not match") diff --git a/wysdom/__init__.py b/wysdom/__init__.py index 08925be..33f92a6 100644 --- a/wysdom/__init__.py +++ b/wysdom/__init__.py @@ -1,10 +1,9 @@ from .__version__ import __version__ from .exceptions import ValidationError from .dom import document, parent, key, schema -from .dom import DOMInfo as DOMInfo -from .dom import DOMElement as Element -from .base_schema import Schema, SchemaAnything, SchemaConst -from .object_schema import SchemaArray, SchemaDict +from . import dom +from .base_schema import Schema, SchemaType, SchemaNone, SchemaPrimitive, SchemaAnything, SchemaConst +from .object_schema import SchemaArray, SchemaDict, SchemaAnyOf, SchemaObject from .user_objects import UserProperty, UserObject from .mixins import ReadsJSON, ReadsYAML, RegistersSubclasses diff --git a/wysdom/base_schema/Schema.py b/wysdom/base_schema/Schema.py index 1b71e96..04a27cb 100644 --- a/wysdom/base_schema/Schema.py +++ b/wysdom/base_schema/Schema.py @@ -40,8 +40,8 @@ def __call__( @property def schema_ref_name(self) -> Optional[str]: """ - A unique reference name to use when this scheme is referred to by other schemas. - If this returns a string, references to this scheme will use the $ref keyword without + A unique reference name to use when this schema is referred to by other schemas. + If this returns a string, references to this schema will use the $ref keyword without replicating the full schema. If this property returns None, the full contents of the schema will be used. diff --git a/wysdom/base_schema/SchemaAnything.py b/wysdom/base_schema/SchemaAnything.py index 0931af9..6f0bcf1 100644 --- a/wysdom/base_schema/SchemaAnything.py +++ b/wysdom/base_schema/SchemaAnything.py @@ -4,6 +4,9 @@ class SchemaAnything(Schema): + """ + A schema where any valid JSON will be accepted. + """ @property def jsonschema_definition(self) -> Dict[str, Any]: diff --git a/wysdom/base_schema/SchemaConst.py b/wysdom/base_schema/SchemaConst.py index c00fb8e..8f4a17e 100644 --- a/wysdom/base_schema/SchemaConst.py +++ b/wysdom/base_schema/SchemaConst.py @@ -4,6 +4,9 @@ class SchemaConst(Schema): + """ + A schema requiring a string constant. + """ value: str = None diff --git a/wysdom/base_schema/SchemaNone.py b/wysdom/base_schema/SchemaNone.py index d570f98..feb0e4d 100644 --- a/wysdom/base_schema/SchemaNone.py +++ b/wysdom/base_schema/SchemaNone.py @@ -2,5 +2,8 @@ class SchemaNone(SchemaType): + """ + A schema requiring a null value. + """ type_name: str = 'null' diff --git a/wysdom/base_schema/SchemaPrimitive.py b/wysdom/base_schema/SchemaPrimitive.py index 1d9caa1..13f4179 100644 --- a/wysdom/base_schema/SchemaPrimitive.py +++ b/wysdom/base_schema/SchemaPrimitive.py @@ -4,6 +4,12 @@ class SchemaPrimitive(SchemaType): + """ + A schema requiring a primitive variable type + + :param python_type: The primitive Python type expected by this schema + """ + JSON_TYPES: Dict[Type, str] = { str: 'string', bool: 'boolean', diff --git a/wysdom/base_schema/SchemaType.py b/wysdom/base_schema/SchemaType.py index 8e294e8..2db8862 100644 --- a/wysdom/base_schema/SchemaType.py +++ b/wysdom/base_schema/SchemaType.py @@ -8,10 +8,16 @@ class SchemaType(Schema, ABC): + """ + Abstract base class for any schema with the "type" keyword + """ @property @abstractmethod def type_name(self) -> str: + """ + :return: Value for the JSON schema "type" keyword + """ pass @property diff --git a/wysdom/dom/DOMDict.py b/wysdom/dom/DOMDict.py index 2ff76b5..7970ba6 100644 --- a/wysdom/dom/DOMDict.py +++ b/wysdom/dom/DOMDict.py @@ -14,17 +14,26 @@ class DOMDict(DOMObject, Generic[T_co]): - - _additional_properties: Schema = None + """ + An object with dynamic properties (corresponding to a Python dict). + """ def __init__( self, value: Optional[Mapping[str, Any]] = None, json_dom_info: Optional[DOMInfo] = None, - _item_type: Optional[Schema] = None + item_type: Optional[Schema] = None ) -> None: + """ + :param value: A dict (or any :class:`collections.abc.Mapping`) containing the data to populate this + object's properties. + :param json_dom_info: A :class:`~wysdom.dom.DOMInfo` named tuple containing information about this object's + position in the DOM. + :param item_type: A :class:`~wysdom.Schema` object specifying what constitutes a valid property + of this object. + """ self.__json_schema_properties__ = DOMProperties( - additional_properties=(_item_type or SchemaAnything()) + additional_properties=(item_type or SchemaAnything()) ) super().__init__( value or {}, diff --git a/wysdom/dom/DOMElement.py b/wysdom/dom/DOMElement.py index d62cb53..f03e202 100644 --- a/wysdom/dom/DOMElement.py +++ b/wysdom/dom/DOMElement.py @@ -8,6 +8,16 @@ class DOMInfo(NamedTuple): + """ + Named tuple containing information about a DOM element's position within the DOM. + + :param element: The :class:`DOMElement` that this DOMInfo tuple provides information for. + :param document: The owning document for a :class:`DOMElement`, if it exists. + :param parent: The parent element of a :class:`DOMElement`, if it exists. + :param element_key: The key of a particular :class:`DOMElement` in its parent element, + if it can be referred to by a key (i.e. if it its parent element + is a Mapping). + """ element: Optional[DOMElement] = None document: Optional[DOMElement] = None @@ -16,6 +26,9 @@ class DOMInfo(NamedTuple): class DOMElement(ABC): + """ + Abstract base class for any DOM element. + """ __json_dom_info__: DOMInfo = None @@ -26,6 +39,12 @@ def __init__( json_dom_info: DOMInfo = None, **kwargs: Any ) -> None: + """ + :param value: A data structure containing the data to populate this element. + :param json_dom_info: A :class:`~wysdom.dom.DOMInfo` named tuple containing information about this object's + position in the DOM. + :param kwargs: Keyword arguments. + """ if value is not None: raise ValueError( "The parameter 'value' must be handled by a non-abstract subclass." @@ -50,7 +69,18 @@ def __json_schema__(cls) -> Schema: @abstractmethod def to_builtin(self) -> Any: + """ + Returns the contents of this DOM object as a Python builtin. Return type + varies depending on the specific object type. + """ pass def walk_elements(self) -> Iterator[DOMInfo]: + """ + Walk through the full tree structure within this DOM element. + Returns an iterator of :class:`~wysdom.dom.DOMInfo` tuples in the form + (element, document, parent element_key). + + :return: An iterator of :class:`~wysdom.dom.DOMInfo` tuples. + """ yield self.__json_dom_info__ diff --git a/wysdom/dom/DOMList.py b/wysdom/dom/DOMList.py index 04ab88e..1595330 100644 --- a/wysdom/dom/DOMList.py +++ b/wysdom/dom/DOMList.py @@ -20,6 +20,9 @@ class DOMList(DOMElement, MutableSequence, Generic[T_co]): + """ + An array element (corresponding to a Python list). + """ __json_element_data__: List[DOMElement] = None @@ -27,16 +30,23 @@ def __init__( self, value: Iterable, json_dom_info: Optional[DOMInfo] = None, - _item_type: Optional[Schema] = None + item_type: Optional[Schema] = None ) -> None: + """ + :param value: A list (or any :class:`Typing.Iterable`) containing the data to populate this + object's items. + :param json_dom_info: A :class:`~wysdom.dom.DOMInfo` named tuple containing information about this object's + position in the DOM. + :param item_type: A :class:`~wysdom.Schema` object specifying what constitutes a valid item in this array. + """ if value and not isinstance(value, Iterable): raise ValidationError( f"Cannot validate input. Object is not iterable: {value}" ) super().__init__(None, json_dom_info) self.__json_element_data__ = [] - if _item_type is not None: - self.item_type = _item_type + if item_type is not None: + self.item_type = item_type self[:] = value @overload @@ -97,6 +107,11 @@ def insert(self, index: int, item: Any) -> None: self.__json_element_data__.insert(index, self._new_child_item(item)) def to_builtin(self) -> List[Any]: + """ + Returns the contents of this DOM object as a Python builtin. + + :return: A Python list containing this object's data + """ return [ ( v.to_builtin() @@ -107,6 +122,7 @@ def to_builtin(self) -> List[Any]: ] def walk_elements(self) -> Iterator[DOMInfo]: + yield self.__json_dom_info__ for value in self: if isinstance(value, DOMElement): yield from value.walk_elements() diff --git a/wysdom/dom/DOMObject.py b/wysdom/dom/DOMObject.py index 1fd8136..3fc94eb 100644 --- a/wysdom/dom/DOMObject.py +++ b/wysdom/dom/DOMObject.py @@ -14,6 +14,9 @@ class DOMObject(DOMElement, MutableMapping): + """ + An object with named properties. + """ __json_schema_properties__: DOMProperties = None __json_element_data__: Dict[str, DOMElement] = None @@ -23,6 +26,12 @@ def __init__( value: Mapping[str, Any] = None, json_dom_info: DOMInfo = None ) -> None: + """ + :param value: A dict (or any :class:`collections.abc.Mapping`) containing the data to populate this + object's properties. + :param json_dom_info: A :class:`~wysdom.dom.DOMInfo` named tuple containing information about this object's + position in the DOM. + """ if value and not isinstance(value, Mapping): raise ValidationError( f"Cannot validate input. Object is not a mapping: {value}" @@ -73,6 +82,7 @@ def __str__(self): return str(self.__json_element_data__) def walk_elements(self) -> Iterator[DOMInfo]: + yield self.__json_dom_info__ for key, value in self.items(): if isinstance(value, DOMElement): yield from value.walk_elements() diff --git a/wysdom/dom/DOMProperties.py b/wysdom/dom/DOMProperties.py index f067761..8273609 100644 --- a/wysdom/dom/DOMProperties.py +++ b/wysdom/dom/DOMProperties.py @@ -4,6 +4,9 @@ class DOMProperties(object): + """ + A container for property information for a :class:`.DOMObject`. + """ properties: Dict[str, Schema] = None additional_properties: Union[bool, Schema] = False @@ -13,5 +16,14 @@ def __init__( properties: Dict[str, Schema] = None, additional_properties: Union[bool, Schema] = False ) -> None: + """ + :param properties: A dictionary of :class:`~wysdom.base_schema.Schema` objects + defining the expected names and types of a :class:`.DOMObject`'s + properties. + :param additional_properties: Defines whether a :class:`.DOMObject` permits additional + dynamically-named properties. Can be True or False, or + can be set to a specific :class:`~wysdom.Schema` to restrict the permitted + types of any additional properties. + """ self.properties = properties or {} self.additional_properties = additional_properties diff --git a/wysdom/dom/functions.py b/wysdom/dom/functions.py index b6efab7..f450e79 100644 --- a/wysdom/dom/functions.py +++ b/wysdom/dom/functions.py @@ -7,18 +7,18 @@ def dom(element: DOMElement) -> DOMInfo: """ - Retrieve a DOMInfo object for a DOMElement containing information about that - element's position in the DOM. + Retrieve a :class:`.DOMInfo` object for a :class:`.DOMElement` containing information + about that element's position in the DOM. :param element: A DOM element - :return: The DOMInfo object for that DOM element + :return: The :class:`.DOMInfo` object for that DOM element """ return element.__json_dom_info__ def document(element: DOMElement) -> Optional[DOMElement]: """ - Retrieve the owning document for a DOMElement, if it exists. + Retrieve the owning document for a :class:`.DOMElement`, if it exists. :param element: A DOM element :return: The owning document for that DOM element, or None if none exists @@ -28,7 +28,7 @@ def document(element: DOMElement) -> Optional[DOMElement]: def parent(element: DOMElement) -> Optional[DOMElement]: """ - Retrieve the parent element of a DOMElement, if it exists. + Retrieve the parent element of a :class:`.DOMElement`, if it exists. :param element: A DOM element :return: The parent element of that DOM element, or None of none exists @@ -38,8 +38,8 @@ def parent(element: DOMElement) -> Optional[DOMElement]: def key(element: DOMElement) -> Optional[str]: """ - Retrieve the key of a particular DOMElement in its parent element, if it can be - referred to by a key (i.e. if it its parent element is a Mapping). + Retrieve the key of a particular :class:`.DOMElement` in its parent element, if it can be + referred to by a key (i.e. if it its parent element is a :class:`collections.abc.Mapping`). :param element: A DOM element :return: The key of that DOM element in its parent, or None if it has no key @@ -49,9 +49,11 @@ def key(element: DOMElement) -> Optional[str]: def schema(element: DOMElement) -> Schema: """ - Retrieve the Schema object for a particular DOMElement. + Retrieve the :class:`~wysdom.base_schema.Schema` object for + a particular :class:`.DOMElement`. :param element: A DOM element - :return: The Schema object associated with that DOM element + :return: The :class:`~wysdom.base_schema.Schema` object associated + with that DOM element """ return element.__json_schema__() diff --git a/wysdom/object_schema/SchemaAnyOf.py b/wysdom/object_schema/SchemaAnyOf.py index 465da6d..0338363 100644 --- a/wysdom/object_schema/SchemaAnyOf.py +++ b/wysdom/object_schema/SchemaAnyOf.py @@ -6,6 +6,14 @@ class SchemaAnyOf(Schema): + """ + A schema requiring a match with any of the permitted schemas supplied. + + :param allowed_schemas: A list (or other Iterable) containing the permitted + `Schema` objects. + :param schema_ref_name: An optional unique reference name to use when this schema + is referred to by other schemas. + """ allowed_schemas: Tuple[Schema] = None schema_ref_name: Optional[str] = None diff --git a/wysdom/object_schema/SchemaArray.py b/wysdom/object_schema/SchemaArray.py index b3b71b7..fea6f82 100644 --- a/wysdom/object_schema/SchemaArray.py +++ b/wysdom/object_schema/SchemaArray.py @@ -7,6 +7,15 @@ class SchemaArray(Schema): + """ + A schema specifying an array (corresponding to a Python list) + + :param items: The permitted data type or schema for the items of this array. Must + be one of: + A primitive Python type (str, int, bool, float) + A subclass of `UserObject` + An instance of `Schema` + """ items: Schema = None @@ -24,7 +33,7 @@ def __call__( return DOMList( value, dom_info, - _item_type=self.items + item_type=self.items ) @property diff --git a/wysdom/object_schema/SchemaDict.py b/wysdom/object_schema/SchemaDict.py index 8c9a691..4036261 100644 --- a/wysdom/object_schema/SchemaDict.py +++ b/wysdom/object_schema/SchemaDict.py @@ -8,6 +8,15 @@ class SchemaDict(SchemaObject): + """ + A schema specifying an object with dynamic properties (corresponding to a Python dict) + + :param items: The permitted data type or schema for the properties of this object. Must + be one of: + A primitive Python type (str, int, bool, float) + A subclass of `UserObject` + An instance of `Schema` + """ def __init__( self, @@ -25,5 +34,5 @@ def __call__( return DOMDict( value, dom_info, - _item_type=self.additional_properties + item_type=self.additional_properties ) diff --git a/wysdom/object_schema/SchemaObject.py b/wysdom/object_schema/SchemaObject.py index 72ad5a7..f32665c 100644 --- a/wysdom/object_schema/SchemaObject.py +++ b/wysdom/object_schema/SchemaObject.py @@ -6,6 +6,20 @@ class SchemaObject(SchemaType): + """ + A schema specifying an object with named properties. + + :param properties: A dictionary of `Schema` objects defining the expected + names and types of this object's properties. + :param additional_properties: Defines whether this object permits additional + dynamically-named properties. Can be True or False, or + can be set to a specific `Schema` to restrict the permitted + types of any additional properties. + :param object_type: A custom object type to use when creating object instances + from this schema. + :param schema_ref_name: An optional unique reference name to use when this schema + is referred to by other schemas. + """ type_name: str = "object" properties: Optional[Dict[str, Schema]] = None diff --git a/wysdom/object_schema/resolve_arg_to_type.py b/wysdom/object_schema/resolve_arg_to_type.py index 56b1915..75e97e1 100644 --- a/wysdom/object_schema/resolve_arg_to_type.py +++ b/wysdom/object_schema/resolve_arg_to_type.py @@ -13,6 +13,15 @@ def resolve_arg_to_schema( arg: Union[Type, Schema] ) -> Schema: + """ + Resolve an argument of heterogeneous type to a `Schema` instance. + + :param arg: Argument to resolve to a Schema. Must be one of: + A primitive Python type (str, int, bool, float) + A subclass of `UserObject` + An instance of `Schema`. + :return: A `Schema` instance corresponding to the supplied argument. + """ if inspect.isclass(arg): if issubclass(arg, DOMElement): return arg.__json_schema__() diff --git a/wysdom/user_objects/UserObject.py b/wysdom/user_objects/UserObject.py index 1c7c175..422ef53 100644 --- a/wysdom/user_objects/UserObject.py +++ b/wysdom/user_objects/UserObject.py @@ -17,12 +17,24 @@ class UserProperties(DOMProperties): + """ + A container for property information for a :class:`.UserObject` subclass. + """ def __init__( self, user_class: Type[UserObject], additional_properties: Union[bool, Schema] = False ): + """ + :param user_class: The subclass of :class:`.UserObject` to extract properties from. + Properties will be extracted from the class's + :class:`~wysdom.user_objects.UserProperty` descriptors. + :param additional_properties: Defines whether a :class:`.DOMObject` permits additional + dynamically-named properties. Can be True or False, or + can be set to a specific :class:`~wysdom.base_schema.Schema` + to restrict the permitted types of any additional properties. + """ self._user_class = user_class properties = {} for superclass in reversed(list(self._schema_superclasses())): diff --git a/wysdom/user_objects/UserProperty.py b/wysdom/user_objects/UserProperty.py index b2f202c..f12a26d 100644 --- a/wysdom/user_objects/UserProperty.py +++ b/wysdom/user_objects/UserProperty.py @@ -8,18 +8,20 @@ class UserProperty(object): """ A data descriptor for creating attributes in user-defined subclasses - of `UserObject` which are mapped to keys in the underlying + of :class:`~.wysdom.user_objects.UserObject` which are mapped to keys in the underlying data object and to the `properties` key in the object's JSON schema. :param property_type: The data type or schema for this property. Must be one of: - A primitive Python type (str, int, bool, float) - A subclass of `UserObject` - An instance of `JSONSchema` + + * A primitive Python type (:class:`str`, :class:`int`, + :class:`bool`, :class:`float`) + * A subclass of :class:`~.wysdom.user_objects.UserObject` + * An instance of :class:`~wysdom.base_schema.Schema` :param name: The name of this property in the underlying data object. If not provided, this defaults to - the name of the attribute on the `UserObject` + the name of the attribute on the :class:`~.wysdom.user_objects.UserObject` instance that owns the property. :param default: A static value which provides a default value @@ -29,7 +31,7 @@ class UserProperty(object): :param default_function: A function which provides a default value for this property. The function must have a single positional argument, `self`, which is - passed the `UserObject` instance that + passed the :class:`~.wysdom.user_objects.UserObject` instance that owns the property. Cannot be set in conjunction with `default`. """ @@ -55,7 +57,7 @@ def __get__( ) -> Any: if instance is None: raise AttributeError( - "JSONSchemaProperty is not valid as a class descriptor") + "UserProperty is not valid as a class data descriptor") if self.name not in instance: if self.default_function: instance[self.name] = self.default_function(instance) diff --git a/wysdom/user_objects/__init__.py b/wysdom/user_objects/__init__.py index 1a0fc39..f38c7e4 100644 --- a/wysdom/user_objects/__init__.py +++ b/wysdom/user_objects/__init__.py @@ -1,2 +1,2 @@ -from .UserObject import UserObject +from .UserObject import UserObject, UserProperties from .UserProperty import UserProperty