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
9 changes: 9 additions & 0 deletions HISTORY
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
2.3
===

- Add meta option to specify how to format field names for serialisation. For example
support being able to specify camelCase.
- Updates to documentation
- Add field_name_format meta option to docs
- Document odin.utils

2.2
===

Expand Down
1 change: 1 addition & 0 deletions docs/ref/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ API Reference
adapters
validators
traversal
utils
19 changes: 18 additions & 1 deletion docs/ref/resources/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ Meta Options
``key_field_names``
Similar to the ``key_field_name`` but for defining multi-part keys.

``field_name_format``
Provide a function that can be used to format field names. Field names are used
to identify values when a resource serialised/deserialised.

For example to use *camelCase* names specify the following option:

.. code-block:: python

from odin.utils import snake_to_camel

class MyResource(Resource):
class Meta:
field_name_format = snake_to_camel


``field_sorting``
Used to customise how fields are sorted (primarily affects the order fields will
be exported during serialisation) during inheritance. The default behaviour is
Expand All @@ -75,7 +90,9 @@ Meta Options
sorts the fields by the order they are defined.

Supplying a callable allows for customisation of the field sorting eg sort by
name::
name:

.. code-block:: python

def sort_by_name(fields):
return sorted(fields, key=lambda f: f.name)
Expand Down
51 changes: 51 additions & 0 deletions docs/ref/utils.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#####
Utils
#####

Collection of utilities for working with Odin as well as generic data manipulation.

Resources
=========

.. autofunc:: odin.utils.getmeta

.. autofunc:: odin.utils.field_iter

.. autofunc:: odin.utils.field_iter_items

.. autofunc:: odin.utils.virtual_field_iter_items

.. autofunc:: odin.utils.attribute_field_iter_items

.. autofunc:: odin.utils.element_field_iter_items

.. autofunc:: odin.utils.extract_fields_from_dict


Name Manipulation
=================

.. autofunc:: odin.utils.camel_to_lower_separated

.. autofunc:: odin.utils.camel_to_lower_underscore

.. autofunc:: odin.utils.camel_to_lower_dash

.. autofunc:: odin.utils.lower_underscore_to_camel

.. autofunc:: odin.utils.lower_dash_to_camel


Choice Generation
=================

.. autofunc:: odin.utils.value_in_choices

.. autofunc:: odin.utils.iter_to_choices



Iterables
=========

.. autofunc:: odin.utils.chunk
941 changes: 470 additions & 471 deletions poetry.lock

Large diffs are not rendered by default.

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.2"
version = "2.3"
description = "Data-structure definition/validation/traversal, mapping and serialisation toolkit for Python"
authors = ["Tim Savage <tim@savage.company>"]
license = "BSD-3-Clause"
Expand Down
23 changes: 8 additions & 15 deletions src/odin/adapters.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from odin.utils import cached_property, field_iter_items, getmeta
from functools import cached_property
from odin.utils import field_iter_items, getmeta

__all__ = ("ResourceAdapter",)


