User API¶
RoleType metaclass¶
- class roles.RoleType¶
RoleType
is a metaclass that provides role support to classes. The initialization process has been altered to provide addition and removal of roles.It starts with a normal class:
>>> class Person: ... def __init__(self, name): self.name = name ... def am(self): print(self.name, 'is')
Apart from that a few roles can be defined. Simple objects with a default
__init__()
(no arguments) and theRoleType
as metaclass:>>> class Carpenter(metaclass=RoleType): ... def chop(self): print(self.name, 'chops') >>> class Biker(metaclass=RoleType): ... def bike(self): print(self.name, 'bikes')
Now, by default an object has no roles (in this case our person).
>>> person = Person('Joe')
Roles can be added by calling the
assign()
method:>>> Carpenter.assign(person) <roles.role.Person+Carpenter object at 0x...>
Or by calling the role on the subject:
>>> Carpenter(person) <roles.role.Person+Carpenter object at 0x...>
The persons methods can be invoked:
>>> person.am() Joe is
As well as the role’s methods:
>>> person.chop() Joe chops
The default behaviour is to apply the role directly to the instance.
>>> person <roles.role.Person+Carpenter object at 0x...>
The module contains a function
clone()
that can be provided to theasign()
method to create proxy instances (the default function is calledinstance()
and can also be found in this module):>>> biker = Biker.assign(person, method=clone) >>> biker <roles.role.Person+Carpenter+Biker object at 0x...> >>> biker is person False
Objects can contain multiple roles:
>>> biker = Biker.assign(person) >>> biker <roles.role.Person+Carpenter+Biker object at 0x...> >>> biker.__class__.__bases__ (<class 'roles.role.Person'>, <class 'roles.role.Carpenter'>, <class 'roles.role.Biker'>)
Note that a new class is assigned, with the roles applied (roles first).
Roles can be revoked:
>>> Carpenter.revoke(biker) <roles.role.Person+Biker object at 0x...> >>> biker.__class__.__bases__ (<class 'roles.role.Person'>, <class 'roles.role.Biker'>)
Revoking a non-existant role has no effect:
>>> Carpenter.revoke(biker) <roles.role.Person+Biker object at 0x...>
Roles do not allow for overriding methods.
>>> class Incognito(metaclass=RoleType): ... def am(self): return 'under cover' >>> Incognito(Person) Traceback (most recent call last): ... TypeError: Can not apply role when overriding methods: am
Caching
One more thing: role classes are cached. This means that if I want to assign a role to a different instance, the same role class is applied:
>>> person = Person('Joe') >>> someone = Person('Jane') >>> Biker(someone).__class__ is Biker(person).__class__ True
Changing role application
If for some reason the role should not be directly applied to the instance, another application method can be assigned.
Here is an example that uses the
clone
method:>>> person = Person('Joe') >>> person.__class__ <class 'roles.role.Person'> >>> biker = Biker(person, method=clone) >>> biker <roles.role.Person+Biker object at 0x...> >>> person.__class__ <class 'roles.role.Person'> >>> biker.bike() Joe bikes
- assign(subj: ~roles.role.T, method: ~typing.Callable[[~typing.Type[~roles.role.R], ~roles.role.T], ~roles.role.R] = <function instance>) Union[T, R] ¶
Call is invoked when a role should be assigned to an object.
- played_by(subj: T) Iterator[Union[T, R]] ¶
Shorthand for using roles in with statements.
>>> class Biker(metaclass=RoleType): ... def bike(self): return 'bike, bike' >>> class Person: ... pass >>> john = Person() >>> with Biker.played_by(john): ... john.bike() 'bike, bike'
- revoke(subj: ~roles.role.R, method: ~typing.Callable[[~typing.Type[~roles.role.T], ~roles.role.R], ~roles.role.T] = <function instance>) T ¶
Retract the role from subj.
By default the
instance
strategy is used.
Using roles in a context¶
Roles are played in a context. The roles.context
module provides a
means to access the context from within your roles. Use this to make your
role’s code simpler and more readable.
- roles.context.context(ctxobj, **bindings)¶
The default application wide context stack.
Put a new context class on the context stack. This functionality should be called with the context class as first argument.
>>> class SomeContext: ... pass # define some methods, define some roles ... def execute(self): ... with context(self): ... pass # do something
Roles can be fetched from the context by calling
context.name
. Just like that.You can provide additional bindings to be performed:
>>> from roles.role import RoleType
>>> class SomeRole(metaclass=RoleType): ... pass
>>> class SomeContext: ... def __init__(self, data_object): ... self.data_object = data_object ... def execute(self): ... with context(self, data_object=SomeRole): ... pass # do something
Those bindings are applied when the context is entered (in this case immediately).
- roles.context.in_context(func)¶
Decorator for running methods in context.
The context is the object (self).
Ways to assign roles¶
There are basically 3 ways to assign a role to an instance. The first one is to manipulate the instance’s class (this is the default) and the second is to proxy the object by referencing the same instance dict (Borg pattern). The this (last resort) is to create an adapter on top of the data instance.
Generic roles¶
As an (non-DCI) extension it is possible to create role implementations tailored for specific classes.
Although this may clutter the clear and readable ways provided by DCI, for specific tasks it may help. Use it wisely.