Passed
Push — main ( 87238c...9f1476 )
by Douglas
02:33
created

pocketutils.core.decorators.removed()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 2
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