Mock

This module extends the capabilities of the unittest.mock library. It allows one to create a mock up of a module on the fly. One may supply the source for the module directly as a string or indirectly through a file not one the usual import paths.

To properly leverage this one should patch sys.meta_path and sys.modules before creating the module one wishes to import. This is done as follows, note that the decorator order is important as they are executed in reverse.

@module("package.module", "", """class Class() : def __init__(self) : print(self.__class__)""")
@patch("sys.modules", sys.modules.copy())
@patch("sys.meta_path", sys.meta_path.copy())
def function(...):
 old = set(sys.modules.keys())
 from package.module import Class
 Class()                          #> <Class "package.module.Class">
 new = set(sys.modules.keys())
 print("Locally  :",new-old)      #> Locally  : "package.module"

old = set(sys.modules.keys())
function()
new = set(sys.modules.keys())
print("Locally  :",new-old)       #> Locally  : Set()

The mock sub-package allows one to fake the existance of a python module. The package provides the machinery necessary to fool the Python import mechanisms into importing a non-existant module.

The users is provided with the module() decorator which will mock the modules existance for the duration of the function. Actually importing the module may cause it to persist beyond the execution of the module and for this reason it is also necessary to freeze sys.modules. Consider the following example illustrating the usage.

import sys
from unittest.mock import patch
from apemant.mock  import module

MODULE = """
print(__name__)
print(__file__)
"""

@module("PACKAGE.MODULE", "PACKAGE/MODULE.py", MODULE)
@patch("sys.modules", sys.modules.copy())
def FUNCTION(*args, **kvps):
 import PACKAGE.MODULE
 ...

if __name__ == "__main__" :
 FUNCITON()

References

When one created the mock machinery it bore out a quirk in how Python handles references. It seems that any object having one or more concrete references to itself may not be garbage collected. While an object with only weak references to itself may be readily destroyed.

Note

When I first wrote these tests apeman.Import was setup as a singleton, this caused a whole bunch of wierd behaviours. If it is setup as a singleton again then the following is again valid.

Pythons’ implementation of del seems to prevent the deletion of builtin methods. Specifically one does not seem to be able to call del on builtins.__import__ which is necessary to remove an ApeMan instance.

I had hoped that making builtins.__import__ a weak reference to an instance of ApeMan might resolve this. Specifically deleting a reference to an instance of ApeMan and performing a garbage collection afterwards does not work as expected either.

A result of this is that one must invoke the magic method, __del__, directly upon a reference to an instance of ApeMan.

Concrete References

In general Python creates concrete references to objects.

Weak References

Implementing the __del__() method within apeman.Import as follows; making a weak reference to the instance from builtins.__import__().

class Impoter(...) :
 def __init__(self) :
  ...
  builtins.__import__ = weakref.ref(self, self.__del__)
  self._import_ = builtins.__import__
  ...

 def __del__(self) :
  builtins.__import__ = self._import_

Makes it possible to have the class clean up after itself once any independent references are deleted.

import ApeMan
apeman = ApeMan()
del apeman

Not doing so makes it necessary to delete the class by deleting builtins.__import__().

import ApeMan
ApeMan()
del builtins.__import__

An action that removes the __import__() attribute from builtins breaking later imports. Invoking del builtins.__import__ may well invoke the substitution in Impoter.__del__() but it inevitably removes the __import__() attribute from builtins.

This change makes it necessary for users to retain a reference to the current instance of apeman.Import. Not doing so leaves the instance open to garbage collection and leaves builtins.__import__() with a dead weak reference.