assignhooks 1.0.2
Instrument code to support obj.__preassign__ / obk.__postassign__ methods. Based on assign package by ryankung
License: MIT

Description
=====================================

``assignhooks`` allows you to instrument your python code so that assignments to a variable can execute an action before and/or after the fact. For instance:

.. code-block:: python
  
 class SampleClass():

    name = ''

    def __assignpre__(self, lhs_name, rhs_name, rhs):
        print('PRE: assigning %s = %s' % (lhs_name, rhs_name))
        # modify rhs if needed before assignment
        rhs.name = lhs_name
        return rhs

    def __assignpost__(self, lhs_name, rhs_name):
        print('POST: lhs', self)
        print('POST: lhs_name', lhs_name)
        print('POST: rhs_name', rhs_name)
        print('POST: assigning %s = %s' % (lhs_name, rhs_name))
        self.name = lhs_name

 b = SampleClass()
 c = b


The first assignment would generate a call to `__assignpost__` as `b` still has no contents so there is no information on its class and hence no way to find out if it has an `__assignpre__` method. `__assignpost__` allows `b` to find out its name in this case. In the example above b contains a `SampleClass` instance whose name is initialized to `b` (the variable name)

In the second assignment, b is being assigned on the right-hand-side. As b is already existing we can check if its class provides an `__assignpre__` implementation. `b` knows is being assigned before the fact so we can used `__assignpre__` to check on the assignment or to provide the suitable value to be assigned (the return from `__assignpre__` method)

Applications:
===================

* Can be used to add automatically names to created objects, for debug for instance:

.. code-block:: python

    class MyClass:
        
        def __init__(self, val, name=None):
            self.val = val
            self.name = name

        def __assignpost__(self, lhs_name, rhs_name):
            if self.name is None:
                self.name = lhs_name

   # will assign x.name as 'x' automatically
   x = MyClass(val=4)
   assert x.name == 'x'


* Can be used to control what gets assigned when a specific instance of a class is assigned


How to use it:
===================

Checkout ``test.py`` and ``testmod.py`` under `examples` directory. ``test.py`` contains an example of how to instrument and use ``testmod.py`` module (the module being object of instrumentation).


1. Automatic patch

Suppose that there is a `testmod.py` that contains a module to instrument. Importing 'assignhooks' gives access to 'assignhooks.instrument.start()'. It should be called before the imports you want to instrument. To stop this behavior invoke `assignhooks.instrument.stop()`


Assuming `testmod.py` contains:

``<testmod.py>``

.. code-block:: python

 class T():
    def __assignpre__(self, lhs_name, rhs_name, rhs):
        print('%s has been copied to %s' % (rhs_name, lhs_name))
        return rhs

 b = T()
 c = b

And ``<test.py>``

.. code-block:: python

 import assignhooks

 assignhooks.instrument.start() # instrument from now on

 import testmod

 assignhooks.instrument.stop() # stop instrumenting


If executed:

 `$ python3 ./test.py`

will produce

  `b has been copied to c`

2. manually patch

.. code-block:: python

 from assignhooks.patch import patch_module
 import testmod

 patch_module(test)

Install
=======
Just do:

``pip3 install assignhooks``


How does it work
================

Internally assignhooks.magic replaces __import__ by a new version that after the import patches the module AST tree. It performs the following transformations:

.. code-block:: python

 x = T()

gets replaced by:

.. code-block:: python

 if True:
     x = T()
     if hasattr(x, '__assignpost__'):
         x.__assignpost__('x', 'T')


and

.. code-block:: python

 x = y

gets replaced by:

.. code-block:: python

 if True:
     if hasattr(y, '__assignpre__'):
         x = y.__assignpre__('x', 'y', y)
     else:
         x = y
     if hasattr(x, '__assignpost__'):
         x.__assignpost__('x', 'y')


NOTE: that ``if True:`` is only used to group the statements while doing node replacements in the AST.

Notes
=====

* Tested with `Python 3.8.7` on MacOS

Credits
=======
Based on original code from `assign <https://pypi.org/project/assign/>`_ module from  **ryankung**. Mainly adapted for my needs to handle object creation. Changes include:

- Original had an ``__assign__`` overload only valid on existing objects (like ``__assignpre__``). Added ``__assignpost__`` to handle object creation cases 
- Renamed ``__assign__`` to ``__assignpre__`` and added extra parameters


Known Issues
=============
1. Won't work under `REPL`
2. May slow down import operation. The effect in run-time is the replacement of raw assignments with conditional + assignment + hook
3. May fail when patching standard modules