A lightweight extension of attrs that adds extras like logging.
Project description
Attrsx
attrsx – An Extension to attrs for Declarative Handler Injection & Integrated Logging
1. Purpose
attrsx builds on top of the attrs data‑class library and contributes two orthogonal capabilities that are frequently needed in service‑layer and infrastructure code:
- Integrated Logging – transparent, boilerplate‑free creation of a configured
logging.Loggerfor every instance. - Declarative Handler Injection – generation of lazy‑initialised strategy objects (“handlers”) declared via
handler_specs.
Both features are opt‑in; if a class does not request them, attrsx.define behaves exactly like attrs.define.
2. Key Features
-
Drop-in replacement for
attrs.define– forwards every keyword parameter thatattrs.defineunderstands (slots,frozen,kw_only, validators, converters, …). -
Integrated logging – injects four fields (
logger,loggerLvl,logger_name,logger_format) and wraps__attrs_post_init__to attaches alogging.loggerto the instance if none is present. The logger’s name defaults to the class name; level and format are configurable per instance. -
Declarative handler injection – for each entry in
handler_specs={"key": HandlerClass, ...}the decorator adds three fields
(<key>_class,<key>_params,<key>_h) plus a lazy factory method_initialize_<key>_h(), which calls
<key>_class(**<key>_params)the first time it is invoked. -
Type-hint compliance – all generated attributes are inserted into
__annotations__, so static type-checkers recognise them. -
Zero runtime overhead – field and method generation is performed once, at import time; normal instance construction stays on the
attrsfast path.
3. Underlying Pattern
The handler mechanism realises Declarative Handler Injection (a variant of the Strategy pattern implemented through composition and a lazy factory method). Configuration is expressed as data, leaving the host class agnostic of concrete strategy classes.
4. When to Use
- You need consistent, ready‑to‑use logging across many small classes.
- You want to supply interchangeable helper or strategy objects without manual glue code.
- You prefer composition over inheritance but dislike factory boilerplate.
If none of the above apply, simply continue to use attrs.define; the migration path is one import statement.
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(f"Processing {self.data}")
self.logger.debug(dline=True)
return f"Processed: {self.data}"
VerboseProcess(data = "data").run()
2025-12-29 20:36:10,019 - VerboseProcess - INFO - Custom post-init logic
2025-12-29 20:36:10,020 - VerboseProcess - DEBUG - Processing DATA
2025-12-29 20:36:10,020 - VerboseProcess - DEBUG -
'Processed: DATA'
Using External Loggers
Non standard loggers can enable additional functionality and normally ignored inputs that would not break normal logger, when attrsx is used.
@attrsx.define
class ExtraLogger:
def info(self, msg):
self.logger.info(msg)
def debug(self, msg = None, dline = False, *args, **kwargs):
if dline is True:
msg = "=" * 50
self.logger.debug(msg, *args, **kwargs)
An external, pre-initialized logger can also be provided to the class using the logger attribute.
extra_logger = ExtraLogger(loggerLvl=logging.DEBUG)
VerboseProcess(
data = "data",
logger = extra_logger
).run()
INFO:ExtraLogger:Custom post-init logic
DEBUG:ExtraLogger:Processing DATA
DEBUG:ExtraLogger:==================================================
'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 allattrsxhandlers (by default set toTrue)loggerLvl: synchronizes logger level for allattrsxhandlers (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'
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file attrsx-0.2.0.tar.gz.
File metadata
- Download URL: attrsx-0.2.0.tar.gz
- Upload date:
- Size: 608.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
649d66e54435cb06e0bfa88f108790f427213cae287d9a8f2eaa3eabbf61af6b
|
|
| MD5 |
49c1caeb81aaf866abf0cefe70157a3b
|
|
| BLAKE2b-256 |
d6504c60fb893e3483a511c39ed282413916b22fd571f32ecc333149c849f145
|
File details
Details for the file attrsx-0.2.0-py3-none-any.whl.
File metadata
- Download URL: attrsx-0.2.0-py3-none-any.whl
- Upload date:
- Size: 648.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
509035a5ae5ee92f2691472598788af8a77601898ea00cb0bac02234dcdf3a6f
|
|
| MD5 |
3b7be0dfb02a7ae48f7356b217f79d02
|
|
| BLAKE2b-256 |
a1a57d05ddc85e381a852e8f3bf56a41b9eccdd6b3e13ba228f2d64e93e7d471
|