Skip to content

Commit

Permalink
Meta: implement option match_args (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
biqqles committed Jun 1, 2021
1 parent 4813f08 commit a8fafbc
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 7 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ If true, a [`__lt__`](https://docs.python.org/3/reference/datamodel.html#object.
##### `kw_only`
If true, all parameters to the generated `__init__` are marked as **keyword-only**. This includes arguments passed through to `__post_init__`.

##### `match_args`
If true (the default), generate a `__match_args__` attribute that enables structural pattern matching on Python 3.10+.

##### `iter`
If true, generate an [`__iter__`](https://docs.python.org/3/reference/datamodel.html#object.__iter__) method that returns the values of the class's fields, in order of definition. This can be used to destructure a data class instance, as with a Scala `case class` or a Python `namedtuple`.

Expand Down
9 changes: 7 additions & 2 deletions dataclassy/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def factory(producer: Callable[[], Factory.Produces]) -> Factory.Produces:
class DataClassMeta(type):
"""The metaclass that implements data class behaviour."""
DEFAULT_OPTIONS = dict(init=True, repr=True, eq=True, frozen=False, order=False, unsafe_hash=False, kw_only=False,
iter=False, kwargs=False, slots=False, hide_internals=True)
match_args=True, iter=False, kwargs=False, slots=False, hide_internals=True)

def __new__(mcs, name, bases, dict_, **kwargs):
"""Create a new data class."""
Expand Down Expand Up @@ -137,10 +137,15 @@ def __new__(mcs, name, bases, dict_, **kwargs):

# noinspection PyMissingConstructor,PyUnresolvedReferences,PyTypeChecker,PyUnusedLocal
def __init__(cls, *args, **kwargs):
if cls.__dataclass__['eq'] and cls.__dataclass__['order']:
options = cls.__dataclass__

if options['eq'] and options['order']:
from functools import total_ordering
total_ordering(cls)

if options['match_args']: # TODO: check before overwriting
cls.__match_args__ = tuple(fields(cls))

# determine a static expression for an instance's fields as a tuple, then evaluate this to create a property
# allowing efficient representation for internal methods
tuple_expr = ', '.join((*(f'self.{f}' for f in fields(cls)), '')) # '' ensures closing comma
Expand Down
9 changes: 5 additions & 4 deletions dataclassy/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ def dataclass(cls: Optional[type] = None, *, meta=DataClassMeta, **options) -> T
:key init: Generate an __init__ method
:key repr: Generate a __repr__ method
:key eq: Generate an __eq__ method
:key iter: Generate an __iter__ method
:key frozen: Allow field reassignment after initialisation
:key kwargs: Append **kwargs to the list of initialiser parameters
:key slots: Generate __slots__
:key order: Generate comparison methods other than __eq__
:key unsafe_hash: Force generation of __hash__
:key hide_internals: Hide internal methods in __repr__
:key kw_only: Make all parameters to the generated __init__ keyword-only
:key match_args: Generate a __match_args__ attribute
:key kwargs: Append **kwargs to the list of initialiser parameters
:key slots: Generate __slots__
:key iter: Generate an __iter__ method
:key hide_internals: Hide internal methods in __repr__
:return: The newly created data class
"""
assert issubclass(meta, DataClassMeta)
Expand Down
23 changes: 22 additions & 1 deletion tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from collections import OrderedDict, namedtuple
from inspect import signature
from platform import python_implementation
from sys import getsizeof
from sys import getsizeof, version_info

from dataclassy import *
from dataclassy.dataclass import DataClassMeta
Expand Down Expand Up @@ -271,6 +271,27 @@ def __post_init__(self, c: float):
with self.assertRaises(TypeError):
KwOnlyWithPostInit(3.0, a=1, b='2')

def test_match_args(self):
"""Test generation of a __match_args__ attribute."""
@dataclass
class PatternMatchable:
x: int
y: int

self.assertEqual(PatternMatchable.__match_args__, ('x', 'y'))

# Python 3.10 pattern matching is invalid syntax on older versions to needs to be parsed at runtime
if version_info < (3, 10):
return

to_be_matched = (0, 1)
namespace = locals().copy()
exec("""match PatternMatchable(*to_be_matched):
case PatternMatchable(a, b):
matched_value = a, b""", {}, namespace)

self.assertEqual(namespace['matched_value'], to_be_matched)

def test_empty_dataclass(self):
"""Test data classes with no fields and data classes with only class fields."""
@dataclass
Expand Down

0 comments on commit a8fafbc

Please sign in to comment.