Skip to content

Commit

Permalink
1.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
MacHu-GWU committed Jan 22, 2024
1 parent 6f00a0f commit bce9dd7
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 6 deletions.
74 changes: 69 additions & 5 deletions attrs_mate/mate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- :class:`LazyClass`
"""

from typing import TypeVar, List, Union, Dict, OrderedDict, Any
from typing import TypeVar, List, Union, Dict, OrderedDict, Any, Optional
import warnings
import collections
from datetime import date, datetime
Expand Down Expand Up @@ -77,7 +77,7 @@ def from_dict(
raise TypeError

@classmethod
def _from_list(
def from_list(
cls,
list_of_dct_or_obj: List[Union[Dict[str, Any], "AttrsClass", None]],
) -> List[Union["AttrsClass", None]]:
Expand All @@ -91,6 +91,24 @@ def _from_list(
else: # pragma: no cover
raise TypeError

@classmethod
def from_mapper(
cls,
map_of_dct_or_obj: Optional[
Dict[str, Union[Dict[str, Any], "AttrsClass", None]]
],
) -> Optional[Dict[str, Union[Dict[str, Any], "AttrsClass", None]]]:
"""
Construct dict of instance from dict of :class:`AttrsClass` liked data.
It could be a dictionary, an instance of this class, or None.
"""
if isinstance(map_of_dct_or_obj, dict):
return {k: cls.from_dict(v) for k, v in map_of_dct_or_obj.items()}
elif map_of_dct_or_obj is None:
return None
else: # pragma: no cover
raise TypeError

# --------------------------------------------------------------------------
# Generic Type
# --------------------------------------------------------------------------
Expand Down Expand Up @@ -212,7 +230,12 @@ def ib_list_of_datetime(cls, nullable=True, **kwargs):
# --------------------------------------------------------------------------
@classmethod
def ib_dict_of_generic(
cls, key_type: K, value_type: V, nullable=True, value_nullable=True, **kwargs
cls,
key_type: K,
value_type: V,
nullable=True,
value_nullable=True,
**kwargs,
):
""" """
if "validator" not in kwargs:
Expand Down Expand Up @@ -243,6 +266,8 @@ def ib_dict_of_generic(
@classmethod
def ib_nested(cls, **kwargs):
"""
Declare a field that is another :class:`AttrsClass`.
.. note::
nested object has default value ``None``, so it has to put it after
Expand All @@ -258,9 +283,11 @@ def ib_nested(cls, **kwargs):

@classmethod
def ib_list_of_nested(cls, **kwargs):
""" """
"""
Declare a field that is a list of other :class:`AttrsClass`.
"""
if "converter" not in kwargs:
kwargs["converter"] = cls._from_list
kwargs["converter"] = cls.from_list
if "validator" not in kwargs:
kwargs["validator"] = vs.deep_iterable(
member_validator=vs.instance_of(cls),
Expand All @@ -270,6 +297,43 @@ def ib_list_of_nested(cls, **kwargs):
kwargs["factory"] = list
return attr.field(**kwargs)

@classmethod
def ib_map_of_nested(
cls,
key_type: K,
nullable=True,
value_nullable=True,
**kwargs,
):
"""
Declare a field that is a mapper that key is any hashable value and
value is instance of :class:`AttrsClass`.
"""
if "converter" not in kwargs:
kwargs["converter"] = cls.from_mapper
if "validator" not in kwargs:
if value_nullable:
key_validator = vs.optional(vs.instance_of(key_type))
value_validator = vs.optional(vs.instance_of(cls))
else: # pragma: no cover
key_validator = vs.instance_of(key_type)
value_validator = vs.instance_of(cls)
if nullable:
kwargs["validator"] = vs.optional(
vs.deep_mapping(
key_validator=key_validator,
value_validator=value_validator,
)
)
else: # pragma: no cover
kwargs["validator"] = vs.deep_mapping(
key_validator=key_validator,
value_validator=value_validator,
)
if "factory" not in kwargs:
kwargs["factory"] = dict
return attr.field(**kwargs)


DictClass = dict
"""
Expand Down
8 changes: 7 additions & 1 deletion release-history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ X.Y.Z (TODO)
**Miscellaneous**


1.1.2 (2024-01-21)
1.2.1 (2024-01-21)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Features and Improvements**

- add :meth:`attrs_mate.mate.AttrsClass.ib_map_of_nested`
- add :meth:`attrs_mate.mate.AttrsClass.from_list`
- add :meth:`attrs_mate.mate.AttrsClass.from_mapper`

**Bugfixes**

- Fix a bug that the deprecate warning should be raised when user attempt to use it, not when the module is imported.
Expand Down
44 changes: 44 additions & 0 deletions tests/test_nesting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-

import typing as T
import pytest

import attrs
from attrs_mate import AttrsClass

Expand All @@ -11,6 +13,7 @@ class Profile(AttrsClass):
"""
firstname, lastname, ssn are generic data type field.
"""

firstname = AttrsClass.ib_str()
lastname = AttrsClass.ib_str()
ssn = AttrsClass.ib_str()
Expand All @@ -28,6 +31,7 @@ class People(AttrsClass):
- ``profile`` is nested field.
- ``degrees`` is collection type field.
"""

id = AttrsClass.ib_int()
profile = Profile.ib_nested()
degrees = Degree.ib_list_of_nested()
Expand Down Expand Up @@ -104,6 +108,46 @@ def test_profile_degrees_default_value(self):
assert people1.profile is None
assert people1.degrees == list()

def test_ib_map_of_nested(self):
@attrs.define
class Record(AttrsClass):
record_id: str = AttrsClass.ib_str()

@attrs.define
class Batch(AttrsClass):
batch_id: str = AttrsClass.ib_str()
records: T.Dict[str, Record] = Record.ib_map_of_nested(key_type=str, value_nullable=True)

batch = Batch(
batch_id="b-1",
records={"r-1": Record(record_id="r-1")},
)
batch_data = batch.to_dict()
batch1 = Batch.from_dict(batch_data)
batch1_data = batch.to_dict()
assert batch == batch1
assert batch_data == batch1_data

batch = Batch(
batch_id="b-1",
records={"r-1": None},
)
batch_data = batch.to_dict()
batch1 = Batch.from_dict(batch_data)
batch1_data = batch.to_dict()
assert batch == batch1
assert batch_data == batch1_data

batch = Batch(
batch_id="b-1",
records=None,
)
batch_data = batch.to_dict()
batch1 = Batch.from_dict(batch_data)
batch1_data = batch.to_dict()
assert batch == batch1
assert batch_data == batch1_data


if __name__ == "__main__":
import os
Expand Down

0 comments on commit bce9dd7

Please sign in to comment.