Skip to content

Commit

Permalink
Merge branch 'release-0.2.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
brandjon committed May 15, 2016
2 parents 1268780 + c3b36f2 commit f2bba77
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 26 deletions.
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Release notes

## 0.2.2 (2016-05-15)

- fields with default values are properly passed to __new__()/__init__()
- added support for coercion of tuples for Struct-typed fields
- added support for `__getitem__` and `__setitem__`
- testing a Struct for equality with itself succeeds quickly

## 0.2.1 (2014-12-20)

- changed type checking keyword argument names: `opt` -> `or_none`
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Jon Brandvein - Author, maintainer
- Bo Lin - Struct indexing, tuple coercion for Struct-typed fields
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2013-2014 Jonathan Brandvein
Copyright (c) 2013-2015 Jonathan Brandvein

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SimpleStruct
# SimpleStruct #

*(Supports Python 3.3 and up)*

Expand All @@ -9,7 +9,7 @@ inheritance-based approach instead of `eval()`ing a code template. If
you like using `namedtuple` classes but wish they were more composable
and extensible, this project is for you.

## Example
## Example ##

Writing struct classes by hand is tedious and error prone. Consider a
simple point class. The bare minimum we can write in Python is
Expand Down Expand Up @@ -93,7 +93,7 @@ File | Purpose
[vector.py](examples/vector.py) | advanced features
[abstract.py](examples/abstract.py) | mixing structs and metaclasses

## Comparison and feature matrix
## Comparison and feature matrix ##

The most important problems mentioned above are solved by using
`namedtuple`, but this approach begins to break down when you
Expand All @@ -114,7 +114,7 @@ the boilerplate methods so they recognize the new fields. This can be
done using multiple inheritance:

```python
BaseEmployee = namedtuple('BaseEmployee', Employee._fields + ('salary',))
BaseEmployee = namedtuple('BaseEmployee', BasePerson._fields + ('salary',))
class Employee(BaseEmployee, Person):
pass
```
Expand All @@ -138,16 +138,17 @@ optional mutability | | ✗
hashing (if immutable) | `__hash__()` | ✓
pickling / deep-copying | | ✓
tuple decomposition | `__len__`, `__iter__` | ✓
indexing | `__getitem__`, `__setitem__` | `__getitem__` only
optional type checking | `__init__()`, `@property` | ✗
`_asdict()` / `_replace()` | | ✓

[MacroPy][2]'s case classes provide similar functionality, but is
[MacroPy][2]'s "case classes" provide similar functionality, but are
implemented in a very different way. Instead of metaclass hacking
or source code templating, it relies on syntactic transformation
or source code templating, MacroPy relies on syntactic transformation
of the module's AST. This allows for a syntax that's very different
from what we've seen above. So different, in fact, that we might view
MacroPy as an extension to the Python language rather than as just
a library. MacroPy case classes are subject to limitations on
a library. Case classes are subject to limitations on
inheritance and class members.

## Installation ##
Expand Down
16 changes: 16 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# Bugs #
- equality testing of cyclic objects can cause infinite recursion.

```python
class A(Struct):
x = Field()
a1 = A(None)
a2 = A(None)
a1.x = a2
a2.x = a1
a1 == a2
```
The proper behavior in this case should probably be to allow `a1 == a2`
to return `False` for simplicity, even though `a1` and `a2` are actually
isomorphic.

# Wishlist #
- add support for `__slots__`
- make exceptions appear to be raised from the stack frame of user code
Expand Down
11 changes: 11 additions & 0 deletions examples/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,14 @@ class NullablePoint(Struct):
z = myfield

p = NullablePoint(1, 2.0, None)


# Struct-typed fields can be coerced from tuple values.

class Line(Struct):
a = TypedField(TypedPoint)
b = TypedField(TypedPoint)

