Module refinery.lib.decorators
A selection of refinery-specific decorators.
Expand source code Browse git
"""
A selection of refinery-specific decorators.
"""
from __future__ import annotations
import ast
import codecs
import copy
import itertools
import re
from functools import WRAPPER_ASSIGNMENTS, wraps
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, overload
if TYPE_CHECKING:
from refinery.units import Chunk, Unit
_F = TypeVar('_F', bound=Callable)
def wraps_without_annotations(method: Callable) -> Callable[[_F], _F]:
"""
This decorator works simila to `wraps` from `functools` but does not update the
type annotations of the wrapped function. This is used in the other decorators
in this module because they change the function signature.
"""
assignments: set[str] = set(WRAPPER_ASSIGNMENTS)
assignments.discard('__annotations__')
wrap = wraps(method, assigned=assignments)
if TYPE_CHECKING:
wrap = cast('Callable[[_F], _F]', wrap)
return wrap
@overload
def unicoded(method: Callable[[Any, str], str]) -> Callable[[Any, Chunk], bytes]:
...
@overload
def unicoded(method: Callable[[Any, str], str | None]) -> Callable[[Any, Chunk], bytes | None]:
...
def unicoded(method: Callable[[Any, str], str | None]) -> Callable[[Any, Chunk], bytes | None]:
"""
Can be used to decorate a `refinery.units.Unit.process` routine that takes a
string argument and also returns one. The resulting routine takes a binary buffer
as input and attempts to decode it as unicode text. If certain characters cannot
be decoded, then these ranges are skipped and the decorated routine is called
once for each string patch that was successfully decoded.
"""
@wraps_without_annotations(method)
def method_wrapper(self: Unit, data: Chunk) -> bytes | None:
input_codec = self.codec if any(data[::2]) else 'UTF-16LE'
partial = re.split(R'([\uDC80-\uDCFF]+)', # surrogate escape range
codecs.decode(data, input_codec, errors='surrogateescape'))
partial[::2] = (method(self, p) or '' if p else '' for p in itertools.islice(iter(partial), 0, None, 2))
nones = sum(1 for p in partial if p is None)
if nones == len(partial):
return None
if nones >= 1:
for k, p in enumerate(partial):
if p is None:
partial[k] = ''
return codecs.encode(''.join(partial), self.codec, errors='surrogateescape')
return method_wrapper
class ArithmeticReducer(ast.NodeTransformer):
"""
Rewrites code so that the result of each arithmetic operation that can leave a range of
integers is reduced back into it; the subclass decides how by implementing `reduced`.
"""
BINARY = (ast.Add, ast.Mult, ast.Sub, ast.LShift, ast.Pow)
UNARY = (ast.UAdd, ast.USub, ast.Invert)
def reduced(self, node: ast.expr) -> ast.expr:
"""
The given expression, reduced into the range.
"""
raise NotImplementedError
def visit_BinOp(self, node: ast.BinOp) -> Any:
self.generic_visit(node)
if not isinstance(node.op, self.BINARY):
return node
return self.reduced(node)
def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
self.generic_visit(node)
if not isinstance(node.op, self.UNARY):
return node
return self.reduced(node)
class _MaskingTransformer(ArithmeticReducer):
"""
Rewrites a function for `refinery.lib.decorators.masked`: the result of each arithmetic
operation is reduced with the given operation and mask, and the function gets a new name, which
is kept in the attribute `name`.
"""
def __init__(self, op: type[ast.operator], mask: int):
self.op = op
self.mask = mask
self.name: str | None = None
def reduced(self, node: ast.expr) -> ast.expr:
return ast.BinOp(node, self.op(), ast.Constant(self.mask))
def visit_AugAssign(self, node: ast.AugAssign) -> Any:
self.generic_visit(node)
if not isinstance(node.op, self.BINARY):
return node
target_load = copy.deepcopy(node.target)
target_load.ctx = ast.Load()
computation = ast.BinOp(left=target_load, op=node.op, right=node.value)
return ast.Assign(targets=[node.target], value=self.reduced(computation))
def visit_FunctionDef(self, node: ast.FunctionDef):
if self.name is None:
node.name = self.name = F'__wrapped_{node.name}'
for k in range(len(node.decorator_list)):
if not isinstance(decorator := node.decorator_list[k], ast.Call):
continue
if not isinstance(decorator := decorator.func, ast.Name):
continue
if decorator.id == masked.__name__:
del node.decorator_list[:k + 1]
break
self.generic_visit(node)
return node
def masked(mask: int, mod: bool = False):
"""
Convert arithmetic operations that occur within the decorated function body in such a way that
the result is reduced with the given bit mask. All additions, subtractions, multiplications,
left shifts, and taking powers are augmented by introducing a binary AND. If the mod parameter
is enabled, a modulo operation is introduced instead.
"""
import inspect
if mod:
op = ast.Mod
else:
op = ast.BitAnd
def decorator(function):
code = inspect.getsource(function)
code = inspect.cleandoc(code)
tree = ast.parse(code)
pp = _MaskingTransformer(op, mask)
fixed = ast.fix_missing_locations(pp.visit(tree))
if (name := pp.name) is None:
raise RuntimeError
import types
freevars = function.__code__.co_freevars
closure = function.__closure__
namespace = {**function.__globals__}
if freevars and closure:
# Wrap the rewritten function definition inside a factory function that takes the free
# variables as parameters, recreating the closure binding. The factory is needed so
# that the inner function has the correct co_freevars, allowing us to reconstruct it
# with the original closure cells to preserve late binding.
factory_name = '__closure_factory'
factory_args = ast.arguments(
posonlyargs=[],
args=[ast.arg(arg=v) for v in freevars],
vararg=None,
kwonlyargs=[],
kw_defaults=[],
kwarg=None,
defaults=[],
)
factory_body: list[ast.stmt] = fixed.body + [
ast.Return(value=ast.Name(id=name, ctx=ast.Load()))]
no_decorators: list[ast.expr] = []
factory_def = ast.FunctionDef(
name=factory_name,
args=factory_args,
body=factory_body,
decorator_list=no_decorators,
returns=None,
type_comment=None,
type_params=[],
)
fixed = ast.Module(body=[factory_def], type_ignores=[])
fixed = ast.fix_missing_locations(fixed)
exec(compile(fixed, function.__code__.co_filename, 'exec'), namespace)
# We can't call the factory directly because the closure cells may not
# have been assigned yet (late binding). Instead, we extract the inner
# function's code object from the factory and reconstruct the function
# with the original closure cells and the real globals dict.
factory_fn = namespace[factory_name]
inner_code = None
for const in factory_fn.__code__.co_consts:
if isinstance(const, types.CodeType) and const.co_name == name:
inner_code = const
break
if inner_code is None:
raise RuntimeError(F'Could not find inner code object {name!r} in factory')
result = types.FunctionType(inner_code, function.__globals__, name, closure=closure)
else:
exec(compile(fixed, function.__code__.co_filename, 'exec'), namespace)
result = types.FunctionType(
namespace[name].__code__, function.__globals__, name)
return wraps(function)(result)
return decorator
Functions
def wraps_without_annotations(method)-
This decorator works simila to
wrapsfromfunctoolsbut does not update the type annotations of the wrapped function. This is used in the other decorators in this module because they change the function signature.Expand source code Browse git
def wraps_without_annotations(method: Callable) -> Callable[[_F], _F]: """ This decorator works simila to `wraps` from `functools` but does not update the type annotations of the wrapped function. This is used in the other decorators in this module because they change the function signature. """ assignments: set[str] = set(WRAPPER_ASSIGNMENTS) assignments.discard('__annotations__') wrap = wraps(method, assigned=assignments) if TYPE_CHECKING: wrap = cast('Callable[[_F], _F]', wrap) return wrap def unicoded(method)-
Can be used to decorate a
Unit.process()routine that takes a string argument and also returns one. The resulting routine takes a binary buffer as input and attempts to decode it as unicode text. If certain characters cannot be decoded, then these ranges are skipped and the decorated routine is called once for each string patch that was successfully decoded.Expand source code Browse git
def unicoded(method: Callable[[Any, str], str | None]) -> Callable[[Any, Chunk], bytes | None]: """ Can be used to decorate a `refinery.units.Unit.process` routine that takes a string argument and also returns one. The resulting routine takes a binary buffer as input and attempts to decode it as unicode text. If certain characters cannot be decoded, then these ranges are skipped and the decorated routine is called once for each string patch that was successfully decoded. """ @wraps_without_annotations(method) def method_wrapper(self: Unit, data: Chunk) -> bytes | None: input_codec = self.codec if any(data[::2]) else 'UTF-16LE' partial = re.split(R'([\uDC80-\uDCFF]+)', # surrogate escape range codecs.decode(data, input_codec, errors='surrogateescape')) partial[::2] = (method(self, p) or '' if p else '' for p in itertools.islice(iter(partial), 0, None, 2)) nones = sum(1 for p in partial if p is None) if nones == len(partial): return None if nones >= 1: for k, p in enumerate(partial): if p is None: partial[k] = '' return codecs.encode(''.join(partial), self.codec, errors='surrogateescape') return method_wrapper def masked(mask, mod=False)-
Convert arithmetic operations that occur within the decorated function body in such a way that the result is reduced with the given bit mask. All additions, subtractions, multiplications, left shifts, and taking powers are augmented by introducing a binary AND. If the mod parameter is enabled, a modulo operation is introduced instead.
Expand source code Browse git
def masked(mask: int, mod: bool = False): """ Convert arithmetic operations that occur within the decorated function body in such a way that the result is reduced with the given bit mask. All additions, subtractions, multiplications, left shifts, and taking powers are augmented by introducing a binary AND. If the mod parameter is enabled, a modulo operation is introduced instead. """ import inspect if mod: op = ast.Mod else: op = ast.BitAnd def decorator(function): code = inspect.getsource(function) code = inspect.cleandoc(code) tree = ast.parse(code) pp = _MaskingTransformer(op, mask) fixed = ast.fix_missing_locations(pp.visit(tree)) if (name := pp.name) is None: raise RuntimeError import types freevars = function.__code__.co_freevars closure = function.__closure__ namespace = {**function.__globals__} if freevars and closure: # Wrap the rewritten function definition inside a factory function that takes the free # variables as parameters, recreating the closure binding. The factory is needed so # that the inner function has the correct co_freevars, allowing us to reconstruct it # with the original closure cells to preserve late binding. factory_name = '__closure_factory' factory_args = ast.arguments( posonlyargs=[], args=[ast.arg(arg=v) for v in freevars], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[], ) factory_body: list[ast.stmt] = fixed.body + [ ast.Return(value=ast.Name(id=name, ctx=ast.Load()))] no_decorators: list[ast.expr] = [] factory_def = ast.FunctionDef( name=factory_name, args=factory_args, body=factory_body, decorator_list=no_decorators, returns=None, type_comment=None, type_params=[], ) fixed = ast.Module(body=[factory_def], type_ignores=[]) fixed = ast.fix_missing_locations(fixed) exec(compile(fixed, function.__code__.co_filename, 'exec'), namespace) # We can't call the factory directly because the closure cells may not # have been assigned yet (late binding). Instead, we extract the inner # function's code object from the factory and reconstruct the function # with the original closure cells and the real globals dict. factory_fn = namespace[factory_name] inner_code = None for const in factory_fn.__code__.co_consts: if isinstance(const, types.CodeType) and const.co_name == name: inner_code = const break if inner_code is None: raise RuntimeError(F'Could not find inner code object {name!r} in factory') result = types.FunctionType(inner_code, function.__globals__, name, closure=closure) else: exec(compile(fixed, function.__code__.co_filename, 'exec'), namespace) result = types.FunctionType( namespace[name].__code__, function.__globals__, name) return wraps(function)(result) return decorator
Classes
class ArithmeticReducer-
Rewrites code so that the result of each arithmetic operation that can leave a range of integers is reduced back into it; the subclass decides how by implementing
reduced.Expand source code Browse git
class ArithmeticReducer(ast.NodeTransformer): """ Rewrites code so that the result of each arithmetic operation that can leave a range of integers is reduced back into it; the subclass decides how by implementing `reduced`. """ BINARY = (ast.Add, ast.Mult, ast.Sub, ast.LShift, ast.Pow) UNARY = (ast.UAdd, ast.USub, ast.Invert) def reduced(self, node: ast.expr) -> ast.expr: """ The given expression, reduced into the range. """ raise NotImplementedError def visit_BinOp(self, node: ast.BinOp) -> Any: self.generic_visit(node) if not isinstance(node.op, self.BINARY): return node return self.reduced(node) def visit_UnaryOp(self, node: ast.UnaryOp) -> Any: self.generic_visit(node) if not isinstance(node.op, self.UNARY): return node return self.reduced(node)Ancestors
- ast.NodeTransformer
- ast.NodeVisitor
Subclasses
- refinery.lib.argformats._ExpressionPostprocessor
- refinery.lib.decorators._MaskingTransformer
Class variables
var BINARY-
The type of the None singleton.
var UNARY-
The type of the None singleton.
Methods
def reduced(self, node)-
The given expression, reduced into the range.
Expand source code Browse git
def reduced(self, node: ast.expr) -> ast.expr: """ The given expression, reduced into the range. """ raise NotImplementedError def visit_BinOp(self, node)-
Expand source code Browse git
def visit_BinOp(self, node: ast.BinOp) -> Any: self.generic_visit(node) if not isinstance(node.op, self.BINARY): return node return self.reduced(node) def visit_UnaryOp(self, node)-
Expand source code Browse git
def visit_UnaryOp(self, node: ast.UnaryOp) -> Any: self.generic_visit(node) if not isinstance(node.op, self.UNARY): return node return self.reduced(node)