From f929ba77d8138225d4a815caa1b86cf69e264b81 2013-11-01 00:38:34 From: Thomas Kluyver Date: 2013-11-01 00:38:34 Subject: [PATCH] Rework setup to allow installing on Python 2 and 3. Scripts named ipython and ipython[23], etc. Neither distutils nor setuptools made this easy. --- diff --git a/setup.py b/setup.py index fd4b36d..020547f 100755 --- a/setup.py +++ b/setup.py @@ -58,8 +58,8 @@ from setupbase import ( setup_args, find_packages, find_package_data, - find_scripts, - build_scripts_rename, + find_entry_points, + build_scripts_entrypt, find_data_files, check_for_dependencies, git_prebuild, @@ -68,6 +68,9 @@ from setupbase import ( require_submodules, UpdateSubmodules, CompileCSS, + install_symlinked, + install_lib_symlink, + install_scripts_for_symlink, ) from setupext import setupext @@ -148,7 +151,6 @@ require_clean_submodules() # update the manuals when building a source dist if len(sys.argv) >= 2 and sys.argv[1] in ('sdist','bdist_rpm'): - import textwrap # List of things to be updated. Each entry is a triplet of args for # target_update() @@ -231,6 +233,9 @@ setup_args['cmdclass'] = { 'upload_wininst' : UploadWindowsInstallers, 'submodule' : UpdateSubmodules, 'css' : CompileCSS, + 'symlink': install_symlinked, + 'install_lib_symlink': install_lib_symlink, + 'install_scripts_sym': install_scripts_for_symlink, } #--------------------------------------------------------------------------- @@ -263,7 +268,7 @@ if 'setuptools' in sys.modules: setup_args['cmdclass']['develop'] = require_submodules(develop) setuptools_extra_args['zip_safe'] = False - setuptools_extra_args['entry_points'] = find_scripts(True, suffix = '3' if PY3 else '') + setuptools_extra_args['entry_points'] = {'console_scripts':find_entry_points()} setup_args['extras_require'] = dict( parallel = 'pyzmq>=2.1.11', qtconsole = ['pyzmq>=2.1.11', 'pygments'], @@ -316,10 +321,10 @@ else: # check for dependencies an inform the user what is needed. This is # just to make life easy for users. check_for_dependencies() - setup_args['scripts'] = find_scripts(False) - if PY3: - # Rename scripts with '3' suffix - setup_args['cmdclass']['build_scripts'] = build_scripts_rename + # scripts has to be a non-empty list, or install_scripts isn't called + setup_args['scripts'] = [e.split('=')[0].strip() for e in find_entry_points()] + + setup_args['cmdclass']['build_scripts'] = build_scripts_entrypt #--------------------------------------------------------------------------- # Do the actual setup now diff --git a/setupbase.py b/setupbase.py index 54ab2ad..03ace4a 100644 --- a/setupbase.py +++ b/setupbase.py @@ -20,15 +20,14 @@ from __future__ import print_function #------------------------------------------------------------------------------- # Imports #------------------------------------------------------------------------------- +import errno import os import sys -try: - from configparser import ConfigParser -except: - from ConfigParser import ConfigParser from distutils.command.build_py import build_py from distutils.command.build_scripts import build_scripts +from distutils.command.install import install +from distutils.command.install_scripts import install_scripts from distutils.cmd import Command from glob import glob from subprocess import call @@ -311,7 +310,7 @@ def target_update(target,deps,cmd): # Find scripts #--------------------------------------------------------------------------- -def find_scripts(entry_points=False, suffix=''): +def find_entry_points(): """Find IPython's scripts. if entry_points is True: @@ -322,8 +321,7 @@ def find_scripts(entry_points=False, suffix=''): suffix is appended to script names if entry_points is True, so that the Python 3 scripts get named "ipython3" etc. """ - if entry_points: - console_scripts = [s % suffix for s in [ + ep = [ 'ipython%s = IPython:start_ipython', 'ipcontroller%s = IPython.parallel.apps.ipcontrollerapp:launch_new_instance', 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance', @@ -331,37 +329,85 @@ def find_scripts(entry_points=False, suffix=''): 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance', 'iptest%s = IPython.testing.iptestcontroller:main', 'irunner%s = IPython.lib.irunner:main', - ]] - gui_scripts = [] - scripts = dict(console_scripts=console_scripts, gui_scripts=gui_scripts) - else: - parallel_scripts = pjoin('IPython','parallel','scripts') - main_scripts = pjoin('IPython','scripts') - scripts = [ - pjoin(parallel_scripts, 'ipengine'), - pjoin(parallel_scripts, 'ipcontroller'), - pjoin(parallel_scripts, 'ipcluster'), - pjoin(parallel_scripts, 'iplogger'), - pjoin(main_scripts, 'ipython'), - pjoin(main_scripts, 'irunner'), - pjoin(main_scripts, 'iptest') ] - return scripts + suffix = str(sys.version_info[0]) + return [e % '' for e in ep] + [e % suffix for e in ep] + +script_src = """#!{executable} +from {mod} import {func} +{func}() +""" + +class build_scripts_entrypt(build_scripts): + def run(self): + self.mkpath(self.build_dir) + outfiles = [] + for script in find_entry_points(): + name, entrypt = script.split('=') + name = name.strip() + entrypt = entrypt.strip() + outfile = os.path.join(self.build_dir, name) + outfiles.append(outfile) + print('Writing script to', outfile) + + mod, func = entrypt.split(':') + with open(outfile, 'w') as f: + f.write(script_src.format(executable=sys.executable, + mod=mod, func=func)) + + return outfiles, outfiles + +class install_lib_symlink(Command): + user_options = [ + ('install-dir=', 'd', "directory to install to"), + ] + + def initialize_options(self): + self.install_dir = None -class build_scripts_rename(build_scripts): - """Use this on Python 3 to rename scripts to ipython3 etc.""" - _suffix = '3' + def finalize_options(self): + self.set_undefined_options('symlink', + ('install_lib', 'install_dir'), + ) + + def run(self): + if sys.platform == 'win32': + raise Exception("This doesn't work on Windows.") + pkg = os.path.join(os.getcwd(), 'IPython') + dest = os.path.join(self.install_dir, 'IPython') + print('symlinking %s -> %s' % (pkg, dest)) + try: + os.symlink(pkg, dest) + except OSError as e: + if e.errno == errno.EEXIST: + print('ALREADY EXISTS') + else: + raise + +class install_symlinked(install): + def run(self): + if sys.platform == 'win32': + raise Exception("This doesn't work on Windows.") + install.run(self) - def copy_scripts(self): - outfiles, updated_files = super(build_scripts_rename, self).copy_scripts() - new_outfiles = [p + self._suffix for p in outfiles] - updated_files = [p + self._suffix for p in updated_files] - for old, new in zip(outfiles, new_outfiles): - if os.path.exists(new): - os.unlink(new) - self.move_file(old, new) - return new_outfiles, updated_files - + # 'sub_commands': a list of commands this command might have to run to + # get its work done. See cmd.py for more info. + sub_commands = [('install_lib_symlink', lambda self:True), + ('install_scripts_sym', lambda self:True), + ] + +class install_scripts_for_symlink(install_scripts): + """Redefined to get options from 'symlink' instead of 'install'. + + I love distutils almost as much as I love setuptools. + """ + def finalize_options(self): + self.set_undefined_options('build', ('build_scripts', 'build_dir')) + self.set_undefined_options('symlink', + ('install_scripts', 'install_dir'), + ('force', 'force'), + ('skip_build', 'skip_build'), + ) #--------------------------------------------------------------------------- # Verify all dependencies