Skip to content

Commit 8792e6a

Browse files
authored
Merge pull request #140 from python-odin/development
Release 2.3
2 parents 5e6b44e + 7c741fa commit 8792e6a

18 files changed

Lines changed: 783 additions & 636 deletions

HISTORY

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
2.3
2+
===
3+
4+
- Add meta option to specify how to format field names for serialisation. For example
5+
support being able to specify camelCase.
6+
- Updates to documentation
7+
- Add field_name_format meta option to docs
8+
- Document odin.utils
9+
110
2.2
211
===
312

docs/ref/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ API Reference
1313
adapters
1414
validators
1515
traversal
16+
utils

docs/ref/resources/options.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ Meta Options
6464
``key_field_names``
6565
Similar to the ``key_field_name`` but for defining multi-part keys.
6666

67+
``field_name_format``
68+
Provide a function that can be used to format field names. Field names are used
69+
to identify values when a resource serialised/deserialised.
70+
71+
For example to use *camelCase* names specify the following option:
72+
73+
.. code-block:: python
74+
75+
from odin.utils import snake_to_camel
76+
77+
class MyResource(Resource):
78+
class Meta:
79+
field_name_format = snake_to_camel
80+
81+
6782
``field_sorting``
6883
Used to customise how fields are sorted (primarily affects the order fields will
6984
be exported during serialisation) during inheritance. The default behaviour is
@@ -75,7 +90,9 @@ Meta Options
7590
sorts the fields by the order they are defined.
7691

7792
Supplying a callable allows for customisation of the field sorting eg sort by
78-
name::
93+
name:
94+
95+
.. code-block:: python
7996
8097
def sort_by_name(fields):
8198
return sorted(fields, key=lambda f: f.name)

docs/ref/utils.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#####
2+
Utils
3+
#####
4+
5+
Collection of utilities for working with Odin as well as generic data manipulation.
6+
7+
Resources
8+
=========
9+
10+
.. autofunc:: odin.utils.getmeta
11+
12+
.. autofunc:: odin.utils.field_iter
13+
14+
.. autofunc:: odin.utils.field_iter_items
15+
16+
.. autofunc:: odin.utils.virtual_field_iter_items
17+
18+
.. autofunc:: odin.utils.attribute_field_iter_items
19+
20+
.. autofunc:: odin.utils.element_field_iter_items
21+
22+
.. autofunc:: odin.utils.extract_fields_from_dict
23+
24+
25+
Name Manipulation
26+
=================
27+
28+
.. autofunc:: odin.utils.camel_to_lower_separated
29+
30+
.. autofunc:: odin.utils.camel_to_lower_underscore
31+
32+
.. autofunc:: odin.utils.camel_to_lower_dash
33+
34+
.. autofunc:: odin.utils.lower_underscore_to_camel
35+
36+
.. autofunc:: odin.utils.lower_dash_to_camel
37+
38+
39+
Choice Generation
40+
=================
41+
42+
.. autofunc:: odin.utils.value_in_choices
43+
44+
.. autofunc:: odin.utils.iter_to_choices
45+
46+
47+
48+
Iterables
49+
=========
50+
51+
.. autofunc:: odin.utils.chunk

poetry.lock

Lines changed: 470 additions & 471 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "odin"
7-
version = "2.2"
7+
version = "2.3"
88
description = "Data-structure definition/validation/traversal, mapping and serialisation toolkit for Python"
99
authors = ["Tim Savage <tim@savage.company>"]
1010
license = "BSD-3-Clause"

src/odin/adapters.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from odin.utils import cached_property, field_iter_items, getmeta
1+
from functools import cached_property
2+
from odin.utils import field_iter_items, getmeta
23

34
__all__ = ("ResourceAdapter",)
45

56

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

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

2222

2323
class ResourceOptionsAdapter:
24-
"""
25-
A lightweight wrapper for the *ResourceOptions* class that filters fields.
26-
"""
24+
"""A lightweight wrapper for the *ResourceOptions* class that filters fields."""
2725