class CurriedAdapter:
"""
Curry wrapper for an Adapter to allow for pre-config of include/exclude and
"""Curry wrapper for an Adapter to allow for pre-config of include/exclude and
any other user defined arguments provided in kwargs.
"""

Expand All @@ -21,9 +21,7 @@ def apply_to(self, sources):


class ResourceOptionsAdapter:
"""
A lightweight wrapper for the *ResourceOptions* class that filters fields.
"""
"""A lightweight wrapper for the *ResourceOptions* class that filters fields."""

def __init__(self, options, include, exclude):
self._wrapped = options
Expand Down Expand Up @@ -54,27 +52,22 @@ def __repr__(self):

@cached_property
def all_fields(self):
"""
All fields both standard and virtual.
"""
"""All fields both standard and virtual."""
return self.fields + self.virtual_fields

@cached_property
def field_map(self):
"""Map of attribute name to field."""
return {f.attname: f for f in self.fields}

@property
def attribute_fields(self):
"""
List of fields where is_attribute is True.
"""
"""List of fields where is_attribute is True."""
return [f for f in self.fields if f.is_attribute]

@property
def element_fields(self):
"""
List of fields where is_attribute is False.
"""
"""List of fields where is_attribute is False."""
return [f for f in self.fields if not f.is_attribute]


Expand Down
4 changes: 4 additions & 0 deletions src/odin/annotated_resource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ def _new_meta_instance(
if base_meta and new_meta.key_field_names is None:
new_meta.key_field_names = base_meta.key_field_names

# Field name format is inherited
if new_meta.field_name_format is NotProvided:
new_meta.field_name_format = base_meta.field_name_format if base_meta else None

# Field sorting is inherited
if new_meta.field_sorting is NotProvided:
new_meta.field_sorting = base_meta.field_sorting if base_meta else False
Expand Down
43 changes: 19 additions & 24 deletions src/odin/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re
import uuid
from functools import cached_property
from typing import Sequence, Tuple, Any, TypeVar, Optional, Type
from typing import Sequence, Tuple, Any, TypeVar, Optional, Type, Dict

from odin import exceptions, datetimeutil, registration
from odin.utils import getmeta
Expand All @@ -24,6 +24,7 @@

__all__ = (
"NotProvided",
"NotProvidedType",
"BaseField",
"Field",
"BooleanField",
Expand Down Expand Up @@ -61,14 +62,14 @@ class NotProvided:
pass


NotProvidedType = Type[NotProvided]

# Backwards compatibility
NOT_PROVIDED = NotProvided


class Field(BaseField):
"""
Base class for fields.
"""
"""Base class for fields."""

default_validators = []
default_error_messages = {
Expand Down Expand Up @@ -102,7 +103,7 @@ def __init__(
default=NotProvided,
help_text: str = "",
validators: Sequence = None,
error_messages=None,
error_messages: Dict[str, str] = None,
is_attribute: bool = False,
doc_text: str = "",
key: bool = False,
Expand Down Expand Up @@ -159,33 +160,32 @@ def __deepcopy__(self, memodict):

@cached_property
def choice_values(self):
"""
Choice values to allow choices to simplify checking if a choice is valid.
"""
"""Choice values to allow choices to simplify checking if a choice is valid."""
if self.choices is not None:
return tuple(c[0] for c in self.choices)

@property
def choices_doc_text(self) -> Sequence[Tuple[str, str]]:
"""
Choices converted for documentation purposes.
"""
"""Choices converted for documentation purposes."""
return self.choices

def contribute_to_class(self, cls, name):
self.set_attributes_from_name(name)
def contribute_to_class(self, cls, name: str):
"""Contribute value this field to a resource class."""
meta = getmeta(cls)
self.set_attributes_from_name(name, meta.field_name_format)
self.resource = cls
getmeta(cls).add_field(self)
meta.add_field(self)

def to_python(self, value):
"""
Converts the input value into the expected Python data type, raising
odin.exceptions.ValidationError if the data can't be converted.
``odin.exceptions.ValidationError`` if the data can't be converted.
Returns the converted value. Subclasses should override this.
"""
raise NotImplementedError()

def run_validators(self, value):
"""Execute validators against supplied value."""
if value in self.empty_values:
return

Expand All @@ -200,6 +200,7 @@ def run_validators(self, value):
raise exceptions.ValidationError(errors)

def validate(self, value):
"""Validate a supplied value."""
if (
self.choice_values
and (value not in self.empty_values)
Expand All @@ -225,25 +226,19 @@ def clean(self, value):
return value

def has_default(self):
"""
Returns a bool of whether this field has a default value.
"""
"""Returns a bool of whether this field has a default value."""
return self.default is not NotProvided

def get_default(self):
"""
Returns the default value for this field.
"""
"""Returns the default value for this field."""
if self.has_default():
if callable(self.default):
return self.default()
return self.default
return None

def value_to_object(self, obj, data):
"""
Assign a value to an object
"""
"""Assign a value to an object."""
setattr(obj, self.attname, data)


Expand Down
36 changes: 19 additions & 17 deletions src/odin/fields/base.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,61 @@
from typing import Optional
from typing import Optional, Callable


class BaseField:
"""Base all field inherit from."""

# These track each time an instance is created. Used to retain order.
creation_counter = 0

def __init__(
self, verbose_name=None, verbose_name_plural=None, name=None, doc_text="",
self,
verbose_name: str = None,
verbose_name_plural: str = None,
name: str = None,
doc_text: str = "",
):
self.verbose_name, self.verbose_name_plural = verbose_name, verbose_name_plural
self.name = name
self.doc_text = doc_text

# Fetch and increment the creation counter
self.creation_counter = BaseField.creation_counter
BaseField.creation_counter += 1

self.attname = None
self.attname: Optional[str] = None

def __hash__(self):
return self.creation_counter

def __repr__(self):
"""
Displays the module, class and name of the field.
"""
"""Displays the module, class and name of the field."""
path = f"{self.__class__.__module__}.{self.__class__.__name__}"
name = getattr(self, "name", None)
if name is not None:
return f"<{path}: {name}>"
return f"<{path}>"

def set_attributes_from_name(self, attname):
def set_attributes_from_name(
self, attname: str, name_formatter: Optional[Callable[[str], str]] = None
):
"""Pre-populate names and accepts an optional name formatter method."""
if not self.name:
self.name = attname
self.name = name_formatter(attname) if name_formatter else attname
self.attname = attname
if self.verbose_name is None and self.name:
self.verbose_name = self.name.replace("_", " ")
if self.verbose_name_plural is None and self.verbose_name:
self.verbose_name_plural = f"{self.verbose_name}s"

def prepare(self, value):
"""
Prepare a value for serialisation.
"""
"""Prepare a value for serialisation."""
return value

def as_string(self, value) -> Optional[str]:
"""
Generate a string representation of a field.
"""
"""Generate a string representation of a field."""
if value is not None:
return str(value)

def value_from_object(self, obj):
"""
Returns the value of this field in the given resource instance.
"""
"""Returns the value of this field in the given resource instance."""
return getattr(obj, self.attname)
5 changes: 3 additions & 2 deletions src/odin/fields/virtual.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ def __set__(self, instance, value):
raise AttributeError("Read only")

def contribute_to_class(self, cls, name):
self.set_attributes_from_name(name)
meta = getmeta(cls)
self.set_attributes_from_name(name, meta.field_name_format)
self.resource = cls
getmeta(cls).add_virtual_field(self)
meta.add_virtual_field(self)
setattr(cls, name, self)


Expand Down
Loading