Description
import attrsx
import attrs
Usage
1. Built-in logger
One of the primary extensions in attrsx
is automatic logging. It can be accessed via self.logger
in any attrsx
-decorated class.
Basic Logger Usage
@attrsx.define
class ProcessData:
data: str = attrs.field(default=None)
def run(self):
self.logger.info("Running data processing...")
self.logger.debug(f"Processing data: {self.data}")
return f"Processed: {self.data}"
ProcessData(data = "data").run()
INFO:ProcessData:Running data processing...
'Processed: data'
Logger Configuration
The logging behavior can be customized using the following optional attributes:
loggerLvl
: Sets the log level (fromlogging
), defaults tologging.INFO
.logger_name
: Specifies the logger name; defaults to the class name.logger_format
: Sets the logging message format, defaults to%(levelname)s:%(name)s:%(message)s
.
self.logger
becomes available starting from __attrs_post_init__
.
import logging
@attrsx.define
class VerboseProcess:
data: str = attrs.field(default=None)
loggerLvl: int = attrs.field(default=logging.DEBUG)
logger_format: str = attrs.field(
default="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
def __attrs_post_init__(self):
self.logger.info("Custom post-init logic")
self.data = "DATA"
def run(self):
self.logger.debug("Processing %s", self.data)
return f"Processed: {self.data}"
VerboseProcess(data = "data").run()
2025-05-01 16:04:38,304 - VerboseProcess - INFO - Custom post-init logic
2025-05-01 16:04:38,305 - VerboseProcess - DEBUG - Processing DATA
'Processed: DATA'
Using External Loggers
An external, pre-initialized logger can also be provided to the class using the logger
attribute.
shared_logger = ProcessData().logger
VerboseProcess(
data = "data",
logger = shared_logger
).run()
INFO:ProcessData:Custom post-init logic
'Processed: DATA'
2. Built-in handlers
Another extension in attrsx
is built-in handlers
. This feature is meant to help plug interchangeable helper objects (“handlers”) into a host class declaratively, without manual wiring, in a way that allows for both providing initialized handlers as well as initializing handlers within a class.
The main class has access to methods of handler classes, can reinitialize them or reset them in a well defined way, where most additional code is added automatically by the library to the class.
Adding handlers to a class
To add handlers to an attrsx
class, one can take advantage of handler_specs
parameter within @attrsx.define
, which takes a dictionary, where key is alias for the handler and value is the handler class.
@attrsx.define(handler_specs={"procd": ProcessData})
class Service:
def run(self, data: str):
self.logger.info("Calling procd handler")
self._initialize_procd_h(uparams={"data": data})
return self.procd_h.run()
Service().run("some data")
INFO:Service:Calling procd handler
INFO:ProcessData:Running data processing...
'Processed: some data'
For each handler in provided via handler_specs
in definition of NewClass
as :
@attrsx.define(handler_specs = {
'handler_alias' : HandlerClass, ...,
'another_handler_alias_n' : AnotherHandlerClass})
class NewClass:
...
the class gets the following attributes:
{handler_alias}_h
: an instance of the handler, by default set toNone
{handler_alias}_class
: a class of the handler, will be used if corresponding instance is None, when initialized{handler_alias}_params
: parameters that should be used for creating new instance of the handler, using handler class
and a function:
def _initialize_{handler_alias}_h(self, params : dict = None, uparams : dict = None):
if params is None:
params = self.{handler_alias}_params
if uparams is not None:
params.update(uparams)
if self.{handler_alias}_h is None:
self.{handler_alias}_h = self.{handler_alias}_class(**params)
which checks is initialized instance was already provided and if not, initializes handler with provided parameters.
To achieve the same with regular attrs
, the NewClass
could be defined in the following way, which would work exactly the same:
@attrs.define
class NewClass:
...
handler_alias_h = attrs.field(default=None)
handler_alias_class = attrs.field(default=HandlerClass)
handler_alias_params = attrs.field(default={})
another_handler_alias_n_h = attrs.field(default=None)
another_handler_alias_n_class = attrs.field(default=AnotherHandlerClass)
another_handler_alias_n_params = attrs.field(default={})
logger_chaining = attrs.field(default={
'loggerLvl' : True,
'logger' : False,
'logger_format' : True})
def _apply_logger_chaining(self, handler_class, params):
if self.logger_chaining.get("logger"):
if ('logger' in handler_class.__dict__) \
and "logger" not in params.keys():
params["logger"] = self.logger
if self.logger_chaining.get("loggerLvl"):
if ('loggerLvl' in handler_class.__dict__) \
and "loggerLvl" not in params.keys():
params["loggerLvl"] = self.loggerLvl
if self.logger_chaining.get("logger_format"):
if ('logger_format' in handler_class.__dict__) \
and "logger_format" not in params.keys():
params["logger_format"] = self.logger_format
return params
def _initialize_handler_alias_h(self, params : dict = None, uparams : dict = None):
if params is None:
params = self.handler_alias_params
if uparams is not None:
params.update(uparams)
params = self._apply_logger_chaining(
handler_class = self.handler_alias_class,
params = params)
if self.handler_alias_h is None:
self.handler_alias_n_h = self.handler_alias_class(**params)
def _initialize_another_handler_alias_n_h(self, params : dict = None, uparams : dict = None):
if params is None:
params = self.another_handler_alias_n_params
if uparams is not None:
params.update(uparams)
params = self._apply_logger_chaining(
handler_class = self.another_handler_alias_n_class,
params = params)
if self.another_handler_alias_n_h is None:
self.another_handler_alias_n_h = self.another_handler_alias_n_class(**params)
Setting default parameters
For each handler there is {handler_alias}_params
within new class, which can be used to provide parameters for handler initialization.
Sometimes there is a need to extend or update default parameters and initialize/reinitialize the handler. Each handler has _initialize_{handler_alias}_h
method within new class to which new default params (parameters that one would use when initializing handler class) could be passed via params
and update to these or {handler_alias}_params
via uparams
.
@attrsx.define(handler_specs = {'procd' : ProcessData})
class Service:
data: str = attrs.field(default=None)
procd_params = attrs.field(default={"loggerLvl" : logging.DEBUG})
def run(self, data : str):
self.logger.info("Running method from procd handler!")
self._initialize_procd_h(uparams={"data" : data})
return self.procd_h.run()
Service().run(data = "some data")
INFO:Service:Running method from procd handler!
INFO:ProcessData:Running data processing...
DEBUG:ProcessData:Processing data: some data
'Processed: some data'
Adding handler initialization to class post init
One of the benefits of using attrs
is the ability to define what happens when class in initialized without making the whole __init__
, by using __attrs_post_init__
. Some handlers could be added there to be initialized with a new class and rdy to be used within its methods.
@attrsx.define(handler_specs = {'procd' : ProcessData})
class Service:
data: str = attrs.field(default=None)
procd_params = attrs.field(default={"data" : "default data"})
def __attrs_post_init__(self):
self._initialize_procd_h()
def run(self, data : str = None):
self.logger.info("Running method from procd handler!")
return self.procd_h.run()
Service().run()
INFO:Service:Running method from procd handler!
INFO:ProcessData:Running data processing...
'Processed: default data'
Using instances of handlers initialized outside of new class
Each new class defined with handler_specs
can use initialized instances of handlers and skip initialization within new class, which allows the code to remain flexible.
@attrsx.define(handler_specs = {'procd' : ProcessData})
class Service:
data: str = attrs.field(default=None)
procd_params = attrs.field(default={"data" : "default data"})
def __attrs_post_init__(self):
self._initialize_procd_h()
def run(self, data : str = None):
self.logger.info("Running method from procd handler!")
return self.procd_h.run()
outside_procd = ProcessData(data = 'some other data')
Service(procd_h=outside_procd).run()
INFO:Service:Running method from procd handler!
INFO:ProcessData:Running data processing...
'Processed: some other data'
Chaining loggers
Each attrsx
class has its own independent built-in logger, it might be useful to control behaviour of handler loggers from main class (for handlers that themselves are attrsx
classes). This package allows to chain loggers of attrsx
classes on 3 different levels via logger_chaining
boolean parameters in @attrsx.define
:
logger_format
: synchronizes logger format for allattrsx
handlers (by default set toTrue
)loggerLvl
: synchronizes logger level for allattrsx
handlers (by default set toTrue
)logger
: uses logger defined for main class within handlers (by default set toFalse
)
@attrsx.define(handler_specs = {'procd' : ProcessData})
class ChainedService:
data: str = attrs.field(default=None)
procd_params = attrs.field(default={"data" : "default data"})
loggerLvl = attrs.field(default=logging.DEBUG)
logger_format = attrs.field(default="%(levelname)s - %(name)s - %(message)s")
def __attrs_post_init__(self):
self._initialize_procd_h()
def run(self, data : str = None):
self.logger.info("Running method from procd handler!")
return self.procd_h.run()
ChainedService().run()
INFO - ChainedService - Running method from procd handler!
INFO - ProcessData - Running data processing...
DEBUG - ProcessData - Processing data: default data
'Processed: default data'
@attrsx.define(handler_specs = {'procd' : ProcessData}, logger_chaining={'logger' : True})
class ChainedService:
data: str = attrs.field(default=None)
procd_params = attrs.field(default={"data" : "default data"})
loggerLvl = attrs.field(default=logging.DEBUG)
logger_format = attrs.field(default="%(levelname)s - %(name)s - %(message)s")
def __attrs_post_init__(self):
self._initialize_procd_h()
def run(self, data : str = None):
self.logger.info("Running method from procd handler!")
return self.procd_h.run()
ChainedService().run()
INFO - ChainedService - Running method from procd handler!
INFO - ChainedService - Running data processing...
DEBUG - ChainedService - Processing data: default data
'Processed: default data'