pocketutils.core.decorators.add_reprs()   C
last analyzed

Complexity

Conditions 9

Size

Total Lines 46
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 35
dl 0
loc 46
rs 6.6666
c 0
b 0
f 0
cc 9
nop 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
# SPDX-FileCopyrightText: Copyright 2020-2023, Contributors to pocketutils
2
# SPDX-PackageHomePage: https://github.com/dmyersturnbull/pocketutils
3
# SPDX-License-Identifier: Apache-2.0
4
"""
5
Decorations.
6
"""
7
from __future__ import annotations
8
9
import enum
10
import html as _html
11
from abc import abstractmethod
12
from collections.abc import Callable, Collection, Generator, Iterable
13
from functools import total_ordering, wraps
14
from typing import Any, Literal, Self, TypeVar, final
15
from warnings import warn
16
17
T = TypeVar("T", bound=type)
18
19
20
def reserved(cls: T) -> T:
21
    """
22
    Empty but is declared for future use.
23
    """
24
    return cls
25
26
27
class CodeIncompleteError(NotImplementedError):
28
    """The code is not finished."""
29
30
31
class CodeRemovedError(NotImplementedError):
32
    """The code was removed."""
33
34
35
class PreviewWarning(UserWarning):
36
    """The code being called is a preview, unstable. or immature."""
37
38
39
@enum.unique
40
class CodeStatus(enum.Enum):
41
    """
42
    An enum for the quality/maturity of code,
43
    ranging from incomplete to deprecate.
44
    """
45
46
    INCOMPLETE = -2
47
    PREVIEW = -1
48
    STABLE = 0
49
    PENDING_DEPRECATION = 1
50
    DEPRECATED = 2
51
    REMOVED = 3
52
53
    @classmethod
54
    def of(cls: type[Self], x: int | str | CodeStatus) -> CodeStatus:
55
        if isinstance(x, str):
56
            return cls[x.lower().strip()]
57
        if isinstance(x, CodeStatus):
58
            return x
59
        if isinstance(x, int):
60
            return cls(x)
61
        msg = f"Invalid type {type(x)} for {x}"
62
        raise TypeError(msg)
63
64
65
def status(level: int | str | CodeStatus, vr: str | None = "", msg: str | None = None) -> Callable[..., Any]:
66
    """
67
    Annotate code quality. Emits a warning if bad code is called.
68
69
    Args:
70
        level: The quality / maturity
71
        vr: First version the status / warning applies to
72
        msg: Explanation and/or when it will be removed or completed
73
    """
74
75
    level = CodeStatus.of(level)
76
77
    @wraps(status)
78
    def dec(func):
79
        func.__status__ = level
80
        if level is CodeStatus.STABLE:
81
            return func
82
        elif level is CodeStatus.REMOVED:
83
84
            def my_fn(*_, **__):
85
                raise CodeRemovedError(f"{func.__name__} was removed (as of version: {vr}). {msg}")
86
87
            return wraps(func)(my_fn)
88
89
        elif level is CodeStatus.INCOMPLETE:
90
91
            def my_fn(*_, **__):
92
                error_msg = f"{func.__name__} is incomplete (as of version: {vr}). {msg}"
93
                raise CodeIncompleteError(error_msg)
94
95
            return wraps(func)(my_fn)
96
        elif level is CodeStatus.PREVIEW:
97
98
            def my_fn(*args, **kwargs):
99
                preview_msg = f"{func.__name__} is a preview or immature (as of version: {vr}). {msg}"
100
                warn(preview_msg, PreviewWarning)
101
                return func(*args, **kwargs)
102
103
            return wraps(func)(my_fn)
104
        elif level is CodeStatus.PENDING_DEPRECATION:
105
106
            def my_fn(*args, **kwargs):
107
                pending_dep_msg = f"{func.__name__} is pending deprecation (as of version: {vr}). {msg}"
108
                warn(pending_dep_msg, PendingDeprecationWarning)
109
                return func(*args, **kwargs)
110
111
            # noinspection PyDeprecation
112
            return wraps(func)(my_fn)
113
        elif level is CodeStatus.DEPRECATED:
114
115
            def my_fn(*args, **kwargs):
116
                dep_msg = f"{func.__name__} is deprecated (as of version: {vr}). {msg}"
117
                warn(dep_msg, DeprecationWarning)
118
                return func(*args, **kwargs)
119
120
            # noinspection PyDeprecation
121
            return wraps(func)(my_fn)
122
        raise AssertionError(f"What is {level}?")
123
124
    return dec
125
126
127
def incomplete(vr: str | None = "", msg: str | None = None) -> Callable[..., Any]:
128
    return status(CodeStatus.INCOMPLETE, vr, msg)
129
130
131
def preview(vr: str | None = "", msg: str | None = None) -> Callable[..., Any]:
132
    return status(CodeStatus.PREVIEW, vr, msg)
133
134
135
def pending_deprecation(vr: str | None = "", msg: str | None = None) -> Callable[..., Any]:
136
    return status(CodeStatus.PENDING_DEPRECATION, vr, msg)
137
138
139
def deprecated(vr: str | None = "", msg: str | None = None) -> Callable[..., Any]:
140
    return status(CodeStatus.DEPRECATED, vr, msg)
141
142
143
def removed(vr: str | None = "", msg: str | None = None) -> Callable[..., Any]:
144
    return status(CodeStatus.REMOVED, vr, msg)
145
146
147
class _Utils:
148
    @classmethod
149
    def exclude_fn(cls: type[Self], *items) -> Callable[str, bool]:
