mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-18 03:10:58 +00:00
feat(python): introduce a deprecation helper
it's a Python class that emits a DeprecationWarning if you try to use it for almost anything (two exceptions that can't be overriden from the wrapper: * isinstance(depr, SomeOtherClass) * depr is None) we will return instances of this class to indicate that a return value of something will be going away
This commit is contained in:
parent
0cae2ab2d0
commit
4610655d77
@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import inspect
|
||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@ -46,6 +48,7 @@ if TYPE_CHECKING:
|
|||||||
from typing_extensions import Concatenate, ParamSpec
|
from typing_extensions import Concatenate, ParamSpec
|
||||||
|
|
||||||
from . import client
|
from . import client
|
||||||
|
from .messages import Success
|
||||||
from .protobuf import MessageType
|
from .protobuf import MessageType
|
||||||
|
|
||||||
MT = TypeVar("MT", bound=MessageType)
|
MT = TypeVar("MT", bound=MessageType)
|
||||||
@ -310,6 +313,82 @@ def expect(
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _deprecation_retval_helper(value: Any, stacklevel: int = 0) -> Any:
|
||||||
|
stack = inspect.stack()
|
||||||
|
func_name = stack[stacklevel + 1].function
|
||||||
|
|
||||||
|
warning_text = (
|
||||||
|
f"The return value {value!r} of function {func_name}() "
|
||||||
|
"is deprecated and it will be removed in a future version."
|
||||||
|
)
|
||||||
|
|
||||||
|
# start with warnings disabled, otherwise we emit a lot of warnings while still
|
||||||
|
# constructing the deprecation warnings helper
|
||||||
|
warning_enabled = False
|
||||||
|
|
||||||
|
def deprecation_warning_wrapper(orig_value: Callable[P, R]) -> Callable[P, R]:
|
||||||
|
def emit(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
|
nonlocal warning_enabled
|
||||||
|
|
||||||
|
if warning_enabled:
|
||||||
|
warnings.warn(warning_text, DeprecationWarning, stacklevel=2)
|
||||||
|
# only warn once per use
|
||||||
|
warning_enabled = False
|
||||||
|
return orig_value(*args, **kwargs)
|
||||||
|
|
||||||
|
return emit
|
||||||
|
|
||||||
|
# Deprecation wrapper class.
|
||||||
|
# Defined as empty at start.
|
||||||
|
class Deprecated(value.__class__):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Here we install the deprecation_warning_wrapper for all dunder methods.
|
||||||
|
# This implicitly includes __getattribute__, which causes all non-dunder attribute
|
||||||
|
# accesses to also raise the warning.
|
||||||
|
for key in dir(value.__class__):
|
||||||
|
if not key.startswith("__"):
|
||||||
|
# skip non-dunder methods
|
||||||
|
continue
|
||||||
|
if key in ("__new__", "__init__", "__class__"):
|
||||||
|
# skip some problematic items
|
||||||
|
continue
|
||||||
|
orig_value = getattr(value.__class__, key)
|
||||||
|
if not callable(orig_value):
|
||||||
|
# skip non-functions
|
||||||
|
continue
|
||||||
|
# replace the method with a wrapper that emits a warning
|
||||||
|
setattr(Deprecated, key, deprecation_warning_wrapper(orig_value))
|
||||||
|
|
||||||
|
from .protobuf import MessageType
|
||||||
|
|
||||||
|
# construct an instance:
|
||||||
|
if isinstance(value, str):
|
||||||
|
# for str, invoke the copy constructor
|
||||||
|
ret = Deprecated(value)
|
||||||
|
elif isinstance(value, MessageType):
|
||||||
|
# MessageTypes don't have a copy constructor, so
|
||||||
|
# 1. we make an explicit copy
|
||||||
|
value = copy.copy(value)
|
||||||
|
# 2. we change the class of the copy
|
||||||
|
value.__class__ = Deprecated
|
||||||
|
# note: we don't need deep copy because all accesses to inner objects already
|
||||||
|
# trigger the warning via __getattribute__
|
||||||
|
ret = value
|
||||||
|
else:
|
||||||
|
# we don't support other types currently
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# enable warnings
|
||||||
|
warning_enabled = True
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _return_success(msg: "Success") -> str | None:
|
||||||
|
return _deprecation_retval_helper(msg.message, stacklevel=1)
|
||||||
|
|
||||||
|
|
||||||
def session(
|
def session(
|
||||||
f: "Callable[Concatenate[TrezorClient, P], R]",
|
f: "Callable[Concatenate[TrezorClient, P], R]",
|
||||||
) -> "Callable[Concatenate[TrezorClient, P], R]":
|
) -> "Callable[Concatenate[TrezorClient, P], R]":
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from trezorlib import tools
|
from trezorlib import messages, tools
|
||||||
|
|
||||||
VECTORS = ( # descriptor, checksum
|
VECTORS = ( # descriptor, checksum
|
||||||
(
|
(
|
||||||
@ -87,3 +87,50 @@ def test_b58encode(data_hex, encoding_b58):
|
|||||||
@pytest.mark.parametrize("data_hex,encoding_b58", BASE58_VECTORS)
|
@pytest.mark.parametrize("data_hex,encoding_b58", BASE58_VECTORS)
|
||||||
def test_b58decode(data_hex, encoding_b58):
|
def test_b58decode(data_hex, encoding_b58):
|
||||||
assert tools.b58decode(encoding_b58).hex() == data_hex
|
assert tools.b58decode(encoding_b58).hex() == data_hex
|
||||||
|
|
||||||
|
|
||||||
|
def test_return_success_deprecation(recwarn):
|
||||||
|
def mkfoo() -> str:
|
||||||
|
ret = tools._return_success(messages.Success(message="foo"))
|
||||||
|
assert ret is not None # too bad we can't hook "is None" check
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# check that just returning success will not cause a warning
|
||||||
|
mkfoo()
|
||||||
|
assert len(recwarn) == 0
|
||||||
|
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# equality is deprecated
|
||||||
|
assert mkfoo() == "foo"
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# comparison is deprecated
|
||||||
|
assert mkfoo() < "fooa"
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# truthiness is deprecated
|
||||||
|
assert mkfoo()
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# addition is deprecated (and hopefully all other operators)
|
||||||
|
assert mkfoo() + "a" == "fooa"
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# indexing is deprecated
|
||||||
|
assert mkfoo()[0] == "f"
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# methods are deprecated
|
||||||
|
assert mkfoo().startswith("f")
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecation_helper(recwarn):
|
||||||
|
def mkfoo() -> messages.Success:
|
||||||
|
return tools._deprecation_retval_helper(messages.Success(message="foo"))
|
||||||
|
|
||||||
|
# check that just returning success will not cause a warning
|
||||||
|
mkfoo()
|
||||||
|
assert len(recwarn) == 0
|
||||||
|
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# attributes are deprecated
|
||||||
|
assert mkfoo().message == "foo"
|
||||||
|
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
# equality is deprecated (along with other operators hopefully)
|
||||||
|
assert mkfoo() != "foo"
|
||||||
|
Loading…
Reference in New Issue
Block a user