line1 = Line(TypedPoint(1, 2), TypedPoint(3, 4))
line2 = Line((1, 2), (3, 4))
assert line1 == line2
9 changes: 5 additions & 4 deletions examples/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,19 @@ class MutablePoint(Struct):
# The arguments are the declared fields, in order of their declaration.
#
# Fields are initialized in __new__(). A subclass that overrides
# __new__() must call super.__new__() (not type.__new__()).
# __new__() must call super().__new__() (not type.__new__()).
# __init__() does not need to call super().__init__() or do any work
# on behalf of the Struct system.
#
# If the fields have default values, these are substituted in before
# calling the constructor. Thus providing default parameter values
# in the constructor argument list is meaningless.
# in the constructor argument list is meaningless, as they will always
# be overridden by the defaults from the field's declaration.

class DoublingVector2D(Struct):

x = Field
y = Field
x = Field(default=0)
y = Field(default=0)

def __new__(cls, x, y):
print('Vector2D.__new__() has been called')
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name = 'SimpleStruct',
version = '0.2.1',
version = '0.2.2',
url = '/~https://github.com/brandjon/simplestruct',

author = 'Jon Brandvein',
Expand Down
2 changes: 1 addition & 1 deletion simplestruct/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
checking, mutability, and inheritance.
"""

__version__ = '0.2.1'
__version__ = '0.2.2'

from .struct import *
from .fields import *
14 changes: 13 additions & 1 deletion simplestruct/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
]


from .struct import Field
from .struct import Field, Struct
from .type import TypeChecker


Expand All @@ -22,6 +22,9 @@ class TypedField(Field, TypeChecker):
determined by kind.__eq__()).
If or_none is True, None is a valid value.
If the kind is a Struct and seq is False, allow the value to
be a tuple and coerce it to an instance of the Struct.
"""

