Developing Service Agent using Python Decorators

Managing preprocess and meta-information of function and class using decorators

3 minute read Read this page in 한국어

When I participated in different IoT testbed development project, using Java annotations were very impressive. Within appropriate annotations, information of the service agents were automatically registered to the server, and also UI was generated to let the people use.

So, I want to use such functionalities in our lab’s IoT testbed development, for managing meta-information of agents. And since our project use Python, now I’m trying to use decorator.

For now, I used descorators for (1) applying pre-processing and post-processing to the functions and (2) writing meta-information of functions and classes, as followings.

Function pre-processing and post-processing by using decorators

This is the common form of decorator. When you want to do some pre-processing or post-processing to multiple functions, you can use decorator to decorate the functions.

Basic usage is like below. Decorator receives function as an argument and return a new function that wraps the given function. Then Python interpreter overrides function body with the function that decorator returns, when it defines the function.

def decorator(f):
	def processing(*args, **kwargs):
		print("pre_processing")
		result = f(*args, **kwargs)
		print("post_processing")
		return result
	return processing

@decorator
def function_to_decorate():
	print("function")

>>> function_to_decorate()
>>> "pre_processing"
>>> "function"
>>> "post_processing"

Maintain docstring

Using decorator purely, function’s docstrings will be erased. You can fix this by using wraps decorator.

from functools import wraps

def decorator(f):
	@wraps(f)
	def processing(*args, **kwargs):
		pre_processing()
		result = f(*args, **kwargs)
		post_processing()
		return result
	return processing

@decorator
def function_to_decorate():
	""" function docstring """
	something()

>>> print(function_to_decorate.__doc__)
>>> """ function docstring """

Class’s method decoration

For our IoT testbed, we define agents as classes and the functionalities provided by agents are defined as methods. So decorators needed to decorator methods that has the self argument, and decorators can deal with certain arguments.

def decorator(f):
	def processing(self, *args, **kwargs):
		pre_processing()
		result = f(self, *args, **kwargs)
		post_processing()
		return result
	return processing

class Agent:
	@decorator
	def method_to_decorate(self):
		something()

Example: authentication_required

For example, authentication_required, authorization_required decorators were developed for our testbed. As its names indicates, those decorators perform authentication or authorization and returns exception if it fails.

def authentication_required(f):
	def check_authentication(self, *args, **kwargs):
		result = authentication()
		if result == 'success':
			return f(self, *args, **kwargs)
		else:
			raise AuthenticationException
	return check_authentication

Decorator with input

Moreover, decorators may receive input argument and process according to the inputs. For this, definition of decorator becomes one step deeper.

def decorator(arguments):
	def wrapper(f):
		def processing(*args, **kwargs):
			pre_processing(arguments)
			result = f(*args, **kwargs)
			post_processing(arguments)
			return result
		return processing
	return wrapper

Pass variable from decorator to function

Decorators can pass variable to function after do some pre-process. There are two ways for this: (1) overriding function’s argument or (2) add value to kwargs.

def decorator(f):
	def processing(*args, **kwargs):
		return f(value, *args, **kwargs)
	return processing

@decorator
def function_to_decorate(value):  # override the argument
	print(value)
def decorator(f):
	def processing(*args, **kwargs):
		kwargs["key"] = value
		return f(*args, **kwargs)
	return processing

@decorator
def function_to_decorate(*args, **kwargs):
	print(kwargs["key"])

Example: resource_required

In out testbed, we developed a decorator resource_required. It receives description of required resource for the agent. Then it discovers and binds it and pass the information for controlling the resource, to the function. After using the resource, decorator can unbind/release the resource automatically.

def resource_required(resource_description):
	def wrapper(f):
		def processing(self, *args, **kwargs):
			resource, success = bind_resource(resource_description)
			
			if success:
				result = f(self, resource, *args, **kwargs)
				unbind_resource(resource)
			else:
				raise ResourceBindFailedException

class Agent:
	@resource_required(url='localhost:8001')
	def action(self, resource=None):
		resource.utilize()

Decorators can be defined in a form of class, but skip that since we haven’t used it for this testbed development.


Meta-information decorator for functions and classes

It was great to do pre-processing and post-processing using decorators, I also wanted to add meta-information to functions and classes automatically. After some searching, I succeed it with decorators.

In this case, decorator should work at definition phase, and not execution phase, so grammar becomes a little bit different. Decorator returns function itself instead of ‘a new wrapper function that execute original function and returns the result’.

def meta_info_decorator(name, description):
	def decorator(f):
		f.meta_info = {
			"name": name,
			"description": description
		}
		return f
	return decorator

@meta_info_decorator(
    name="function", 
    description="example description"
)
def function():
	pass

>>> print(function.meta_info)
>>> {'name': 'function', 'description': 'example description'}

Class decoration

Furthermore, not only functions but also classes can be decorated. Grammar is same as functions.

def class_decorator(name, description):
	def decorator(cls):
		cls.meta_info = {
			"name": name,
			"description": description
		}
		return cls
	return decorator

@class_decorator(
    name="class", 
    description="example description"
)
class Agent:
	pass

>>> print(Agent.meta_info)
>>> {'name': 'class', 'description': 'example description'}

Example: Service Agent

Finally, the example of our service agent becomes like below, as a summary of above usages.

@meta_info(
	name="LightingServiceAgent",
	description="Service agent for lighting of the testbed",
	url="localhost:8000"
)
class LightingServiceAgent(Agent):
	@authorization_required
	@resource_required(
		type="LightingDevice"
	)
	def turnon(self, resource):
		resource.turnon()

It was great to automate many functionalities clearly.

Leave a comment