Skip to content

Commit

Permalink
Release 0.2.0.
Browse files Browse the repository at this point in the history
  • Loading branch information
brandjon committed Dec 16, 2014
2 parents f348237 + 311eaca commit 22d4bd7
Show file tree
Hide file tree
Showing 17 changed files with 517 additions and 429 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
__pycache__
.tox
SimpleStruct.egg-info
dist
142 changes: 85 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,104 @@
SimpleStruct
============

*(Requires Python 3)*

This is a small utility for making it easier to write simple struct
classes in Python, without having to write boilerplate code. Structs
are similar to the standard library's namedtuple, but support type
checking, mutability, and derived data (i.e. cached properties).

A struct is a class defining one or more named fields. The constructor
takes in the non-derived fields, in the order they were declared.
Structs can be compared for equality -- two instances of the same
struct compare equal if their fields are equal. The struct may be
declared as immutable or mutable; if immutable, modification is not
allowed after `__init__()` finishes, and the struct becomes hashable.
Structs are pretty-printed by `str()` and `repr()`.

Each field is declared with an optional type and modifiers. Types
are checked dynamically upon assignment (or reassignment). Modifiers
allow for lists of values, automatic type coersion, and for marking
fields as derived (computed by a user-defined `__init__()`).

This is a small toy project, so no backwards compatability guarantees
are made.


### To use ###

For the simplest case, just use
# SimpleStruct

*(Supports Python 3.3 and up)*

This is a small utility for making it easier to create "struct" classes
in Python without writing boilerplate code. Structs are similar to the
standard library's `collections.namedtuple` but are more flexible,
relying on an inheritance-based approach instead of `eval()`ing a code
template.

## Example

Writing struct classes by hand is tedious and error prone. Consider a
simple Point2D class. The bare minimum we can write is

```python
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
```

but for it to be of any use, we'll need structural equality semantics
and perhaps some pretty printing for debugging.

```python
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
print('Point2D({}, {})'.format(self.x, self.y))
__str__ = __repr__
def __eq__(self, other):
# Nevermind type-checking and subtyping.
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash(self.x) ^ hash(self.y)
```

If you're the sort of heathen who likes to use dynamic type checks
in Python code, you'll want to add extra argument checking to the
constructor. And we'll probably want to disallow inadvertently
reassigning to x and y after construction, or else the hash value
could become inconsistent -- a big problem if the point is stored
in a hash-based collection.

Even if we do all that, the code isn't robust to change. If we decide
to make this a Point3D class, we'll have to update each method to
accommodate the new z coordinate. One oversight and we're in for a
potentially hard-to-find bug.

`namedtuple` takes care of many of these problems, but it's not
extensible. You can't easily derive a new class from a namedtuple
class without implementing much of this boilerplate. It also forces
immutability, which may be inappropriate for your use case.

SimpleStruct provides a simple alternative. For the above case,
we just write

from simplestruct import Struct, Field

class Point(Struct):
x = Field(int)
y = Field(int)
class Point2D(Struct):
x = Field
y = Field

## Feature matrix

Feature | Avoids boilerplate for | Supported by `namedtuple`?
---|:---:|:---:
construction | `__init__()` | ✓
extra attributes on self | | ✗
pretty printing | `__str()__`, `__repr()__` | ✓
structural equality | `__eq__()` | ✓
inheritance | | ✗
optional mutability | | ✗
hashing (if immutable) | `__hash__()` | ✓
pickling / deep-copying | | ✓
tuple decomposition | `__len__`, `__iter__` | ✓
optional type checking | | ✗

to get a simple Point class. No need to define `__init__()`, `__str__()`,
`__eq__()`, `__hash__()`, etc. See the examples/ directory for more.
The `_asdict()` and `_replace()` methods from `namedtuple` are also
provided.

One advantage that `namedtuple` does have is speed. It is based on
the built-in Python tuple type, whereas SimpleStruct has the added
overhead of descriptor function calls.

### Comparison to namedtuple ###

The standard library's [namedtuple](http://docs.python.org/3/library/collections#collections.namedtuple)
feature can generate classes similar to what this library produces.
Specifically, namedtuple classes automatically get constructors, pretty-
printing, equality, and hashing, as well as sequential access (so you can use
decomposing assignment such as `x, y = mypoint`). They do *not* support type
checks and mutability, nor can you define auxiliary attributes on the object
since it is constructed all-at-once.
## To use ###

Namedtuples are implemented by specializing and then `eval()`ing a source code
template that describes the desired class. In contrast, SimpleStruct uses
inheritance and metaclasses to implement all struct's behavior in a generic
way. There is a performance penalty to this, since each operation results in
more function calls. An application that requires top performance out of each
struct operation should go with namedtuple if possible, especially because
much of its functionality is provided by the built-in Python tuple type.
See the `examples/` directory.


### TODO ###
## TODO ###

Features TODO:
- add support for `__slots__`
- support iteration of fields (like namedtuple)
- make exceptions appear to be raised from the stack frame of user code
where the type error occurred, rather than inside this library (with
a flag to disable, for debugging)
- possibly make it so the same Field object can be used to declare multiple
structs, and the metaclass replaces this Field object with a copy so they
can have different "name" attributes. This would allow defining a reusable
kind of field without repeating kind/mods each time.

Packaging TODO:
- make usage examples
- fix up setup.py, make installable
14 changes: 7 additions & 7 deletions examples/abstract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Illustrates how to combine Struct with abstract base classes.
"""Demonstrates how to combine Struct with abstract base classes."""

