Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions HISTORY
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
2.3.1
=====

Bug fix
-------

- ResourceOptions.composite_fields filtered composite field by Resource instead of ResourceBase.

2.3
===

Expand Down
17 changes: 17 additions & 0 deletions docs/intro/loading-and-saving-data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 17 additions & 1 deletion docs/ref/traversal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
30 changes: 15 additions & 15 deletions docs/ref/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tim@savage.company>"]
license = "BSD-3-Clause"
Expand Down
4 changes: 1 addition & 3 deletions src/odin/filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@


class FilterAtom(abc.ABC):
"""
Base filter statement
"""
"""Base filter statement"""

__slots__ = ()

Expand Down
4 changes: 3 additions & 1 deletion src/odin/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 33 additions & 13 deletions src/odin/traversal.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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"<TraversalPath: {self}>"

def __str__(self):
def __str__(self) -> str:
atoms = []
for value, key, field in self._path:
if value is NotSupplied:
Expand All @@ -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))

Expand All @@ -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):
Expand Down Expand Up @@ -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)])]
Expand All @@ -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.
Expand Down Expand Up @@ -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]
2 changes: 1 addition & 1 deletion tests/test_traversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down