-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
517 additions
and
429 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
__pycache__ | ||
.tox | ||
SimpleStruct.egg-info | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 * |
Oops, something went wrong.