150
        fns = [cls._exclude_fn(x) for x in items]
151
152
        def exclude(s: str):
153
            return any(fn(s) for fn in fns)
154
155
        return exclude
156
157
    @classmethod
158
    def _exclude_fn(cls: type[Self], exclude) -> Callable[str, bool]:
159
        if exclude is None or exclude is False:
160
            return lambda s: False
161
        if isinstance(exclude, str):
162
            return lambda s: s == exclude
163
        elif isinstance(exclude, Collection):
164
            return lambda s: s in exclude
165
        elif callable(exclude):
166
            return exclude
167
        else:
168
            raise TypeError(str(exclude))
169
170
    @classmethod
171
    def gen_str(
172
        cls: type[Self],
173
        obj,
174
        fields: Collection[str] | None = None,
175
        *,
176
        exclude: Callable[[str], bool] = lambda _: False,
177
        address: bool = False,
178
    ) -> str:
179
        _name = obj.__class__.__name__
180
        _fields = ", ".join(
181
            k + "=" + ('"' + v + '"' if isinstance(v, str) else str(v))
182
            for k, v in cls.gen_list(obj, fields, exclude=exclude, address=address)
183
        )
184
        return f"{_name}({_fields})"
185
186
    @classmethod
187
    def gen_html(
188
        cls: type[Self],
189
        obj,
190
        fields: Collection[str] | None = None,
191
        *,
192
        exclude: Callable[[str], bool] = lambda _: False,
193
        address: bool = True,
194
    ) -> str:
195
        _name = obj.__class__.__name__
196
        _fields = ", ".join(
197
            f"{k}={_html.escape(v)}" for k, v in cls.gen_list(obj, fields, exclude=exclude, address=address)
198
        )
199
        return f"{_name}({_fields})"
200
201
    @classmethod
202
    def gen_list(
203
        cls: type[Self],
204
        obj: Any,
205
        fields: Collection[str] | None = None,
206
        *,
207
        exclude: Callable[[str], bool] = lambda _: False,
208
        address: bool = False,
209
    ) -> str:
210
        yield from _Utils.var_items(obj, fields, exclude=exclude)
211
        if address:
212
            yield "@", str(hex(id(obj)))
213
214
    @classmethod
215
    def var_items(
216
        cls: type[Self],
217
        obj: Any,
218
        fields: Collection[str] | None,
219
        exclude: Callable[[str], bool],
220
    ) -> Generator[tuple[str, Any], None, None]:
221
        yield from [
222
            (key, value)
223
            for key, value in vars(obj).items()
224
            if (fields is None) or (key in fields)
225
            if not key.startswith(f"_{obj.__class__.__name__}__") and not exclude(key)  # imperfect exclude mangled
226
        ]
227
228
229
def add_reprs(
230
    fields: Collection[str] | None = None,
231
    *,
232
    exclude: Collection[str] | Callable[[str], bool] | None | Literal[False] = None,
233
    exclude_from_str: Collection[str] | Callable[[str], bool] | None = None,
234
    exclude_from_repr: Collection[str] | Callable[[str], bool] | None = None,
235
    exclude_from_html: Collection[str] | Callable[[str], bool] | None = None,
236
    exclude_from_rich: Collection[str] | Callable[[str], bool] | None = None,
237
    address: bool = False,
238
    html: bool = True,
239
    rich: bool = True,
240
) -> Callable[..., Any]:
241
    """
242
    Auto-adds `__repr__`, `__str__`, `_repr_html`, and `__rich_repr__`
243
    that use instances' attributes (`vars`).
244
    """
245
    exclude_from_repr = _Utils.exclude_fn(exclude, exclude_from_repr)
246
    exclude_from_str = _Utils.exclude_fn(exclude, exclude_from_str)
247
    exclude_from_html = _Utils.exclude_fn(exclude, exclude_from_html)
248
    exclude_from_rich = _Utils.exclude_fn(exclude, exclude_from_rich)
249
250
    def __repr(self) -> str:
251
        return _Utils.gen_str(self, fields, exclude=exclude_from_repr, address=address)
252
253
    def __str(self) -> str:
254
        return _Utils.gen_str(self, fields, exclude=exclude_from_str)
255
256
    def __html(self) -> str:
257
        return _Utils.gen_html(self, fields, exclude=exclude_from_html)
258
259
    def __rich(self) -> Iterable[Any | tuple[Any] | tuple[str, Any] | tuple[str, Any, Any]]:
260
        yield from _Utils.gen_list(self, fields, exclude=exclude_from_rich)
261
262
    @wraps(add_reprs)
263
    def dec(cls: type) -> type:
264
        if cls.__str__ is object.__str__:
265
            cls.__str__ = __str
266
        if cls.__repr__ is object.__repr__:
267
            cls.__repr__ = __repr
268
        if html and not hasattr(cls, "_repr_html") and html:
269
            cls._repr_html_ = __html
270
        if rich and not hasattr(cls, "__rich_repr__") and rich:
271
            cls.__rich_repr__ = __rich
272
        return cls
273
274
    return dec
275
276
277
__all__ = [
278
    "abstractmethod",
279
    "total_ordering",
280
    "final",
281
    "CodeStatus",
282
    "status",
283
    "CodeIncompleteError",
284
    "PreviewWarning",
285
    "CodeRemovedError",
286
    "add_reprs",
287
    "deprecated",
288
    "pending_deprecation",
289
    "incomplete",
290
    "preview",
291
    "removed",
292
]
293