isik 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isik/__init__.py +1 -0
- isik/_internal/__init__.py +11 -0
- isik/common/__init__.py +0 -0
- isik/common/config/__init__.py +25 -0
- isik/common/config/builder.py +105 -0
- isik/common/config/casters.py +56 -0
- isik/common/config/exceptions.py +1 -0
- isik/common/utils/__init__.py +46 -0
- isik/common/utils/caching.py +21 -0
- isik/common/utils/concurrency.py +83 -0
- isik/common/utils/error_handling.py +155 -0
- isik/common/utils/functional.py +190 -0
- isik/common/utils/iterables.py +41 -0
- isik/common/utils/metaclasses.py +70 -0
- isik/common/utils/sentinel.py +38 -0
- isik/common/utils/strings.py +23 -0
- isik/django/__init__.py +3 -0
- isik/django/apps/__init__.py +0 -0
- isik/django/apps/common/__init__.py +0 -0
- isik/django/apps/common/admin.py +119 -0
- isik/django/apps/common/apps.py +15 -0
- isik/django/apps/common/backends/__init__.py +0 -0
- isik/django/apps/common/backends/auth.py +20 -0
- isik/django/apps/common/db.py +13 -0
- isik/django/apps/common/email.py +10 -0
- isik/django/apps/common/fields/__init__.py +0 -0
- isik/django/apps/common/fields/gfk.py +101 -0
- isik/django/apps/common/lookups.py +6 -0
- isik/django/apps/common/middleware/__init__.py +0 -0
- isik/django/apps/common/middleware/media_white_noise.py +42 -0
- isik/django/apps/common/middleware/session.py +26 -0
- isik/django/apps/common/models.py +89 -0
- isik/django/apps/common/orm.py +22 -0
- isik/django/apps/common/skippable_validators.py +72 -0
- isik/django/drf/__init__.py +3 -0
- isik/django/drf/error_handling.py +17 -0
- isik/django/drf/filters.py +12 -0
- isik/django/drf/pagination.py +40 -0
- isik/django/drf/permissions.py +108 -0
- isik/django/http_exceptions/__init__.py +0 -0
- isik/django/http_exceptions/decorators.py +29 -0
- isik/django/http_exceptions/exceptions.py +107 -0
- isik/django/http_exceptions/middleware.py +55 -0
- isik/sentry/__init__.py +3 -0
- isik/sentry/utils/__init__.py +9 -0
- isik-0.1.0.dist-info/METADATA +40 -0
- isik-0.1.0.dist-info/RECORD +49 -0
- isik-0.1.0.dist-info/WHEEL +4 -0
- isik-0.1.0.dist-info/licenses/LICENCE +21 -0
isik/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def check_extra(extra_name, package_name):
|
|
5
|
+
try:
|
|
6
|
+
importlib.import_module(package_name)
|
|
7
|
+
except ImportError as exception:
|
|
8
|
+
raise ImportError(
|
|
9
|
+
f"The module you are trying to use requires '{extra_name}'. "
|
|
10
|
+
f"Please install it with: pip install isik[{extra_name}]"
|
|
11
|
+
) from exception
|
isik/common/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from isik.common.config.builder import Config, config
|
|
2
|
+
from isik.common.config.casters import (
|
|
3
|
+
boolean,
|
|
4
|
+
caster,
|
|
5
|
+
comma_separated_float_list,
|
|
6
|
+
comma_separated_int_list,
|
|
7
|
+
comma_separated_list,
|
|
8
|
+
integer,
|
|
9
|
+
string,
|
|
10
|
+
)
|
|
11
|
+
from isik.common.config.exceptions import ConfigError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Config",
|
|
16
|
+
"ConfigError",
|
|
17
|
+
"boolean",
|
|
18
|
+
"caster",
|
|
19
|
+
"comma_separated_float_list",
|
|
20
|
+
"comma_separated_int_list",
|
|
21
|
+
"comma_separated_list",
|
|
22
|
+
"config",
|
|
23
|
+
"integer",
|
|
24
|
+
"string",
|
|
25
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from isik.common.config.exceptions import ConfigError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_MISSING = object()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Config(dict):
|
|
10
|
+
"""
|
|
11
|
+
A dict that also allows attribute access, e.g. config.DATABASE.HOST, and can reload its
|
|
12
|
+
values from the environment via refresh() - callable on the root config or on any nested
|
|
13
|
+
one, refreshing just that subtree in place so existing references to it see the update too.
|
|
14
|
+
|
|
15
|
+
A schema key that collides with a dict method name (items, keys, ...) or with refresh
|
|
16
|
+
itself is only reachable via item access (config["refresh"]), not attribute access.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__setattr__ = dict.__setitem__
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name):
|
|
22
|
+
try:
|
|
23
|
+
return self[name]
|
|
24
|
+
except KeyError as exc:
|
|
25
|
+
raise AttributeError(name) from exc
|
|
26
|
+
|
|
27
|
+
def refresh(self, *path):
|
|
28
|
+
"""
|
|
29
|
+
Re-read values from the environment, in place. With no arguments, refreshes every
|
|
30
|
+
value under this node. With a path of key names, refreshes only that one nested
|
|
31
|
+
value instead - e.g. config.refresh("DATABASE", "HOST") or, equivalently,
|
|
32
|
+
config.DATABASE.refresh("HOST").
|
|
33
|
+
"""
|
|
34
|
+
if not path:
|
|
35
|
+
for key in self._schema:
|
|
36
|
+
self._refresh_key(key)
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
key, *rest = path
|
|
40
|
+
if rest:
|
|
41
|
+
self[key].refresh(*rest)
|
|
42
|
+
else:
|
|
43
|
+
self._refresh_key(key)
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def _refresh_key(self, key):
|
|
47
|
+
try:
|
|
48
|
+
value = self._schema[key]
|
|
49
|
+
except KeyError:
|
|
50
|
+
raise ConfigError(f"'{key}' is not a key in this config.") from None
|
|
51
|
+
if isinstance(value, dict):
|
|
52
|
+
self[key].refresh()
|
|
53
|
+
else:
|
|
54
|
+
self[key] = _read_leaf(value, _environment_key(self._prefix, [*self._path, key], self._sep))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _environment_key(prefix, path, sep):
|
|
58
|
+
return sep.join([*([prefix] if prefix else []), *path])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_leaf(caster, environment_key):
|
|
62
|
+
try:
|
|
63
|
+
raw_value = os.environ[environment_key]
|
|
64
|
+
except KeyError:
|
|
65
|
+
default = getattr(caster, "missing_default", _MISSING)
|
|
66
|
+
if default is _MISSING:
|
|
67
|
+
raise ConfigError(
|
|
68
|
+
f"Environment variable {environment_key} not found."
|
|
69
|
+
f" Please set it or provide a `missing_default` to your caster."
|
|
70
|
+
) from None
|
|
71
|
+
return default
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
return caster(raw_value)
|
|
75
|
+
except Exception as exception:
|
|
76
|
+
default = getattr(caster, "error_default", _MISSING)
|
|
77
|
+
if default is _MISSING:
|
|
78
|
+
raise ConfigError(
|
|
79
|
+
f"Error while parsing {environment_key}={raw_value!r} with '{caster}'."
|
|
80
|
+
" Please check the value and the caster or provide an `error_default` to your caster."
|
|
81
|
+
) from exception
|
|
82
|
+
return default
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _build(data, path, prefix, sep):
|
|
86
|
+
result = Config()
|
|
87
|
+
object.__setattr__(result, "_schema", data)
|
|
88
|
+
object.__setattr__(result, "_path", path)
|
|
89
|
+
object.__setattr__(result, "_prefix", prefix)
|
|
90
|
+
object.__setattr__(result, "_sep", sep)
|
|
91
|
+
for key, value in data.items():
|
|
92
|
+
key_path = [*path, key]
|
|
93
|
+
if isinstance(value, dict):
|
|
94
|
+
result[key] = _build(value, key_path, prefix, sep)
|
|
95
|
+
elif callable(value):
|
|
96
|
+
result[key] = _read_leaf(value, _environment_key(prefix, key_path, sep))
|
|
97
|
+
else:
|
|
98
|
+
raise ConfigError(
|
|
99
|
+
f"Values either must be callables or other mappings, not {type(value)}. Key={'.'.join(key_path)}."
|
|
100
|
+
)
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def config(data, *, prefix=None, sep="__"):
|
|
105
|
+
return _build(data, [], prefix, sep)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def caster(f):
|
|
5
|
+
"""
|
|
6
|
+
Turns a plain string->value function into a config caster that accepts an optional
|
|
7
|
+
missing_default (used when the environment variable isn't set) and error_default (used
|
|
8
|
+
when f(value) raises). Neither is set by default, meaning a missing or unparseable
|
|
9
|
+
value raises ConfigError instead.
|
|
10
|
+
"""
|
|
11
|
+
sentinel = object()
|
|
12
|
+
|
|
13
|
+
def wrapper(missing_default=sentinel, error_default=sentinel):
|
|
14
|
+
@wraps(f)
|
|
15
|
+
def function_clone(value):
|
|
16
|
+
return f(value)
|
|
17
|
+
|
|
18
|
+
if missing_default is not sentinel:
|
|
19
|
+
function_clone.missing_default = missing_default
|
|
20
|
+
if error_default is not sentinel:
|
|
21
|
+
function_clone.error_default = error_default
|
|
22
|
+
return function_clone
|
|
23
|
+
|
|
24
|
+
return wrapper
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@caster
|
|
28
|
+
def comma_separated_list(value):
|
|
29
|
+
return value.split(",")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@caster
|
|
33
|
+
def comma_separated_int_list(value):
|
|
34
|
+
return [int(i) for i in value.split(",")]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@caster
|
|
38
|
+
def comma_separated_float_list(value):
|
|
39
|
+
return [float(i) for i in value.split(",")]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@caster
|
|
43
|
+
def boolean(value):
|
|
44
|
+
truthy = ["true", "True", "1"]
|
|
45
|
+
falsy = ["false", "False", "0"]
|
|
46
|
+
|
|
47
|
+
if value in truthy:
|
|
48
|
+
return True
|
|
49
|
+
elif value in falsy:
|
|
50
|
+
return False
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError(f"Value {value!r} can not be parsed into a boolean.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
string = caster(str)
|
|
56
|
+
integer = caster(int)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class ConfigError(ValueError): ...
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from isik.common.utils.caching import get_cached
|
|
2
|
+
from isik.common.utils.concurrency import ContextLocal, ThreadLocal, ThreadLock
|
|
3
|
+
from isik.common.utils.error_handling import SuppressAndRun, TransformExceptions, suppress_callable
|
|
4
|
+
from isik.common.utils.functional import (
|
|
5
|
+
cloned,
|
|
6
|
+
enabled_if,
|
|
7
|
+
identity,
|
|
8
|
+
noop,
|
|
9
|
+
raises,
|
|
10
|
+
require_exclusive_keys,
|
|
11
|
+
returns,
|
|
12
|
+
with_attrs,
|
|
13
|
+
)
|
|
14
|
+
from isik.common.utils.iterables import all_combinations, first_of, not_none, purge_iterable, purge_mapping
|
|
15
|
+
from isik.common.utils.metaclasses import transform
|
|
16
|
+
from isik.common.utils.sentinel import Sentinel
|
|
17
|
+
from isik.common.utils.strings import camel_to_snake, snake_to_human, snake_to_pascal
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ContextLocal",
|
|
22
|
+
"Sentinel",
|
|
23
|
+
"SuppressAndRun",
|
|
24
|
+
"ThreadLocal",
|
|
25
|
+
"ThreadLock",
|
|
26
|
+
"TransformExceptions",
|
|
27
|
+
"all_combinations",
|
|
28
|
+
"camel_to_snake",
|
|
29
|
+
"cloned",
|
|
30
|
+
"enabled_if",
|
|
31
|
+
"first_of",
|
|
32
|
+
"get_cached",
|
|
33
|
+
"identity",
|
|
34
|
+
"noop",
|
|
35
|
+
"not_none",
|
|
36
|
+
"purge_iterable",
|
|
37
|
+
"purge_mapping",
|
|
38
|
+
"raises",
|
|
39
|
+
"require_exclusive_keys",
|
|
40
|
+
"returns",
|
|
41
|
+
"snake_to_human",
|
|
42
|
+
"snake_to_pascal",
|
|
43
|
+
"suppress_callable",
|
|
44
|
+
"transform",
|
|
45
|
+
"with_attrs",
|
|
46
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def get_cached(obj, attr, factory):
|
|
2
|
+
"""
|
|
3
|
+
Get a cached attribute from an object, computing and storing it if absent.
|
|
4
|
+
|
|
5
|
+
Bypasses __getattribute__ via object.__getattribute__ and object.__setattr__,
|
|
6
|
+
making it safe to call from within a custom __getattribute__ implementation.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
obj: The object to cache the attribute on.
|
|
10
|
+
attr: The attribute name to use as the cache key.
|
|
11
|
+
factory: A zero-argument callable that computes the value on cache miss.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
The cached value if present, otherwise the result of calling factory().
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
return object.__getattribute__(obj, attr)
|
|
18
|
+
except AttributeError:
|
|
19
|
+
value = factory()
|
|
20
|
+
object.__setattr__(obj, attr, value)
|
|
21
|
+
return value
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from contextvars import ContextVar
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ThreadLocal:
|
|
6
|
+
"""
|
|
7
|
+
A factory for thread local identified by name.
|
|
8
|
+
Calling ThreadLocal("FOO") returns a thread local object shared across all calls with the same name.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
_registry = {}
|
|
12
|
+
_registry_lock = threading.Lock()
|
|
13
|
+
|
|
14
|
+
def __new__(cls, name):
|
|
15
|
+
if name not in cls._registry:
|
|
16
|
+
with cls._registry_lock:
|
|
17
|
+
if name not in cls._registry:
|
|
18
|
+
cls._registry[name] = threading.local()
|
|
19
|
+
return cls._registry[name]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ThreadLock:
|
|
23
|
+
"""
|
|
24
|
+
A factory for thread lock identified by name.
|
|
25
|
+
Calling ThreadLock("FOO") returns a lock object shared across all calls with the same name.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_registry = {}
|
|
29
|
+
_registry_lock = threading.Lock()
|
|
30
|
+
|
|
31
|
+
def __new__(cls, name):
|
|
32
|
+
if name not in cls._registry:
|
|
33
|
+
with cls._registry_lock:
|
|
34
|
+
if name not in cls._registry:
|
|
35
|
+
cls._registry[name] = threading.Lock()
|
|
36
|
+
return cls._registry[name]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ContextLocal:
|
|
40
|
+
"""
|
|
41
|
+
A named, registry-backed namespace of ContextVars.
|
|
42
|
+
Calling ContextLocal("FOO") returns the same instance across all calls with the same name,
|
|
43
|
+
making it safe for async and coroutine contexts.
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
local = ContextLocal("MY_NAMESPACE")
|
|
47
|
+
token = local.set("foo", "bar")
|
|
48
|
+
local.get("foo") # "bar"
|
|
49
|
+
local.get("foo", "default") # "bar"
|
|
50
|
+
local.reset("foo", token)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
_registry = {}
|
|
54
|
+
_registry_lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
def __new__(cls, name):
|
|
57
|
+
if name not in cls._registry:
|
|
58
|
+
with cls._registry_lock:
|
|
59
|
+
if name not in cls._registry:
|
|
60
|
+
instance = super().__new__(cls)
|
|
61
|
+
object.__setattr__(instance, "_name", name)
|
|
62
|
+
object.__setattr__(instance, "_vars", {})
|
|
63
|
+
cls._registry[name] = instance
|
|
64
|
+
return cls._registry[name]
|
|
65
|
+
|
|
66
|
+
def _get_var(self, key):
|
|
67
|
+
vars_ = object.__getattribute__(self, "_vars")
|
|
68
|
+
if key not in vars_:
|
|
69
|
+
name = object.__getattribute__(self, "_name")
|
|
70
|
+
vars_[key] = ContextVar(f"{name}.{key}")
|
|
71
|
+
return vars_[key]
|
|
72
|
+
|
|
73
|
+
def get(self, key, *args):
|
|
74
|
+
"""Get a value. Accepts an optional default as a second argument, like dict.get()."""
|
|
75
|
+
return self._get_var(key).get(*args)
|
|
76
|
+
|
|
77
|
+
def set(self, key, value):
|
|
78
|
+
"""Set a value and return a token that can be used to restore the previous state."""
|
|
79
|
+
return self._get_var(key).set(value)
|
|
80
|
+
|
|
81
|
+
def reset(self, key, token):
|
|
82
|
+
"""Reset a value to its state before the corresponding set() call."""
|
|
83
|
+
self._get_var(key).reset(token)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from contextlib import ContextDecorator, suppress
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from isik.common.utils.functional import require_exclusive_keys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TransformExceptions(ContextDecorator):
|
|
8
|
+
"""
|
|
9
|
+
A context manager and decorator that catches the given exception types and transforms
|
|
10
|
+
them into a new exception via a transformation function.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
*exception_types: One or more exception types to catch and transform.
|
|
14
|
+
transform: A callable that receives the original exception and returns a new one.
|
|
15
|
+
Can be left out and filled in later - see the two-step form below.
|
|
16
|
+
keep_original: If True (default), the original exception is chained to the new one
|
|
17
|
+
via `raise new from original`, preserving the traceback context.
|
|
18
|
+
If False, the original exception is suppressed and the new one is
|
|
19
|
+
raised in isolation.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
Whatever exception `transform` returns: at call time if a matching exception is caught.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
@TransformExceptions(ValueError, transform=lambda e: MyCustomError(str(e)))
|
|
26
|
+
def parse(x):
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
with TransformExceptions(ValueError, KeyError, transform=lambda e: MyCustomError(str(e))):
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
# Without chaining:
|
|
33
|
+
with TransformExceptions(ValueError, transform=lambda e: MyCustomError(str(e)), keep_original=False):
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
# Two-step form: give it exception types now, decorate the transform function
|
|
37
|
+
# later - useful when the transform itself is more than a lambda one-liner.
|
|
38
|
+
# The transform function becomes a named, reusable decorator.
|
|
39
|
+
@TransformExceptions(ValueError)
|
|
40
|
+
def value_error_to_my_error(e):
|
|
41
|
+
return MyCustomError(str(e))
|
|
42
|
+
|
|
43
|
+
@value_error_to_my_error
|
|
44
|
+
def parse(x):
|
|
45
|
+
...
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *exception_types, transform=None, keep_original=True):
|
|
49
|
+
self.exception_types = exception_types
|
|
50
|
+
self.transform = transform
|
|
51
|
+
self.keep_original = keep_original
|
|
52
|
+
|
|
53
|
+
def __call__(self, func):
|
|
54
|
+
if self.transform is None:
|
|
55
|
+
# two-step form: `func` is the transform, not the guarded code
|
|
56
|
+
self.transform = func
|
|
57
|
+
return self
|
|
58
|
+
return super().__call__(func)
|
|
59
|
+
|
|
60
|
+
def __enter__(self):
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
64
|
+
if isinstance(exc_val, self.exception_types):
|
|
65
|
+
if self.transform is None:
|
|
66
|
+
raise TypeError(
|
|
67
|
+
"TransformExceptions has no transform set - "
|
|
68
|
+
"pass transform=... or decorate a transform function with it first."
|
|
69
|
+
)
|
|
70
|
+
new_exception = self.transform(exc_val)
|
|
71
|
+
raise new_exception from (exc_val if self.keep_original else None)
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SuppressAndRun(suppress):
|
|
76
|
+
"""
|
|
77
|
+
A context manager that suppresses the given exceptions and calls a function with the
|
|
78
|
+
suppressed exception.
|
|
79
|
+
|
|
80
|
+
Extends contextlib.suppress with the ability to run a callable when an exception is
|
|
81
|
+
suppressed, for example to log it, print it, or send it to an error tracker.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
*exceptions: One or more exception types to suppress.
|
|
85
|
+
func: A callable that receives the suppressed exception as its only argument.
|
|
86
|
+
Defaults to print. Must accept a single exception argument.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
with SuppressAndRun(ValueError, func=logger.warning):
|
|
90
|
+
raise ValueError("oops") # suppressed, logger.warning called with the exception
|
|
91
|
+
|
|
92
|
+
with SuppressAndRun(ValueError, KeyError, func=my_handler):
|
|
93
|
+
...
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, *exceptions, func=print):
|
|
97
|
+
super().__init__(*exceptions)
|
|
98
|
+
self.func = func
|
|
99
|
+
|
|
100
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
101
|
+
suppressed = super().__exit__(exc_type, exc_val, exc_tb)
|
|
102
|
+
if suppressed:
|
|
103
|
+
self.func(exc_val)
|
|
104
|
+
return suppressed
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@require_exclusive_keys(
|
|
108
|
+
{"by_return_value": ["return_value"]},
|
|
109
|
+
{"by_return_func": ["return_func"]},
|
|
110
|
+
allow_empty=True,
|
|
111
|
+
)
|
|
112
|
+
def suppress_callable(*exceptions, func=print, return_value=None, return_func=None):
|
|
113
|
+
"""
|
|
114
|
+
A decorator that suppresses the given exceptions, calls func with the suppressed
|
|
115
|
+
exception, and returns either a static value or the result of a replacement function.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
*exceptions: One or more exception types to suppress.
|
|
119
|
+
func: A callable that receives the suppressed exception as its only argument.
|
|
120
|
+
Defaults to print.
|
|
121
|
+
return_value: A static value to return when an exception is suppressed.
|
|
122
|
+
Mutually exclusive with return_func.
|
|
123
|
+
return_func: A callable with the same signature as the decorated function.
|
|
124
|
+
Called with the original arguments when an exception is suppressed.
|
|
125
|
+
Mutually exclusive with return_value.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If both return_value and return_func are provided.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
@suppress_callable(ValueError, func=logger.warning, return_value=0)
|
|
132
|
+
def parse(x):
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
@suppress_callable(ValueError, return_func=lambda x: x * 0)
|
|
136
|
+
def parse(x):
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
@suppress_callable(ValueError)
|
|
140
|
+
def parse(x):
|
|
141
|
+
... # returns None when suppressed
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def decorator(f):
|
|
145
|
+
@wraps(f)
|
|
146
|
+
def wrapper(*a, **kw):
|
|
147
|
+
with SuppressAndRun(*exceptions, func=func):
|
|
148
|
+
return f(*a, **kw)
|
|
149
|
+
if return_func is not None:
|
|
150
|
+
return return_func(*a, **kw)
|
|
151
|
+
return return_value
|
|
152
|
+
|
|
153
|
+
return wrapper
|
|
154
|
+
|
|
155
|
+
return decorator
|