attrs/docs/examples.rst

320 lines
9.2 KiB
ReStructuredText
Raw Normal View History

2015-01-27 16:53:17 +00:00
.. _examples:
Examples
========
2015-01-28 15:28:47 +00:00
2015-02-05 10:04:25 +00:00
Basics
------
2015-01-28 15:28:47 +00:00
The simplest possible usage would be:
.. doctest::
>>> import attr
>>> @attr.s
... class Empty(object):
... pass
>>> Empty()
Empty()
>>> Empty() == Empty()
True
>>> Empty() is Empty()
False
So in other words: ``attrs`` useful even without actual attributes!
But you'll usually want some data on your classes, so let's add some:
.. doctest::
>>> @attr.s
... class Coordinates(object):
... x = attr.ib()
... y = attr.ib()
These by default, all features are added, so you have immediately a fully functional data class with a nice ``repr`` string and comparison methods.
.. doctest::
>>> c1 = Coordinates(1, 2)
>>> c1
Coordinates(x=1, y=2)
>>> c2 = Coordinates(x=2, y=1)
>>> c2
Coordinates(x=2, y=1)
>>> c1 == c2
False
As shown, the generated ``__init__`` method allows both for positional and keyword arguments.
2015-01-28 16:36:58 +00:00
If playful naming turns you off, ``attrs`` comes with no-nonsense aliases:
.. doctest::
>>> @attr.attributes
... class SeriousCoordinates(object):
... x = attr.attr()
... y = attr.attr()
>>> SeriousCoordinates(1, 2)
SeriousCoordinates(x=1, y=2)
2015-01-29 20:55:25 +00:00
>>> attr.fields(Coordinates) == attr.fields(SeriousCoordinates)
True
For private attributes, ``attrs`` will strip the leading underscores for keyword arguments:
.. doctest::
>>> @attr.s
... class C(object):
... _x = attr.ib()
>>> C(x=1)
C(_x=1)
An additional way (not unlike ``characteristic``) of defining attributes is supported too.
This is useful in times when you want to enhance classes that are not yours (nice ``__repr__`` for Django models anyone?):
.. doctest::
>>> class SomethingFromSomeoneElse(object):
... def __init__(self, x):
... self.x = x
>>> SomethingFromSomeoneElse = attr.s(these={"x": attr.ib()}, no_init=True)(SomethingFromSomeoneElse)
>>> SomethingFromSomeoneElse(1)
SomethingFromSomeoneElse(x=1)
Or if you want to use properties:
.. doctest::
>>> @attr.s(these={"_x": attr.ib()})
... class ReadOnlyXSquared(object):
... @property
... def x(self):
... return self._x ** 2
>>> rox = ReadOnlyXSquared(x=5)
>>> rox
ReadOnlyXSquared(_x=5)
>>> rox.x
25
>>> rox.x = 6
Traceback (most recent call last):
...
AttributeError: can't set attribute
2015-02-05 10:04:25 +00:00
Converting to Dictionaries
--------------------------
When you have a class with data, it often is very convenient to transform that class into a :class:`dict` (for example if you want to serialize it to JSON):
.. doctest::
>>> attr.asdict(Coordinates(x=1, y=2))
{'y': 2, 'x': 1}
Some fields cannot or should not be transformed.
For that, :func:`attr.asdict` offers a callback that decides whether an attribute should be included:
2015-02-05 10:04:25 +00:00
.. doctest::
>>> @attr.s
... class UserList(object):
... users = attr.ib()
>>> @attr.s
... class User(object):
... email = attr.ib()
... password = attr.ib()
>>> attr.asdict(UserList([User("jane@doe.invalid", "s33kred"),
... User("joe@doe.invalid", "p4ssw0rd")]),
... filter=lambda attr, value: attr.name != "password")
2015-02-05 10:04:25 +00:00
{'users': [{'email': 'jane@doe.invalid'}, {'email': 'joe@doe.invalid'}]}
Defaults
--------
2015-01-28 16:36:58 +00:00
Sometimes you want to have default values for your initializer.
And sometimes you even want mutable objects as default values (ever used accidentally ``def f(arg=[])``?).
``attrs`` has you covered in both cases:
.. doctest::
>>> import collections
>>> @attr.s
... class Connection(object):
... socket = attr.ib()
... @classmethod
... def connect(cl, db_string):
... # connect somehow to db_string
... return cl(socket=42)
>>> @attr.s
... class ConnectionPool(object):
... db_string = attr.ib()
... pool = attr.ib(default=attr.Factory(collections.deque))
2015-01-29 21:32:41 +00:00
... debug = attr.ib(default=False)
2015-01-28 16:36:58 +00:00
... def get_connection(self):
... try:
... return self.pool.pop()
... except IndexError:
... if self.debug:
... print "New connection!"
... return Connection.connect(self.db_string)
... def free_connection(self, conn):
... if self.debug:
... print "Connection returned!"
... self.pool.appendleft(conn)
...
>>> cp = ConnectionPool("postgres://localhost")
>>> cp
ConnectionPool(db_string='postgres://localhost', pool=deque([]), debug=False)
>>> conn = cp.get_connection()
>>> conn
Connection(socket=42)
>>> cp.free_connection(conn)
>>> cp
ConnectionPool(db_string='postgres://localhost', pool=deque([Connection(socket=42)]), debug=False)
More information on why class methods for constructing objects are awesome can be found in this insightful `blog post <http://as.ynchrono.us/2014/12/asynchronous-object-initialization.html>`_.
2015-01-29 11:20:17 +00:00
2015-02-05 10:04:25 +00:00
Validators
----------
2015-01-29 11:20:17 +00:00
Although your initializers should be a dumb as possible, it can come handy to do some kind of validation on the arguments.
That's when :func:`attr.ib`\ s ``validator`` argument comes into play.
A validator is simply a callable that takes three arguments:
#. The *instance* that's being validated.
#. The *attribute* that it's validating
#. and finally the *value* that is passed for it.
If the value does not pass the validator's standards, it just raises an appropriate exception.
Since the validator runs *after* the instance is initialized, you can refer to other attributes while validating :
2015-01-29 11:26:25 +00:00
.. doctest::
>>> def x_smaller_than_y(instance, attribute, value):
... if value >= instance.y:
... raise ValueError("'x' has to be smaller than 'y'!")
2015-01-29 11:26:25 +00:00
>>> @attr.s
... class C(object):
... x = attr.ib(validator=x_smaller_than_y)
... y = attr.ib()
>>> C(x=3, y=4)
C(x=3, y=4)
>>> C(x=4, y=3)
2015-01-29 11:26:25 +00:00
Traceback (most recent call last):
...
ValueError: 'x' has to be smaller than 'y'!
2015-01-29 11:26:25 +00:00
2015-02-02 15:08:43 +00:00
``attrs`` won't intercept your changes to those attributes but you can always call :func:`attr.validate` on any instance to verify, that it's still valid:
2015-02-02 11:13:11 +00:00
.. doctest::
>>> i = C(4, 5)
2015-02-02 11:13:11 +00:00
>>> i.x = 5 # works, no magic here
>>> attr.validate(i)
2015-02-02 11:13:11 +00:00
Traceback (most recent call last):
...
ValueError: 'x' has to be smaller than 'y'!
2015-02-02 11:13:11 +00:00
2015-01-29 11:26:25 +00:00
``attrs`` ships with a bunch of validators, make sure to :ref:`check them out <api_validators>` before writing your own:
2015-01-29 11:20:17 +00:00
.. doctest::
>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
2015-01-29 21:32:41 +00:00
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>), <type 'int'>, '42')
2015-01-29 11:20:17 +00:00
2015-01-29 18:04:23 +00:00
If you like `zope.interface <http://docs.zope.org/zope.interface/api.html#zope-interface-interface-specification>`_, ``attrs`` also comes with a :func:`attr.validators.provides` validator:
2015-01-29 18:04:23 +00:00
.. doctest::
>>> import zope.interface
>>> class IFoo(zope.interface.Interface):
... def f():
... """A function called f."""
>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.provides(IFoo))
>>> C(x=object())
Traceback (most recent call last):
...
2015-01-29 21:32:41 +00:00
TypeError: ("'x' must provide <InterfaceClass __builtin__.IFoo> which <object object at 0x10bafaaf0> doesn't.", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<provides validator for interface <InterfaceClass __builtin__.IFoo>>), <InterfaceClass __builtin__.IFoo>, <object object at 0x10bafaaf0>)
2015-01-29 18:04:23 +00:00
>>> @zope.interface.implementer(IFoo)
... @attr.s
... class Foo(object):
... def f(self):
... print("hello, world")
>>> C(Foo())
C(x=Foo())
2015-02-05 10:04:25 +00:00
Other Goodies
-------------
2015-01-29 19:50:42 +00:00
Do you like Rich Hickey?
I'm glad to report that Clojure's core feature is part of ``attrs``: `assoc <https://clojuredocs.org/clojure.core/assoc>`_!
I guess that means Clojure can be shut down now, sorry Rich!
.. doctest::
>>> @attr.s
... class C(object):
... x = attr.ib()
... y = attr.ib()
>>> i1 = C(1, 2)
>>> i1
C(x=1, y=2)
>>> i2 = attr.assoc(i1, y=3)
>>> i2
C(x=1, y=3)
>>> i1 == i2
False
2015-01-30 07:57:33 +00:00
Sometimes you may want to create a class programmatically.
``attrs`` won't let you down:
.. doctest::
>>> @attr.s
... class C1(object):
... x = attr.ib()
... y = attr.ib()
>>> C2 = attr.make_class("C2", ["x", "y"])
>>> attr.fields(C1) == attr.fields(C2)
True
You can still have power over the attributes if you pass a dictionary of name: ``attr.ib`` mappings and can pass arguments to ``@attr.s``:
.. doctest::
>>> C = attr.make_class("C", {"x": attr.ib(default=42),
... "y": attr.ib(default=attr.Factory(list))},
... no_repr=True)
2015-01-30 07:57:33 +00:00
>>> i = C()
>>> i # no repr added!
<attr._make.C object at ...>
>>> i.x
42
>>> i.y
[]
2015-01-30 16:48:34 +00:00
Finally, you can exclude single attributes from certain methods:
.. doctest::
>>> @attr.s
... class C(object):
... user = attr.ib()
... password = attr.ib(no_repr=True)
>>> C("me", "s3kr3t")
C(user='me')