from abc import ABCMeta, abstractmethod
from simplestruct import Struct, Field, MetaStruct
Expand All @@ -11,9 +11,9 @@ def foo(self):
# If we ran this code
#
# class Concrete(Abstract, Struct):
# f = Field(int)
# f = Field
# def foo(self):
# return self.f
# return self.f ** 2
#
# we would get the following error:
#
Expand All @@ -26,12 +26,12 @@ class ABCMetaStruct(MetaStruct, ABCMeta):
pass

class Concrete(Abstract, Struct, metaclass=ABCMetaStruct):
f = Field(int)
f = Field
def foo(self):
return self.f ** 2

c = Concrete(5)
print(c.foo())
print(c.foo()) # 25

# For convenience we can also do

Expand All @@ -41,9 +41,9 @@ class ABCStruct(Struct, metaclass=ABCMetaStruct):
# and then

class Concrete(Abstract, ABCStruct):
f = Field(int)
f = Field
def foo(self):
return self.f ** 2

c = Concrete(5)
print(c.foo())
print(c.foo()) # 25
56 changes: 38 additions & 18 deletions examples/point.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
# Illustrates use of Struct to define a simple point class.
"""Illustrates the use of Struct classes and their differences
from normal classes.
"""

from simplestruct import Struct, Field
from simplestruct import Struct, Field, TypedField

# Standard Python class.
class PointA:
def __init__(self, x, y):
self.x = x
self.y = y

# The constructor is implicitly defined.
# Struct class.
class PointB(Struct):
x = Field(int)
y = Field(int)
# Field declaration order matters.
x = Field
y = Field
# The constructor is implicitly defined.

# Initialization is the same for both.
# Keywords, *args, and *kargs are allowed.
pa1 = PointA(1, 2)
pa2 = PointA(1, 2)

pb1 = PointB(1, 2)
pb2 = PointB(1, 2)
pa2 = PointA(1, y=2)
pb1 = PointB(*[1, 2])
pb2 = PointB(**{'x': 1, 'y': 2})

# Structs have pretty-printing.
print((pa1, pa2))
print((pb1, pb2))
print()
print('==== Printing ====')
print(pa1) # <__main__.PointA object at ...>
print(pb1) # PointB(x=1, y=2)

# Structs have structural equality (for like-typed objects).
print(pa1 == pa2)
print(pb1 == pb2)
print()
# Structs have structural equality (for like-typed objects)...
print('\n==== Equality ====')
print(pa1 == pa2) # False
print(pb1 == pb2) # True
print(pa1 == pb1) # False

# ... with a corresponding hash function.
print((hash(pa1) == hash(pa2)))
print((hash(pb1) == hash(pb2)))
print('\n==== Hashing ====')
print((hash(pa1) == hash(pa2))) # False (almost certainly)
print((hash(pb1) == hash(pb2))) # True

# Struct with typed fields.
class TypedPoint(Struct):
x = TypedField(int)
y = TypedField(int)

print('\n==== Type checking ====')
tp1 = TypedPoint(1, 2)
try:
tp2 = TypedPoint(1, 'b')
except TypeError:
print('Exception')
63 changes: 55 additions & 8 deletions examples/vector.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,59 @@
# Vector class that stores its magnitude.
"""Illustrates inheritance, non-field data, and mutability."""

from simplestruct import Struct, Field

class Vector(Struct):
vals = Field(int, 'seq')
mag = Field(float, '!')
def __init__(self, vals):
self.mag = sum(v ** 2 for v in vals) ** .5
class Point2D(Struct):
x = Field
y = Field

v = Vector([1, 2, 5, 10])
print(v.mag)
# Derived class that adds a computed magnitude data.
class Vector2D(Point2D):
# Special flag to inherit x and y fields without
# needing to redeclare.
_inherit_fields = True

# Constructor takes in the field values.
def __init__(self, x, y):
# mag is not a field for the purposes of pretty printing,
# equality comparison, etc. It could alternatively be
# implemented as a @property.
self.mag = (x**2 + y**2) ** .5

# self.x and self.y are already automatically initialized,
# but can be modified in __init__(), even though this
# Struct is immutable. Be careful not to hash self until
# after __init__() is done.

# No need to call super().__init__().

p1 = Point2D(3, 4)
v1 = Vector2D(3, 4)

print(p1) # Point2D(x=3, y=4)
print(v1) # Vector2D(x=3, y=4)
print(v1.mag) # 5.0

# Equality does not hold between different types.
print(p1 == v1) # False

# Structs are immutable by default.
try:
p1.x = 7
except AttributeError:
print('Exception')

# Let's make a mutable 3D point.
class Point3D(Point2D):
_inherit_fields = True
_immutable = False
z = Field

p2 = Point3D(3, 4, 5)
print(p2) # Point3D(x=3, y=4, z=5)
p2.x = 7
print(p2) # Point3D(x=7, y=4, z=5)

try:
hash(p2)
except TypeError:
print('Exception')
26 changes: 18 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
from setuptools import setup

import simplestruct

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

author = 'Jon Brandvein',
author_email = 'jon.brandvein@gmail.com',
license = 'MIT License',
description = 'A library for defining struct-like classes',

classifiers = [
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Libraries :: Python Modules',
],

author='Jon Brandvein',
license='MIT License',
description='A Python library for defining struct-like classes',
packages = ['simplestruct'],

packages=['simplestruct'],
test_suite = 'tests',
)
9 changes: 5 additions & 4 deletions simplestruct/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Provides a mechanism for defining classes with a fixed number of
fields, possibly type-checked and immutable. Methods are provided for
pretty-printing, equality testing, and hashing.
"""Provides a mechanism for defining struct-like classes. These are
similar to collections.namedtuple classes but support optional type-
checking, mutability, and inheritance.
"""

__version__ = '0.1.0'
__version__ = '0.2.0'

from .struct import *
from .fields import *
Loading

0 comments on commit 22d4bd7

Please sign in to comment.