├── .gitignore ├── test ├── dummy.py ├── setup.py └── runtest.py ├── .travis.yml ├── setup.py ├── LICENSE ├── README.rst └── fastentrypoints.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | dist 3 | __pycache__ 4 | *.pyc 5 | -------------------------------------------------------------------------------- /test/dummy.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | print('hello, world.') 3 | 4 | 5 | if __name__ == '__main__': 6 | main() 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | arch: 3 | - amd64 4 | - ppc64le 5 | python: 6 | - 2.7 7 | - 3.7 8 | before_install: pip install virtualenv pathlib2 9 | install: pip install . 10 | script: python test/runtest.py 11 | -------------------------------------------------------------------------------- /test/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import fastentrypoints 3 | 4 | setup( 5 | name='dummypkg', 6 | version='0.0.0', 7 | py_modules=['dummy'], 8 | description='dummy package for the test', 9 | entry_points={'console_scripts': ['hello=dummy:main']}, 10 | ) 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import fastentrypoints 3 | import io 4 | 5 | setup( 6 | name='fastentrypoints', 7 | version='0.12', 8 | py_modules=['fastentrypoints'], 9 | description='Makes entry_points specified in setup.py load more quickly', 10 | long_description=io.open('README.rst', 'r', encoding='utf-8').read(), 11 | url='https://github.com/ninjaaron/fast-entry_points', 12 | author='Aaron Christianson', 13 | author_email='ninjaaron@gmail.com', 14 | license='BSD', 15 | entry_points={'console_scripts': ['fastep=fastentrypoints:main']}, 16 | ) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Aaron Christianson 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 6 | met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 16 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 17 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 21 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 22 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 24 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /test/runtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import difflib 3 | import os 4 | #import pathlib 5 | import shutil 6 | import subprocess 7 | import sys 8 | try: 9 | import pathlib2 as pathlib 10 | except: 11 | import pathlib 12 | 13 | TEST_DIR = pathlib.Path(__file__).absolute().parent 14 | PROJECT_DIR = TEST_DIR.parent 15 | EXPECTED_OUTPUT = r"""# -*- coding: utf-8 -*- 16 | # EASY-INSTALL-ENTRY-SCRIPT: 'dummypkg{version}','console_scripts','hello' 17 | __requires__ = 'dummypkg{version}' 18 | import re 19 | import sys 20 | 21 | from dummy import main 22 | 23 | if __name__ == '__main__': 24 | sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) 25 | sys.exit(main())""" 26 | 27 | # Python 2 needs virtualenv, and Travis already executes in a virtualenv 28 | use_virtualenv = True 29 | use_editable = True 30 | 31 | if use_editable: 32 | EXPECTED_OUTPUT = EXPECTED_OUTPUT.format(version="") 33 | else: 34 | EXPECTED_OUTPUT = EXPECTED_OUTPUT.format(version="==0.0.0") 35 | 36 | 37 | def run(*args, **kwargs): 38 | if sys.version_info >= (3, 5): 39 | subprocess.run(*args, check=True, **kwargs) 40 | else: 41 | subprocess.call(*args, **kwargs) 42 | 43 | 44 | def main(): 45 | fep_copy = TEST_DIR / "fastentrypoints.py" 46 | shutil.copy2(str(PROJECT_DIR/"fastentrypoints.py"), str(fep_copy)) 47 | 48 | testenv = pathlib.Path("testenv") 49 | pip = testenv / "bin" / "pip" 50 | if use_virtualenv: 51 | run([sys.executable, "-m", "virtualenv", str(testenv)]) 52 | else: 53 | run([sys.executable, "-m", "venv", str(testenv)]) 54 | 55 | if use_editable: 56 | run([str(pip), "install", "-e", str(TEST_DIR)]) 57 | else: 58 | run([str(pip), "install", str(TEST_DIR)]) 59 | 60 | try: 61 | with open(str(testenv / "bin" / "hello")) as output: 62 | output.readline() # eat shabang line, which is non-deterministic. 63 | result = output.read().strip() 64 | assert result == EXPECTED_OUTPUT 65 | finally: 66 | shutil.rmtree(str(testenv)) 67 | os.remove(str(fep_copy)) 68 | 69 | 70 | if __name__ == '__main__': 71 | main() 72 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Fast entry_points 2 | ================= 3 | Using ``entry_points`` in your setup.py makes scripts that start really 4 | slowly because it imports ``pkg_resources``, which is a horrible thing 5 | to do if you want your trivial script to execute more or less instantly. 6 | Check it out: https://github.com/pypa/setuptools/issues/510 7 | 8 | 9 | 10 | Importing ``fastentrypoints`` in your setup.py file produces scripts 11 | that looks (more or less) like this: 12 | 13 | .. code:: python 14 | 15 | # -*- coding: utf-8 -*- 16 | import re 17 | import sys 18 | 19 | from package.module import entry_function 20 | 21 | if __name__ == '__main__': 22 | sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) 23 | sys.exit(entry_function()) 24 | 25 | This is ripped directly from the way wheels do it and is faster than 26 | whatever the heck the normal console scripts do. 27 | 28 | Note: 29 | 30 | This bug in setuptools only affects packages built with the normal 31 | setup.py method. Building wheels avoids the problem and has many other 32 | benefits as well. ``fastentrypoints`` simply ensures that your user 33 | scripts will not automatically import pkg_resources, no matter how 34 | they are built. 35 | 36 | When using Python 3.8 and setuptools 47.2 (or newer), console scripts 37 | do not import pkg_resources. 38 | 39 | Usage 40 | ----- 41 | To use fastentrypoints, simply copy fastentrypoints.py into your project 42 | folder in the same directory as setup.py, and ``import fastentrypoints`` 43 | in your setup.py file. This monkey-patches 44 | ``setuptools.command.easy_install.ScriptWriter.get_args()`` in the 45 | background, which in turn produces simple entry scripts (like the one 46 | above) when you install the package. 47 | 48 | If you install fastentrypoints as a module, you have the ``fastep`` 49 | executable, which will copy fastentrypoints.py into the working 50 | directory (or into a list of directories you give it as arguments) and 51 | append ``include fastentrypoints.py`` to the MANIFEST.in file, and 52 | add an import statement to setup.py. It is available from PyPI. 53 | 54 | Be sure to add ``fastentrypoints.py`` to MANIFEST.ini if you want to 55 | distribute your package on PyPI. 56 | 57 | Alternatively, you can specify ``fastentrypoints`` as a build system 58 | dependency by adding a ``pyproject.toml`` file (`PEP 518 59 | `_) with these lines to 60 | your project folder:: 61 | 62 | [build-system] 63 | requires = ["setuptools", "wheel", "fastentrypoints"] 64 | 65 | It is also possible to install it from PyPI with easy_install in 66 | the setup script: 67 | 68 | .. code:: python 69 | 70 | try: 71 | import fastentrypoints 72 | except ImportError: 73 | from setuptools.command import easy_install 74 | import pkg_resources 75 | easy_install.main(['fastentrypoints']) 76 | pkg_resources.require('fastentrypoints') 77 | import fastentrypoint 78 | 79 | Let me know if there are places where this doesn't work well. I've 80 | mostly tested it with ``console_scripts`` so far, since I don't write 81 | the other thing. 82 | 83 | Test 84 | ---- 85 | There is one test. To run it, do ``test/runtest.py``. It installs a 86 | dummy package with fastentrypoints and ensures the generated script is 87 | what is expected. 88 | -------------------------------------------------------------------------------- /fastentrypoints.py: -------------------------------------------------------------------------------- 1 | # noqa: D300,D400 2 | # Copyright (c) 2016, Aaron Christianson 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are 7 | # met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright 13 | # notice, this list of conditions and the following disclaimer in the 14 | # documentation and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 17 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 18 | # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 19 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | ''' 28 | Monkey patch setuptools to write faster console_scripts with this format: 29 | 30 | import sys 31 | from mymodule import entry_function 32 | sys.exit(entry_function()) 33 | 34 | This is better. 35 | 36 | (c) 2016, Aaron Christianson 37 | http://github.com/ninjaaron/fast-entry_points 38 | ''' 39 | from setuptools.command import easy_install 40 | import re 41 | TEMPLATE = r''' 42 | # -*- coding: utf-8 -*- 43 | # EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}' 44 | __requires__ = '{3}' 45 | import re 46 | import sys 47 | 48 | from {0} import {1} 49 | 50 | if __name__ == '__main__': 51 | sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) 52 | sys.exit({2}()) 53 | '''.lstrip() 54 | 55 | 56 | @classmethod 57 | def get_args(cls, dist, header=None): # noqa: D205,D400 58 | """ 59 | Yield write_script() argument tuples for a distribution's 60 | console_scripts and gui_scripts entry points. 61 | """ 62 | if header is None: 63 | # pylint: disable=E1101 64 | header = cls.get_header() 65 | spec = str(dist.as_requirement()) 66 | for type_ in 'console', 'gui': 67 | group = type_ + '_scripts' 68 | for name, ep in dist.get_entry_map(group).items(): 69 | # ensure_safe_name 70 | if re.search(r'[\\/]', name): 71 | raise ValueError("Path separators not allowed in script names") 72 | script_text = TEMPLATE.format( 73 | ep.module_name, ep.attrs[0], '.'.join(ep.attrs), 74 | spec, group, name) 75 | # pylint: disable=E1101 76 | args = cls._get_script_args(type_, name, header, script_text) 77 | for res in args: 78 | yield res 79 | 80 | 81 | # pylint: disable=E1101 82 | easy_install.ScriptWriter.get_args = get_args 83 | 84 | 85 | def main(): 86 | import os 87 | import shutil 88 | import sys 89 | dests = sys.argv[1:] or ['.'] 90 | filename = re.sub(r'\.pyc$', '.py', __file__) 91 | 92 | for dst in dests: 93 | shutil.copy(filename, dst) 94 | manifest_path = os.path.join(dst, 'MANIFEST.in') 95 | setup_path = os.path.join(dst, 'setup.py') 96 | 97 | # Insert the include statement to MANIFEST.in if not present 98 | with open(manifest_path, 'a+') as manifest: 99 | manifest.seek(0) 100 | manifest_content = manifest.read() 101 | if 'include fastentrypoints.py' not in manifest_content: 102 | manifest.write(('\n' if manifest_content else '') + 103 | 'include fastentrypoints.py') 104 | 105 | # Insert the import statement to setup.py if not present 106 | with open(setup_path, 'a+') as setup: 107 | setup.seek(0) 108 | setup_content = setup.read() 109 | if 'import fastentrypoints' not in setup_content: 110 | setup.seek(0) 111 | setup.truncate() 112 | setup.write('import fastentrypoints\n' + setup_content) 113 | --------------------------------------------------------------------------------