Name | izulu JSON |
Version |
0.5.3
JSON |
| download |
home_page | None |
Summary | The exceptional library |
upload_time | 2024-12-10 00:07:30 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.6 |
license | Copyright (c) 2023-2024 Dmitry Burmistrov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization. |
keywords |
error
exception
oop
izulu
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
izulu
#####
.. image:: https://gitlab.com/uploads/-/system/project/avatar/50698236/izulu_logo_512.png?width=128
|
*"The exceptional library"*
|
**Installation**
::
pip install izulu
Presenting ``izulu``
********************
Bring OOP into exception/error management
=========================================
You can read docs *from top to bottom* or jump straight into **"Quickstart"** section.
For details note **"Specifications"** sections below.
Neat #1: Stop messing with raw strings and manual message formatting
--------------------------------------------------------------------
.. code-block:: python
if not data:
raise ValueError("Data is invalid: no data")
amount = data["amount"]
if amount < 0:
raise ValueError(f"Data is invalid: amount can't be negative ({amount})")
elif amount > 1000:
raise ValueError(f"Data is invalid: amount is too large ({amount})")
if data["status"] not in {"READY", "IN_PROGRESS"}:
raise ValueError("Data is invalid: unprocessable status")
With ``izulu`` you can forget about manual error message management all over the codebase!
.. code-block:: python
class ValidationError(Error):
__template__ = "Data is invalid: {reason}"
class AmountValidationError(ValidationError):
__template__ = "Invalid amount: {amount}"
if not data:
raise ValidationError(reason="no data")
amount = data["amount"]
if amount < 0:
raise AmountValidationError(reason="amount can't be negative", amount=amount)
elif amount > 1000:
raise AmountValidationError(reason="amount is too large", amount=amount)
if data["status"] not in {"READY", "IN_PROGRESS"}:
raise ValidationError(reason="unprocessable status")
Provide only variable data for error instantiations. Keep static data within error class.
Under the hood ``kwargs`` are used to format ``__template__`` into final error message.
Neat #2: Attribute errors with useful fields
--------------------------------------------
.. code-block:: python
from falcon import HTTPBadRequest
class AmountValidationError(ValidationError):
__template__ = "Data is invalid: {reason} ({amount})"
amount: int
try:
validate(data)
except AmountValidationError as e:
if e.amount < 0:
raise HTTPBadRequest(f"Bad amount: {e.amount}")
raise
Annotated instance attributes automatically populated from ``kwargs``.
Neat #3: Static and dynamic defaults
------------------------------------
.. code-block:: python
class AmountValidationError(ValidationError):
__template__ = "Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}"
_MAX: ClassVar[int] = 1000
amount: int
reason: str = "amount is too large"
ts: datetime = factory(datetime.now)
print(AmountValidationError(amount=15000))
# Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 22:59:25.132699
print(AmountValidationError(amount=-1, reason="amount can't be negative"))
# Data is invalid: amount can't be negative (-1; MAX=1000) at 2024-01-13 22:59:54.482577
Quickstart
==========
.. note::
**Prepare playground**
::
pip install ipython izulu
ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'
Let's start with defining our initial error class (exception)
-------------------------------------------------------------
#. subclass ``Error``
#. provide special message template for each of your exceptions
#. use **only kwargs** to instantiate exception *(no more message copying across the codebase)*
.. code-block:: python
class MyError(Error):
__template__ = "Having count={count} for owner={owner}"
print(MyError(count=10, owner="me"))
# MyError: Having count=10 for owner=me
MyError(10, owner="me")
# TypeError: __init__() takes 1 positional argument but 2 were given
Move on and improve our class with attributes
---------------------------------------------
#. define annotations for fields you want to publish as exception instance attributes
#. you have to define desired template fields in annotations too
(see ``AttributeError`` for ``owner``)
#. you can provide annotation for attributes not included in template (see ``timestamp``)
#. **type hinting from annotations are not enforced or checked** (see ``timestamp``)
.. code-block:: python
class MyError(Error):
__template__ = "Having count={count} for owner={owner}"
count: int
timestamp: datetime
e = MyError(count=10, owner="me", timestamp=datetime.now())
print(e.count)
# 10
print(e.timestamp)
# 2023-09-27 18:18:22.957925
e.owner
# AttributeError: 'MyError' object has no attribute 'owner'
We can provide defaults for our attributes
------------------------------------------
#. define *default static values* after field annotation just as usual
#. for *dynamic defaults* use provided ``factory`` tool with your callable - it would be
evaluated without arguments during exception instantiation
#. now fields would receive values from ``kwargs`` if present - otherwise from *defaults*
.. code-block:: python
class MyError(Error):
__template__ = "Having count={count} for owner={owner}"
count: int
owner: str = "nobody"
timestamp: datetime = factory(datetime.now)
e = MyError(count=10)
print(e.count)
# 10
print(e.owner)
# nobody
print(e.timestamp)
# 2023-09-27 18:19:37.252577
Dynamic defaults also supported
-------------------------------
.. code-block:: python
class MyError(Error):
__template__ = "Having count={count} for owner={owner}"
count: int
begin: datetime
owner: str = "nobody"
timestamp: datetime = factory(datetime.now)
duration: timedelta = factory(lambda self: self.timestamp - self.begin, self=True)
begin = datetime.fromordinal(date.today().toordinal())
e = MyError(count=10, begin=begin)
print(e.begin)
# 2023-09-27 00:00:00
print(e.duration)
# 18:45:44.502490
print(e.timestamp)
# 2023-09-27 18:45:44.502490
* very similar to dynamic defaults, but callable must accept single
argument - your exception fresh instance
* **don't forget** to provide second ``True`` argument for ``factory`` tool
(keyword or positional - doesn't matter)
Specifications
**************
``izulu`` bases on class definitions to provide handy instance creation.
The 6 pillars of ``izulu``
==========================
* all behavior is defined on the class-level
* ``__template__`` class attribute defines the template for target error message
* template may contain *"fields"* for substitution from ``kwargs`` and *"defaults"* to produce final error message
* ``__features__`` class attribute defines constraints and behaviour (see "Features" section below)
* by default all constraints are enabled
* *"class hints"* annotated with ``ClassVar`` are noted by ``izulu``
* annotated class attributes normally should have values (treated as *"class defaults"*)
* *"class defaults"* can only be static
* *"class defaults"* may be referred within ``__template__``
* *"instance hints"* regularly annotated (not with ``ClassVar``) are noted by ``izulu``
* all annotated attributes are treated as *"instance attributes"*
* each *"instance attribute"* will automatically obtain value from the ``kwarg`` of the same name
* *"instance attributes"* with default are also treated as *"instance defaults"*
* *"instance defaults"* may be **static and dynamic**
* *"instance defaults"* may be referred within ``__template__``
* ``kwargs`` — the new and main way to form exceptions/error instance
* forget about creating exception instances from message strings
* ``kwargs`` are the datasource for template *"fields"* and *"instance attributes"*
(shared input for templating attribution)
.. warning:: **Types from type hints are not validated or enforced!**
Mechanics
=========
.. note::
**Prepare playground**
::
pip install ipython izulu
ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'
* inheritance from ``izulu.root.Error`` is required
.. code-block:: python
class AmountError(Error):
pass
* **optionally** behaviour can be adjusted with ``__features__`` (not recommended)
.. code-block:: python
class AmountError(Error):
__features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS
* you should provide a template for the target error message with ``__template__``
.. code-block:: python
class AmountError(Error):
__template__ = "Data is invalid: {reason} (amount={amount})"
print(AmountError(reason="negative amount", amount=-10.52))
# [2024-01-23 19:16] Data is invalid: negative amount (amount=-10.52)
* sources of formatting arguments:
* *"class defaults"*
* *"instance defaults"*
* ``kwargs`` (overlap any *"default"*)
* new style formatting is used:
.. code-block:: python
class AmountError(Error):
__template__ = "[{ts:%Y-%m-%d %H:%M}] Data is invalid: {reason:_^20} (amount={amount:06.2f})"
print(AmountError(ts=datetime.now(), reason="negative amount", amount=-10.52))
# [2024-01-23 19:16] Data is invalid: __negative amount___ (amount=-10.52)
* ``help(str.format)``
* https://pyformat.info/
* https://docs.python.org/3/library/string.html#format-string-syntax
.. warning::
There is a difference between docs and actual behaviour:
https://discuss.python.org/t/format-string-syntax-specification-differs-from-actual-behaviour/46716
* only named fields are allowed
* positional (digit) and empty field are forbidden
* error instantiation requires data to format ``__template__``
* all data for ``__template__`` fields must be provided
.. code-block:: python
class AmountError(Error):
__template__ = "Data is invalid: {reason} (amount={amount})"
print(AmountError(reason="amount can't be negative", amount=-10))
# Data is invalid: amount can't be negative (amount=-10)
AmountError()
# TypeError: Missing arguments: 'reason', 'amount'
AmountError(amount=-10)
# TypeError: Missing arguments: 'reason'
* only named arguments allowed: ``__init__()`` accepts only ``kwargs``
.. code-block:: python
class AmountError(Error):
__template__ = "Data is invalid: {reason} (amount={amount})"
print(AmountError(reason="amount can't be negative", amount=-10))
# Data is invalid: amount can't be negative (amount=-10)
AmountError("amount can't be negative", -10)
# TypeError: __init__() takes 1 positional argument but 3 were given
AmountError("amount can't be negative", amount=-10)
# TypeError: __init__() takes 1 positional argument but 2 were given
* *"class defaults"* can be defined and used
* *"class defaults"* must be type hinted with ``ClassVar`` annotation and provide static values
* template *"fields"* may refer *"class defaults"*
.. code-block:: python
class AmountError(Error):
LIMIT: ClassVar[int] = 10_000
__template__ = "Amount is too large: amount={amount} limit={LIMIT}"
amount: int
print(AmountError(amount=10_500))
# Amount is too large: amount=10500 limit=10000
* *"instance attributes"* are populated from relevant ``kwargs``
.. code-block:: python
class AmountError(Error):
amount: int
print(AmountError(amount=-10).amount)
# -10
* instance and class attribute types from **annotations are not validated or enforced**
(``izulu`` uses type hints just for attribute discovery and only ``ClassVar`` marker
is processed for instance/class segregation)
.. code-block:: python
class AmountError(Error):
amount: int
print(AmountError(amount="lots of money").amount)
# lots of money
* static *"instance defaults"* can be provided regularly with instance type hints and static values
.. code-block:: python
class AmountError(Error):
amount: int = 500
print(AmountError().amount)
# 500
* dynamic *"instance defaults"* are also supported
* they must be type hinted and have special value
* value must be a callable object wrapped with ``factory`` helper
* ``factory`` provides 2 modes depending on value of the ``self`` flag:
* ``self=False`` (default): callable accepting no arguments
.. code-block:: python
class AmountError(Error):
ts: datetime = factory(datetime.now)
print(AmountError().ts)
# 2024-01-23 23:18:22.019963
* ``self=True``: provide callable accepting single argument (error instance)
.. code-block:: python
class AmountError(Error):
LIMIT = 10_000
amount: int
overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)
print(AmountError(amount=10_500).overflow)
# 500
* *"instance defaults"* and *"instance attributes"* may be referred in ``__template__``
.. code-block:: python
class AmountError(Error):
__template__ = "[{ts:%Y-%m-%d %H:%M}] Amount is too large: {amount}"
amount: int
ts: datetime = factory(datetime.now)
print(AmountError(amount=10_500))
# [2024-01-23 23:21] Amount is too large: 10500
* *Pause and sum up: defaults, attributes and template*
.. code-block:: python
class AmountError(Error):
LIMIT: ClassVar[int] = 10_000
__template__ = "[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}"
amount: int
overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)
ts: datetime = factory(datetime.now)
err = AmountError(amount=15_000)
print(err.amount)
# 15000
print(err.LIMIT)
# 10000
print(err.overflow)
# 5000
print(err.ts)
# 2024-01-23 23:21:26
print(err)
# [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000
* ``kwargs`` overlap *"instance defaults"*
.. code-block:: python
class AmountError(Error):
LIMIT: ClassVar[int] = 10_000
__template__ = "[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}"
amount: int = 15_000
overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)
ts: datetime = factory(datetime.now)
print(AmountError())
# [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000
print(AmountError(amount=10_333, overflow=42, ts=datetime(1900, 1, 1)))
# [2024-01-23 23:21] Amount is too large: amount=10333 limit=10000 overflow=42
* ``izulu`` provides flexibility for templates, fields, attributes and defaults
* *"defaults"* are not required to be ``__template__`` *"fields"*
.. code-block:: python
class AmountError(Error):
LIMIT: ClassVar[int] = 10_000
__template__ = "Amount is too large"
print(AmountError().LIMIT)
# 10000
print(AmountError())
# Amount is too large
* there can be hints for attributes not present in error message template
.. code-block:: python
class AmountError(Error):
__template__ = "Amount is too large"
amount: int
print(AmountError(amount=500).amount)
# 500
print(AmountError(amount=500))
# Amount is too large
* *"fields"* don't have to be hinted as instance attributes
.. code-block:: python
class AmountError(Error):
__template__ = "Amount is too large: {amount}"
print(AmountError(amount=500))
# Amount is too large: 500
print(AmountError(amount=500).amount)
# AttributeError: 'AmountError' object has no attribute 'amount'
Features
========
The ``izulu`` error class behaviour is controlled by ``__features__`` class attribute.
(For details about "runtime" and "class definition" stages
see **Validation and behavior in case of problems** below)
Supported features
------------------
* ``FORBID_MISSING_FIELDS``: checks provided ``kwargs`` contain data for all template *"fields"*
and *"instance attributes"* that have no *"defaults"*
* always should be enabled (provides consistent and detailed ``TypeError`` exceptions
for appropriate arguments)
* if disabled raw exceptions from ``izulu`` machinery internals could appear
======= =============
Stage Raises
======= =============
runtime ``TypeError``
======= =============
.. code-block:: python
class AmountError(Error):
__template__ = "Some {amount} of money for {client_id} client"
client_id: int
# I. enabled
AmountError()
# TypeError: Missing arguments: client_id, amount
# II. disabled
AmountError.__features__ ^= Features.FORBID_MISSING_FIELDS
AmountError()
# ValueError: Failed to format template with provided kwargs:
* ``FORBID_UNDECLARED_FIELDS``: forbids undefined arguments in provided ``kwargs``
(names not present in template *"fields"* and *"instance/class hints"*)
* if disabled allows and **completely ignores** unknown data in ``kwargs``
======= =============
Stage Raises
======= =============
runtime ``TypeError``
======= =============
.. code-block:: python
class MyError(Error):
__template__ = "My error occurred"
# I. enabled
MyError(unknown_data="data")
# Undeclared arguments: unknown_data
# II. disabled
MyError.__features__ ^= Features.FORBID_UNDECLARED_FIELDS
err = MyError(unknown_data="data")
print(err)
# Unspecified error
print(repr(err))
# __main__.MyError(unknown_data='data')
err.unknown_data
# AttributeError: 'MyError' object has no attribute 'unknown_data'
* ``FORBID_KWARG_CONSTS``: checks provided ``kwargs`` not to contain attributes defined as ``ClassVar``
* if disabled allows data in ``kwargs`` to overlap class attributes during template formatting
* overlapping data won't modify class attribute values
======= =============
Stage Raises
======= =============
runtime ``TypeError``
======= =============
.. code-block:: python
class MyError(Error):
__template__ = "My error occurred {_TYPE}"
_TYPE: ClassVar[str]
# I. enabled
MyError(_TYPE="SOME_ERROR_TYPE")
# TypeError: Constants in arguments: _TYPE
# II. disabled
MyError.__features__ ^= Features.FORBID_KWARG_CONSTS
err = MyError(_TYPE="SOME_ERROR_TYPE")
print(err)
# My error occurred SOME_ERROR_TYPE
print(repr(err))
# __main__.MyError(_TYPE='SOME_ERROR_TYPE')
err._TYPE
# AttributeError: 'MyError' object has no attribute '_TYPE'
* ``FORBID_NON_NAMED_FIELDS``: forbids empty and digit field names in ``__template__``
* if disabled validation (runtime issues)
* ``izulu`` relies on ``kwargs`` and named fields
* by default it's forbidden to provide empty (``{}``) and digit (``{0}``) fields in ``__template__``
================ ==============
Stage Raises
================ ==============
class definition ``ValueError``
================ ==============
.. code-block:: python
class MyError(Error):
__template__ = "My error occurred {_TYPE}"
_TYPE: ClassVar[str]
# I. enabled
MyError(_TYPE="SOME_ERROR_TYPE")
# TypeError: Constants in arguments: _TYPE
# II. disabled
MyError.__features__ ^= Features.FORBID_KWARG_CONSTS
err = MyError(_TYPE="SOME_ERROR_TYPE")
print(err)
# My error occurred SOME_ERROR_TYPE
print(repr(err))
# __main__.MyError(_TYPE='SOME_ERROR_TYPE')
err._TYPE
# AttributeError: 'MyError' object has no attribute '_TYPE'
Tuning ``__features__``
-----------------------
Features are represented as *"Flag Enum"*, so you can use regular operations
to configure desired behaviour.
Examples:
* Use single option
.. code-block:: python
class AmountError(Error):
__features__ = Features.FORBID_MISSING_FIELDS
* Use presets
.. code-block:: python
class AmountError(Error):
__features__ = Features.NONE
* Combining wanted features:
.. code-block:: python
class AmountError(Error):
__features__ = Features.FORBID_MISSING_FIELDS | Features.FORBID_KWARG_CONSTS
* Discarding unwanted feature from default feature set:
.. code-block:: python
class AmountError(Error):
__features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS
Validation and behavior in case of problems
===========================================
``izulu`` may trigger native Python exceptions on invalid data during validation process.
By default you should expect following ones
* ``TypeError``: argument constraints issues
* ``ValueError``: template and formatting issues
Some exceptions are *raised from* original exception (e.g. template formatting issues),
so you can check ``e.__cause__`` and traceback output for details.
The validation behavior depends on the set of enabled features.
Changing feature set may cause different and raw exceptions being raised.
Read and understand **"Features"** section to predict and experiment with different situations and behaviours.
``izulu`` has **2 validation stages:**
* class definition stage
* validation is made during error class definition
.. code-block:: python
# when you import error module
from izulu import root
# when you import error from module
from izulu.root import Error
# when you interactively define new error classes
class MyError(Error):
pass
* class attributes ``__template__`` and ``__features__`` are validated
.. code-block:: python
class MyError(Error):
__template__ = "Hello {}"
# ValueError: Field names can't be empty
* runtime stage
* validation is made during error instantiation
.. code-block:: python
root.Error()
* ``kwargs`` are validated according to enabled features
.. code-block:: python
class MyError(Error):
__template__ = "Hello {name}"
MyError()
# TypeError: Missing arguments: 'name'
Additional APIs
===============
Representations
---------------
.. code-block:: python
class AmountValidationError(Error):
__template__ = "Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}"
_MAX: ClassVar[int] = 1000
amount: int
reason: str = "amount is too large"
ts: datetime = factory(datetime.now)
err = AmountValidationError(amount=15000)
print(str(err))
# Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586
print(repr(err))
# __main__.AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')
* ``str`` and ``repr`` output differs
* ``str`` is for humans and Python (Python dictates the result to be exactly and only the message)
* ``repr`` allows to reconstruct the same error instance from its output
(if data provided into ``kwargs`` supports ``repr`` the same way)
**note:** class name is fully qualified name of class (dot-separated module full path with class name)
.. code-block:: python
reconstructed = eval(repr(err).replace("__main__.", "", 1))
print(str(reconstructed))
# Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586
print(repr(reconstructed))
# AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')
* in addition to ``str`` there is another human-readable representations provided by ``.as_str()`` method;
it prepends message with class name:
.. code-block:: python
print(err.as_str())
# AmountValidationError: Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586
Pickling, dumping and loading
-----------------------------
Pickling
""""""""
``izulu``-based errors **support pickling** by default.
Dumping
"""""""
* ``.as_kwargs()`` dumps shallow copy of original ``kwargs``
.. code-block:: python
err.as_kwargs()
# {'amount': 15000}
* ``.as_dict()`` by default, combines original ``kwargs`` and all *"instance attribute"* values into *"full state"*
.. code-block:: python
err.as_dict()
# {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large'}
Additionally, there is the ``wide`` flag for enriching the result with *"class defaults"*
(note additional ``_MAX`` data)
.. code-block:: python
err.as_dict(True)
# {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large', '_MAX': 1000}
Data combination process follows prioritization — if there are multiple values for same name then high priority data
will overlap data with lower priority. Here is the prioritized list of data sources:
#. ``kwargs`` (max priority)
#. *"instance attributes"*
#. *"class defaults"*
Loading
"""""""
* ``.as_kwargs()`` result can be used to create **inaccurate** copy of original error,
but pay attention to dynamic factories — ``datetime.now()``, ``uuid()`` and many others would produce new values
for data missing in ``kwargs`` (note ``ts`` field in the example below)
.. code-block:: python
inaccurate_copy = AmountValidationError(**err.as_kwargs())
print(inaccurate_copy)
# Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080
print(repr(inaccurate_copy))
# __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))
* ``.as_dict()`` result can be used to create **accurate** copy of original error;
flag ``wide`` should be ``False`` by default according to ``FORBID_KWARG_CONSTS`` restriction
(if you disable ``FORBID_KWARG_CONSTS`` then you may need to use ``wide=True`` depending on your situation)
.. code-block:: python
accurate_copy = AmountValidationError(**err.as_dict())
print(accurate_copy)
# Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080
print(repr(accurate_copy))
# __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))
(advanced) Wedge
----------------
There is a special method you can override and additionally manage the machinery.
But it should not be need in 99,9% cases. Avoid it, please.
.. code-block:: python
def _hook(self,
store: _utils.Store,
kwargs: dict[str, t.Any],
msg: str) -> str:
"""Adapter method to wedge user logic into izulu machinery
This is the place to override message/formatting if regular mechanics
don't work for you. It has to return original or your flavored message.
The method is invoked between izulu preparations and original
`Exception` constructor receiving the result of this hook.
You can also do any other logic here. You will be provided with
complete set of prepared data from izulu. But it's recommended
to use classic OOP inheritance for ordinary behaviour extension.
Params:
* store: dataclass containing inner error class specifications
* kwargs: original kwargs from user
* msg: formatted message from the error template
"""
return msg
Tips
****
1. inheritance / root exception
===============================
.. code-block:: python
# intermediate class to centrally control the default behaviour
class BaseError(Error): # <-- inherit from this in your code (not directly from ``izulu``)
__features__ = Features.None
class MyRealError(BaseError):
__template__ = "Having count={count} for owner={owner}"
2. factories
============
TODO: self=True / self.as_kwargs() (as_dict forbidden? - recursion)
* stdlib factories
.. code-block:: python
from uuid import uuid4
class MyError(Error):
id: datetime = factory(uuid4)
timestamp: datetime = factory(datetime.now)
* lambdas
.. code-block:: python
class MyError(Error):
timestamp: datetime = factory(lambda: datetime.now().isoformat())
* function
.. code-block:: python
from random import randint
def flip_coin():
return "TAILS" if randint(0, 100) % 2 else "HEADS
class MyError(Error):
coin: str = factory(flip_coin)
* method
.. code-block:: python
class MyError(Error):
__template__ = "Having count={count} for owner={owner}"
def __make_duration(self) -> timedelta:
kwargs = self.as_kwargs()
return self.timestamp - kwargs["begin"]
timestamp: datetime = factory(datetime.now)
duration: timedelta = factory(__make_duration, self=True)
begin = datetime.fromordinal(date.today().toordinal())
e = MyError(count=10, begin=begin)
print(e.begin)
# 2023-09-27 00:00:00
print(e.duration)
# 18:45:44.502490
print(e.timestamp)
# 2023-09-27 18:45:44.502490
3. handling errors in presentation layers / APIs
================================================
.. code-block:: python
err = Error()
view = RespModel(error=err.as_dict(wide=True)
class MyRealError(BaseError):
__template__ = "Having count={count} for owner={owner}"
Additional examples
-------------------
TBD
For developers
**************
* Running tests::
tox
* Building package::
tox -e build
* Contributing: contact me through `Issues <https://gitlab.com/pyctrl/izulu/-/issues>`__
Versioning
**********
`SemVer <http://semver.org/>`__ used for versioning.
For available versions see the repository
`tags <https://gitlab.com/pyctrl/izulu/-/tags>`__
and `releases <https://gitlab.com/pyctrl/izulu/-/releases>`__.
Authors
*******
- **Dima Burmistrov** - *Initial work* -
`pyctrl <https://gitlab.com/pyctrl/>`__
*Special thanks to* `Eugene Frolov <https://github.com/phantomii/>`__ *for inspiration.*
License
*******
This project is licensed under the X11 License (extended MIT) - see the
`LICENSE <https://gitlab.com/pyctrl/izulu/-/blob/main/LICENSE>`__ file for details
Raw data
{
"_id": null,
"home_page": null,
"name": "izulu",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.6",
"maintainer_email": null,
"keywords": "error, exception, oop, izulu",
"author": null,
"author_email": "Dima Burmistrov <pyctrl.dev@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/45/ec/4bf45aaba7831efa9ea35925e0ec1ac6422ed82eddac34322f53cc6cff77/izulu-0.5.3.tar.gz",
"platform": null,
"description": "izulu\n#####\n\n.. image:: https://gitlab.com/uploads/-/system/project/avatar/50698236/izulu_logo_512.png?width=128\n\n|\n\n *\"The exceptional library\"*\n\n|\n\n\n**Installation**\n\n::\n\n pip install izulu\n\n\nPresenting ``izulu``\n********************\n\nBring OOP into exception/error management\n=========================================\n\nYou can read docs *from top to bottom* or jump straight into **\"Quickstart\"** section.\nFor details note **\"Specifications\"** sections below.\n\n\nNeat #1: Stop messing with raw strings and manual message formatting\n--------------------------------------------------------------------\n\n.. code-block:: python\n\n if not data:\n raise ValueError(\"Data is invalid: no data\")\n\n amount = data[\"amount\"]\n if amount < 0:\n raise ValueError(f\"Data is invalid: amount can't be negative ({amount})\")\n elif amount > 1000:\n raise ValueError(f\"Data is invalid: amount is too large ({amount})\")\n\n if data[\"status\"] not in {\"READY\", \"IN_PROGRESS\"}:\n raise ValueError(\"Data is invalid: unprocessable status\")\n\nWith ``izulu`` you can forget about manual error message management all over the codebase!\n\n.. code-block:: python\n\n class ValidationError(Error):\n __template__ = \"Data is invalid: {reason}\"\n\n class AmountValidationError(ValidationError):\n __template__ = \"Invalid amount: {amount}\"\n\n\n if not data:\n raise ValidationError(reason=\"no data\")\n\n amount = data[\"amount\"]\n if amount < 0:\n raise AmountValidationError(reason=\"amount can't be negative\", amount=amount)\n elif amount > 1000:\n raise AmountValidationError(reason=\"amount is too large\", amount=amount)\n\n if data[\"status\"] not in {\"READY\", \"IN_PROGRESS\"}:\n raise ValidationError(reason=\"unprocessable status\")\n\n\nProvide only variable data for error instantiations. Keep static data within error class.\n\nUnder the hood ``kwargs`` are used to format ``__template__`` into final error message.\n\n\nNeat #2: Attribute errors with useful fields\n--------------------------------------------\n\n.. code-block:: python\n\n from falcon import HTTPBadRequest\n\n class AmountValidationError(ValidationError):\n __template__ = \"Data is invalid: {reason} ({amount})\"\n amount: int\n\n\n try:\n validate(data)\n except AmountValidationError as e:\n if e.amount < 0:\n raise HTTPBadRequest(f\"Bad amount: {e.amount}\")\n raise\n\n\nAnnotated instance attributes automatically populated from ``kwargs``.\n\n\nNeat #3: Static and dynamic defaults\n------------------------------------\n\n.. code-block:: python\n\n class AmountValidationError(ValidationError):\n __template__ = \"Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}\"\n _MAX: ClassVar[int] = 1000\n amount: int\n reason: str = \"amount is too large\"\n ts: datetime = factory(datetime.now)\n\n\n print(AmountValidationError(amount=15000))\n # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 22:59:25.132699\n\n print(AmountValidationError(amount=-1, reason=\"amount can't be negative\"))\n # Data is invalid: amount can't be negative (-1; MAX=1000) at 2024-01-13 22:59:54.482577\n\n\nQuickstart\n==========\n\n.. note::\n\n **Prepare playground**\n\n ::\n\n pip install ipython izulu\n\n ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'\n\n\nLet's start with defining our initial error class (exception)\n-------------------------------------------------------------\n\n#. subclass ``Error``\n#. provide special message template for each of your exceptions\n#. use **only kwargs** to instantiate exception *(no more message copying across the codebase)*\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"Having count={count} for owner={owner}\"\n\n\n print(MyError(count=10, owner=\"me\"))\n # MyError: Having count=10 for owner=me\n\n MyError(10, owner=\"me\")\n # TypeError: __init__() takes 1 positional argument but 2 were given\n\n\nMove on and improve our class with attributes\n---------------------------------------------\n\n#. define annotations for fields you want to publish as exception instance attributes\n#. you have to define desired template fields in annotations too\n (see ``AttributeError`` for ``owner``)\n#. you can provide annotation for attributes not included in template (see ``timestamp``)\n#. **type hinting from annotations are not enforced or checked** (see ``timestamp``)\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"Having count={count} for owner={owner}\"\n count: int\n timestamp: datetime\n\n e = MyError(count=10, owner=\"me\", timestamp=datetime.now())\n\n print(e.count)\n # 10\n print(e.timestamp)\n # 2023-09-27 18:18:22.957925\n\n e.owner\n # AttributeError: 'MyError' object has no attribute 'owner'\n\n\nWe can provide defaults for our attributes\n------------------------------------------\n\n#. define *default static values* after field annotation just as usual\n#. for *dynamic defaults* use provided ``factory`` tool with your callable - it would be\n evaluated without arguments during exception instantiation\n#. now fields would receive values from ``kwargs`` if present - otherwise from *defaults*\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"Having count={count} for owner={owner}\"\n count: int\n owner: str = \"nobody\"\n timestamp: datetime = factory(datetime.now)\n\n e = MyError(count=10)\n\n print(e.count)\n # 10\n print(e.owner)\n # nobody\n print(e.timestamp)\n # 2023-09-27 18:19:37.252577\n\n\nDynamic defaults also supported\n-------------------------------\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"Having count={count} for owner={owner}\"\n\n count: int\n begin: datetime\n owner: str = \"nobody\"\n timestamp: datetime = factory(datetime.now)\n duration: timedelta = factory(lambda self: self.timestamp - self.begin, self=True)\n\n\n begin = datetime.fromordinal(date.today().toordinal())\n e = MyError(count=10, begin=begin)\n\n print(e.begin)\n # 2023-09-27 00:00:00\n print(e.duration)\n # 18:45:44.502490\n print(e.timestamp)\n # 2023-09-27 18:45:44.502490\n\n\n* very similar to dynamic defaults, but callable must accept single\n argument - your exception fresh instance\n* **don't forget** to provide second ``True`` argument for ``factory`` tool\n (keyword or positional - doesn't matter)\n\n\nSpecifications\n**************\n\n``izulu`` bases on class definitions to provide handy instance creation.\n\n\nThe 6 pillars of ``izulu``\n==========================\n\n* all behavior is defined on the class-level\n\n* ``__template__`` class attribute defines the template for target error message\n\n * template may contain *\"fields\"* for substitution from ``kwargs`` and *\"defaults\"* to produce final error message\n\n* ``__features__`` class attribute defines constraints and behaviour (see \"Features\" section below)\n\n * by default all constraints are enabled\n\n* *\"class hints\"* annotated with ``ClassVar`` are noted by ``izulu``\n\n * annotated class attributes normally should have values (treated as *\"class defaults\"*)\n * *\"class defaults\"* can only be static\n * *\"class defaults\"* may be referred within ``__template__``\n\n* *\"instance hints\"* regularly annotated (not with ``ClassVar``) are noted by ``izulu``\n\n * all annotated attributes are treated as *\"instance attributes\"*\n * each *\"instance attribute\"* will automatically obtain value from the ``kwarg`` of the same name\n * *\"instance attributes\"* with default are also treated as *\"instance defaults\"*\n * *\"instance defaults\"* may be **static and dynamic**\n * *\"instance defaults\"* may be referred within ``__template__``\n\n* ``kwargs`` \u2014 the new and main way to form exceptions/error instance\n\n * forget about creating exception instances from message strings\n * ``kwargs`` are the datasource for template *\"fields\"* and *\"instance attributes\"*\n (shared input for templating attribution)\n\n.. warning:: **Types from type hints are not validated or enforced!**\n\n\nMechanics\n=========\n\n.. note::\n\n **Prepare playground**\n\n ::\n\n pip install ipython izulu\n\n ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'\n\n\n* inheritance from ``izulu.root.Error`` is required\n\n.. code-block:: python\n\n class AmountError(Error):\n pass\n\n* **optionally** behaviour can be adjusted with ``__features__`` (not recommended)\n\n.. code-block:: python\n\n class AmountError(Error):\n __features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS\n\n* you should provide a template for the target error message with ``__template__``\n\n .. code-block:: python\n\n class AmountError(Error):\n __template__ = \"Data is invalid: {reason} (amount={amount})\"\n\n print(AmountError(reason=\"negative amount\", amount=-10.52))\n # [2024-01-23 19:16] Data is invalid: negative amount (amount=-10.52)\n\n * sources of formatting arguments:\n\n * *\"class defaults\"*\n * *\"instance defaults\"*\n * ``kwargs`` (overlap any *\"default\"*)\n\n * new style formatting is used:\n\n .. code-block:: python\n\n class AmountError(Error):\n __template__ = \"[{ts:%Y-%m-%d %H:%M}] Data is invalid: {reason:_^20} (amount={amount:06.2f})\"\n\n print(AmountError(ts=datetime.now(), reason=\"negative amount\", amount=-10.52))\n # [2024-01-23 19:16] Data is invalid: __negative amount___ (amount=-10.52)\n\n * ``help(str.format)``\n * https://pyformat.info/\n * https://docs.python.org/3/library/string.html#format-string-syntax\n\n .. warning::\n There is a difference between docs and actual behaviour:\n https://discuss.python.org/t/format-string-syntax-specification-differs-from-actual-behaviour/46716\n\n * only named fields are allowed\n\n * positional (digit) and empty field are forbidden\n\n* error instantiation requires data to format ``__template__``\n\n * all data for ``__template__`` fields must be provided\n\n .. code-block:: python\n\n class AmountError(Error):\n __template__ = \"Data is invalid: {reason} (amount={amount})\"\n\n print(AmountError(reason=\"amount can't be negative\", amount=-10))\n # Data is invalid: amount can't be negative (amount=-10)\n\n AmountError()\n # TypeError: Missing arguments: 'reason', 'amount'\n AmountError(amount=-10)\n # TypeError: Missing arguments: 'reason'\n\n * only named arguments allowed: ``__init__()`` accepts only ``kwargs``\n\n .. code-block:: python\n\n class AmountError(Error):\n __template__ = \"Data is invalid: {reason} (amount={amount})\"\n\n print(AmountError(reason=\"amount can't be negative\", amount=-10))\n # Data is invalid: amount can't be negative (amount=-10)\n\n AmountError(\"amount can't be negative\", -10)\n # TypeError: __init__() takes 1 positional argument but 3 were given\n AmountError(\"amount can't be negative\", amount=-10)\n # TypeError: __init__() takes 1 positional argument but 2 were given\n\n* *\"class defaults\"* can be defined and used\n\n * *\"class defaults\"* must be type hinted with ``ClassVar`` annotation and provide static values\n * template *\"fields\"* may refer *\"class defaults\"*\n\n.. code-block:: python\n\n class AmountError(Error):\n LIMIT: ClassVar[int] = 10_000\n __template__ = \"Amount is too large: amount={amount} limit={LIMIT}\"\n amount: int\n\n print(AmountError(amount=10_500))\n # Amount is too large: amount=10500 limit=10000\n\n* *\"instance attributes\"* are populated from relevant ``kwargs``\n\n.. code-block:: python\n\n class AmountError(Error):\n amount: int\n\n print(AmountError(amount=-10).amount)\n # -10\n\n* instance and class attribute types from **annotations are not validated or enforced**\n (``izulu`` uses type hints just for attribute discovery and only ``ClassVar`` marker\n is processed for instance/class segregation)\n\n.. code-block:: python\n\n class AmountError(Error):\n amount: int\n\n print(AmountError(amount=\"lots of money\").amount)\n # lots of money\n\n* static *\"instance defaults\"* can be provided regularly with instance type hints and static values\n\n.. code-block:: python\n\n class AmountError(Error):\n amount: int = 500\n\n print(AmountError().amount)\n # 500\n\n* dynamic *\"instance defaults\"* are also supported\n\n * they must be type hinted and have special value\n * value must be a callable object wrapped with ``factory`` helper\n * ``factory`` provides 2 modes depending on value of the ``self`` flag:\n\n * ``self=False`` (default): callable accepting no arguments\n\n .. code-block:: python\n\n class AmountError(Error):\n ts: datetime = factory(datetime.now)\n\n print(AmountError().ts)\n # 2024-01-23 23:18:22.019963\n\n * ``self=True``: provide callable accepting single argument (error instance)\n\n .. code-block:: python\n\n class AmountError(Error):\n LIMIT = 10_000\n amount: int\n overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)\n\n print(AmountError(amount=10_500).overflow)\n # 500\n\n* *\"instance defaults\"* and *\"instance attributes\"* may be referred in ``__template__``\n\n.. code-block:: python\n\n class AmountError(Error):\n __template__ = \"[{ts:%Y-%m-%d %H:%M}] Amount is too large: {amount}\"\n amount: int\n ts: datetime = factory(datetime.now)\n\n print(AmountError(amount=10_500))\n # [2024-01-23 23:21] Amount is too large: 10500\n\n* *Pause and sum up: defaults, attributes and template*\n\n.. code-block:: python\n\n class AmountError(Error):\n LIMIT: ClassVar[int] = 10_000\n __template__ = \"[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}\"\n amount: int\n overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)\n ts: datetime = factory(datetime.now)\n\n err = AmountError(amount=15_000)\n\n print(err.amount)\n # 15000\n print(err.LIMIT)\n # 10000\n print(err.overflow)\n # 5000\n print(err.ts)\n # 2024-01-23 23:21:26\n\n print(err)\n # [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000\n\n* ``kwargs`` overlap *\"instance defaults\"*\n\n.. code-block:: python\n\n class AmountError(Error):\n LIMIT: ClassVar[int] = 10_000\n __template__ = \"[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}\"\n amount: int = 15_000\n overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)\n ts: datetime = factory(datetime.now)\n\n print(AmountError())\n # [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000\n\n print(AmountError(amount=10_333, overflow=42, ts=datetime(1900, 1, 1)))\n # [2024-01-23 23:21] Amount is too large: amount=10333 limit=10000 overflow=42\n\n* ``izulu`` provides flexibility for templates, fields, attributes and defaults\n\n * *\"defaults\"* are not required to be ``__template__`` *\"fields\"*\n\n .. code-block:: python\n\n class AmountError(Error):\n LIMIT: ClassVar[int] = 10_000\n __template__ = \"Amount is too large\"\n\n print(AmountError().LIMIT)\n # 10000\n print(AmountError())\n # Amount is too large\n\n * there can be hints for attributes not present in error message template\n\n .. code-block:: python\n\n class AmountError(Error):\n __template__ = \"Amount is too large\"\n amount: int\n\n print(AmountError(amount=500).amount)\n # 500\n print(AmountError(amount=500))\n # Amount is too large\n\n * *\"fields\"* don't have to be hinted as instance attributes\n\n .. code-block:: python\n\n class AmountError(Error):\n __template__ = \"Amount is too large: {amount}\"\n\n print(AmountError(amount=500))\n # Amount is too large: 500\n print(AmountError(amount=500).amount)\n # AttributeError: 'AmountError' object has no attribute 'amount'\n\n\nFeatures\n========\n\nThe ``izulu`` error class behaviour is controlled by ``__features__`` class attribute.\n\n(For details about \"runtime\" and \"class definition\" stages\nsee **Validation and behavior in case of problems** below)\n\n\nSupported features\n------------------\n\n* ``FORBID_MISSING_FIELDS``: checks provided ``kwargs`` contain data for all template *\"fields\"*\n and *\"instance attributes\"* that have no *\"defaults\"*\n\n * always should be enabled (provides consistent and detailed ``TypeError`` exceptions\n for appropriate arguments)\n * if disabled raw exceptions from ``izulu`` machinery internals could appear\n\n ======= =============\n Stage Raises\n ======= =============\n runtime ``TypeError``\n ======= =============\n\n.. code-block:: python\n\n class AmountError(Error):\n __template__ = \"Some {amount} of money for {client_id} client\"\n client_id: int\n\n # I. enabled\n AmountError()\n # TypeError: Missing arguments: client_id, amount\n\n # II. disabled\n AmountError.__features__ ^= Features.FORBID_MISSING_FIELDS\n\n AmountError()\n # ValueError: Failed to format template with provided kwargs:\n\n* ``FORBID_UNDECLARED_FIELDS``: forbids undefined arguments in provided ``kwargs``\n (names not present in template *\"fields\"* and *\"instance/class hints\"*)\n\n * if disabled allows and **completely ignores** unknown data in ``kwargs``\n\n ======= =============\n Stage Raises\n ======= =============\n runtime ``TypeError``\n ======= =============\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"My error occurred\"\n\n # I. enabled\n MyError(unknown_data=\"data\")\n # Undeclared arguments: unknown_data\n\n # II. disabled\n MyError.__features__ ^= Features.FORBID_UNDECLARED_FIELDS\n err = MyError(unknown_data=\"data\")\n\n print(err)\n # Unspecified error\n print(repr(err))\n # __main__.MyError(unknown_data='data')\n err.unknown_data\n # AttributeError: 'MyError' object has no attribute 'unknown_data'\n\n* ``FORBID_KWARG_CONSTS``: checks provided ``kwargs`` not to contain attributes defined as ``ClassVar``\n\n * if disabled allows data in ``kwargs`` to overlap class attributes during template formatting\n * overlapping data won't modify class attribute values\n\n ======= =============\n Stage Raises\n ======= =============\n runtime ``TypeError``\n ======= =============\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"My error occurred {_TYPE}\"\n _TYPE: ClassVar[str]\n\n # I. enabled\n MyError(_TYPE=\"SOME_ERROR_TYPE\")\n # TypeError: Constants in arguments: _TYPE\n\n # II. disabled\n MyError.__features__ ^= Features.FORBID_KWARG_CONSTS\n err = MyError(_TYPE=\"SOME_ERROR_TYPE\")\n\n print(err)\n # My error occurred SOME_ERROR_TYPE\n print(repr(err))\n # __main__.MyError(_TYPE='SOME_ERROR_TYPE')\n err._TYPE\n # AttributeError: 'MyError' object has no attribute '_TYPE'\n\n* ``FORBID_NON_NAMED_FIELDS``: forbids empty and digit field names in ``__template__``\n\n * if disabled validation (runtime issues)\n * ``izulu`` relies on ``kwargs`` and named fields\n * by default it's forbidden to provide empty (``{}``) and digit (``{0}``) fields in ``__template__``\n\n ================ ==============\n Stage Raises\n ================ ==============\n class definition ``ValueError``\n ================ ==============\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"My error occurred {_TYPE}\"\n _TYPE: ClassVar[str]\n\n # I. enabled\n MyError(_TYPE=\"SOME_ERROR_TYPE\")\n # TypeError: Constants in arguments: _TYPE\n\n # II. disabled\n MyError.__features__ ^= Features.FORBID_KWARG_CONSTS\n err = MyError(_TYPE=\"SOME_ERROR_TYPE\")\n\n print(err)\n # My error occurred SOME_ERROR_TYPE\n print(repr(err))\n # __main__.MyError(_TYPE='SOME_ERROR_TYPE')\n err._TYPE\n # AttributeError: 'MyError' object has no attribute '_TYPE'\n\n\nTuning ``__features__``\n-----------------------\n\nFeatures are represented as *\"Flag Enum\"*, so you can use regular operations\nto configure desired behaviour.\nExamples:\n\n* Use single option\n\n.. code-block:: python\n\n class AmountError(Error):\n __features__ = Features.FORBID_MISSING_FIELDS\n\n* Use presets\n\n.. code-block:: python\n\n class AmountError(Error):\n __features__ = Features.NONE\n\n* Combining wanted features:\n\n.. code-block:: python\n\n class AmountError(Error):\n __features__ = Features.FORBID_MISSING_FIELDS | Features.FORBID_KWARG_CONSTS\n\n* Discarding unwanted feature from default feature set:\n\n.. code-block:: python\n\n class AmountError(Error):\n __features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS\n\n\nValidation and behavior in case of problems\n===========================================\n\n``izulu`` may trigger native Python exceptions on invalid data during validation process.\nBy default you should expect following ones\n\n* ``TypeError``: argument constraints issues\n* ``ValueError``: template and formatting issues\n\nSome exceptions are *raised from* original exception (e.g. template formatting issues),\nso you can check ``e.__cause__`` and traceback output for details.\n\n\nThe validation behavior depends on the set of enabled features.\nChanging feature set may cause different and raw exceptions being raised.\nRead and understand **\"Features\"** section to predict and experiment with different situations and behaviours.\n\n\n``izulu`` has **2 validation stages:**\n\n* class definition stage\n\n * validation is made during error class definition\n\n .. code-block:: python\n\n # when you import error module\n from izulu import root\n\n # when you import error from module\n from izulu.root import Error\n\n # when you interactively define new error classes\n class MyError(Error):\n pass\n\n * class attributes ``__template__`` and ``__features__`` are validated\n\n .. code-block:: python\n\n class MyError(Error):\n __template__ = \"Hello {}\"\n\n # ValueError: Field names can't be empty\n\n* runtime stage\n\n * validation is made during error instantiation\n\n .. code-block:: python\n\n root.Error()\n\n * ``kwargs`` are validated according to enabled features\n\n .. code-block:: python\n\n class MyError(Error):\n __template__ = \"Hello {name}\"\n\n MyError()\n # TypeError: Missing arguments: 'name'\n\n\nAdditional APIs\n===============\n\n\nRepresentations\n---------------\n\n.. code-block:: python\n\n class AmountValidationError(Error):\n __template__ = \"Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}\"\n _MAX: ClassVar[int] = 1000\n amount: int\n reason: str = \"amount is too large\"\n ts: datetime = factory(datetime.now)\n\n\n err = AmountValidationError(amount=15000)\n\n print(str(err))\n # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586\n\n print(repr(err))\n # __main__.AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')\n\n\n* ``str`` and ``repr`` output differs\n* ``str`` is for humans and Python (Python dictates the result to be exactly and only the message)\n* ``repr`` allows to reconstruct the same error instance from its output\n (if data provided into ``kwargs`` supports ``repr`` the same way)\n\n **note:** class name is fully qualified name of class (dot-separated module full path with class name)\n\n .. code-block:: python\n\n reconstructed = eval(repr(err).replace(\"__main__.\", \"\", 1))\n\n print(str(reconstructed))\n # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586\n\n print(repr(reconstructed))\n # AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')\n\n* in addition to ``str`` there is another human-readable representations provided by ``.as_str()`` method;\n it prepends message with class name:\n\n .. code-block:: python\n\n print(err.as_str())\n # AmountValidationError: Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586\n\n\nPickling, dumping and loading\n-----------------------------\n\nPickling\n\"\"\"\"\"\"\"\"\n\n``izulu``-based errors **support pickling** by default.\n\n\nDumping\n\"\"\"\"\"\"\"\n\n* ``.as_kwargs()`` dumps shallow copy of original ``kwargs``\n\n.. code-block:: python\n\n err.as_kwargs()\n # {'amount': 15000}\n\n* ``.as_dict()`` by default, combines original ``kwargs`` and all *\"instance attribute\"* values into *\"full state\"*\n\n .. code-block:: python\n\n err.as_dict()\n # {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large'}\n\n Additionally, there is the ``wide`` flag for enriching the result with *\"class defaults\"*\n (note additional ``_MAX`` data)\n\n .. code-block:: python\n\n err.as_dict(True)\n # {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large', '_MAX': 1000}\n\n Data combination process follows prioritization \u2014 if there are multiple values for same name then high priority data\n will overlap data with lower priority. Here is the prioritized list of data sources:\n\n #. ``kwargs`` (max priority)\n #. *\"instance attributes\"*\n #. *\"class defaults\"*\n\n\nLoading\n\"\"\"\"\"\"\"\n\n* ``.as_kwargs()`` result can be used to create **inaccurate** copy of original error,\n but pay attention to dynamic factories \u2014 ``datetime.now()``, ``uuid()`` and many others would produce new values\n for data missing in ``kwargs`` (note ``ts`` field in the example below)\n\n.. code-block:: python\n\n inaccurate_copy = AmountValidationError(**err.as_kwargs())\n\n print(inaccurate_copy)\n # Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080\n print(repr(inaccurate_copy))\n # __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))\n\n* ``.as_dict()`` result can be used to create **accurate** copy of original error;\n flag ``wide`` should be ``False`` by default according to ``FORBID_KWARG_CONSTS`` restriction\n (if you disable ``FORBID_KWARG_CONSTS`` then you may need to use ``wide=True`` depending on your situation)\n\n.. code-block:: python\n\n accurate_copy = AmountValidationError(**err.as_dict())\n\n print(accurate_copy)\n # Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080\n print(repr(accurate_copy))\n # __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))\n\n\n(advanced) Wedge\n----------------\n\nThere is a special method you can override and additionally manage the machinery.\n\nBut it should not be need in 99,9% cases. Avoid it, please.\n\n.. code-block:: python\n\n def _hook(self,\n store: _utils.Store,\n kwargs: dict[str, t.Any],\n msg: str) -> str:\n \"\"\"Adapter method to wedge user logic into izulu machinery\n\n This is the place to override message/formatting if regular mechanics\n don't work for you. It has to return original or your flavored message.\n The method is invoked between izulu preparations and original\n `Exception` constructor receiving the result of this hook.\n\n You can also do any other logic here. You will be provided with\n complete set of prepared data from izulu. But it's recommended\n to use classic OOP inheritance for ordinary behaviour extension.\n\n Params:\n * store: dataclass containing inner error class specifications\n * kwargs: original kwargs from user\n * msg: formatted message from the error template\n \"\"\"\n\n return msg\n\n\nTips\n****\n\n1. inheritance / root exception\n===============================\n\n.. code-block:: python\n\n # intermediate class to centrally control the default behaviour\n class BaseError(Error): # <-- inherit from this in your code (not directly from ``izulu``)\n __features__ = Features.None\n\n\n class MyRealError(BaseError):\n __template__ = \"Having count={count} for owner={owner}\"\n\n\n2. factories\n============\n\nTODO: self=True / self.as_kwargs() (as_dict forbidden? - recursion)\n\n\n* stdlib factories\n\n.. code-block:: python\n\n from uuid import uuid4\n\n class MyError(Error):\n id: datetime = factory(uuid4)\n timestamp: datetime = factory(datetime.now)\n\n* lambdas\n\n.. code-block:: python\n\n class MyError(Error):\n timestamp: datetime = factory(lambda: datetime.now().isoformat())\n\n* function\n\n.. code-block:: python\n\n from random import randint\n\n def flip_coin():\n return \"TAILS\" if randint(0, 100) % 2 else \"HEADS\n\n class MyError(Error):\n coin: str = factory(flip_coin)\n\n\n* method\n\n.. code-block:: python\n\n class MyError(Error):\n __template__ = \"Having count={count} for owner={owner}\"\n\n def __make_duration(self) -> timedelta:\n kwargs = self.as_kwargs()\n return self.timestamp - kwargs[\"begin\"]\n\n timestamp: datetime = factory(datetime.now)\n duration: timedelta = factory(__make_duration, self=True)\n\n\n begin = datetime.fromordinal(date.today().toordinal())\n e = MyError(count=10, begin=begin)\n\n print(e.begin)\n # 2023-09-27 00:00:00\n print(e.duration)\n # 18:45:44.502490\n print(e.timestamp)\n # 2023-09-27 18:45:44.502490\n\n\n3. handling errors in presentation layers / APIs\n================================================\n\n.. code-block:: python\n\n err = Error()\n view = RespModel(error=err.as_dict(wide=True)\n\n\n class MyRealError(BaseError):\n __template__ = \"Having count={count} for owner={owner}\"\n\n\nAdditional examples\n-------------------\n\nTBD\n\n\nFor developers\n**************\n\n* Running tests::\n\n tox\n\n* Building package::\n\n tox -e build\n\n* Contributing: contact me through `Issues <https://gitlab.com/pyctrl/izulu/-/issues>`__\n\n\nVersioning\n**********\n\n`SemVer <http://semver.org/>`__ used for versioning.\nFor available versions see the repository\n`tags <https://gitlab.com/pyctrl/izulu/-/tags>`__\nand `releases <https://gitlab.com/pyctrl/izulu/-/releases>`__.\n\n\nAuthors\n*******\n\n- **Dima Burmistrov** - *Initial work* -\n `pyctrl <https://gitlab.com/pyctrl/>`__\n\n*Special thanks to* `Eugene Frolov <https://github.com/phantomii/>`__ *for inspiration.*\n\n\nLicense\n*******\n\nThis project is licensed under the X11 License (extended MIT) - see the\n`LICENSE <https://gitlab.com/pyctrl/izulu/-/blob/main/LICENSE>`__ file for details\n",
"bugtrack_url": null,
"license": "Copyright (c) 2023-2024 Dmitry Burmistrov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization. ",
"summary": "The exceptional library",
"version": "0.5.3",
"project_urls": {
"Bug Tracker": "https://gitlab.com/pyctrl/izulu/-/issues",
"Homepage": "https://gitlab.com/pyctrl/izulu"
},
"split_keywords": [
"error",
" exception",
" oop",
" izulu"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "0119b0d61c3731f9fe62d8e0ae54d928d2e3c35117852a5e3370584c63768eab",
"md5": "e256ffebc0a6a448cc828cab4dada825",
"sha256": "9120697416aed99c33b7f873cb6a6d4dc822d16958f97cfd56db25aacbecb3db"
},
"downloads": -1,
"filename": "izulu-0.5.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "e256ffebc0a6a448cc828cab4dada825",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.6",
"size": 15005,
"upload_time": "2024-12-10T00:07:27",
"upload_time_iso_8601": "2024-12-10T00:07:27.830714Z",
"url": "https://files.pythonhosted.org/packages/01/19/b0d61c3731f9fe62d8e0ae54d928d2e3c35117852a5e3370584c63768eab/izulu-0.5.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "45ec4bf45aaba7831efa9ea35925e0ec1ac6422ed82eddac34322f53cc6cff77",
"md5": "581aee6b56e66facbe4c78c88f1c81b2",
"sha256": "54987b8eec77808984cd5e91b629ad3971d01161c7ebea7166851ec7a19e2fe0"
},
"downloads": -1,
"filename": "izulu-0.5.3.tar.gz",
"has_sig": false,
"md5_digest": "581aee6b56e66facbe4c78c88f1c81b2",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.6",
"size": 34788,
"upload_time": "2024-12-10T00:07:30",
"upload_time_iso_8601": "2024-12-10T00:07:30.918989Z",
"url": "https://files.pythonhosted.org/packages/45/ec/4bf45aaba7831efa9ea35925e0ec1ac6422ed82eddac34322f53cc6cff77/izulu-0.5.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-10 00:07:30",
"github": false,
"gitlab": true,
"bitbucket": false,
"codeberg": false,
"gitlab_user": "pyctrl",
"gitlab_project": "izulu",
"lcname": "izulu"
}