diff --git a/HISTORY b/HISTORY index 664e4d0..0a8d38a 100644 --- a/HISTORY +++ b/HISTORY @@ -1,3 +1,11 @@ +2.3.1 +===== + +Bug fix +------- + +- ResourceOptions.composite_fields filtered composite field by Resource instead of ResourceBase. + 2.3 === diff --git a/docs/intro/loading-and-saving-data.rst b/docs/intro/loading-and-saving-data.rst index 7d76c90..952bb86 100644 --- a/docs/intro/loading-and-saving-data.rst +++ b/docs/intro/loading-and-saving-data.rst @@ -32,3 +32,20 @@ Using the Book and Author resources presented in the :doc:`creating-resources` s deserialization of data. Similarly data can be deserialized back into an object graph using the :py:meth:`odin.codecs.json_codec.loads` method. + + +Other file formats +================== + +Odin includes codecs for many different file formats including: + +- :doc:`../ref/codecs/yaml_codec` +- :doc:`../ref/codecs/toml_codec` +- :doc:`../ref/codecs/msgpack_codec` +- :doc:`../ref/codecs/xml_codec` [#f1]_ + +Or using each resource as a row: + +- :doc:`../ref/codecs/csv_codec` + +.. [#f1] XML is write only diff --git a/docs/ref/traversal.rst b/docs/ref/traversal.rst index 65b5da8..c87015a 100644 --- a/docs/ref/traversal.rst +++ b/docs/ref/traversal.rst @@ -10,7 +10,20 @@ Traversal package provides tools for iterating and navigating a resource tree. TraversalPath ============= -*Todo*: In progress... +A method of defining a location within a data structure, which can then be applied to +the datastructure to extract the value. + +A ``TraversalPath`` can be expressed as a string using ``.`` as a separator:: + + field1.field2 + +Both lists and dicts can be included using ``[]`` and ``{}`` syntax:: + + field[1].field2 + +or:: + + field{key=value}.field2 ResourceTraversalIterator @@ -23,3 +36,6 @@ This class has hooks that can be used by subclasses to customise the behaviour o - *on_enter* - Called after entering a new resource. - *on_exit* - Called after exiting a resource. + +.. autoclass:: odin.traversal.ResourceTraversalIterator + :members: diff --git a/docs/ref/utils.rst b/docs/ref/utils.rst index e0775c9..402ebb8 100644 --- a/docs/ref/utils.rst +++ b/docs/ref/utils.rst @@ -7,45 +7,45 @@ Collection of utilities for working with Odin as well as generic data manipulati Resources ========= -.. autofunc:: odin.utils.getmeta +.. autofunction:: odin.utils.getmeta -.. autofunc:: odin.utils.field_iter +.. autofunction:: odin.utils.field_iter -.. autofunc:: odin.utils.field_iter_items +.. autofunction:: odin.utils.field_iter_items -.. autofunc:: odin.utils.virtual_field_iter_items +.. autofunction:: odin.utils.virtual_field_iter_items -.. autofunc:: odin.utils.attribute_field_iter_items +.. autofunction:: odin.utils.attribute_field_iter_items -.. autofunc:: odin.utils.element_field_iter_items +.. autofunction:: odin.utils.element_field_iter_items -.. autofunc:: odin.utils.extract_fields_from_dict +.. autofunction:: odin.utils.extract_fields_from_dict Name Manipulation ================= -.. autofunc:: odin.utils.camel_to_lower_separated +.. autofunction:: odin.utils.camel_to_lower_separated -.. autofunc:: odin.utils.camel_to_lower_underscore +.. autofunction:: odin.utils.camel_to_lower_underscore -.. autofunc:: odin.utils.camel_to_lower_dash +.. autofunction:: odin.utils.camel_to_lower_dash -.. autofunc:: odin.utils.lower_underscore_to_camel +.. autofunction:: odin.utils.lower_underscore_to_camel -.. autofunc:: odin.utils.lower_dash_to_camel +.. autofunction:: odin.utils.lower_dash_to_camel Choice Generation ================= -.. autofunc:: odin.utils.value_in_choices +.. autofunction:: odin.utils.value_in_choices -.. autofunc:: odin.utils.iter_to_choices +.. autofunction:: odin.utils.iter_to_choices Iterables ========= -.. autofunc:: odin.utils.chunk +.. autofunction:: odin.utils.chunk diff --git a/pyproject.toml b/pyproject.toml index 1b9ad04..036ea27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "odin" -version = "2.3" +version = "2.3.1" description = "Data-structure definition/validation/traversal, mapping and serialisation toolkit for Python" authors = ["Tim Savage "] license = "BSD-3-Clause" diff --git a/src/odin/filtering.py b/src/odin/filtering.py index fb485ca..55178e7 100644 --- a/src/odin/filtering.py +++ b/src/odin/filtering.py @@ -8,9 +8,7 @@ class FilterAtom(abc.ABC): - """ - Base filter statement - """ + """Base filter statement""" __slots__ = () diff --git a/src/odin/resources.py b/src/odin/resources.py index 4327100..d54bee9 100644 --- a/src/odin/resources.py +++ b/src/odin/resources.py @@ -171,7 +171,9 @@ def composite_fields(self) -> Sequence[Field]: """All composite fields.""" # Not the nicest solution but is a fairly safe way of detecting a composite field. return tuple( - f for f in self.fields if (hasattr(f, "of") and issubclass(f.of, Resource)) + f + for f in self.fields + if (hasattr(f, "of") and issubclass(f.of, ResourceBase)) ) @cached_property diff --git a/src/odin/traversal.py b/src/odin/traversal.py index 26dc49a..aa972d1 100644 --- a/src/odin/traversal.py +++ b/src/odin/traversal.py @@ -1,4 +1,5 @@ -from typing import Union +"""Traversal of a datastructure.""" +from typing import Union, Sequence, Iterable, Optional, Tuple, Type from odin.utils import getmeta @@ -10,7 +11,13 @@ class NotSupplied: pass -def _split_atom(atom): +NotSuppliedType = Type[NotSupplied] +OptionalStr = Union[str, NotSuppliedType] +PathAtom = Tuple[OptionalStr, OptionalStr, str] + + +def _split_atom(atom: str) -> PathAtom: + """Split a section of a path into lookups that can be used to navigate a path.""" if "[" in atom: field, _, idx = atom.rstrip("]").partition("[") return idx, NotSupplied, field @@ -26,19 +33,23 @@ class TraversalPath: """A path through a resource structure.""" @classmethod - def parse(cls, path: Union["TraversalPath", str]): + def parse(cls, path: Union["TraversalPath", str]) -> Optional["TraversalPath"]: + """Parse a traversal path string.""" if isinstance(path, TraversalPath): return path if isinstance(path, str): return cls(*[_split_atom(a) for a in path.split(".")]) - def __init__(self, *path): + __slots__ = ("_path",) + + def __init__(self, *path: PathAtom): + """Initialise traversal path""" self._path = path def __repr__(self): return f"" - def __str__(self): + def __str__(self) -> str: atoms = [] for value, key, field in self._path: if value is NotSupplied: @@ -49,15 +60,18 @@ def __str__(self): atoms.append(f"{field}{{{key}={value}}}") return ".".join(atoms) - def __hash__(self): + def __hash__(self) -> int: + """Hash of the path.""" return hash(self._path) - def __eq__(self, other): + def __eq__(self, other) -> bool: + """Compare to another path.""" if isinstance(other, TraversalPath): return hash(self) == hash(other) return NotImplemented - def __add__(self, other): + def __add__(self, other) -> "TraversalPath": + """Join paths together.""" if isinstance(other, TraversalPath): return TraversalPath(*(self._path + other._path)) @@ -69,7 +83,8 @@ def __add__(self, other): raise TypeError(f"Cannot add '{other}' to a path.") - def __iter__(self): + def __iter__(self) -> Iterable[PathAtom]: + """Iterate a path returning each element on the path.""" return iter(self._path) def get_value(self, root_resource: ResourceBase): @@ -126,7 +141,10 @@ class ResourceTraversalIterator: """ - def __init__(self, resource): + __slots__ = ("_resource_iters", "_field_iters", "_path", "_resource_stack") + + def __init__(self, resource: Union[ResourceBase, Sequence[ResourceBase]]): + """Initialise instance with the initial resource or sequence of resources.""" if isinstance(resource, (list, tuple)): # Stack of resource iterators (starts initially with entries from the list) self._resource_iters = [iter([(i, r) for i, r in enumerate(resource)])] @@ -139,10 +157,12 @@ def __init__(self, resource): self._path = [(NotSupplied, NotSupplied, NotSupplied)] self._resource_stack = [None] - def __iter__(self): + def __iter__(self) -> Iterable[ResourceBase]: + """Obtain an iterable instance.""" return self - def __next__(self): + def __next__(self) -> ResourceBase: + """Get next resource instance.""" if self._resource_iters: if self._field_iters: # Check if the last entry in the field stack has any unprocessed fields. @@ -211,7 +231,7 @@ def depth(self) -> int: return len(self._path) - 1 @property - def current_resource(self): + def current_resource(self) -> Optional[ResourceBase]: """The current resource being traversed.""" if self._resource_stack: return self._resource_stack[-1] diff --git a/tests/test_traversal.py b/tests/test_traversal.py index 3974739..d6eccec 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -67,7 +67,7 @@ class Meta: class ResourceTraversalIteratorTest(traversal.ResourceTraversalIterator): def __init__(self, resource): - super(ResourceTraversalIteratorTest, self).__init__(resource) + super().__init__(resource) self.events = [] def on_pre_enter(self):