characteristic: Say ‘yes’ to types but ‘no’ to typing!

Release v0.1.0 (What’s new?).

characteristic is an MIT-licensed Python package with class decorators that ease the chores of implementing the most common attribute-related object protocols.

You just specify the attributes to work with and characteristic gives you:

  • a nice human-readable __repr__,
  • a complete set of comparison methods,
  • and a kwargs-based initializer (that cooperates with your existing one)

without writing dull boilerplate code again and again.

So put down that type-less data structures and welcome some class into your life!

characteristic’s documentation lives at Read the Docs, the code on GitHub. It’s rigorously tested on Python 2.6, 2.7, 3.3+, and PyPy.

Teaser

>>> from characteristic import attributes
>>> @attributes(["a", "b"])
... class AClass(object):
...     pass
>>> @attributes(["a", "b"], defaults={"b": "abc"})
... class AnotherClass(object):
...     pass
>>> obj1 = AClass(a=1, b="abc")
>>> obj2 = AnotherClass(a=1, b="abc")
>>> obj3 = AnotherClass(a=1)
>>> print obj1, obj2, obj3
<AClass(a=1, b='abc')> <AnotherClass(a=1, b='abc')> <AnotherClass(a=1, b='abc')>
>>> obj1 == obj2
False
>>> obj2 == obj3
True

User’s Guide

Why?

The difference between namedtuples and classes decorated by characteristic is that the latter are type-sensitive and less typing aside regular classes:

>>> from characteristic import attributes
>>> @attributes(["a",])
... class C1(object):
...     def __init__(self):
...         if not isinstance(self.a, int):
...             raise ValueError("'a' must be an integer.")
...     def print_a(self):
...         print self.a
>>> @attributes(["a",])
... class C2(object):
...     pass
>>> c1 = C1(a=1)
>>> c2 = C2(a=1)
>>> c1 == c2
False
>>> c1.print_a()
1
>>> C1(a="hello")
Traceback (most recent call last):
   ...
ValueError: 'a' must be an integer.

…while namedtuple’s purpose is explicitly to behave like tuples:

>>> from collections import namedtuple
>>> NT1 = namedtuple("NT1", "a")
>>> NT2 = namedtuple("NT2", "b")
>>> t1 = NT1._make([1,])
>>> t2 = NT2._make([1,])
>>> t1 == t2 == (1,)
True

This can easily lead to surprising and unintended behaviors.

API

characteristic consists of class decorators that add features to your classes.. There are three that start with @with_ that add one feature to your class based on a list of attributes. Then there’s the helper @attributes that combines them all into one decorator so you don’t have to repeat the attribute list multiple times.

@characteristic.with_repr(attrs)

A class decorator that adds a human readable __repr__ method to your class using attrs.

>>> from characteristic import with_repr
>>> @with_repr(["a", "b"])
... class RClass(object):
...     def __init__(self, a, b):
...         self.a = a
...         self.b = b
>>> c = RClass(42, "abc")
>>> print c
<RClass(a=42, b='abc')>
Parameters:attrs (list of native strings) – Attributes to work with.
@characteristic.with_cmp(attrs)

A class decorator that adds comparison methods based on attrs.

For that, each class is treated like a tuple of the values of attrs.

>>> from characteristic import with_cmp
>>> @with_cmp(["a", "b"])
... class CClass(object):
...     def __init__(self, a, b):
...         self.a = a
...         self.b = b
>>> o1 = CClass(1, "abc")
>>> o2 = CClass(1, "abc")
>>> o1 == o2  # o1.a == o2.a and o1.b == o2.b
True
>>> o1.c = 23
>>> o2.c = 42
>>> o1 == o2  # attributes that are not passed to with_cmp are ignored
True
>>> o3 = CClass(2, "abc")
>>> o1 < o3  # because 1 < 2
True
>>> o4 = CClass(1, "bca")
>>> o1 < o4  # o1.a == o4.a, but o1.b < o4.b
True
Parameters:attrs (list of native strings) – Attributes to work with.
@characteristic.with_init(attrs, defaults=None)

A class decorator that wraps the __init__ method of a class and sets attrs using passed keyword arguments before calling the original __init__.

Those keyword arguments that are used, are removed from the kwargs that is passed into your original __init__. Optionally, a dictionary of default values for some of attrs can be passed too.

