pygx.detouring¶
Redirecting class instantiation to other classes or functions in a scope.
detouring
¶
Redirecting class instantiation to other classes or functions in a scope.
It is straightforward to symbolize existing classes and functions, but in order to use them, we need to replace the classes that were used in existing code with the symbolic ones. Sometimes, we just cannot modify existing source code. Or in some other cases, objects created within a function or a class method are not exposed to the external, therefore we cannot manipulate them as a part of the symbolic tree. For example:
@pg.symbolize
def foo():
# Object `a` is not a part of `foo`'s interface,
# therefore it cannot be seen from the symbolic tree
# that contains a `foo` object.
a = A(1)
return a.do_something()
Symbolic detour is introduced to address these use cases, which redirects
the __new__ method of a class to another class or function when it’s
evaluated under a context manager. Symbolic detour is not dependent on
symbolization, so in theory it can be used for detouring any classes.
Therefore, it does not require the presence of symbolic objects for mutating
the program.
current_mappings
¶
Returns the detour mappings in effect on the current thread.
Nested scopes are already merged into the returned dict (with outer-scope and transitive mappings resolved). Returns an empty dict when no detour scope is active.
Source code in pygx/detouring/_class_detour.py
detour
¶
detour(
mappings: Sequence[tuple[type[Any], type[Any] | FunctionType]],
) -> Iterator[dict[type[Any], type[Any] | FunctionType]]
Context manager for detouring object creation.
At times, we want to replace an object of a class with an object of a different class. Usually, we do so by passing the object as a function argument using dependency injection. However, it's not always possible to expose those internal objects as parameters to the class, as we cannot predict what needs to be customized in the future. Also, exposing too many arguments hurts usability; it's a big burden for a user to figure out 20 arguments of a function just to get started.
pg.detour provides another option for object replacement in Python,
which creates a context in which some source classes can be detoured to
specified destination classes or functions. For example, the code snippet
below will detour instantiation of class A to class B, and vice-versa:
class A:
pass
class B:
pass
# Exchange class A and class B.
with pg.detour([(A, B), (B, A)]):
a = A() # a is a B object.
b = B() # b is an A object.
Detour destination can be a function, which allows users to intercept the arguments passed to the class constructor. For example:
class Foo:
def __init__(self, value):
self.value = value
class Bar:
def __init__(self, value):
self.value = value
def detoured_foo(cls, value):
# cls is the original class before detour.
return Bar(value + 1)
with pg.detour([(Foo, detoured_foo)]):
f = Foo(1) # f will be Bar(2).
Detours can be nested. The outer scope mappings take precedence over the mappings from the inner scope, allowing users to change object creation behaviors from the outside. For example, the following code will detour class A to class C:
Detour is transitive across the inner and outer scopes. For example, the code below will detour class A to class C through B:
with pg.detour([(B, C)]):
a1 = A() # a1 is an A object.
with pg.detour([(A, B)]):
a2 = A() # a2 is a C object. (A -> B -> C)
Detour is thread-safe: mappings only take effect on the thread that entered the context manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mappings
|
Sequence[tuple[type[Any], type[Any] | FunctionType]]
|
A sequence of tuples (src_cls, dest_cls_or_fn) as mappings
for the detour - |
required |
Yields:
| Type | Description |
|---|---|
dict[type[Any], type[Any] | FunctionType]
|
The resolved detour mappings in effect for the new scope: a dict |
dict[type[Any], type[Any] | FunctionType]
|
mapping each source class to its destination, with outer-scope |
dict[type[Any], type[Any] | FunctionType]
|
precedence and transitivity applied. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the first item in each mapping is not a class, or the second item in each mapping is neither a class nor a function. |
Source code in pygx/detouring/_class_detour.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
undetoured_new
¶
Creates an instance of cls, bypassing any detour on cls itself.
Only the top-level creation is undetoured: if cls.__init__ creates
sub-objects, those creations still honor the detour mappings in scope.
For example:
class A:
def __init__(self, x):
if x < 0:
self.child = A(-x)
else:
self.x = x
class B(A):
pass
with pg.detour([(A, B)]):
a = pg.detouring.undetoured_new(A, -1)
assert type(a) is A # `a` itself is not detoured.
assert type(a.child) is B # But its sub-object is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[Any]
|
The class whose instance will be created. |
required |
*args
|
Any
|
Positional arguments to be passed to the class |
()
|
**kwargs
|
Any
|
Keyword arguments to be passed to the class |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
An instance of |
Source code in pygx/detouring/_class_detour.py
options: show_root_heading: false show_root_toc_entry: false members_order: source heading_level: 2