Skip to content

pygx.detouring

Symbolic detour.

detouring

Symbolic detour.

It is straighforward 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

current_mappings() -> dict[type[Any], type[Any] | FunctionType]

Returns detour mappings under current scope.

Source code in pygx/detouring/_class_detour.py
def current_mappings() -> dict[type[Any], type[Any] | types.FunctionType]:
    """Returns detour mappings under current scope."""
    return _global_detour_context.current_mappings

detour

detour(
    mappings: Sequence[tuple[type[Any], type[Any] | FunctionType]],
) -> Iterator[Sequence[tuple[type[Any], type[Any] | FunctionType]]]

Context manager for detouring object creation.

At times, we want to replace an object of a class to 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 future. Also, exposing too many arguments will hurt usability, it's big burden to figure out 20 arguments of a function for a user 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 instantation 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).

Detour can be nested. The outer scope mappings take precedence over the mappings from the inner loop, allowing users to change object creation behaviors from the outside. For example, the following code will detour class A to class C::

with pg.detour([(A, C)]): with pg.detour([A, B]): a = A() # a is a C object.

Detour is transisive across the inner and outer scope. 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-sfe.

Parameters:

Name Type Description Default
mappings Sequence[tuple[type[Any], type[Any] | FunctionType]]

A sequence of tuple (src_cls, dest_cls_or_fn) as mappings for the detour - 'src_cls' is the source class to be detoured, while 'dest_cls_or_fn' is the destination class or function. When it's a class, its __init__ method should have the same signature as the __init__ of the original class. When it's a function, it should accept a positional argument cls, for passing the original class that is being detoured, followed by all the arguments that the original class should accept. For example, a class with __init__(self, x, *args, y, **kwargs) can be detoured to a function with signature (cls, x, *args, y, **kwargs).

required

Yields:

Type Description
Sequence[tuple[type[Any], type[Any] | FunctionType]]

Resolved detour mappings.

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
@contextlib.contextmanager
def detour(
    mappings: Sequence[
        tuple[
            type[Any],  # Source class
            type[Any] | types.FunctionType,  # Target class or function
        ]
    ],
) -> Iterator[Sequence[tuple[type[Any], type[Any] | types.FunctionType]]]:
    """Context manager for detouring object creation.

    At times, we want to replace an object of a class to 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 future. Also, exposing too many arguments will hurt
    usability, it's big burden to figure out 20 arguments of a function for a user
    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 instantation 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).

    Detour can be nested. The outer scope mappings take precedence over the
    mappings from the inner loop, allowing users to change object creation
    behaviors from the outside. For example, the following code will detour
    class A to class C::

      with pg.detour([(A, C)]):
        with pg.detour([A, B]):
          a = A()   # a is a C object.

    Detour is transisive across the inner and outer scope. 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-sfe.

    Args:
      mappings: A sequence of tuple (src_cls, dest_cls_or_fn) as mappings for the
        detour - 'src_cls' is the source class to be detoured, while
        'dest_cls_or_fn' is the destination class or function. When it's a class,
        its `__init__` method should have the same signature as the `__init__` of
        the original class. When it's a function, it should accept a positional
        argument `cls`, for passing the original class that is being detoured,
        followed by all the arguments that the original class should accept. For
        example, a class with `__init__(self, x, *args, y, **kwargs)` can be
        detoured to a function with signature `(cls, x, *args, y, **kwargs)`.

    Yields:
      Resolved detour mappings.

    Raises:
      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.
    """
    # Placeholder for Google-internal usage instrumentation.

    for src, dest in mappings:
        if not inspect.isclass(src):
            raise TypeError(f'Detour source {src!r} is not a class.')
        if not inspect.isclass(dest) and not inspect.isfunction(dest):
            raise TypeError(
                f'Detour destination {dest!r} is not a class or a function.'
            )

    try:
        yield _global_detour_context.enter_scope(mappings)
    finally:
        _global_detour_context.leave_scope()

undetoured_new

undetoured_new(cls: type[Any], *args: Any, **kwargs: Any) -> Any

Create a new instance of cls without detouring.

If cls.init creates sub-objects, creation of sub-objects maybe detoured based on current context. For example::

class A:

def __init__(self, x):
  if x < 0:
    self.child = A(x)
  else:
    self.x = x

with pg.detour([A, B]): a = A(-1) assert isinstance(a, A) assert isinstance(a.child, B)

Parameters:

Name Type Description Default
cls type[Any]

The class whose instance will be created.

required
*args Any

Positional arguments to be passed to class init method.

()
**kwargs Any

Keyword arguments to be passed to class init method.

{}

Returns:

Type Description
Any

A instance of cls.

Source code in pygx/detouring/_class_detour.py
def undetoured_new(cls: type[Any], *args: Any, **kwargs: Any) -> Any:
    """Create a new instance of cls without detouring.

    If cls.__init__ creates sub-objects, creation of sub-objects
    maybe detoured based on current context. For example::

      class A:

        def __init__(self, x):
          if x < 0:
            self.child = A(x)
          else:
            self.x = x

      with pg.detour([A, B]):
        a = A(-1)
        assert isinstance(a, A)
        assert isinstance(a.child, B)

    Args:
      cls: The class whose instance will be created.
      *args: Positional arguments to be passed to class __init__ method.
      **kwargs: Keyword arguments to be passed to class __init__ method.

    Returns:
      A instance of `cls`.
    """
    new_method = _global_detour_context.get_original_new(cls)
    if new_method is object.__new__:
        instance = new_method(cls)
    else:
        instance = new_method(cls, *args, **kwargs)
    # pylint: disable-next=unnecessary-dunder-call
    instance.__init__(*args, **kwargs)
    return instance

options: show_root_heading: false show_root_toc_entry: false members_order: source heading_level: 2