>>> from characteristic import with_init
>>> @with_init(["a", "b"], defaults={"b": 2})
... class IClass(object):
...     def __init__(self):
...         if self.b != 2:
...             raise ValueError("'b' must be 2!")
>>> o1 = IClass(a=1, b=2)
>>> o2 = IClass(a=1)
>>> o1.a == o2.a
True
>>> o1.b == o2.b
True
>>> IClass()
Traceback (most recent call last):
  ...
ValueError: Missing value for 'a'.
>>> IClass(a=1, b=3)  # the custom __init__ is called after the attributes are initialized
Traceback (most recent call last):
  ...
ValueError: 'b' must be 2!
Parameters:
  • attrs (list of native strings) – Attributes to work with.
  • defaults (dict or None) – Default values if attributes are omitted on instantiation.
Raises ValueError:
 

If the value for a non-optional attribute hasn’t been passed.

@characteristic.attributes(attrs, defaults=None, create_init=True)

A convenience class decorator that combines with_cmp(), with_repr(), and optionally with_init() to avoid code duplication.

See Examples for @attributes in action!

Parameters:
  • attrs (Iterable of native strings.) – Attributes to work with.
  • defaults (dict or None) – Default values if attributes are omitted on instantiation.
  • create_init (bool) – Also apply with_init() (default: True)
Raises ValueError:
 

If the value for a non-optional attribute hasn’t been passed.

Examples

@attributes([attr1, attr2, …]) enhances your class by:

  • a nice __repr__,
  • comparison methods that compare instances as if they were tuples of their attributes,
  • and – optionally but by default – an initializer that uses the keyword arguments to initialize the specified attributes before running the class’ own initializer (you just write the validator!).
>>> from characteristic import attributes
>>> @attributes(["a", "b",])
... class C(object):
...     pass
>>> obj1 = C(a=1, b="abc")
>>> obj1
<C(a=1, b='abc')>
>>> obj2 = C(a=2, b="abc")
>>> obj1 == obj2
False
>>> obj1 < obj2
True
>>> obj3 = C(a=1, b="bca")
>>> obj3 > obj1
True
>>> @attributes(["a", "b", "c",], defaults={"c": 3})
... class CWithDefaults(object):
...     pass
>>> obj4 = CWithDefaults(a=1, b=2)
>>> obj5 = CWithDefaults(a=1, b=2, c=3)
>>> obj4 == obj5
True

Project Infortmation

License and Hall of Fame

characteristic is licensed under the permissive MIT license. The full license text can be also found in the source code repository.

Authors

characteristic is written and maintained by Hynek Schlawack.

The development is kindly supported by Variomedia AG.

It’s inspired by Twisted’s FancyEqMixin but is implemented using class decorators because sub-classing is bad for you, m’kay?

The following folks helped forming characteristic into what it is now:

How To Contribute

Every open source project lives from the generous help by contributors that sacrifice their time and characteristic is no different.

To make participation as pleasant as possible, this project adheres to the Code of Conduct by the Python Software Foundation.

Here are a few guidelines to get you started:

  • Add yourself to the AUTHORS.rst file in an alphabetical fashion. Every contribution is valuable and shall be credited.
  • If your change is noteworthy, add an entry to the changelog.
  • No contribution is too small; please submit as many fixes for typos and grammar bloopers as you can!
  • Don’t ever break backward compatibility. If it ever has to happen for higher reasons, characteristic will follow the proven procedures of the Twisted project.
  • Always add tests and docs for your code. This is a hard rule; patches with missing tests or documentation won’t be merged. If a feature is not tested or documented, it doesn’t exist.
  • Obey PEP 8 and PEP 257.
  • Write good commit messages.
  • Ideally, squash your commits, i.e. make your pull requests just one commit.

Note

If you have something great but aren’t sure whether it adheres – or even can adhere – to the rules above: please submit a pull request anyway!

In the best case, we can mold it into something, in the worst case the pull request gets politely closed. There’s absolutely nothing to fear.

Thank you for considering to contribute to characteristic! If you have any question or concerns, feel free to reach out to me.

Changelog

0.1.0 2014-05-11

  • [Feature]: Initial work.

Indices and tables