Skip to content

Commit

Permalink
Merge pull request #141 from python-odin/development
Browse files Browse the repository at this point in the history
Release 2.3.1
  • Loading branch information
timsavage authored Feb 19, 2023
2 parents 8792e6a + ff11c6b commit 004b425
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 35 deletions.
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 <[email protected]>"]
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

0 comments on commit 004b425

Please sign in to comment.