diff --git a/deferrer/__init__.py b/deferrer/__init__.py index c5bb9f4..1465e56 100644 --- a/deferrer/__init__.py +++ b/deferrer/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.2.7" +__version__ = "0.2.8" -from .__public__ import * +from ._core import * diff --git a/deferrer/__public__.py b/deferrer/__public__.py deleted file mode 100644 index 9dbedaa..0000000 --- a/deferrer/__public__.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -__all__ = [ - "defer", - "defer_scope", -] - -from ._defer import defer -from ._defer_scope import defer_scope diff --git a/deferrer/_core/__init__.py b/deferrer/_core/__init__.py new file mode 100644 index 0000000..975b20b --- /dev/null +++ b/deferrer/_core/__init__.py @@ -0,0 +1,2 @@ +from ._defer import * +from ._defer_scope import * diff --git a/deferrer/_core/_defer/__init__.py b/deferrer/_core/_defer/__init__.py new file mode 100644 index 0000000..ad079ef --- /dev/null +++ b/deferrer/_core/_defer/__init__.py @@ -0,0 +1 @@ +from ._mixed import * diff --git a/deferrer/_core/_defer/_mixed.py b/deferrer/_core/_defer/_mixed.py new file mode 100644 index 0000000..a84e6cb --- /dev/null +++ b/deferrer/_core/_defer/_mixed.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +__all__ = ["Defer", "defer"] + +from . import _sugarful, _sugarless + + +class Defer(_sugarful.Defer, _sugarless.Defer): + """ + Provides `defer` functionality in both sugarful and sugarless ways. + + Examples + -------- + >>> import sys + >>> from deferrer import defer_scope + + >>> def f_0(): + ... defer and print(0) + ... defer and print(1) + ... print(2) + ... defer and print(3) + ... defer and print(4) + + >>> if sys.version_info < (3, 12): + ... f_0 = defer_scope(f_0) + + >>> f_0() + 2 + 4 + 3 + 1 + 0 + + >>> def f_1(): + ... defer(print)(0) + ... defer(print)(1) + ... print(2) + ... defer(print)(3) + ... defer(print)(4) + + >>> if sys.version_info < (3, 12): + ... f_1 = defer_scope(f_1) + + >>> f_1() + 2 + 4 + 3 + 1 + 0 + """ + + pass + + +defer = Defer() diff --git a/deferrer/_core/_defer/_sugarful.py b/deferrer/_core/_defer/_sugarful.py new file mode 100644 index 0000000..11b55c6 --- /dev/null +++ b/deferrer/_core/_defer/_sugarful.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +__all__ = ["Defer"] + +import re +import sys +from collections.abc import Callable +from types import CellType, FunctionType +from typing import Any, Final, Literal, cast, final +from warnings import warn + +from .._deferred_actions import DeferredAction, ensure_deferred_actions +from ..._utils import ( + Opcode, + build_instruction_code_bytes, + build_instruction_pattern, + extract_argument_from_instruction, + get_code_location, + get_outer_frame, +) + +_MISSING = cast("Any", object()) + + +class Defer: + @staticmethod + def __bool__() -> Literal[False]: + """ + **DO NOT INVOKE** + + This method is only meant to be used during `defer and ...`. + + If used in other ways, the return value will always be `False` + and a warning will be emitted. + """ + + frame = get_outer_frame() + + code = frame.f_code + code_bytes = code.co_code + + i_code_byte = frame.f_lasti + for _ in range(3): + temp_i_code_byte = i_code_byte - 2 + if ( + temp_i_code_byte < 0 + or code_bytes[temp_i_code_byte] != Opcode.EXTENDED_ARG + ): + break + i_code_byte = temp_i_code_byte + code_bytes_0 = code_bytes[i_code_byte:] + + match_0 = re.match(_PATTERN_0, code_bytes_0) + if match_0 is None: + code_location = get_code_location(frame) + message = ( + "" + + ( + "" + + "Method `defer.__bool__()` is called in an unsupported way (" + + code_location + + ")." + ) + + " " + + "It is only designed to be invoked during `defer and ...`." + ) + warn(message) + return False + + n_skipped_bytes = extract_argument_from_instruction(match_0.group(1)) * 2 + code_bytes_1 = match_0.group(2)[:n_skipped_bytes] + + match_1 = re.fullmatch(_PATTERN_1, code_bytes_1) + assert match_1 is not None + code_bytes_2 = match_1.group(1) + + global_scope = frame.f_globals + local_scope = frame.f_locals + + dummy_code_bytes = bytes() + dummy_closure = () + dummy_consts = code.co_consts + + # If the original function has local variables, pass their current values by + # appending these values to constants and using some instruction pairs of + # "LOAD_CONST" and "STORE_FAST". + local_var_names = code.co_varnames + for i_local_var, name in enumerate(local_var_names): + if (value := local_scope.get(name, _MISSING)) is _MISSING: + # The value does not exist, so there is nothing to store. + continue + + dummy_code_bytes += build_instruction_code_bytes( + Opcode.LOAD_CONST, len(dummy_consts) + ) + dummy_code_bytes += build_instruction_code_bytes( + Opcode.STORE_FAST, i_local_var + ) + dummy_consts += (value,) + + # If the original function has cell variables, add some instructions of + # "MAKE_CELL". + # For non-local cell variables, pass their current values by appending these + # values to constants and using some instruction pairs of "LOAD_CONST" and + # "STORE_DEREF". + cell_var_names = code.co_cellvars + next_i_nonlocal_cell_var = len(local_var_names) + for name in cell_var_names: + try: + i_local_var = local_var_names.index(name) + except ValueError: + i_local_var = None + + if i_local_var is not None: + dummy_code_bytes += build_instruction_code_bytes( + Opcode.MAKE_CELL, i_local_var + ) + else: + i_nonlocal_cell_var = next_i_nonlocal_cell_var + next_i_nonlocal_cell_var += 1 + + dummy_code_bytes += build_instruction_code_bytes( + Opcode.MAKE_CELL, i_nonlocal_cell_var + ) + + if (value := local_scope.get(name, _MISSING)) is _MISSING: + # The value does not exist, so there is nothing to store. + continue + + dummy_code_bytes += build_instruction_code_bytes( + Opcode.LOAD_CONST, len(dummy_consts) + ) + dummy_code_bytes += build_instruction_code_bytes( + Opcode.STORE_DEREF, i_nonlocal_cell_var + ) + dummy_consts += (value,) + + # If the original function has free variables, create a closure based on their + # current values, and add a "COPY_FREE_VARS" instruction. + free_var_names = code.co_freevars + n_free_vars = len(free_var_names) + if n_free_vars != 0: + dummy_closure += tuple( + ( + CellType() + if (value := frame.f_locals.get(name, _MISSING)) is _MISSING + else CellType(value) + ) + for name in free_var_names + ) + dummy_code_bytes += build_instruction_code_bytes( + Opcode.COPY_FREE_VARS, n_free_vars + ) + + # Copy the bytecode of the RHS part in `defer and ...` into the dummy function. + dummy_code_bytes += code_bytes_2 + + # The dummy function should return something. The simplest way is to return + # whatever value is currently active. + dummy_code_bytes += build_instruction_code_bytes(Opcode.RETURN_VALUE) + + # The dummy function will be called with no argument. + dummy_code = code.replace( + co_argcount=0, + co_posonlyargcount=0, + co_kwonlyargcount=0, + co_code=dummy_code_bytes, + co_consts=dummy_consts, + co_linetable=bytes(), + co_exceptiontable=bytes(), + ) + + new_function = FunctionType( + code=dummy_code, globals=global_scope, closure=dummy_closure + ) + deferred_call = _DeferredCall(new_function) + + deferred_actions = ensure_deferred_actions(frame) + deferred_actions.append(deferred_call) + + return False + + +if sys.version_info >= (3, 13) and sys.version_info < (3, 14): + # ``` + # LOAD_GLOBAL ? (defer) + # COPY + # --> TO_BOOL + # POP_JUMP_IF_FALSE ? + # POP_TOP + # + # ``` + + _PATTERN_0 = re.compile( + pattern=( + ( + "%(TO_BOOL)s(%(POP_JUMP_IF_FALSE)s)(%(POP_TOP)s.*)" + % { + "TO_BOOL": build_instruction_pattern(Opcode.TO_BOOL), + "POP_JUMP_IF_FALSE": build_instruction_pattern( + Opcode.POP_JUMP_IF_FALSE + ), + "POP_TOP": build_instruction_pattern(Opcode.POP_TOP), + } + ).encode("iso8859-1") + ), + flags=re.DOTALL, + ) + _PATTERN_1 = re.compile( + pattern=( + ( + "%(POP_TOP)s(.*?)(?:%(POP_TOP)s%(JUMP_BACKWARD)s)?" + % { + "POP_TOP": build_instruction_pattern(Opcode.POP_TOP), + "JUMP_BACKWARD": build_instruction_pattern(Opcode.JUMP_BACKWARD), + } + ).encode("iso8859-1") + ), + flags=re.DOTALL, + ) + +if sys.version_info >= (3, 12) and sys.version_info < (3, 13): + # ``` + # LOAD_GLOBAL ? (defer) + # COPY + # --> POP_JUMP_IF_FALSE ? + # POP_TOP + # + # ``` + + _PATTERN_0 = re.compile( + pattern=( + ( + "(%(POP_JUMP_IF_FALSE)s)(%(POP_TOP)s.*)" + % { + "POP_JUMP_IF_FALSE": build_instruction_pattern( + Opcode.POP_JUMP_IF_FALSE + ), + "POP_TOP": build_instruction_pattern(Opcode.POP_TOP), + } + ).encode("iso8859-1") + ), + flags=re.DOTALL, + ) + _PATTERN_1 = re.compile( + pattern=( + ( + "%(POP_TOP)s(.*)" + % { + "POP_TOP": build_instruction_pattern(Opcode.POP_TOP), + } + ).encode("iso8859-1") + ), + flags=re.DOTALL, + ) + +if sys.version_info >= (3, 11) and sys.version_info < (3, 12): + # ``` + # LOAD_GLOBAL ? (defer) + # --> JUMP_IF_FALSE_OR_POP ? + # + # ``` + + _PATTERN_0 = re.compile( + pattern=( + ( + "(%(JUMP_IF_FALSE_OR_POP)s)(.*)" + % { + "JUMP_IF_FALSE_OR_POP": build_instruction_pattern( + Opcode.JUMP_IF_FALSE_OR_POP + ), + } + ).encode("iso8859-1") + ), + flags=re.DOTALL, + ) + _PATTERN_1 = re.compile( + pattern="(.*)".encode("iso8859-1"), + flags=re.DOTALL, + ) + + +@final +class _DeferredCall(DeferredAction): + def __init__(self, body: Callable[[], Any], /) -> None: + self._body: Final = body + + def perform(self, /) -> None: + self._body() diff --git a/deferrer/_core/_defer/_sugarless.py b/deferrer/_core/_defer/_sugarless.py new file mode 100644 index 0000000..aed9a06 --- /dev/null +++ b/deferrer/_core/_defer/_sugarless.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +__all__ = ["Defer"] + +from collections.abc import Callable +from typing import Any, Final, Generic, ParamSpec, final +from warnings import warn + +from .._deferred_actions import DeferredAction, ensure_deferred_actions +from ..._utils import get_code_location, get_outer_frame + +_P = ParamSpec("_P") + + +class Defer: + @staticmethod + def __call__(callable: Callable[_P, Any], /) -> Callable[_P, None]: + """ + Converts a callable into a deferred callable. + + Return value of the given callable will always be ignored. + """ + + frame = get_outer_frame() + code_location = get_code_location(frame) + deferred_actions = ensure_deferred_actions(frame) + + deferred_callable = _DeferredCallable(callable, code_location) + deferred_actions.append(deferred_callable) + + return deferred_callable + + +@final +class _DeferredCallable(DeferredAction, Generic[_P]): + _body: Final[Callable[..., Any]] + _code_location: Final[str] + + _args_and_kwargs: tuple[tuple[Any, ...], dict[str, Any]] | None + + def __init__(self, body: Callable[_P, Any], /, code_location: str) -> None: + self._body = body + self._code_location = code_location + + self._args_and_kwargs = None + + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> None: + if self._args_and_kwargs is not None: + raise RuntimeError("`defer(...)` gets further called more than once.") + + self._args_and_kwargs = (args, kwargs) + + def perform(self, /) -> None: + body = self._body + args_and_kwargs = self._args_and_kwargs + + if args_and_kwargs is not None: + args, kwargs = args_and_kwargs + body(*args, **kwargs) + return + + try: + body() + except TypeError as e: + traceback = e.__traceback__ + assert traceback is not None + + if traceback.tb_next is not None: + # This `TypeError` was raised by user. We should not do anthing special. + raise + + # This `TypeError` was raised on function call, which means that there was + # a signature error. + # It is typically because a deferred callable with at least one required + # argument doesn't ever get further called with appropriate arguments. + code_location = self._code_location + message = f"`defer(...)` has never got further called ({code_location})." + warn(message) diff --git a/deferrer/_core/_defer_scope.py b/deferrer/_core/_defer_scope.py new file mode 100644 index 0000000..eb8994a --- /dev/null +++ b/deferrer/_core/_defer_scope.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +__all__ = ["defer_scope"] + +import operator +from collections.abc import Callable, Iterable, Iterator +from contextlib import AbstractContextManager +from functools import update_wrapper +from types import FrameType, TracebackType +from typing import Any, Final, Generic, ParamSpec, TypeVar, final, overload + +from ._deferred_actions import ( + DeferredActions, + callable_deferred_actions_recorder, + context_deferred_actions_recorder, +) +from .._utils import get_current_frame, get_outer_frame + +_Wrapped_t = TypeVar("_Wrapped_t") + +_P = ParamSpec("_P") +_R = TypeVar("_R") + +_E = TypeVar("_E") + + +@overload +def defer_scope() -> AbstractContextManager: ... +@overload +def defer_scope(wrapped: Callable[_P, _R], /) -> Callable[_P, _R]: ... +@overload +def defer_scope(wrapped: Iterable[_E], /) -> Iterable[_E]: ... + + +def defer_scope(wrapped: Any = None, /) -> Any: + if wrapped is None: + return _DeferScopeContextManager() + + return update_wrapper(_DeferScopeWrapper(wrapped), wrapped) + + +@final +class _DeferScopeContextManager(AbstractContextManager): + _frame: FrameType | None = None + _deferred_actions: DeferredActions | None = None + + def __enter__(self, /) -> Any: + frame = get_outer_frame() + assert self._frame is None + self._frame = frame + + deferred_actions = context_deferred_actions_recorder.setup(frame) + assert self._deferred_actions is None + self._deferred_actions = deferred_actions + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + /, + ) -> None: + frame = self._frame + assert frame is not None + del self._frame + + deferred_actions = context_deferred_actions_recorder.teardown(frame) + assert self._deferred_actions is deferred_actions + del self._deferred_actions + + +@final +class _DeferScopeWrapper(Generic[_Wrapped_t]): + def __init__(self, wrapped: _Wrapped_t, /) -> None: + self._wrapped: Final = wrapped + + def __call__( + self: _DeferScopeWrapper[Callable[_P, _R]], + /, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _R: + frame = get_current_frame() + + deferred_actions = callable_deferred_actions_recorder.setup(frame) + try: + result = self._wrapped(*args, **kwargs) + finally: + __ = callable_deferred_actions_recorder.teardown(frame) + assert __ is deferred_actions + deferred_actions.drain() + + return result + + def __iter__(self: _DeferScopeWrapper[Iterable[_E]], /) -> Iterator[_E]: + frame = get_outer_frame() + + @iter + @operator.call + def _() -> Iterable[_E]: + for element in self._wrapped: + deferred_actions = context_deferred_actions_recorder.setup(frame) + try: + yield element + finally: + __ = context_deferred_actions_recorder.teardown(frame) + assert __ is deferred_actions + deferred_actions.drain() + + iterator = _ + return iterator diff --git a/deferrer/_core/_deferred_actions.py b/deferrer/_core/_deferred_actions.py new file mode 100644 index 0000000..4af85a8 --- /dev/null +++ b/deferrer/_core/_deferred_actions.py @@ -0,0 +1,203 @@ +from __future__ import annotations + +__all__ = [ + "CallableDeferredActionsRecorder", + "ContextDeferredActionsRecorder", + "DeferredAction", + "DeferredActions", + "callable_deferred_actions_recorder", + "context_deferred_actions_recorder", + "ensure_deferred_actions", +] + +import sys +from abc import ABC, abstractmethod +from types import FrameType +from typing import Any, Final, Never, cast, final + +from .._utils import is_class_frame, is_global_frame + + +class DeferredAction(ABC): + """ + An object that stands for an action that is meant to be performed + later. + """ + + @abstractmethod + def perform(self, /) -> None: ... + + +@final +class DeferredActions: + """ + A list-like object that holds `DeferredAction` objects. + + When a `DeferredActions` object is being disposed, all + `DeferredAction` objects it holds will get performed in a FILO + order. + """ + + _internal_list: Final[list[DeferredAction]] + + def __init__(self, /) -> None: + self._internal_list = [] + + def append(self, deferred_call: DeferredAction, /) -> None: + self._internal_list.append(deferred_call) + + def drain(self, /) -> None: + exceptions: list[Exception] = [] + + internal_list = self._internal_list + while len(internal_list) > 0: + deferred_call = internal_list.pop() + + try: + deferred_call.perform() + except Exception as e: + exceptions.append(e) + + n_exceptions = len(exceptions) + if n_exceptions == 0: + return + + exception_group = ExceptionGroup("deferred exception(s)", exceptions) + raise exception_group + + def __del__(self, /) -> None: + self.drain() + + +@final +class CallableDeferredActionsRecorder: + """ + A recorder that records `DeferredActions` objects associated with + callables. + """ + + _internal_dict: Final[dict[FrameType, DeferredActions]] + + def __init__(self, /) -> None: + self._internal_dict = {} + + def setup(self, /, outer_frame: FrameType) -> DeferredActions: + internal_dict = self._internal_dict + assert outer_frame not in internal_dict + deferred_actions = DeferredActions() + internal_dict[outer_frame] = deferred_actions + return deferred_actions + + def teardown(self, /, outer_frame: FrameType) -> DeferredActions: + return self._internal_dict.pop(outer_frame) + + def get(self, /, frame: FrameType) -> DeferredActions | None: + outer_frame = frame.f_back + assert outer_frame is not None + deferred_actions = self._internal_dict.get(outer_frame) + return deferred_actions + + +callable_deferred_actions_recorder = CallableDeferredActionsRecorder() +""" +The singleton instance of `CallableDeferredActionsRecorder`. +""" + + +@final +class ContextDeferredActionsRecorder: + """ + A recorder that records `DeferredActions` objects associated with + context managers. + """ + + _internal_dict: Final[dict[FrameType, list[DeferredActions]]] + + def __init__(self, /) -> None: + self._internal_dict = {} + + def setup(self, /, frame: FrameType) -> DeferredActions: + internal_dict = self._internal_dict + deferred_actions_list = internal_dict.setdefault(frame, []) + deferred_actions = DeferredActions() + deferred_actions_list.append(deferred_actions) + return deferred_actions + + def teardown(self, /, frame: FrameType) -> DeferredActions: + internal_dict = self._internal_dict + deferred_actions_list = internal_dict[frame] + deferred_actions = deferred_actions_list.pop() + if len(deferred_actions_list) == 0: + del internal_dict[frame] + return deferred_actions + + def get(self, /, frame: FrameType) -> DeferredActions | None: + deferred_actions_list = self._internal_dict.get(frame) + if deferred_actions_list is None: + return None + deferred_actions = deferred_actions_list[-1] + return deferred_actions + + +context_deferred_actions_recorder = ContextDeferredActionsRecorder() +""" +The singleton instance of `ContextDeferredActionsRecorder`. +""" + + +def ensure_deferred_actions( + frame: FrameType, + *, + # We are using a newly created `object` instance as a secret key in local scopes + # so that it will never conflict with any existing key. + # It is intentially typed as `Never` so that it will never be assigned another value + # when the function is getting called. + __KEY__: Never = cast( + "Any", + object(), # pyright: ignore[reportCallInDefaultInitializer] + ), +) -> DeferredActions: + """ + Returns the most active `DeferredActions` object for the given + frame. + """ + + # Try to find one in `context_deferred_actions_recorder` and then + # `callable_deferred_actions_recorder`. + for recorder in ( + context_deferred_actions_recorder, + callable_deferred_actions_recorder, + ): + deferred_actions = recorder.get(frame) + if deferred_actions is not None: + return deferred_actions + + # No match. We shall check local scope soon. + + # There is no way to inject an object into a local scope in Python 3.11. + if sys.version_info < (3, 12): + raise RuntimeError( + ( + "cannot inject deferred actions into local scope with" + " Python older than 3.12" + ) + ) + + # If we injected an object into a global scope or a class scope, it would not get + # released in time. + if is_global_frame(frame): + raise RuntimeError("cannot inject deferred actions into global scope") + if is_class_frame(frame): + raise RuntimeError("cannot inject deferred actions into class scope") + + local_scope = frame.f_locals + + # If one existing instance is already in the local scope, just reuse it. + deferred_actions = local_scope.get(__KEY__) + if deferred_actions is not None: + return deferred_actions + + # We are now forced to deploy a new instance. + deferred_actions = DeferredActions() + local_scope[__KEY__] = deferred_actions + return deferred_actions diff --git a/deferrer/_defer.py b/deferrer/_defer.py deleted file mode 100644 index 8b0acbe..0000000 --- a/deferrer/_defer.py +++ /dev/null @@ -1,394 +0,0 @@ -from __future__ import annotations - -__all__ = ["defer"] - -import sys -from collections.abc import Callable -from types import CellType, FunctionType -from typing import Any, Final, Generic, Literal, ParamSpec, cast, final -from warnings import warn - -from ._code_location import get_code_location -from ._defer_scope import ensure_deferred_actions -from ._deferred_actions import DeferredAction -from ._frame import get_outer_frame -from ._opcode import Opcode, build_code_byte_sequence, build_code_bytes -from ._sequence_matching import WILDCARD, sequence_has_prefix, sequence_has_suffix - -_P = ParamSpec("_P") - -_MISSING = cast("Any", object()) - - -@final -class Defer: - """ - Provides `defer` functionality in both sugarful and sugarless ways. - - Examples - -------- - >>> import sys - >>> from deferrer import defer_scope - - >>> def f_0(): - ... defer and print(0) - ... defer and print(1) - ... print(2) - ... defer and print(3) - ... defer and print(4) - - >>> if sys.version_info < (3, 12): - ... f_0 = defer_scope(f_0) - - >>> f_0() - 2 - 4 - 3 - 1 - 0 - - >>> def f_1(): - ... defer(print)(0) - ... defer(print)(1) - ... print(2) - ... defer(print)(3) - ... defer(print)(4) - - >>> if sys.version_info < (3, 12): - ... f_1 = defer_scope(f_1) - - >>> f_1() - 2 - 4 - 3 - 1 - 0 - """ - - @staticmethod - def __bool__() -> Literal[False]: - """ - **DO NOT INVOKE** - - This method is only meant to be used during `defer and ...`. - - If used in other ways, the return value will always be `False` - and a warning will be emitted. - """ - - frame = get_outer_frame() - - code = frame.f_code - code_bytes = code.co_code - i_code_byte = frame.f_lasti - - if not sequence_has_prefix( - code_bytes[i_code_byte:], _expected_code_bytes_prefix - ): - code_location = get_code_location(frame) - message = ( - "" - + ( - "" - + "Method `defer.__bool__()` is called in an unsupported way (" - + code_location - + ")." - ) - + " " - + "It is only designed to be invoked during `defer and ...`." - ) - warn(message) - return False - - global_scope = frame.f_globals - local_scope = frame.f_locals - - dummy_code_bytes = bytes() - dummy_closure = () - dummy_consts = code.co_consts - - # If the original function has local variables, pass their current values by - # appending these values to constants and using some instruction pairs of - # "LOAD_CONST" and "STORE_FAST". - local_var_names = code.co_varnames - for i_local_var, name in enumerate(local_var_names): - if (value := local_scope.get(name, _MISSING)) is _MISSING: - # The value does not exist, so there is nothing to store. - continue - - dummy_code_bytes += build_code_bytes(Opcode.LOAD_CONST, len(dummy_consts)) - dummy_code_bytes += build_code_bytes(Opcode.STORE_FAST, i_local_var) - dummy_consts += (value,) - - # If the original function has cell variables, add some instructions of - # "MAKE_CELL". - # For non-local cell variables, pass their current values by appending these - # values to constants and using some instruction pairs of "LOAD_CONST" and - # "STORE_DEREF". - cell_var_names = code.co_cellvars - next_i_nonlocal_cell_var = len(local_var_names) - for name in cell_var_names: - try: - i_local_var = local_var_names.index(name) - except ValueError: - i_local_var = None - - if i_local_var is not None: - dummy_code_bytes += build_code_bytes(Opcode.MAKE_CELL, i_local_var) - else: - i_nonlocal_cell_var = next_i_nonlocal_cell_var - next_i_nonlocal_cell_var += 1 - - dummy_code_bytes += build_code_bytes( - Opcode.MAKE_CELL, i_nonlocal_cell_var - ) - - if (value := local_scope.get(name, _MISSING)) is _MISSING: - # The value does not exist, so there is nothing to store. - continue - - dummy_code_bytes += build_code_bytes( - Opcode.LOAD_CONST, len(dummy_consts) - ) - dummy_code_bytes += build_code_bytes( - Opcode.STORE_DEREF, i_nonlocal_cell_var - ) - dummy_consts += (value,) - - # If the original function has free variables, create a closure based on their - # current values, and add a "COPY_FREE_VARS" instruction. - free_var_names = code.co_freevars - n_free_vars = len(free_var_names) - if n_free_vars != 0: - dummy_closure += tuple( - ( - CellType() - if (value := frame.f_locals.get(name, _MISSING)) is _MISSING - else CellType(value) - ) - for name in free_var_names - ) - dummy_code_bytes += build_code_bytes(Opcode.COPY_FREE_VARS, n_free_vars) - - # Copy the bytecode of the RHS part in `defer and ...` into the dummy function. - n_skipped_instructions = code_bytes[i_code_byte + _jumping_start_offset + 1] - n_skipped_bytes = n_skipped_instructions * 2 - dummy_code_bytes += code_bytes[ - (i_code_byte + _rhs_offset) : ( - i_code_byte + _jumping_stop_offset + n_skipped_bytes - ) - ] - - # For Python 3.13, if the current expression is the last expression in a loop, - # there will be a duplicated `POP_TOP + JUMP_BACKWARD` instruction pair. - # Cut it off before it can cause any trouble. - if sequence_has_suffix(dummy_code_bytes, _unneeded_code_bytes_suffix): - dummy_code_bytes = dummy_code_bytes[: -len(_unneeded_code_bytes_suffix)] - - # The dummy function should return something. The simplest way is to return - # whatever value is currently active. - dummy_code_bytes += build_code_bytes(Opcode.RETURN_VALUE) - - # The dummy function will be called with no argument. - dummy_code = code.replace( - co_argcount=0, - co_posonlyargcount=0, - co_kwonlyargcount=0, - co_code=dummy_code_bytes, - co_consts=dummy_consts, - co_linetable=bytes(), - co_exceptiontable=bytes(), - ) - - new_function = FunctionType( - code=dummy_code, globals=global_scope, closure=dummy_closure - ) - deferred_call = _DeferredCall(new_function) - - deferred_actions = ensure_deferred_actions(frame) - deferred_actions.append(deferred_call) - - return False - - @staticmethod - def __call__(callable: Callable[_P, Any], /) -> Callable[_P, None]: - """ - Converts a callable into a deferred callable. - - Return value of the given callable will always be ignored. - """ - - frame = get_outer_frame() - code_location = get_code_location(frame) - - deferred_callable = _DeferredCallable(callable, code_location) - - deferred_actions = ensure_deferred_actions(frame) - deferred_actions.append(deferred_callable) - - return deferred_callable - - -_expected_code_bytes_prefix: list[int] -""" -Code bytes pattern when `defer.__bool__()` is invoked upon `defer and ...`. -""" - -_jumping_start_offset: int -""" -Distance in bytes between the current instruction and the jumping instruction. -""" - -_jumping_stop_offset: int -""" -Distance in bytes between the current instruction and the next instruction to the -jumping instruction. -""" - -_rhs_offset: int -""" -Distance in bytes between the current instruction and the instructions of RHS in -`defer and ...`. -""" - - -if sys.version_info >= (3, 13) and sys.version_info < (3, 14): - # ``` - # LOAD_GLOBAL ? (defer) - # COPY - # --> TO_BOOL - # POP_JUMP_IF_FALSE ? - # CACHE - # POP_TOP - # - # ``` - - _expected_code_bytes_prefix = [] - - _expected_code_bytes_prefix.extend( - build_code_byte_sequence(Opcode.TO_BOOL, cache_value=WILDCARD) - ) - _jumping_start_offset = len(_expected_code_bytes_prefix) - - _expected_code_bytes_prefix.extend( - build_code_byte_sequence( - Opcode.POP_JUMP_IF_FALSE, WILDCARD, cache_value=WILDCARD - ) - ) - _jumping_stop_offset = len(_expected_code_bytes_prefix) - - _expected_code_bytes_prefix.extend( - build_code_byte_sequence(Opcode.POP_TOP, cache_value=WILDCARD) - ) - _rhs_offset = len(_expected_code_bytes_prefix) - -if sys.version_info >= (3, 12) and sys.version_info < (3, 13): - # ``` - # LOAD_GLOBAL ? (defer) - # COPY - # --> POP_JUMP_IF_FALSE ? - # POP_TOP - # - # ``` - - _expected_code_bytes_prefix = [] - _jumping_start_offset = 0 - - _expected_code_bytes_prefix.extend( - build_code_byte_sequence( - Opcode.POP_JUMP_IF_FALSE, WILDCARD, cache_value=WILDCARD - ) - ) - _jumping_stop_offset = len(_expected_code_bytes_prefix) - - _expected_code_bytes_prefix.extend( - build_code_byte_sequence(Opcode.POP_TOP, cache_value=WILDCARD) - ) - _rhs_offset = len(_expected_code_bytes_prefix) - -if sys.version_info >= (3, 11) and sys.version_info < (3, 12): - # ``` - # LOAD_GLOBAL ? (defer) - # --> JUMP_IF_FALSE_OR_POP ? - # - # ``` - - _expected_code_bytes_prefix = [] - _jumping_start_offset = 0 - - _expected_code_bytes_prefix.extend( - build_code_byte_sequence( - Opcode.JUMP_IF_FALSE_OR_POP, WILDCARD, cache_value=WILDCARD - ) - ) - _jumping_stop_offset = len(_expected_code_bytes_prefix) - _rhs_offset = _jumping_stop_offset - - -_unneeded_code_bytes_suffix = ( - *build_code_byte_sequence(Opcode.POP_TOP, cache_value=WILDCARD), - *build_code_byte_sequence(Opcode.JUMP_BACKWARD, WILDCARD, cache_value=WILDCARD), -) - - -defer = Defer() - - -@final -class _DeferredCall(DeferredAction): - def __init__(self, body: Callable[[], Any], /) -> None: - self._body: Final = body - - def perform(self, /) -> None: - self._body() - - -@final -class _DeferredCallable(DeferredAction, Generic[_P]): - _body: Final[Callable[..., Any]] - _code_location: Final[str] - - _args_and_kwargs: tuple[tuple[Any, ...], dict[str, Any]] | None - - def __init__(self, body: Callable[_P, Any], /, code_location: str) -> None: - self._body = body - self._code_location = code_location - - self._args_and_kwargs = None - - def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> None: - if self._args_and_kwargs is not None: - raise RuntimeError("`defer(...)` gets further called more than once.") - - self._args_and_kwargs = (args, kwargs) - - def perform(self, /) -> None: - body = self._body - args_and_kwargs = self._args_and_kwargs - - if args_and_kwargs is not None: - args, kwargs = args_and_kwargs - body(*args, **kwargs) - return - - try: - body() - except Exception as e: - if isinstance(e, TypeError): - traceback = e.__traceback__ - assert traceback is not None - - if traceback.tb_next is None: - # This `TypeError` was raised on function call, which means that - # there was a signature error. - # It is typically because a deferred callable with at least one - # required argument doesn't ever get further called with appropriate - # arguments. - code_location = self._code_location - message = ( - f"`defer(...)` has never got further called ({code_location})." - ) - warn(message) - return - - raise e diff --git a/deferrer/_defer_scope.py b/deferrer/_defer_scope.py deleted file mode 100644 index df25b31..0000000 --- a/deferrer/_defer_scope.py +++ /dev/null @@ -1,229 +0,0 @@ -from __future__ import annotations - -from types import TracebackType - -__all__ = [ - "_DeferScopeContextManager", - "defer_scope", - "ensure_deferred_actions", -] - -import operator -import sys -from collections.abc import Callable, Iterable, Iterator -from contextlib import AbstractContextManager -from functools import update_wrapper -from types import FrameType -from typing import Any, Final, Generic, ParamSpec, TypeVar, cast, final, overload - -from ._deferred_actions import DeferredActions -from ._frame import get_current_frame, get_outer_frame, is_class_frame, is_global_frame - -_Wrapped_t = TypeVar("_Wrapped_t") - -_P = ParamSpec("_P") -_R = TypeVar("_R") - -_E = TypeVar("_E") - -_LOCAL_KEY = cast("Any", object()) - - -@overload -def defer_scope() -> AbstractContextManager: ... -@overload -def defer_scope(wrapped: Callable[_P, _R], /) -> Callable[_P, _R]: ... -@overload -def defer_scope(wrapped: Iterable[_E], /) -> Iterable[_E]: ... - - -def defer_scope(wrapped: Any = None, /) -> Any: - if wrapped is None: - return _DeferScopeContextManager() - else: - wrapper = _DeferScopeWrapper(wrapped) - wrapper = update_wrapper(wrapper, wrapped) - return wrapper - - -def ensure_deferred_actions(frame: FrameType) -> DeferredActions: - for recorder in ( - _context_deferred_actions_recorder, - _callable_deferred_actions_recorder, - ): - deferred_actions = recorder.find(frame) - if deferred_actions is not None: - return deferred_actions - - if sys.version_info < (3, 12): - raise RuntimeError( - "cannot inject deferred actions into local scope" - + " with Python older than 3.12" - ) - - local_scope = frame.f_locals - - deferred_actions = local_scope.get(_LOCAL_KEY) - if deferred_actions is not None: - return deferred_actions - - if is_global_frame(frame): - raise RuntimeError("cannot inject deferred actions into global scope") - - if is_class_frame(frame): - raise RuntimeError("cannot inject deferred actions into class scope") - - deferred_actions = DeferredActions() - local_scope[_LOCAL_KEY] = deferred_actions - - return deferred_actions - - -@final -class _DeferScopeContextManager(AbstractContextManager): - _frame: FrameType | None = None - _deferred_actions: DeferredActions | None = None - - def __enter__(self, /) -> Any: - frame = get_outer_frame() - assert self._frame is None - self._frame = frame - - deferred_actions = DeferredActions() - assert self._deferred_actions is None - self._deferred_actions = deferred_actions - - _context_deferred_actions_recorder.add(frame, deferred_actions) - - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - /, - ) -> None: - frame = self._frame - assert frame is not None - del self._frame - - deferred_actions = self._deferred_actions - assert deferred_actions is not None - del self._deferred_actions - - _context_deferred_actions_recorder.remove(frame, deferred_actions) - - return None - - -@final -class _DeferScopeWrapper(Generic[_Wrapped_t]): - def __init__(self, wrapped: _Wrapped_t, /) -> None: - self._wrapped: Final = wrapped - - def __call__( - self: _DeferScopeWrapper[Callable[_P, _R]], - /, - *args: _P.args, - **kwargs: _P.kwargs, - ) -> _R: - wrapped = self._wrapped - - frame = get_current_frame() - deferred_actions = DeferredActions() - - _callable_deferred_actions_recorder.add(frame, deferred_actions) - try: - result = wrapped(*args, **kwargs) - finally: - _callable_deferred_actions_recorder.remove(frame, deferred_actions) - - return result - - def __iter__(self: _DeferScopeWrapper[Iterable[_E]], /) -> Iterator[_E]: - frame = get_outer_frame() - wrapped = self._wrapped - - @iter - @operator.call - def _() -> Iterable[_E]: - for element in wrapped: - deferred_actions = DeferredActions() - - _context_deferred_actions_recorder.add(frame, deferred_actions) - try: - yield element - finally: - _context_deferred_actions_recorder.remove(frame, deferred_actions) - - iterator = _ - return iterator - - -class _CallableDeferredActionsRecorder: - _internal_dict: Final[dict[FrameType, DeferredActions]] - - def __init__(self, /) -> None: - self._internal_dict = {} - - def add(self, outer_frame: FrameType, deferred_actions: DeferredActions, /) -> None: - __ = self._internal_dict.setdefault(outer_frame, deferred_actions) - assert __ is deferred_actions - - def remove( - self, outer_frame: FrameType, deferred_actions: DeferredActions, / - ) -> None: - __ = self._internal_dict.pop(outer_frame) - assert __ is deferred_actions - - deferred_actions.drain() - - def find(self, frame: FrameType, /) -> DeferredActions | None: - outer_frame = frame.f_back - assert outer_frame is not None - deferred_actions = self._internal_dict.get(outer_frame) - return deferred_actions - - -_callable_deferred_actions_recorder = _CallableDeferredActionsRecorder() - - -class _ContextDeferredActionsRecorder: - _internal_dict: Final[dict[FrameType, list[DeferredActions]]] - - def __init__(self, /) -> None: - self._internal_dict = {} - - def add(self, frame: FrameType, deferred_actions: DeferredActions, /) -> None: - internal_dict = self._internal_dict - - deferred_actions_list = internal_dict.get(frame) - if deferred_actions_list is None: - deferred_actions_list = [] - internal_dict[frame] = deferred_actions_list - - deferred_actions_list.append(deferred_actions) - - def remove(self, frame: FrameType, deferred_actions: DeferredActions, /) -> None: - internal_dict = self._internal_dict - - deferred_actions_list = internal_dict[frame] - __ = deferred_actions_list.pop() - assert __ is deferred_actions - - if len(deferred_actions_list) == 0: - del internal_dict[frame] - - deferred_actions.drain() - - def find(self, frame: FrameType, /) -> DeferredActions | None: - deferred_actions_list = self._internal_dict.get(frame) - if deferred_actions_list is None: - return None - - deferred_actions = deferred_actions_list[-1] - return deferred_actions - - -_context_deferred_actions_recorder = _ContextDeferredActionsRecorder() diff --git a/deferrer/_deferred_actions.py b/deferrer/_deferred_actions.py deleted file mode 100644 index 6179fcb..0000000 --- a/deferrer/_deferred_actions.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -__all__ = [ - "DeferredAction", - "DeferredActions", -] - -from abc import ABC, abstractmethod -from typing import Final, final - - -class DeferredAction(ABC): - """ - An object that stands for an action that is meant to be performed - later. - """ - - @abstractmethod - def perform(self, /) -> None: ... - - -@final -class DeferredActions: - """ - A list-like object that holds `DeferredAction` objects. - - When a `DeferredActions` object is being disposed, all - `DeferredAction` objects it holds will get performed in a FILO - order. - """ - - _internal_list: Final[list[DeferredAction]] - - def __init__(self, /) -> None: - self._internal_list = [] - - def append(self, deferred_call: DeferredAction, /) -> None: - self._internal_list.append(deferred_call) - - def drain(self, /) -> None: - exceptions: list[Exception] = [] - - internal_list = self._internal_list - while len(internal_list) > 0: - deferred_call = internal_list.pop() - - try: - deferred_call.perform() - except Exception as e: - exceptions.append(e) - - n_exceptions = len(exceptions) - if n_exceptions == 0: - return - - exception_group = ExceptionGroup("deferred exception(s)", exceptions) - raise exception_group - - def __del__(self, /) -> None: - self.drain() diff --git a/deferrer/_opcode.py b/deferrer/_opcode.py deleted file mode 100644 index a7b17db..0000000 --- a/deferrer/_opcode.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -__all__ = [ - "Opcode", - "build_code_byte_sequence", - "build_code_bytes", -] - -import sys -from collections.abc import Sequence -from enum import IntEnum -from types import MappingProxyType -from typing import cast - -from opcode import _cache_format # pyright: ignore[reportAttributeAccessIssue] -from opcode import opmap - - -class Opcode(IntEnum): - """ - All op-code values used in this project. - """ - - COPY_FREE_VARS = opmap["COPY_FREE_VARS"] - JUMP_BACKWARD = opmap["JUMP_BACKWARD"] - LOAD_CONST = opmap["LOAD_CONST"] - LOAD_NAME = opmap["LOAD_NAME"] - MAKE_CELL = opmap["MAKE_CELL"] - POP_TOP = opmap["POP_TOP"] - RESUME = opmap["RESUME"] - RETURN_VALUE = opmap["RETURN_VALUE"] - STORE_FAST = opmap["STORE_FAST"] - STORE_NAME = opmap["STORE_NAME"] - STORE_DEREF = opmap["STORE_DEREF"] - - if sys.version_info >= (3, 13): - TO_BOOL = opmap["TO_BOOL"] - - if sys.version_info >= (3, 12): - POP_JUMP_IF_FALSE = opmap["POP_JUMP_IF_FALSE"] - - if sys.version_info >= (3, 11) and sys.version_info < (3, 12): - JUMP_IF_FALSE_OR_POP = opmap["JUMP_IF_FALSE_OR_POP"] - - -_n_caches_map = MappingProxyType( - { - cast("Opcode", opcode): ( - 0 if (d := _cache_format.get(name)) is None else sum(d.values()) - ) - for name, opcode in Opcode._member_map_.items() - } -) - - -def build_code_bytes(opcode: Opcode, arg: int = 0) -> bytes: - return bytes(build_code_byte_sequence(opcode, arg)) - - -def build_code_byte_sequence( - opcode: Opcode, arg: int = 0, *, cache_value: int = 0 -) -> Sequence[int]: - return [opcode, arg] + [cache_value] * (_n_caches_map[opcode] * 2) diff --git a/deferrer/_sequence_matching.py b/deferrer/_sequence_matching.py deleted file mode 100644 index 0cbc44c..0000000 --- a/deferrer/_sequence_matching.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -__all__ = [ - "WILDCARD", - "sequence_has_prefix", - "sequence_has_suffix", -] - -from collections.abc import Sequence -from typing import Any - - -class _Wildcard(Any): - """ - An object that equals any object. - """ - - def __eq__(self, other: object, /) -> bool: - return True - - -WILDCARD = _Wildcard() - - -def sequence_has_prefix(sequence: Sequence[Any], prefix: Sequence[Any], /) -> bool: - return tuple(sequence[: len(prefix)]) == tuple(prefix) - - -def sequence_has_suffix(sequence: Sequence[Any], suffix: Sequence[Any], /) -> bool: - return tuple(sequence[-len(suffix) :]) == tuple(suffix) diff --git a/deferrer/_utils/__init__.py b/deferrer/_utils/__init__.py new file mode 100644 index 0000000..30f1888 --- /dev/null +++ b/deferrer/_utils/__init__.py @@ -0,0 +1,3 @@ +from ._code_location import * +from ._frame import * +from ._opcode import * diff --git a/deferrer/_code_location.py b/deferrer/_utils/_code_location.py similarity index 100% rename from deferrer/_code_location.py rename to deferrer/_utils/_code_location.py diff --git a/deferrer/_frame.py b/deferrer/_utils/_frame.py similarity index 69% rename from deferrer/_frame.py rename to deferrer/_utils/_frame.py index 9861eba..948af46 100644 --- a/deferrer/_frame.py +++ b/deferrer/_utils/_frame.py @@ -7,11 +7,11 @@ "is_global_frame", ] +import re import sys -from collections.abc import Sequence from types import FrameType -from ._opcode import Opcode +from ._opcode import Opcode, build_instruction_pattern def get_current_frame() -> FrameType: @@ -123,34 +123,39 @@ def is_class_frame(frame: FrameType, /) -> bool: code = frame.f_code names = code.co_names - if not _sequence_has_prefix(names, ("__name__", "__module__", "__qualname__")): + if ( + len(names) < 3 + or names[0] != "__name__" + or names[1] != "__module__" + or names[2] != "__qualname__" + ): return False code_bytes = code.co_code - # In some cases (e.g. embedded class), there may be a COPY_FREE_VARS instruction. - if _sequence_has_prefix(code_bytes, (Opcode.COPY_FREE_VARS,)): - code_bytes = code_bytes[2:] - if not _sequence_has_prefix( - code_bytes, - ( - Opcode.RESUME, - 0, - Opcode.LOAD_NAME, - 0, # __name__ - Opcode.STORE_NAME, - 1, # __module__ - Opcode.LOAD_CONST, - 0, - Opcode.STORE_NAME, - 2, # __qualname__ - ), - ): + if re.match(_PATTERN, code_bytes) is None: return False return True -def _sequence_has_prefix( - sequence: Sequence[object], prefix: Sequence[object], / -) -> bool: - return tuple(sequence[: len(prefix)]) == tuple(prefix) +_PATTERN = re.compile( + pattern=( + "".join( + [ + # "COPY_FREE_VARS ?". Optional. + "(?:%s)?" % build_instruction_pattern(Opcode.COPY_FREE_VARS), + # "RESUME". Optional. + "(?:%s)?" % build_instruction_pattern(Opcode.RESUME), + # "LOAD_NAME 0 (__name__)". + build_instruction_pattern(Opcode.LOAD_NAME, 0), + # "STORE_NAME 1 (__module__)". + build_instruction_pattern(Opcode.STORE_NAME, 1), + # "LOAD_CONST 0 {qualname}". + build_instruction_pattern(Opcode.LOAD_CONST, 0), + # "STORE_NAME 2 (__qualname__)" + build_instruction_pattern(Opcode.STORE_NAME, 2), + ] + ).encode("iso8859-1") + ), + flags=re.DOTALL, +) diff --git a/deferrer/_utils/_opcode.py b/deferrer/_utils/_opcode.py new file mode 100644 index 0000000..1072bfb --- /dev/null +++ b/deferrer/_utils/_opcode.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +__all__ = [ + "Opcode", + "build_instruction_code_bytes", + "build_instruction_pattern", + "extract_argument_from_instruction", +] + +import sys +from enum import IntEnum +from types import MappingProxyType +from typing import cast + +from opcode import _cache_format # pyright: ignore[reportAttributeAccessIssue] +from opcode import opmap + + +class Opcode(IntEnum): + """ + All op-code values used in this project. + """ + + COPY_FREE_VARS = opmap["COPY_FREE_VARS"] + EXTENDED_ARG = opmap["EXTENDED_ARG"] + JUMP_BACKWARD = opmap["JUMP_BACKWARD"] + LOAD_CONST = opmap["LOAD_CONST"] + LOAD_NAME = opmap["LOAD_NAME"] + MAKE_CELL = opmap["MAKE_CELL"] + POP_TOP = opmap["POP_TOP"] + RESUME = opmap["RESUME"] + RETURN_VALUE = opmap["RETURN_VALUE"] + STORE_FAST = opmap["STORE_FAST"] + STORE_NAME = opmap["STORE_NAME"] + STORE_DEREF = opmap["STORE_DEREF"] + + if sys.version_info >= (3, 13): + TO_BOOL = opmap["TO_BOOL"] + + if sys.version_info >= (3, 12): + POP_JUMP_IF_FALSE = opmap["POP_JUMP_IF_FALSE"] + + if sys.version_info >= (3, 11) and sys.version_info < (3, 12): + JUMP_IF_FALSE_OR_POP = opmap["JUMP_IF_FALSE_OR_POP"] + + +_n_caches_map = MappingProxyType( + { + cast("Opcode", opcode): ( + 0 if (d := _cache_format.get(name)) is None else sum(d.values()) + ) + for name, opcode in Opcode._member_map_.items() + } +) + + +def build_instruction_code_bytes(opcode: Opcode, argument: int = 0) -> bytes: + assert 0 <= opcode <= ((1 << 8) - 1) + assert 0 <= argument <= ((1 << 32) - 1) + + argument_bytes = _get_bytes(argument) + + code_byte_list: list[int] = [] + for argument_byte in argument_bytes[:-1]: + code_byte_list.append(Opcode.EXTENDED_ARG) + code_byte_list.append(argument_byte) + else: + code_byte_list.append(opcode) + code_byte_list.append(argument_bytes[-1]) + code_byte_list.extend([0] * (_n_caches_map[opcode] * 2)) + + code_bytes = bytes(code_byte_list) + return code_bytes + + +def build_instruction_pattern(opcode: Opcode, argument: int | None = None) -> str: + if argument is None: + argument_bytes = None + else: + argument_bytes = _get_bytes(argument) + + pattern = "" + + if argument_bytes is None: + pattern += r"(?:\x%02x.){0,3}" % Opcode.EXTENDED_ARG + else: + pattern += "".join( + r"\x%02x\x%02x" % (Opcode.EXTENDED_ARG, argument_byte) + for argument_byte in argument_bytes[:-1] + ) + + pattern += r"\x%02x" % opcode + + if argument_bytes is None: + pattern += "." + else: + pattern += r"\x%02x" % argument_bytes[-1] + + n_caches = _n_caches_map[opcode] + if n_caches > 0: + pattern += ".{%d}" % (n_caches * 2) + + return pattern + + +def extract_argument_from_instruction(code_bytes: bytes, /) -> int: + argument = 0 + + for i in range(0, len(code_bytes), 2): + argument <<= 8 + argument |= code_bytes[i + 1] + + if code_bytes[i] != Opcode.EXTENDED_ARG: + break + + return argument + + +def _get_bytes(value: int, /) -> bytes: + assert value >= 0 + n_bytes = 1 if value == 0 else (value.bit_length() + 7) // 8 + bytes_ = value.to_bytes(n_bytes, "big", signed=False) + return bytes_ diff --git a/deferrer_tests/defer.py b/deferrer_tests/defer.py index d29ba05..b696636 100644 --- a/deferrer_tests/defer.py +++ b/deferrer_tests/defer.py @@ -115,6 +115,1068 @@ def f() -> None: f() assert nums == [1, 2, 0] + @staticmethod + def test__works_in_sugarful_form_with_very_large_rhs() -> None: + """ + When RHS part is very large, there will be "EXTENDED_ARG" in RHS + instructions. + """ + + nums = [] + + def f() -> None: + defer and ( + nums.append( + 1 + # There are 512 `int()`s in following lines. + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + | int() # type: ignore + ) + ) + nums.append(0) + + if sys.version_info < (3, 12): + from deferrer import defer_scope + + f = defer_scope(f) + + f() + assert nums == [0, 1] + + @staticmethod + def test__works_in_sugarful_form_with_very_huge_amount_of_variables() -> None: + """ + When there is a lot of variables in current function, + "EXTENDED_ARG" may be required to specify a certain variable. + """ + + nums = [] + + def f( + # There are 500 arguments here. + _000 = None, # type: ignore + _001 = None, # type: ignore + _002 = None, # type: ignore + _003 = None, # type: ignore + _004 = None, # type: ignore + _005 = None, # type: ignore + _006 = None, # type: ignore + _007 = None, # type: ignore + _008 = None, # type: ignore + _009 = None, # type: ignore + _010 = None, # type: ignore + _011 = None, # type: ignore + _012 = None, # type: ignore + _013 = None, # type: ignore + _014 = None, # type: ignore + _015 = None, # type: ignore + _016 = None, # type: ignore + _017 = None, # type: ignore + _018 = None, # type: ignore + _019 = None, # type: ignore + _020 = None, # type: ignore + _021 = None, # type: ignore + _022 = None, # type: ignore + _023 = None, # type: ignore + _024 = None, # type: ignore + _025 = None, # type: ignore + _026 = None, # type: ignore + _027 = None, # type: ignore + _028 = None, # type: ignore + _029 = None, # type: ignore + _030 = None, # type: ignore + _031 = None, # type: ignore + _032 = None, # type: ignore + _033 = None, # type: ignore + _034 = None, # type: ignore + _035 = None, # type: ignore + _036 = None, # type: ignore + _037 = None, # type: ignore + _038 = None, # type: ignore + _039 = None, # type: ignore + _040 = None, # type: ignore + _041 = None, # type: ignore + _042 = None, # type: ignore + _043 = None, # type: ignore + _044 = None, # type: ignore + _045 = None, # type: ignore + _046 = None, # type: ignore + _047 = None, # type: ignore + _048 = None, # type: ignore + _049 = None, # type: ignore + _050 = None, # type: ignore + _051 = None, # type: ignore + _052 = None, # type: ignore + _053 = None, # type: ignore + _054 = None, # type: ignore + _055 = None, # type: ignore + _056 = None, # type: ignore + _057 = None, # type: ignore + _058 = None, # type: ignore + _059 = None, # type: ignore + _060 = None, # type: ignore + _061 = None, # type: ignore + _062 = None, # type: ignore + _063 = None, # type: ignore + _064 = None, # type: ignore + _065 = None, # type: ignore + _066 = None, # type: ignore + _067 = None, # type: ignore + _068 = None, # type: ignore + _069 = None, # type: ignore + _070 = None, # type: ignore + _071 = None, # type: ignore + _072 = None, # type: ignore + _073 = None, # type: ignore + _074 = None, # type: ignore + _075 = None, # type: ignore + _076 = None, # type: ignore + _077 = None, # type: ignore + _078 = None, # type: ignore + _079 = None, # type: ignore + _080 = None, # type: ignore + _081 = None, # type: ignore + _082 = None, # type: ignore + _083 = None, # type: ignore + _084 = None, # type: ignore + _085 = None, # type: ignore + _086 = None, # type: ignore + _087 = None, # type: ignore + _088 = None, # type: ignore + _089 = None, # type: ignore + _090 = None, # type: ignore + _091 = None, # type: ignore + _092 = None, # type: ignore + _093 = None, # type: ignore + _094 = None, # type: ignore + _095 = None, # type: ignore + _096 = None, # type: ignore + _097 = None, # type: ignore + _098 = None, # type: ignore + _099 = None, # type: ignore + _100 = None, # type: ignore + _101 = None, # type: ignore + _102 = None, # type: ignore + _103 = None, # type: ignore + _104 = None, # type: ignore + _105 = None, # type: ignore + _106 = None, # type: ignore + _107 = None, # type: ignore + _108 = None, # type: ignore + _109 = None, # type: ignore + _110 = None, # type: ignore + _111 = None, # type: ignore + _112 = None, # type: ignore + _113 = None, # type: ignore + _114 = None, # type: ignore + _115 = None, # type: ignore + _116 = None, # type: ignore + _117 = None, # type: ignore + _118 = None, # type: ignore + _119 = None, # type: ignore + _120 = None, # type: ignore + _121 = None, # type: ignore + _122 = None, # type: ignore + _123 = None, # type: ignore + _124 = None, # type: ignore + _125 = None, # type: ignore + _126 = None, # type: ignore + _127 = None, # type: ignore + _128 = None, # type: ignore + _129 = None, # type: ignore + _130 = None, # type: ignore + _131 = None, # type: ignore + _132 = None, # type: ignore + _133 = None, # type: ignore + _134 = None, # type: ignore + _135 = None, # type: ignore + _136 = None, # type: ignore + _137 = None, # type: ignore + _138 = None, # type: ignore + _139 = None, # type: ignore + _140 = None, # type: ignore + _141 = None, # type: ignore + _142 = None, # type: ignore + _143 = None, # type: ignore + _144 = None, # type: ignore + _145 = None, # type: ignore + _146 = None, # type: ignore + _147 = None, # type: ignore + _148 = None, # type: ignore + _149 = None, # type: ignore + _150 = None, # type: ignore + _151 = None, # type: ignore + _152 = None, # type: ignore + _153 = None, # type: ignore + _154 = None, # type: ignore + _155 = None, # type: ignore + _156 = None, # type: ignore + _157 = None, # type: ignore + _158 = None, # type: ignore + _159 = None, # type: ignore + _160 = None, # type: ignore + _161 = None, # type: ignore + _162 = None, # type: ignore + _163 = None, # type: ignore + _164 = None, # type: ignore + _165 = None, # type: ignore + _166 = None, # type: ignore + _167 = None, # type: ignore + _168 = None, # type: ignore + _169 = None, # type: ignore + _170 = None, # type: ignore + _171 = None, # type: ignore + _172 = None, # type: ignore + _173 = None, # type: ignore + _174 = None, # type: ignore + _175 = None, # type: ignore + _176 = None, # type: ignore + _177 = None, # type: ignore + _178 = None, # type: ignore + _179 = None, # type: ignore + _180 = None, # type: ignore + _181 = None, # type: ignore + _182 = None, # type: ignore + _183 = None, # type: ignore + _184 = None, # type: ignore + _185 = None, # type: ignore + _186 = None, # type: ignore + _187 = None, # type: ignore + _188 = None, # type: ignore + _189 = None, # type: ignore + _190 = None, # type: ignore + _191 = None, # type: ignore + _192 = None, # type: ignore + _193 = None, # type: ignore + _194 = None, # type: ignore + _195 = None, # type: ignore + _196 = None, # type: ignore + _197 = None, # type: ignore + _198 = None, # type: ignore + _199 = None, # type: ignore + _200 = None, # type: ignore + _201 = None, # type: ignore + _202 = None, # type: ignore + _203 = None, # type: ignore + _204 = None, # type: ignore + _205 = None, # type: ignore + _206 = None, # type: ignore + _207 = None, # type: ignore + _208 = None, # type: ignore + _209 = None, # type: ignore + _210 = None, # type: ignore + _211 = None, # type: ignore + _212 = None, # type: ignore + _213 = None, # type: ignore + _214 = None, # type: ignore + _215 = None, # type: ignore + _216 = None, # type: ignore + _217 = None, # type: ignore + _218 = None, # type: ignore + _219 = None, # type: ignore + _220 = None, # type: ignore + _221 = None, # type: ignore + _222 = None, # type: ignore + _223 = None, # type: ignore + _224 = None, # type: ignore + _225 = None, # type: ignore + _226 = None, # type: ignore + _227 = None, # type: ignore + _228 = None, # type: ignore + _229 = None, # type: ignore + _230 = None, # type: ignore + _231 = None, # type: ignore + _232 = None, # type: ignore + _233 = None, # type: ignore + _234 = None, # type: ignore + _235 = None, # type: ignore + _236 = None, # type: ignore + _237 = None, # type: ignore + _238 = None, # type: ignore + _239 = None, # type: ignore + _240 = None, # type: ignore + _241 = None, # type: ignore + _242 = None, # type: ignore + _243 = None, # type: ignore + _244 = None, # type: ignore + _245 = None, # type: ignore + _246 = None, # type: ignore + _247 = None, # type: ignore + _248 = None, # type: ignore + _249 = None, # type: ignore + _250 = None, # type: ignore + _251 = None, # type: ignore + _252 = None, # type: ignore + _253 = None, # type: ignore + _254 = None, # type: ignore + _255 = None, # type: ignore + _256 = None, # type: ignore + _257 = None, # type: ignore + _258 = None, # type: ignore + _259 = None, # type: ignore + _260 = None, # type: ignore + _261 = None, # type: ignore + _262 = None, # type: ignore + _263 = None, # type: ignore + _264 = None, # type: ignore + _265 = None, # type: ignore + _266 = None, # type: ignore + _267 = None, # type: ignore + _268 = None, # type: ignore + _269 = None, # type: ignore + _270 = None, # type: ignore + _271 = None, # type: ignore + _272 = None, # type: ignore + _273 = None, # type: ignore + _274 = None, # type: ignore + _275 = None, # type: ignore + _276 = None, # type: ignore + _277 = None, # type: ignore + _278 = None, # type: ignore + _279 = None, # type: ignore + _280 = None, # type: ignore + _281 = None, # type: ignore + _282 = None, # type: ignore + _283 = None, # type: ignore + _284 = None, # type: ignore + _285 = None, # type: ignore + _286 = None, # type: ignore + _287 = None, # type: ignore + _288 = None, # type: ignore + _289 = None, # type: ignore + _290 = None, # type: ignore + _291 = None, # type: ignore + _292 = None, # type: ignore + _293 = None, # type: ignore + _294 = None, # type: ignore + _295 = None, # type: ignore + _296 = None, # type: ignore + _297 = None, # type: ignore + _298 = None, # type: ignore + _299 = None, # type: ignore + _300 = None, # type: ignore + _301 = None, # type: ignore + _302 = None, # type: ignore + _303 = None, # type: ignore + _304 = None, # type: ignore + _305 = None, # type: ignore + _306 = None, # type: ignore + _307 = None, # type: ignore + _308 = None, # type: ignore + _309 = None, # type: ignore + _310 = None, # type: ignore + _311 = None, # type: ignore + _312 = None, # type: ignore + _313 = None, # type: ignore + _314 = None, # type: ignore + _315 = None, # type: ignore + _316 = None, # type: ignore + _317 = None, # type: ignore + _318 = None, # type: ignore + _319 = None, # type: ignore + _320 = None, # type: ignore + _321 = None, # type: ignore + _322 = None, # type: ignore + _323 = None, # type: ignore + _324 = None, # type: ignore + _325 = None, # type: ignore + _326 = None, # type: ignore + _327 = None, # type: ignore + _328 = None, # type: ignore + _329 = None, # type: ignore + _330 = None, # type: ignore + _331 = None, # type: ignore + _332 = None, # type: ignore + _333 = None, # type: ignore + _334 = None, # type: ignore + _335 = None, # type: ignore + _336 = None, # type: ignore + _337 = None, # type: ignore + _338 = None, # type: ignore + _339 = None, # type: ignore + _340 = None, # type: ignore + _341 = None, # type: ignore + _342 = None, # type: ignore + _343 = None, # type: ignore + _344 = None, # type: ignore + _345 = None, # type: ignore + _346 = None, # type: ignore + _347 = None, # type: ignore + _348 = None, # type: ignore + _349 = None, # type: ignore + _350 = None, # type: ignore + _351 = None, # type: ignore + _352 = None, # type: ignore + _353 = None, # type: ignore + _354 = None, # type: ignore + _355 = None, # type: ignore + _356 = None, # type: ignore + _357 = None, # type: ignore + _358 = None, # type: ignore + _359 = None, # type: ignore + _360 = None, # type: ignore + _361 = None, # type: ignore + _362 = None, # type: ignore + _363 = None, # type: ignore + _364 = None, # type: ignore + _365 = None, # type: ignore + _366 = None, # type: ignore + _367 = None, # type: ignore + _368 = None, # type: ignore + _369 = None, # type: ignore + _370 = None, # type: ignore + _371 = None, # type: ignore + _372 = None, # type: ignore + _373 = None, # type: ignore + _374 = None, # type: ignore + _375 = None, # type: ignore + _376 = None, # type: ignore + _377 = None, # type: ignore + _378 = None, # type: ignore + _379 = None, # type: ignore + _380 = None, # type: ignore + _381 = None, # type: ignore + _382 = None, # type: ignore + _383 = None, # type: ignore + _384 = None, # type: ignore + _385 = None, # type: ignore + _386 = None, # type: ignore + _387 = None, # type: ignore + _388 = None, # type: ignore + _389 = None, # type: ignore + _390 = None, # type: ignore + _391 = None, # type: ignore + _392 = None, # type: ignore + _393 = None, # type: ignore + _394 = None, # type: ignore + _395 = None, # type: ignore + _396 = None, # type: ignore + _397 = None, # type: ignore + _398 = None, # type: ignore + _399 = None, # type: ignore + _400 = None, # type: ignore + _401 = None, # type: ignore + _402 = None, # type: ignore + _403 = None, # type: ignore + _404 = None, # type: ignore + _405 = None, # type: ignore + _406 = None, # type: ignore + _407 = None, # type: ignore + _408 = None, # type: ignore + _409 = None, # type: ignore + _410 = None, # type: ignore + _411 = None, # type: ignore + _412 = None, # type: ignore + _413 = None, # type: ignore + _414 = None, # type: ignore + _415 = None, # type: ignore + _416 = None, # type: ignore + _417 = None, # type: ignore + _418 = None, # type: ignore + _419 = None, # type: ignore + _420 = None, # type: ignore + _421 = None, # type: ignore + _422 = None, # type: ignore + _423 = None, # type: ignore + _424 = None, # type: ignore + _425 = None, # type: ignore + _426 = None, # type: ignore + _427 = None, # type: ignore + _428 = None, # type: ignore + _429 = None, # type: ignore + _430 = None, # type: ignore + _431 = None, # type: ignore + _432 = None, # type: ignore + _433 = None, # type: ignore + _434 = None, # type: ignore + _435 = None, # type: ignore + _436 = None, # type: ignore + _437 = None, # type: ignore + _438 = None, # type: ignore + _439 = None, # type: ignore + _440 = None, # type: ignore + _441 = None, # type: ignore + _442 = None, # type: ignore + _443 = None, # type: ignore + _444 = None, # type: ignore + _445 = None, # type: ignore + _446 = None, # type: ignore + _447 = None, # type: ignore + _448 = None, # type: ignore + _449 = None, # type: ignore + _450 = None, # type: ignore + _451 = None, # type: ignore + _452 = None, # type: ignore + _453 = None, # type: ignore + _454 = None, # type: ignore + _455 = None, # type: ignore + _456 = None, # type: ignore + _457 = None, # type: ignore + _458 = None, # type: ignore + _459 = None, # type: ignore + _460 = None, # type: ignore + _461 = None, # type: ignore + _462 = None, # type: ignore + _463 = None, # type: ignore + _464 = None, # type: ignore + _465 = None, # type: ignore + _466 = None, # type: ignore + _467 = None, # type: ignore + _468 = None, # type: ignore + _469 = None, # type: ignore + _470 = None, # type: ignore + _471 = None, # type: ignore + _472 = None, # type: ignore + _473 = None, # type: ignore + _474 = None, # type: ignore + _475 = None, # type: ignore + _476 = None, # type: ignore + _477 = None, # type: ignore + _478 = None, # type: ignore + _479 = None, # type: ignore + _480 = None, # type: ignore + _481 = None, # type: ignore + _482 = None, # type: ignore + _483 = None, # type: ignore + _484 = None, # type: ignore + _485 = None, # type: ignore + _486 = None, # type: ignore + _487 = None, # type: ignore + _488 = None, # type: ignore + _489 = None, # type: ignore + _490 = None, # type: ignore + _491 = None, # type: ignore + _492 = None, # type: ignore + _493 = None, # type: ignore + _494 = None, # type: ignore + _495 = None, # type: ignore + _496 = None, # type: ignore + _497 = None, # type: ignore + _498 = None, # type: ignore + _499 = None, # type: ignore + ) -> None: + defer and nums.append(0) + nums.append(1) + defer and nums.append(2) + + if sys.version_info < (3, 12): + from deferrer import defer_scope + + f = defer_scope(f) + + f() + assert nums == [1, 2, 0] + @staticmethod def test__works_in_sugarless_form() -> None: """ @@ -445,7 +1507,6 @@ def f() -> None: f() assert nums == [1, 2, 0] - class Test__deferred_call: @staticmethod def test__emits_warning_if_left_out() -> None: diff --git a/pyproject.toml b/pyproject.toml index d60cd58..3463e81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,7 +156,6 @@ addopts = [ "--doctest-modules", "--doctest-glob=*.md", "--cov=deferrer", - "--cov-append", "--cov-report=xml", "--ignore-glob=**/samples/*", ]