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 the RoleType 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 the asign() method to create proxy instances (the default function is called instance() 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.