├── src ├── __init__.py ├── examples.py ├── bootstrap.py ├── main.py └── __startup__ │ └── sitecustomize.py ├── MANIFEST.in ├── autowrapt-init.pth ├── .gitignore ├── setup.py ├── README.rst └── LICENSE /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include autowrapt-init.pth 2 | -------------------------------------------------------------------------------- /autowrapt-init.pth: -------------------------------------------------------------------------------- 1 | import os, sys; os.environ.get('AUTOWRAPT_BOOTSTRAP') and __import__('autowrapt.bootstrap') and sys.modules['autowrapt.bootstrap'].bootstrap() 2 | -------------------------------------------------------------------------------- /src/examples.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | def autowrapt_this(module): 4 | print('The wrapt package is absolutely amazing and you should use it.') 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | man 22 | include 23 | .Python 24 | MANIFEST 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Editor save files. 43 | .*.swp 44 | 45 | # Sphinx documentation. 46 | docs/_build 47 | 48 | # Coverage. 49 | htmlcov 50 | .coverage 51 | .tddium* 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | from setuptools import setup 5 | from distutils.sysconfig import get_python_lib 6 | 7 | setup_kwargs = dict( 8 | name = 'autowrapt', 9 | version = '1.1', 10 | description = 'Boostrap mechanism for monkey patches.', 11 | author = 'Graham Dumpleton', 12 | author_email = 'Graham.Dumpleton@gmail.com', 13 | license = 'BSD', 14 | url = 'https://github.com/GrahamDumpleton/autowrapt', 15 | packages = ['autowrapt'], 16 | package_dir = {'autowrapt': 'src'}, 17 | package_data = {'autowrapt': ['__startup__/sitecustomize.py']}, 18 | data_files = [(get_python_lib(prefix=''), ['autowrapt-init.pth'])], 19 | entry_points = {'console_scripts': ['autowrapt = autowrapt.main:main'], 20 | 'autowrapt.examples': ['this = autowrapt.examples:autowrapt_this']}, 21 | install_requires = ['wrapt>=1.10.4'], 22 | ) 23 | 24 | setup(**setup_kwargs) 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | autowrapt 3 | ========= 4 | 5 | A Python module for triggering monkey patching of a Python application, 6 | without the need to actually modify the Python application itself to 7 | setup the monkey patches. 8 | 9 | The package works in conjunction with the ``wrapt`` module. One would 10 | create post import hook patch modules per ``wrapt`` module requirements, 11 | and then list the names of the setuptools entrypoints you wish to activate 12 | in the ``AUTOWRAPT_BOOTSTRAP`` environment variable, when executing Python 13 | within the environment that the ``autowrapt`` module is installed. 14 | 15 | To understand what is possible, a set of examples is also installed with 16 | this package. To see the examples in action run the following:: 17 | 18 | AUTOWRAPT_BOOTSTRAP=autowrapt.examples python 19 | 20 | At the Python interpreter prompt then enter:: 21 | 22 | import this 23 | 24 | This should print out the Zen of Python as normal, but with an extra line 25 | added to the end. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Graham Dumpleton 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /src/bootstrap.py: -------------------------------------------------------------------------------- 1 | '''Provides the bootstrap functions to be invoked on Python interpreter 2 | startup to register any post import hook callback functions. These would 3 | be invoked from either a '.pth' file, or from a custom 'sitecustomize' 4 | module setup by the 'autowrapt' wrapper script. 5 | 6 | ''' 7 | 8 | import os 9 | import site 10 | 11 | _registered = False 12 | 13 | def register_bootstrap_functions(): 14 | '''Discover and register all post import hooks named in the 15 | 'AUTOWRAPT_BOOTSTRAP' environment variable. The value of the 16 | environment variable must be a comma separated list. 17 | 18 | ''' 19 | 20 | # This can be called twice if '.pth' file bootstrapping works and 21 | # the 'autowrapt' wrapper script is still also used. We therefore 22 | # protect ourselves just in case it is called a second time as we 23 | # only want to force registration once. 24 | 25 | global _registered 26 | 27 | if _registered: 28 | return 29 | 30 | _registered = True 31 | 32 | # It should be safe to import wrapt at this point as this code will 33 | # be executed after all Python module search directories have been 34 | # added to the module search path. 35 | 36 | from wrapt import discover_post_import_hooks 37 | 38 | for name in os.environ.get('AUTOWRAPT_BOOTSTRAP', '').split(','): 39 | discover_post_import_hooks(name) 40 | 41 | def _execsitecustomize_wrapper(wrapped): 42 | def _execsitecustomize(*args, **kwargs): 43 | try: 44 | return wrapped(*args, **kwargs) 45 | finally: 46 | # Check whether 'usercustomize' module support is disabled. 47 | # In the case of 'usercustomize' module support being 48 | # disabled we must instead do our work here after the 49 | # 'sitecustomize' module has been loaded. 50 | 51 | if not site.ENABLE_USER_SITE: 52 | register_bootstrap_functions() 53 | 54 | return _execsitecustomize 55 | 56 | def _execusercustomize_wrapper(wrapped): 57 | def _execusercustomize(*args, **kwargs): 58 | try: 59 | return wrapped(*args, **kwargs) 60 | finally: 61 | register_bootstrap_functions() 62 | 63 | return _execusercustomize 64 | 65 | _patched = False 66 | 67 | def bootstrap(): 68 | '''Patches the 'site' module such that the bootstrap functions for 69 | registering the post import hook callback functions are called as 70 | the last thing done when initialising the Python interpreter. This 71 | function would normally be called from the special '.pth' file. 72 | 73 | ''' 74 | 75 | global _patched 76 | 77 | if _patched: 78 | return 79 | 80 | _patched = True 81 | 82 | # We want to do our real work as the very last thing in the 'site' 83 | # module when it is being imported so that the module search path is 84 | # initialised properly. What is the last thing executed depends on 85 | # whether the 'usercustomize' module support is enabled. Support for 86 | # the 'usercustomize' module will not be enabled in Python virtual 87 | # enviromments. We therefore wrap the functions for the loading of 88 | # both the 'sitecustomize' and 'usercustomize' modules but detect 89 | # when 'usercustomize' support is disabled and in that case do what 90 | # we need to after the 'sitecustomize' module is loaded. 91 | # 92 | # In wrapping these functions though, we can't actually use wrapt to 93 | # do so. This is because depending on how wrapt was installed it may 94 | # technically be dependent on '.pth' evaluation for Python to know 95 | # where to import it from. The addition of the directory which 96 | # contains wrapt may not yet have been done. We thus use a simple 97 | # function wrapper instead. 98 | 99 | site.execsitecustomize = _execsitecustomize_wrapper(site.execsitecustomize) 100 | site.execusercustomize = _execusercustomize_wrapper(site.execusercustomize) 101 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | '''Implements a wrapper script for executing a Python program from the 2 | command line and which implements an alternate way of bootstrapping the 3 | registration of post import hook callback functions when the '.pth' file 4 | mechanism doesn't work. This can be necessary when using 'buildout' but 5 | may still fail with 'buildout' if 'buildout' has been setup itself to 6 | override any 'sitecustomize' module with it ignoring any existing one. 7 | 8 | The wrapper script works by adding a special directory into the 9 | 'PYTHONPATH' environment variable, describing additional Python module 10 | search directories, which contains a custom 'sitecustomize' module. When 11 | the Python interpreter is started that custom 'sitecustomize' module 12 | will be automatically loaded. This allows the custom 'sitecustomize' 13 | file to then load any original 'sitecustomize' file which may have been 14 | hidden and then bootstrap the registration of the post import hook 15 | callback functions. 16 | 17 | ''' 18 | 19 | import sys 20 | import os 21 | import time 22 | 23 | _debug = os.environ.get('AUTOWRAPT_DEBUG', 24 | 'off').lower() in ('on', 'true', '1') 25 | 26 | def log_message(text, *args): 27 | if _debug: 28 | text = text % args 29 | timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) 30 | print('AUTOWRAPT: %s (%d) - %s' % (timestamp, os.getpid(), text)) 31 | 32 | def run_program(args): 33 | log_message('autowrapt - wrapper (%s)', __file__) 34 | 35 | log_message('working_directory = %r', os.getcwd()) 36 | log_message('current_command = %r', sys.argv) 37 | 38 | log_message('sys.prefix = %r', os.path.normpath(sys.prefix)) 39 | 40 | try: 41 | log_message('sys.real_prefix = %r', sys.real_prefix) 42 | except AttributeError: 43 | pass 44 | 45 | log_message('sys.version_info = %r', sys.version_info) 46 | log_message('sys.executable = %r', sys.executable) 47 | log_message('sys.flags = %r', sys.flags) 48 | log_message('sys.path = %r', sys.path) 49 | 50 | # Determine the location of the special bootstrap directory. Add 51 | # this into the 'PYTHONPATH' environment variable, preserving any 52 | # existing value the 'PYTHONPATH' environment variable may have. 53 | 54 | root_directory = os.path.dirname(__file__) 55 | boot_directory = os.path.join(root_directory, '__startup__') 56 | 57 | log_message('root_directory = %r', root_directory) 58 | log_message('boot_directory = %r', boot_directory) 59 | 60 | python_path = boot_directory 61 | 62 | if 'PYTHONPATH' in os.environ: 63 | path = os.environ['PYTHONPATH'].split(os.path.pathsep) 64 | if not boot_directory in path: 65 | python_path = "%s%s%s" % (boot_directory, os.path.pathsep, 66 | os.environ['PYTHONPATH']) 67 | 68 | os.environ['PYTHONPATH'] = python_path 69 | 70 | # Set special environment variables which record the location of the 71 | # Python installation or virtual environment being used as well as 72 | # the Python version. The values of these are compared in the 73 | # 'sitecustomize' module with the values for the Python interpreter 74 | # which is later executed by the wrapper. If they don't match then 75 | # nothing will be done. This check is made as using the wrapper 76 | # script from one Python installation around 'python' executing from 77 | # a different installation can cause problems. 78 | 79 | os.environ['AUTOWRAPT_PYTHON_PREFIX'] = os.path.realpath( 80 | os.path.normpath(sys.prefix)) 81 | os.environ['AUTOWRAPT_PYTHON_VERSION'] = '.'.join( 82 | map(str, sys.version_info[:2])) 83 | 84 | # Now launch the wrapped program. If the program to run was not an 85 | # absolute or relative path then we need to search the directories 86 | # specified in the 'PATH' environment variable to try and work out 87 | # where it is actually located. 88 | 89 | program_exe_path = args[0] 90 | 91 | if not os.path.dirname(program_exe_path): 92 | program_search_path = os.environ.get('PATH', 93 | '').split(os.path.pathsep) 94 | 95 | for path in program_search_path: 96 | path = os.path.join(path, program_exe_path) 97 | if os.path.exists(path) and os.access(path, os.X_OK): 98 | program_exe_path = path 99 | break 100 | 101 | log_message('program_exe_path = %r', program_exe_path) 102 | log_message('execl_arguments = %r', [program_exe_path]+args) 103 | 104 | os.execl(program_exe_path, *args) 105 | 106 | def main(): 107 | if len(sys.argv) <= 1: 108 | sys.exit('Usage: %s program [options]' % os.path.basename( 109 | sys.argv[0])) 110 | 111 | run_program(sys.argv[1:]) 112 | 113 | if __name__ == '__main__': 114 | main() 115 | -------------------------------------------------------------------------------- /src/__startup__/sitecustomize.py: -------------------------------------------------------------------------------- 1 | '''Provides a custom 'sitecustomize' module which will be used when the 2 | 'autowrapt' wrapper script is used when launching a Python program. This 3 | custom 'sitecustomize' module will find any existing 'sitecustomize' 4 | module which may have been overridden and ensures that that is imported 5 | as well. Once that is done then the monkey patches for ensuring any 6 | bootstrapping is done for registering post import hook callback 7 | functions after the 'usercustomize' module is loaded will be applied. If 8 | however 'usercustomize' support is not enabled, then the registration 9 | will be forced immediately. 10 | 11 | ''' 12 | 13 | import os 14 | import sys 15 | import site 16 | import time 17 | 18 | _debug = os.environ.get('AUTOWRAPT_DEBUG', 19 | 'off').lower() in ('on', 'true', '1') 20 | 21 | def log_message(text, *args): 22 | if _debug: 23 | text = text % args 24 | timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) 25 | print('AUTOWRAPT: %s (%d) - %s' % (timestamp, os.getpid(), text)) 26 | 27 | log_message('autowrapt - sitecustomize (%s)', __file__) 28 | 29 | log_message('working_directory = %r', os.getcwd()) 30 | 31 | log_message('sys.prefix = %r', os.path.normpath(sys.prefix)) 32 | 33 | try: 34 | log_message('sys.real_prefix = %r', sys.real_prefix) 35 | except AttributeError: 36 | pass 37 | 38 | log_message('sys.version_info = %r', sys.version_info) 39 | log_message('sys.executable = %r', sys.executable) 40 | 41 | if hasattr(sys, 'flags'): 42 | log_message('sys.flags = %r', sys.flags) 43 | 44 | log_message('sys.path = %r', sys.path) 45 | 46 | # This 'sitecustomize' module will override any which may already have 47 | # existed, be it one supplied by the user or one which has been placed 48 | # in the 'site-packages' directory of the Python installation. We need 49 | # to ensure that the existing 'sitecustomize' module is still loaded. To 50 | # do that we remove the special startup directory containing this module 51 | # from 'sys.path' and use the 'imp' module to find any original 52 | # 'sitecustomize' module and load it. 53 | 54 | import imp 55 | 56 | boot_directory = os.path.dirname(__file__) 57 | pkgs_directory = os.path.dirname(os.path.dirname(boot_directory)) 58 | 59 | log_message('pkgs_directory = %r', pkgs_directory) 60 | log_message('boot_directory = %r', boot_directory) 61 | 62 | path = list(sys.path) 63 | 64 | try: 65 | path.remove(boot_directory) 66 | except ValueError: 67 | pass 68 | 69 | try: 70 | (file, pathname, description) = imp.find_module('sitecustomize', path) 71 | except ImportError: 72 | pass 73 | else: 74 | log_message('sitecustomize = %r', (file, pathname, description)) 75 | 76 | imp.load_module('sitecustomize', file, pathname, description) 77 | 78 | # Before we try and setup or trigger the bootstrapping for the 79 | # registration of the post import hook callback functions, we need to 80 | # make sure that we are still executing in the context of the same 81 | # Python installation as the 'autowrapt' script was installed in. This 82 | # is necessary because if it isn't and we were now running out of a 83 | # different Python installation, then it may not have the 'autowrapt' 84 | # package installed and so our attempts to import it will fail causing 85 | # startup of the Python interpreter to fail in an obscure way. 86 | 87 | expected_python_prefix = os.environ.get('AUTOWRAPT_PYTHON_PREFIX') 88 | actual_python_prefix = os.path.realpath(os.path.normpath(sys.prefix)) 89 | 90 | expected_python_version = os.environ.get('AUTOWRAPT_PYTHON_VERSION') 91 | actual_python_version = '.'.join(map(str, sys.version_info[:2])) 92 | 93 | python_prefix_matches = expected_python_prefix == actual_python_prefix 94 | python_version_matches = expected_python_version == actual_python_version 95 | 96 | log_message('python_prefix_matches = %r', python_prefix_matches) 97 | log_message('python_version_matches = %r', python_version_matches) 98 | 99 | if python_prefix_matches and python_version_matches: 100 | bootstrap_packages = os.environ.get('AUTOWRAPT_BOOTSTRAP') 101 | 102 | log_message('bootstrap_packages = %r', bootstrap_packages) 103 | 104 | if bootstrap_packages: 105 | # When the 'autowrapt' script is run from out of a Python egg 106 | # directory under 'buildout', then the path to the egg directory 107 | # will not actually be listed in 'sys.path' as yet. This is 108 | # because 'buildout' sets up any scripts so that 'sys.path' is 109 | # specified only within the script. So that we can find the 110 | # 'autowrapt' package, we need to ensure that in this case the 111 | # egg directory for 'autowrapt' is manually added to 'sys.path' 112 | # before we can import it. 113 | 114 | pkgs_directory_missing = pkgs_directory not in sys.path 115 | 116 | if pkgs_directory_missing: 117 | sys.path.insert(0, pkgs_directory) 118 | 119 | from autowrapt.bootstrap import bootstrap 120 | from autowrapt.bootstrap import register_bootstrap_functions 121 | 122 | # If we had to add the egg directory above corresponding to the 123 | # 'autowrapt' package, now remove it to ensure the presence of 124 | # the directory doesn't cause any later problems. It is quite 125 | # possible that the directory will be added back in by scripts 126 | # run under 'buildout' but that would be the normal behaviour 127 | # and better off letting it do it how it wants to rather than 128 | # leave the directory in place. 129 | 130 | if pkgs_directory_missing: 131 | try: 132 | sys.path.remove(pkgs_directory) 133 | except ValueError: 134 | pass 135 | 136 | # Trigger the application of the monkey patches to the 'site' 137 | # module so that actual registration of the post import hook 138 | # callback functions is only run after any 'usercustomize' 139 | # module has been imported. If 'usercustomize' module support 140 | # is disabled, as it will be in a Python virtual environment, 141 | # then trigger the registration immediately. 142 | 143 | bootstrap() 144 | 145 | if not site.ENABLE_USER_SITE: 146 | register_bootstrap_functions() 147 | --------------------------------------------------------------------------------