def __init__(self, kind, *,
Expand Down Expand Up @@ -64,6 +67,15 @@ def normalize(self, inst, value):
return value

def __set__(self, inst, value):
# Special case: If our type is a non-sequence Struct, allow
# coercion of a tuple value to the Struct. This is done
# prior to the type check and normalization.
if (not self.seq and len(self.kind) == 1 and
isinstance(self.kind[0], type) and
issubclass(self.kind[0], Struct) and
isinstance(value, tuple)):
value = self.kind[0](*value)

self.check(inst, value)
value = self.normalize(inst, value)
super().__set__(inst, value)
55 changes: 46 additions & 9 deletions simplestruct/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,12 @@ class MetaStruct(type):
Upon instantiation of a Struct subtype, set the instance's
_initialized attribute to True after __init__() returns.
Preprocess its __new__/__init__() arguments as well.
"""

# Use OrderedDict to preserve Field declaration order.
@classmethod
def __prepare__(mcls, name, bases, **kargs):
def __prepare__(cls, name, bases, **kargs):
return OrderedDict()

# Construct the _struct attribute on the new class.
Expand Down Expand Up @@ -144,9 +145,23 @@ def __new__(mcls, clsname, bases, namespace, **kargs):

return cls

def get_boundargs(cls, *args, **kargs):
"""Return an inspect.BoundArguments object for the application
of this Struct's signature to its arguments. Add missing values
for default fields as keyword arguments.
"""
boundargs = cls._signature.bind(*args, **kargs)
# Include default arguments.
for param in cls._signature.parameters.values():
if (param.name not in boundargs.arguments and
param.default is not param.empty):
boundargs.arguments[param.name] = param.default
return boundargs

# Mark the class as _initialized after construction.
def __call__(mcls, *args, **kargs):
inst = super().__call__(*args, **kargs)
def __call__(cls, *args, **kargs):
boundargs = cls.get_boundargs(*args, **kargs)
inst = super().__call__(*boundargs.args, **boundargs.kwargs)
inst._initialized = True
return inst

Expand Down Expand Up @@ -193,12 +208,7 @@ def __new__(cls, *args, **kargs):

f = None
try:
boundargs = cls._signature.bind(*args, **kargs)
# Include default arguments.
for param in cls._signature.parameters.values():
if (param.name not in boundargs.arguments and
param.default is not param.empty):
boundargs.arguments[param.name] = param.default
boundargs = cls.get_boundargs(*args, **kargs)
for f in cls._struct:
setattr(inst, f.name, boundargs.arguments[f.name])
f = None
Expand Down Expand Up @@ -231,6 +241,13 @@ def __repr__(self):
return self._fmt_helper(repr)

def __eq__(self, other):
# Succeed immediately if we're being tested against ourselves
# (identical object in memory). This avoids an unnecessary
# walk over the fields, which can be expensive if the field
# values are themselves Structs, and so on.
if self is other:
return True

# Two struct instances are equal if they have the same
# type and same field values.
if type(self) != type(other):
Expand All @@ -257,6 +274,26 @@ def __len__(self):
def __iter__(self):
return (getattr(self, f.name) for f in self._struct)

def __getitem__(self, index):
# Index may also be a slice.
return tuple(getattr(self, f.name) for f in self._struct)[index]

def __setitem__(self, index, value):
if isinstance(index, slice):
fnames = [f.name for f in self._struct][index]
values = list(value)
if len(values) < len(fnames):
word = 'value' if len(values) == 1 else 'values'
raise ValueError('need more than {} {} to '
'unpack'.format(len(fnames), word))
elif len(values) > len(fnames):
raise ValueError('too many values to unpack (expected '
'{})'.format(len(fnames)))
for fname, v in zip(fnames, values):
setattr(self, fname, v)
else:
setattr(self, self._struct[index].name, value)

def __reduce_ex__(self, protocol):
# We use __reduce_ex__() rather than __getnewargs__() so that
# the metaclass's __call__() will still run. This is needed to
Expand Down
14 changes: 14 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,19 @@ class Foo(Struct):
bar = TypedField(int, or_none=True)
f1 = Foo(None)

def test_nestedstructs(self):
class Bar(Struct):
a = Field
class Foo(Struct):
b = Field
c = TypedField(Bar)
f = Foo(1, (2,))
self.assertEqual(f.c.a, 2)
self.assertEqual(f.b, 1)
with self.assertRaises(TypeError):
f = Foo(1, 2)
with self.assertRaises(TypeError):
f = Foo(1, (2, 3))

if __name__ == '__main__':
unittest.main()
39 changes: 37 additions & 2 deletions tests/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,38 @@ class Foo(Struct):
self.assertEqual(len(f), 2)
self.assertEqual((a, b), (1, 2))

def test_indexing(self):
class Bar(Struct):
a = Field
b = Field
f = Bar(5, 6)
self.assertEqual(f[0], 5)
self.assertEqual(f[1], 6)
self.assertEqual(f[0:2], (5, 6))
self.assertEqual(f[0:0], ())
self.assertEqual(f[0:2:2], (5,))
self.assertEqual(f[1:-1], ())
self.assertEqual(f[-1:10], (6,))
with self.assertRaises(IndexError):
f[2]
with self.assertRaises(AttributeError):
f[0] = 4

class Bar(Struct):
_immutable = False
a = Field()
b = Field()
f = Bar(5, 6)
f[0] = 4
self.assertEqual(f.a, 4)
f[::-1] = (1, 2)
self.assertEqual(f.a, 2)
self.assertEqual(f.b, 1)
with self.assertRaises(ValueError):
f[:] = (1,)
with self.assertRaises(ValueError):
f[:] = (1, 2, 3)

def test_construct(self):
# Construction by keyword.
class Foo(Struct):
Expand All @@ -101,10 +133,13 @@ class Foo(Struct):
class Foo(Struct):
a = Field()
b = Field(default='b')
# Make sure default field values are passed to __init__() too.
def __init__(self, a, b):
self.c = b
f = Foo(1, 2)
self.assertEqual((f.a, f.b), (1, 2))
self.assertEqual((f.a, f.b, f.c), (1, 2, 2))
f = Foo(1)
self.assertEqual((f.a, f.b), (1, 'b'))
self.assertEqual((f.a, f.b, f.c), (1, 'b', 'b'))

# Parentheses-less shorthand.
class Foo(Struct):
Expand Down

0 comments on commit f2bba77

Please sign in to comment.