2826
def __init__(self, options, include, exclude):
2927
self._wrapped = options
@@ -54,27 +52,22 @@ def __repr__(self):
5452

5553
@cached_property
5654
def all_fields(self):
57-
"""
58-
All fields both standard and virtual.
59-
"""
55+
"""All fields both standard and virtual."""
6056
return self.fields + self.virtual_fields
6157

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

6663
@property
6764
def attribute_fields(self):
68-
"""
69-
List of fields where is_attribute is True.
70-
"""
65+
"""List of fields where is_attribute is True."""
7166
return [f for f in self.fields if f.is_attribute]
7267

7368
@property
7469
def element_fields(self):
75-
"""
76-
List of fields where is_attribute is False.
77-
"""
70+
"""List of fields where is_attribute is False."""
7871
return [f for f in self.fields if not f.is_attribute]
7972

8073

src/odin/annotated_resource/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ def _new_meta_instance(
6363
if base_meta and new_meta.key_field_names is None:
6464
new_meta.key_field_names = base_meta.key_field_names
6565

66+
# Field name format is inherited
67+
if new_meta.field_name_format is NotProvided:
68+
new_meta.field_name_format = base_meta.field_name_format if base_meta else None
69+
6670
# Field sorting is inherited
6771
if new_meta.field_sorting is NotProvided:
6872
new_meta.field_sorting = base_meta.field_sorting if base_meta else False

src/odin/fields/__init__.py

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
import uuid
77
from functools import cached_property
8-
from typing import Sequence, Tuple, Any, TypeVar, Optional, Type
8+
from typing import Sequence, Tuple, Any, TypeVar, Optional, Type, Dict
99

1010
from odin import exceptions, datetimeutil, registration
1111
from odin.utils import getmeta
@@ -24,6 +24,7 @@
2424

2525
__all__ = (
2626
"NotProvided",
27+
"NotProvidedType",
2728
"BaseField",
2829
"Field",
2930
"BooleanField",
@@ -61,14 +62,14 @@ class NotProvided:
6162
pass
6263

6364

65+
NotProvidedType = Type[NotProvided]
66+
6467
# Backwards compatibility
6568
NOT_PROVIDED = NotProvided
6669

6770

6871
class Field(BaseField):
69-
"""
70-
Base class for fields.
71-
"""
72+
"""Base class for fields."""
7273

7374
default_validators = []
7475
default_error_messages = {
@@ -102,7 +103,7 @@ def __init__(
102103
default=NotProvided,
103104
help_text: str = "",
104105
validators: Sequence = None,
105-
error_messages=None,
106+
error_messages: Dict[str, str] = None,
106107
is_attribute: bool = False,
107108
doc_text: str = "",
108109
key: bool = False,
@@ -159,33 +160,32 @@ def __deepcopy__(self, memodict):
159160

160161
@cached_property
161162
def choice_values(self):
162-
"""
163-
Choice values to allow choices to simplify checking if a choice is valid.
164-
"""
163+
"""Choice values to allow choices to simplify checking if a choice is valid."""
165164
if self.choices is not None:
166165
return tuple(c[0] for c in self.choices)
167166

168167
@property
169168
def choices_doc_text(self) -> Sequence[Tuple[str, str]]:
170-
"""
171-
Choices converted for documentation purposes.
172-
"""
169+
"""Choices converted for documentation purposes."""
173170
return self.choices
174171

175-
def contribute_to_class(self, cls, name):
176-
self.set_attributes_from_name(name)
172+
def contribute_to_class(self, cls, name: str):
173+
"""Contribute value this field to a resource class."""
174+
meta = getmeta(cls)
175+
self.set_attributes_from_name(name, meta.field_name_format)
177176
self.resource = cls
178-
getmeta(cls).add_field(self)
177+
meta.add_field(self)
179178

180179
def to_python(self, value):
181180
"""
182181
Converts the input value into the expected Python data type, raising
183-
odin.exceptions.ValidationError if the data can't be converted.
182+
``odin.exceptions.ValidationError`` if the data can't be converted.
184183
Returns the converted value. Subclasses should override this.
185184
"""
186185
raise NotImplementedError()
187186

188187
def run_validators(self, value):
188+
"""Execute validators against supplied value."""
189189
if value in self.empty_values:
190190
return
191191

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

202202
def validate(self, value):
203+
"""Validate a supplied value."""
203204
if (
204205
self.choice_values
205206
and (value not in self.empty_values)
@@ -225,25 +226,19 @@ def clean(self, value):
225226
return value
226227

227228
def has_default(self):
228-
"""
229-
Returns a bool of whether this field has a default value.
230-
"""
229+
"""Returns a bool of whether this field has a default value."""
231230
return self.default is not NotProvided
232231

233232
def get_default(self):
234-
"""
235-
Returns the default value for this field.
236-
"""
233+
"""Returns the default value for this field."""
237234
if self.has_default():
238235
if callable(self.default):
239236
return self.default()
240237
return self.default
241238
return None
242239

243240
def value_to_object(self, obj, data):
244-
"""
245-
Assign a value to an object
246-
"""
241+
"""Assign a value to an object."""
247242
setattr(obj, self.attname, data)
248243

249244

src/odin/fields/base.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,61 @@
1-
from typing import Optional
1+
from typing import Optional, Callable
22

33

44
class BaseField:
5+
"""Base all field inherit from."""
6+
57
# These track each time an instance is created. Used to retain order.
68
creation_counter = 0
79

810
def __init__(
9-
self, verbose_name=None, verbose_name_plural=None, name=None, doc_text="",
11+
self,
12+
verbose_name: str = None,
13+
verbose_name_plural: str = None,
14+
name: str = None,
15+
doc_text: str = "",
1016
):
1117
self.verbose_name, self.verbose_name_plural = verbose_name, verbose_name_plural
1218
self.name = name
1319
self.doc_text = doc_text
1420

21+
# Fetch and increment the creation counter
1522
self.creation_counter = BaseField.creation_counter
1623
BaseField.creation_counter += 1
1724

18-
self.attname = None
25+
self.attname: Optional[str] = None
1926

2027
def __hash__(self):
2128
return self.creation_counter
2229

2330
def __repr__(self):
24-
"""
25-
Displays the module, class and name of the field.
26-
"""
31+
"""Displays the module, class and name of the field."""
2732
path = f"{self.__class__.__module__}.{self.__class__.__name__}"
2833
name = getattr(self, "name", None)
2934
if name is not None:
3035
return f"<{path}: {name}>"
3136
return f"<{path}>"
3237

33-
def set_attributes_from_name(self, attname):
38+
def set_attributes_from_name(
39+
self, attname: str, name_formatter: Optional[Callable[[str], str]] = None
40+
):
41+
"""Pre-populate names and accepts an optional name formatter method."""
3442
if not self.name:
35-
self.name = attname
43+
self.name = name_formatter(attname) if name_formatter else attname
3644
self.attname = attname
3745
if self.verbose_name is None and self.name:
3846
self.verbose_name = self.name.replace("_", " ")
3947
if self.verbose_name_plural is None and self.verbose_name:
4048
self.verbose_name_plural = f"{self.verbose_name}s"
4149

4250
def prepare(self, value):
43-
"""
44-
Prepare a value for serialisation.
45-
"""
51+
"""Prepare a value for serialisation."""
4652
return value
4753

4854
def as_string(self, value) -> Optional[str]:
49-
"""
50-
Generate a string representation of a field.
51-
"""
55+
"""Generate a string representation of a field."""
5256
if value is not None:
5357
return str(value)
5458

5559
def value_from_object(self, obj):
56-
"""
57-
Returns the value of this field in the given resource instance.
58-
"""
60+
"""Returns the value of this field in the given resource instance."""
5961
return getattr(obj, self.attname)

0 commit comments

Comments
 (0)