diff options
| author | Jason R. Coombs <jaraco@jaraco.com> | 2015-03-06 17:04:35 -0500 |
|---|---|---|
| committer | Jason R. Coombs <jaraco@jaraco.com> | 2015-03-06 17:04:35 -0500 |
| commit | dbb11565d5880616866a608ce04f93b2b3efedd8 (patch) | |
| tree | 1ed31bcaf743f05ad91e4ef9742b0ea5245b0c22 | |
| parent | a979a4494369abbe40fcf65738fc78fc5ae88d7d (diff) | |
| parent | 4334bd87fdb0e60ed6e019ad2eb3ee103032d316 (diff) | |
| download | python-setuptools-bitbucket-dbb11565d5880616866a608ce04f93b2b3efedd8.tar.gz | |
Merge pull request #25 from dhellmann/fix-tox
Fix tox settings so they work
29 files changed, 747 insertions, 473 deletions
diff --git a/.travis.yml b/.travis.yml index 0e648b38..45cace4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,5 +11,5 @@ script: # update egg_info based on setup.py in checkout - python bootstrap.py - - python setup.py ptr --addopts='-rs' - - python ez_setup.py --version 10.2.1 + - python setup.py test --addopts='-rs' + - python ez_setup.py --version 12.2 diff --git a/CHANGES.txt b/CHANGES.txt index afec8156..608dd77d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,115 @@ CHANGES ======= ---- +14.0 +---- + +* Bootstrap script now accepts ``--to-dir`` to customize save directory or + allow for re-use of existing repository of setuptools versions. See + Pull Request #112 for an alternate implementation. +* Issue #285: ``easy_install`` no longer will default to installing + packages to the "user site packages" directory if it is itself installed + there. Instead, the user must pass ``--user`` in all cases to install + packages to the user site packages. + This behavior now matches that of "pip install". To configure + an environment to always install to the user site packages, consider + using the "install-dir" and "scripts-dir" parameters to easy_install + through an appropriate distutils config file. + +------ +13.0.2 +------ + +* Issue #359: Include pytest.ini in the sdist so invocation of py.test on the + sdist honors the pytest configuration. + +------ +13.0.1 +------ + +Re-release of 13.0. Intermittent connectivity issues caused the release +process to fail and PyPI uploads no longer accept files for 13.0. + +---- +13.0 +---- + +* Issue #356: Back out Pull Request #119 as it requires Setuptools 10 or later + as the source during an upgrade. +* Removed build_py class from setup.py. According to 892f439d216e, this + functionality was added to support upgrades from old Distribute versions, + 0.6.5 and 0.6.6. + +---- +12.4 +---- + +* Pull Request #119: Restore writing of ``setup_requires`` to metadata + (previously added in 8.4 and removed in 9.0). + +---- +12.3 +---- + +* Documentation is now linked using the rst.linker package. +* Fix ``setuptools.command.easy_install.extract_wininst_cfg()`` + with Python 2.6 and 2.7. +* Issue #354. Added documentation on building setuptools + documentation. + +---- +12.2 +---- + +* Issue #345: Unload all modules under pkg_resources during + ``ez_setup.use_setuptools()``. +* Issue #336: Removed deprecation from ``ez_setup.use_setuptools``, + as it is clearly still used by buildout's bootstrap. ``ez_setup`` + remains deprecated for use by individual packages. +* Simplified implementation of ``ez_setup.use_setuptools``. + +---- +12.1 +---- + +* Pull Request #118: Soften warning for non-normalized versions in + Distribution. + +------ +12.0.5 +------ + +* Issue #339: Correct Attribute reference in ``cant_write_to_target``. +* Issue #336: Deprecated ``ez_setup.use_setuptools``. + +------ +12.0.4 +------ + +* Issue #335: Fix script header generation on Windows. + +------ +12.0.3 +------ + +* Fixed incorrect class attribute in ``install_scripts``. Tests would be nice. + +------ +12.0.2 +------ + +* Issue #331: Fixed ``install_scripts`` command on Windows systems corrupting + the header. + +------ +12.0.1 +------ + +* Restore ``setuptools.command.easy_install.sys_executable`` for pbr + compatibility. For the future, tools should construct a CommandSpec + explicitly. + +---- 12.0 ---- @@ -10,6 +119,8 @@ CHANGES ``build.executable``, such that an executable of "/usr/bin/env my-python" may be specified. This means that systems with a specified executable whose name has spaces in the path must be updated to escape or quote that value. +* Deprecated ``easy_install.ScriptWriter.get_writer``, replaced by ``.best()`` + with slightly different semantics (no force_windows flag). ------ 11.3.1 diff --git a/MANIFEST.in b/MANIFEST.in index 428bbd1e..8a4523a9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,3 +9,4 @@ include *.txt include MANIFEST.in include launcher.c include msvc-build-launcher.cmd +include pytest.ini diff --git a/docs/conf.py b/docs/conf.py index 5ea2e05e..24830987 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ import setup as setup_script # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['linkify'] +extensions = ['rst.linker'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -198,3 +198,50 @@ latex_documents = [ # If false, no module index is generated. #latex_use_modindex = True + +link_files = { + 'CHANGES.txt': dict( + using=dict( + BB='https://bitbucket.org', + GH='https://github.com', + ), + replace=[ + dict( + pattern=r"(Issue )?#(?P<issue>\d+)", + url='{BB}/pypa/setuptools/issue/{issue}', + ), + dict( + pattern=r"Pull Request ?#(?P<pull_request>\d+)", + url='{BB}/pypa/setuptools/pull-request/{pull_request}', + ), + dict( + pattern=r"Distribute #(?P<distribute>\d+)", + url='{BB}/tarek/distribute/issue/{distribute}', + ), + dict( + pattern=r"Buildout #(?P<buildout>\d+)", + url='{GH}/buildout/buildout/issues/{buildout}', + ), + dict( + pattern=r"Old Setuptools #(?P<old_setuptools>\d+)", + url='http://bugs.python.org/setuptools/issue{old_setuptools}', + ), + dict( + pattern=r"Jython #(?P<jython>\d+)", + url='http://bugs.jython.org/issue{jython}', + ), + dict( + pattern=r"Python #(?P<python>\d+)", + url='http://bugs.python.org/issue{python}', + ), + dict( + pattern=r"Interop #(?P<interop>\d+)", + url='{GH}/pypa/interoperability-peps/issues/{interop}', + ), + dict( + pattern=r"Pip #(?P<pip>\d+)", + url='{GH}/pypa/pip/issues/{pip}', + ), + ], + ), +} diff --git a/docs/developer-guide.txt b/docs/developer-guide.txt index 558d6ee7..27c304e5 100644 --- a/docs/developer-guide.txt +++ b/docs/developer-guide.txt @@ -109,3 +109,20 @@ Setuptools follows ``semver`` with some exceptions: - Omits 'v' prefix for tags. .. explain value of reflecting meaning in versions. + +---------------------- +Building Documentation +---------------------- + +Setuptools relies on the Sphinx system for building documentation and in +particular the ``build_sphinx`` distutils command. To build the +documentation, invoke:: + + python setup.py build_sphinx + +from the root of the repository. Setuptools will download a compatible +build of Sphinx and any requisite plugins and then build the +documentation in the build/sphinx directory. + +Setuptools does not support invoking the doc builder from the docs/ +directory as some tools expect. diff --git a/docs/development.txt b/docs/development.txt index 6fe30f6e..455f038a 100644 --- a/docs/development.txt +++ b/docs/development.txt @@ -33,4 +33,3 @@ setuptools changes. You have been warned. developer-guide formats releases - diff --git a/docs/index.txt b/docs/index.txt index d8eb968b..529f08f3 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -19,7 +19,6 @@ Documentation content: history roadmap python3 - using setuptools easy_install pkg_resources diff --git a/docs/using.txt b/docs/using.txt deleted file mode 100644 index bd80893d..00000000 --- a/docs/using.txt +++ /dev/null @@ -1,13 +0,0 @@ -================================ -Using Setuptools in your project -================================ - -To use Setuptools in your project, the recommended way is to ship -`ez_setup.py` alongside your `setup.py` script and call -it at the very beginning of `setup.py` like this:: - - from ez_setup import use_setuptools - use_setuptools() - -More info on `ez_setup.py` can be found at `the project home page -<https://pypy.python.org/pypi/setuptools>`_. diff --git a/ez_setup.py b/ez_setup.py index f1e4aeae..9ece89d7 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -1,18 +1,11 @@ #!/usr/bin/env python -"""Bootstrap setuptools installation -To use setuptools in your package's setup.py, include this -file in the same directory and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -To require a specific version of setuptools, set a download -mirror, or use an alternate download directory, simply supply -the appropriate options to ``use_setuptools()``. +""" +Setuptools bootstrapping installer. -This file can also be run as a script to install or upgrade setuptools. +Run this script to install or upgrade setuptools. """ + import os import shutil import sys @@ -23,6 +16,7 @@ import subprocess import platform import textwrap import contextlib +import warnings from distutils import log @@ -36,11 +30,15 @@ try: except ImportError: USER_SITE = None -DEFAULT_VERSION = "11.3.2" +DEFAULT_VERSION = "13.0.2" DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" +DEFAULT_SAVE_DIR = os.curdir + def _python_cmd(*args): """ + Execute a command. + Return True if the command succeeded. """ args = (sys.executable,) + args @@ -48,6 +46,7 @@ def _python_cmd(*args): def _install(archive_filename, install_args=()): + """Install Setuptools.""" with archive_context(archive_filename): # installing log.warn('Installing Setuptools') @@ -59,6 +58,7 @@ def _install(archive_filename, install_args=()): def _build_egg(egg, archive_filename, to_dir): + """Build Setuptools egg.""" with archive_context(archive_filename): # building an egg log.warn('Building a Setuptools egg in %s', to_dir) @@ -70,9 +70,8 @@ def _build_egg(egg, archive_filename, to_dir): class ContextualZipFile(zipfile.ZipFile): - """ - Supplement ZipFile class to support context manager for Python 2.6 - """ + + """Supplement ZipFile class to support context manager for Python 2.6.""" def __enter__(self): return self @@ -81,9 +80,7 @@ class ContextualZipFile(zipfile.ZipFile): self.close() def __new__(cls, *args, **kwargs): - """ - Construct a ZipFile or ContextualZipFile as appropriate - """ + """Construct a ZipFile or ContextualZipFile as appropriate.""" if hasattr(zipfile.ZipFile, '__exit__'): return zipfile.ZipFile(*args, **kwargs) return super(ContextualZipFile, cls).__new__(cls) @@ -91,7 +88,11 @@ class ContextualZipFile(zipfile.ZipFile): @contextlib.contextmanager def archive_context(filename): - # extracting the archive + """ + Unzip filename to a temporary directory, set to the cwd. + + The unzipped target is cleaned up after. + """ tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() @@ -112,6 +113,7 @@ def archive_context(filename): def _do_download(version, download_base, to_dir, download_delay): + """Download Setuptools.""" egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' % (version, sys.version_info[0], sys.version_info[1])) if not os.path.exists(egg): @@ -129,41 +131,77 @@ def _do_download(version, download_base, to_dir, download_delay): setuptools.bootstrap_install_from = egg -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15): +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=DEFAULT_SAVE_DIR, download_delay=15): + """ + Ensure that a setuptools version is installed. + + Return None. Raise SystemExit if the requested version + or later cannot be installed. + """ to_dir = os.path.abspath(to_dir) + + # prior to importing, capture the module state for + # representative modules. rep_modules = 'pkg_resources', 'setuptools' imported = set(sys.modules).intersection(rep_modules) + try: import pkg_resources - except ImportError: - return _do_download(version, download_base, to_dir, download_delay) - try: pkg_resources.require("setuptools>=" + version) + # a suitable version is already installed return + except ImportError: + # pkg_resources not available; setuptools is not installed; download + pass except pkg_resources.DistributionNotFound: - return _do_download(version, download_base, to_dir, download_delay) + # no version of setuptools was found; allow download + pass except pkg_resources.VersionConflict as VC_err: if imported: - msg = textwrap.dedent(""" - The required version of setuptools (>={version}) is not available, - and can't be installed while this script is running. Please - install a more recent version first, using - 'easy_install -U setuptools'. + _conflict_bail(VC_err, version) + + # otherwise, unload pkg_resources to allow the downloaded version to + # take precedence. + del pkg_resources + _unload_pkg_resources() - (Currently using {VC_err.args[0]!r}) - """).format(VC_err=VC_err, version=version) - sys.stderr.write(msg) - sys.exit(2) + return _do_download(version, download_base, to_dir, download_delay) + + +def _conflict_bail(VC_err, version): + """ + Setuptools was imported prior to invocation, so it is + unsafe to unload it. Bail out. + """ + conflict_tmpl = textwrap.dedent(""" + The required version of setuptools (>={version}) is not available, + and can't be installed while this script is running. Please + install a more recent version first, using + 'easy_install -U setuptools'. + + (Currently using {VC_err.args[0]!r}) + """) + msg = conflict_tmpl.format(**locals()) + sys.stderr.write(msg) + sys.exit(2) + + +def _unload_pkg_resources(): + del_modules = [ + name for name in sys.modules + if name.startswith('pkg_resources') + ] + for mod_name in del_modules: + del sys.modules[mod_name] - # otherwise, reload ok - del pkg_resources, sys.modules['pkg_resources'] - return _do_download(version, download_base, to_dir, download_delay) def _clean_check(cmd, target): """ - Run the command to download target. If the command fails, clean up before - re-raising the error. + Run the command to download target. + + If the command fails, clean up before re-raising the error. """ try: subprocess.check_call(cmd) @@ -172,10 +210,13 @@ def _clean_check(cmd, target): os.unlink(target) raise + def download_file_powershell(url, target): """ - Download the file at url to target using Powershell (which will validate - trust). Raise an exception if the command cannot complete. + Download the file at url to target using Powershell. + + Powershell will validate trust. + Raise an exception if the command cannot complete. """ target = os.path.abspath(target) ps_cmd = ( @@ -191,7 +232,9 @@ def download_file_powershell(url, target): ] _clean_check(cmd, target) + def has_powershell(): + """Determine if Powershell is available.""" if platform.system() != 'Windows': return False cmd = ['powershell', '-Command', 'echo test'] @@ -201,13 +244,14 @@ def has_powershell(): except Exception: return False return True - download_file_powershell.viable = has_powershell + def download_file_curl(url, target): cmd = ['curl', url, '--silent', '--output', target] _clean_check(cmd, target) + def has_curl(): cmd = ['curl', '--version'] with open(os.path.devnull, 'wb') as devnull: @@ -216,13 +260,14 @@ def has_curl(): except Exception: return False return True - download_file_curl.viable = has_curl + def download_file_wget(url, target): cmd = ['wget', url, '--quiet', '--output-document', target] _clean_check(cmd, target) + def has_wget(): cmd = ['wget', '--version'] with open(os.path.devnull, 'wb') as devnull: @@ -231,14 +276,11 @@ def has_wget(): except Exception: return False return True - download_file_wget.viable = has_wget + def download_file_insecure(url, target): - """ - Use Python to download the file, even though it cannot authenticate the - connection. - """ + """Use Python to download the file, without connection authentication.""" src = urlopen(url) try: # Read all the data in one block. @@ -249,9 +291,9 @@ def download_file_insecure(url, target): # Write all the data in one block to avoid creating a partial file. with open(target, "wb") as dst: dst.write(data) - download_file_insecure.viable = lambda: True + def get_best_downloader(): downloaders = ( download_file_powershell, @@ -262,10 +304,13 @@ def get_best_downloader(): viable_downloaders = (dl for dl in downloaders if dl.viable()) return next(viable_downloaders, None) -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=DEFAULT_SAVE_DIR, delay=15, + downloader_factory=get_best_downloader): """ - Download setuptools from a specified location and return its filename + Download setuptools from a specified location and return its filename. `version` should be a valid setuptools version number that is available as an sdist for download under the `download_base` URL (which should end @@ -287,16 +332,18 @@ def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, downloader(url, saveto) return os.path.realpath(saveto) + def _build_install_args(options): """ - Build the arguments to 'python setup.py install' on the setuptools package + Build the arguments to 'python setup.py install' on the setuptools package. + + Returns list of command line arguments. """ return ['--user'] if options.user_install else [] + def _parse_args(): - """ - Parse the command line for options - """ + """Parse the command line for options.""" parser = optparse.OptionParser() parser.add_option( '--user', dest='user_install', action='store_true', default=False, @@ -314,18 +361,30 @@ def _parse_args(): '--version', help="Specify which version to download", default=DEFAULT_VERSION, ) + parser.add_option( + '--to-dir', + help="Directory to save (and re-use) package", + default=DEFAULT_SAVE_DIR, + ) options, args = parser.parse_args() # positional arguments are ignored return options + +def _download_args(options): + """Return args for download_setuptools function from cmdline args.""" + return dict( + version=options.version, + download_base=options.download_base, + downloader_factory=options.downloader_factory, + to_dir=options.to_dir, + ) + + def main(): - """Install or upgrade setuptools and EasyInstall""" + """Install or upgrade setuptools and EasyInstall.""" options = _parse_args() - archive = download_setuptools( - version=options.version, - download_base=options.download_base, - downloader_factory=options.downloader_factory, - ) + archive = download_setuptools(**_download_args(options)) return _install(archive, _build_install_args(options)) if __name__ == '__main__': diff --git a/linkify.py b/linkify.py deleted file mode 100644 index 5c6e16b4..00000000 --- a/linkify.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Sphinx plugin to add links to the changelog. -""" - -import re -import os - - -link_patterns = [ - r"(Issue )?#(?P<issue>\d+)", - r"Pull Request ?#(?P<pull_request>\d+)", - r"Distribute #(?P<distribute>\d+)", - r"Buildout #(?P<buildout>\d+)", - r"Old Setuptools #(?P<old_setuptools>\d+)", - r"Jython #(?P<jython>\d+)", - r"Python #(?P<python>\d+)", - r"Interop #(?P<interop>\d+)", - r"Pip #(?P<pip>\d+)", -] - -issue_urls = dict( - pull_request='https://bitbucket.org' - '/pypa/setuptools/pull-request/{pull_request}', - issue='https://bitbucket.org/pypa/setuptools/issue/{issue}', - distribute='https://bitbucket.org/tarek/distribute/issue/{distribute}', - buildout='https://github.com/buildout/buildout/issues/{buildout}', - old_setuptools='http://bugs.python.org/setuptools/issue{old_setuptools}', - jython='http://bugs.jython.org/issue{jython}', - python='http://bugs.python.org/issue{python}', - interop='https://github.com/pypa/interoperability-peps/issues/{interop}', - pip='https://github.com/pypa/pip/issues/{pip}', -) - - -def _linkify(source, dest): - pattern = '|'.join(link_patterns) - with open(source) as source: - out = re.sub(pattern, replacer, source.read()) - with open(dest, 'w') as dest: - dest.write(out) - - -def replacer(match): - text = match.group(0) - match_dict = match.groupdict() - for key in match_dict: - if match_dict[key]: - url = issue_urls[key].format(**match_dict) - return "`{text} <{url}>`_".format(text=text, url=url) - -def setup(app): - _linkify('CHANGES.txt', 'CHANGES (links).txt') - app.connect('build-finished', remove_file) - -def remove_file(app, exception): - os.remove('CHANGES (links).txt') diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index c0c095b2..2ce663d2 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -182,8 +182,10 @@ class _SetuptoolsVersionMixin(object): "You have iterated over the result of " "pkg_resources.parse_version. This is a legacy behavior which is " "inconsistent with the new version class introduced in setuptools " - "8.0. That class should be used directly instead of attempting to " - "iterate over the result.", + "8.0. In most cases, conversion to a tuple is unnecessary. For " + "comparison of versions, sort the Version instances directly. If " + "you have another use case requiring the tuple, please file a " + "bug with the setuptools project describing that need.", RuntimeWarning, stacklevel=1, ) diff --git a/pytest.ini b/pytest.ini new file mode 100755 index 00000000..91d64bb8 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts=--doctest-modules --ignore release.py --ignore setuptools/lib2to3_ex.py --ignore tests/manual_test.py --ignore tests/shlib_test --doctest-glob=pkg_resources/api_tests.txt +norecursedirs=dist build *.egg @@ -5,6 +5,7 @@ tag_build = dev release = egg_info -RDb '' source = register sdist binary binary = bdist_egg upload --show-response +test = pytest [build_sphinx] source-dir = docs/ @@ -19,7 +20,3 @@ formats = gztar zip [wheel] universal=1 - -[pytest] -addopts=--doctest-modules --ignore release.py --ignore setuptools/lib2to3_ex.py --ignore tests/manual_test.py --ignore tests/shlib_test --doctest-glob=pkg_resources/api_tests.txt -norecursedirs=dist build *.egg @@ -1,5 +1,8 @@ #!/usr/bin/env python -"""Distutils setup file, used to install or test 'setuptools'""" +""" +Distutils setup file, used to install or test 'setuptools' +""" + import io import os import sys @@ -25,7 +28,6 @@ with open(ver_path) as ver_file: exec(ver_file.read(), main_ns) import setuptools -from setuptools.command.build_py import build_py as _build_py scripts = [] @@ -46,20 +48,6 @@ def _gen_console_scripts(): console_scripts = list(_gen_console_scripts()) - -# specific command that is used to generate windows .exe files -class build_py(_build_py): - def build_package_data(self): - """Copy data files into build directory""" - for package, src_dir, build_dir, filenames in self.data_files: - for filename in filenames: - target = os.path.join(build_dir, filename) - self.mkpath(os.path.dirname(target)) - srcfile = os.path.join(src_dir, filename) - outf, copied = self.copy_file(srcfile, target) - srcfile = os.path.abspath(srcfile) - - readme_file = io.open('README.txt', encoding='utf-8') with readme_file: @@ -75,7 +63,10 @@ if sys.platform == 'win32' or force_windows_specific_files: package_data.setdefault('setuptools', []).extend(['*.exe']) package_data.setdefault('setuptools.command', []).extend(['*.xml']) -pytest_runner = ['pytest-runner'] if 'ptr' in sys.argv else [] +needs_pytest = set(['ptr', 'pytest', 'test']).intersection(sys.argv) +pytest_runner = ['pytest-runner'] if needs_pytest else [] +needs_sphinx = set(['build_sphinx', 'upload_docs']).intersection(sys.argv) +sphinx = ['sphinx', 'rst.linker'] if needs_sphinx else [] setup_params = dict( name="setuptools", @@ -170,10 +161,9 @@ setup_params = dict( tests_require=[ 'setuptools[ssl]', 'pytest', - 'mock', - ], + ] + (['mock'] if sys.version_info[:2] < (3, 3) else []), setup_requires=[ - ] + pytest_runner, + ] + sphinx + pytest_runner, ) if __name__ == '__main__': diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 34fdeec2..87dce882 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -2,7 +2,6 @@ Build .egg distributions""" -# This module should be kept compatible with Python 2.3 from distutils.errors import DistutilsSetupError from distutils.dir_util import remove_tree, mkpath from distutils import log @@ -406,10 +405,6 @@ def scan_module(egg_dir, base, name, stubs): if bad in symbols: log.warn("%s: module MAY be using inspect.%s", module, bad) safe = False - if '__name__' in symbols and '__main__' in symbols and '.' not in module: - if sys.version[:3] == "2.4": # -m works w/zipfiles in 2.5 - log.warn("%s: top-level module may be 'python -m' script", module) - safe = False return safe @@ -441,7 +436,7 @@ INSTALL_DIRECTORY_ATTRS = [ ] -def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=None, +def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True, mode='w'): """Create a zip file from all the files under 'base_dir'. The output zip file will be named 'base_dir' + ".zip". Uses either the "zipfile" @@ -463,11 +458,7 @@ def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=None, z.write(path, p) log.debug("adding '%s'" % p) - if compress is None: - # avoid 2.3 zipimport bug when 64 bits - compress = (sys.version >= "2.4") - - compression = [zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED][bool(compress)] + compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED if not dry_run: z = zipfile.ZipFile(zip_filename, mode, compression=compression) for dirname, dirs, files in os.walk(base_dir): diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py index 98080694..a873d54b 100644 --- a/setuptools/command/build_py.py +++ b/setuptools/command/build_py.py @@ -136,22 +136,7 @@ class build_py(orig.build_py, Mixin2to3): mf.setdefault(src_dirs[d], []).append(path) def get_data_files(self): - pass # kludge 2.4 for lazy computation - - if sys.version < "2.4": # Python 2.4 already has this code - def get_outputs(self, include_bytecode=1): - """Return complete list of files copied to the build directory - - This includes both '.py' files and data files, as well as '.pyc' - and '.pyo' files if 'include_bytecode' is true. (This method is - needed for the 'install_lib' command to do its job properly, and to - generate a correct installation manifest.) - """ - return orig.build_py.get_outputs(self, include_bytecode) + [ - os.path.join(build_dir, filename) - for package, src_dir, build_dir, filenames in self.data_files - for filename in filenames - ] + pass # Lazily compute data files in _get_data_files() function. def check_package(self, package, package_dir): """Check namespace packages' __init__ for declare_namespace""" diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 340b1fac..4e841520 100755 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -37,6 +37,7 @@ import struct import contextlib import subprocess import shlex +import io from setuptools import Command from setuptools.sandbox import run_setup @@ -56,7 +57,6 @@ from pkg_resources import ( ) import pkg_resources - # Turn on PEP440Warnings warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) @@ -152,12 +152,9 @@ class easy_install(Command): create_index = PackageIndex def initialize_options(self): - if site.ENABLE_USER_SITE: - whereami = os.path.abspath(__file__) - self.user = whereami.startswith(site.USER_SITE) - else: - self.user = 0 - + # the --user option seemst to be an opt-in one, + # so the default should be False. + self.user = 0 self.zip_ok = self.local_snapshots_ok = None self.install_dir = self.script_dir = self.exclude_scripts = None self.index_url = None @@ -446,43 +443,49 @@ class easy_install(Command): self.pth_file = None # and don't create a .pth file self.install_dir = instdir - def cant_write_to_target(self): - template = """can't create or remove files in install directory + __cant_write_msg = textwrap.dedent(""" + can't create or remove files in install directory -The following error occurred while trying to add or remove files in the -installation directory: + The following error occurred while trying to add or remove files in the + installation directory: - %s + %s -The installation directory you specified (via --install-dir, --prefix, or -the distutils default setting) was: + The installation directory you specified (via --install-dir, --prefix, or + the distutils default setting) was: - %s -""" - msg = template % (sys.exc_info()[1], self.install_dir,) + %s + """).lstrip() - if not os.path.exists(self.install_dir): - msg += """ -This directory does not currently exist. Please create it and try again, or -choose a different installation directory (using the -d or --install-dir -option). -""" - else: - msg += """ -Perhaps your account does not have write access to this directory? If the -installation directory is a system-owned directory, you may need to sign in -as the administrator or "root" account. If you do not have administrative -access to this machine, you may wish to choose a different installation -directory, preferably one that is listed in your PYTHONPATH environment -variable. + __not_exists_id = textwrap.dedent(""" + This directory does not currently exist. Please create it and try again, or + choose a different installation directory (using the -d or --install-dir + option). + """).lstrip() -For information on other options, you may wish to consult the -documentation at: + __access_msg = textwrap.dedent(""" + Perhaps your account does not have write access to this directory? If the + installation directory is a system-owned directory, you may need to sign in + as the administrator or "root" account. If you do not have administrative + access to this machine, you may wish to choose a different installation + directory, preferably one that is listed in your PYTHONPATH environment + variable. - https://pythonhosted.org/setuptools/easy_install.html + For information on other options, you may wish to consult the + documentation at: -Please make the appropriate changes for your system and try again. -""" + https://pythonhosted.org/setuptools/easy_install.html + + Please make the appropriate changes for your system and try again. + """).lstrip() + + def cant_write_to_target(self): + msg = self.__cant_write_msg % (sys.exc_info()[1], self.install_dir,) + + if not os.path.exists(self.install_dir): + msg += '\n' + self.__not_exists_id + else: + msg += '\n' + self.__access_msg raise DistutilsError(msg) def check_pth_processing(self): @@ -742,7 +745,7 @@ Please make the appropriate changes for your system and try again. def install_wrapper_scripts(self, dist): if not self.exclude_scripts: - for args in ScriptWriter.get_args(dist): + for args in ScriptWriter.best().get_args(dist): self.write_script(*args) def install_script(self, dist, script_name, script_text, dev_path=None): @@ -980,46 +983,52 @@ Please make the appropriate changes for your system and try again. f.write('\n'.join(locals()[name]) + '\n') f.close() + __mv_warning = textwrap.dedent(""" + Because this distribution was installed --multi-version, before you can + import modules from this package in an application, you will need to + 'import pkg_resources' and then use a 'require()' call similar to one of + these examples, in order to select the desired version: + + pkg_resources.require("%(name)s") # latest installed version + pkg_resources.require("%(name)s==%(version)s") # this exact version + pkg_resources.require("%(name)s>=%(version)s") # this version or higher + """).lstrip() + + __id_warning = textwrap.dedent(""" + Note also that the installation directory must be on sys.path at runtime for + this to work. (e.g. by being the application's script directory, by being on + PYTHONPATH, or by being added to sys.path by your code.) + """) + def installation_report(self, req, dist, what="Installed"): """Helpful installation message for display to package users""" msg = "\n%(what)s %(eggloc)s%(extras)s" if self.multi_version and not self.no_report: - msg += """ - -Because this distribution was installed --multi-version, before you can -import modules from this package in an application, you will need to -'import pkg_resources' and then use a 'require()' call similar to one of -these examples, in order to select the desired version: - - pkg_resources.require("%(name)s") # latest installed version - pkg_resources.require("%(name)s==%(version)s") # this exact version - pkg_resources.require("%(name)s>=%(version)s") # this version or higher -""" + msg += '\n' + self.__mv_warning if self.install_dir not in map(normalize_path, sys.path): - msg += """ + msg += '\n' + self.__id_warning -Note also that the installation directory must be on sys.path at runtime for -this to work. (e.g. by being the application's script directory, by being on -PYTHONPATH, or by being added to sys.path by your code.) -""" eggloc = dist.location name = dist.project_name version = dist.version extras = '' # TODO: self.report_extras(req, dist) return msg % locals() - def report_editable(self, spec, setup_script): - dirname = os.path.dirname(setup_script) - python = sys.executable - return """\nExtracted editable version of %(spec)s to %(dirname)s + __editable_msg = textwrap.dedent(""" + Extracted editable version of %(spec)s to %(dirname)s -If it uses setuptools in its setup script, you can activate it in -"development" mode by going to that directory and running:: + If it uses setuptools in its setup script, you can activate it in + "development" mode by going to that directory and running:: - %(python)s setup.py develop + %(python)s setup.py develop -See the setuptools documentation for the "develop" command for more info. -""" % locals() + See the setuptools documentation for the "develop" command for more info. + """).lstrip() + + def report_editable(self, spec, setup_script): + dirname = os.path.dirname(setup_script) + python = sys.executable + return '\n' + self.__editable_msg % locals() def run_setup(self, setup_script, setup_base, args): sys.modules.setdefault('distutils.command.bdist_egg', bdist_egg) @@ -1170,35 +1179,38 @@ See the setuptools documentation for the "develop" command for more info. finally: log.set_verbosity(self.verbose) # restore original verbosity - def no_default_version_msg(self): - template = """bad install directory or PYTHONPATH + __no_default_msg = textwrap.dedent(""" + bad install directory or PYTHONPATH + + You are attempting to install a package to a directory that is not + on PYTHONPATH and which Python does not read ".pth" files from. The + installation directory you specified (via --install-dir, --prefix, or + the distutils default setting) was: -You are attempting to install a package to a directory that is not -on PYTHONPATH and which Python does not read ".pth" files from. The -installation directory you specified (via --install-dir, --prefix, or -the distutils default setting) was: + %s - %s + and your PYTHONPATH environment variable currently contains: -and your PYTHONPATH environment variable currently contains: + %r - %r + Here are some of your options for correcting the problem: -Here are some of your options for correcting the problem: + * You can choose a different installation directory, i.e., one that is + on PYTHONPATH or supports .pth files -* You can choose a different installation directory, i.e., one that is - on PYTHONPATH or supports .pth files + * You can add the installation directory to the PYTHONPATH environment + variable. (It must then also be on PYTHONPATH whenever you run + Python and want to use the package(s) you are installing.) -* You can add the installation directory to the PYTHONPATH environment - variable. (It must then also be on PYTHONPATH whenever you run - Python and want to use the package(s) you are installing.) + * You can set up the installation directory to support ".pth" files by + using one of the approaches described here: -* You can set up the installation directory to support ".pth" files by - using one of the approaches described here: + https://pythonhosted.org/setuptools/easy_install.html#custom-installation-locations - https://pythonhosted.org/setuptools/easy_install.html#custom-installation-locations + Please make the appropriate changes for your system and try again.""").lstrip() -Please make the appropriate changes for your system and try again.""" + def no_default_version_msg(self): + template = self.__no_default_msg return template % (self.install_dir, os.environ.get('PYTHONPATH', '')) def install_site_py(self): @@ -1398,13 +1410,8 @@ def extract_wininst_cfg(dist_filename): {'version': '', 'target_version': ''}) try: part = f.read(cfglen) - # part is in bytes, but we need to read up to the first null - # byte. - if sys.version_info >= (2, 6): - null_byte = bytes([0]) - else: - null_byte = chr(0) - config = part.split(null_byte, 1)[0] + # Read up to the first null byte. + config = part.split(b'\0', 1)[0] # Now the config is in bytes, but for RawConfigParser, it should # be text, so decode it. config = config.decode(sys.getfilesystemencoding()) @@ -1787,9 +1794,8 @@ def is_python(text, filename='<string>'): def is_sh(executable): """Determine if the specified executable is a .sh (contains a #! line)""" try: - fp = open(executable) - magic = fp.read(2) - fp.close() + with io.open(executable, encoding='latin-1') as fp: + magic = fp.read(2) except (OSError, IOError): return executable return magic == '#!' @@ -1831,25 +1837,14 @@ def chmod(path, mode): def fix_jython_executable(executable, options): - if sys.platform.startswith('java') and is_sh(executable): - # Workaround for Jython is not needed on Linux systems. - import java + warnings.warn("Use JythonCommandSpec", DeprecationWarning, stacklevel=2) - if java.lang.System.getProperty("os.name") == "Linux": - return executable + if not JythonCommandSpec.relevant(): + return executable - # Workaround Jython's sys.executable being a .sh (an invalid - # shebang line interpreter) - if options: - # Can't apply the workaround, leave it broken - log.warn( - "WARNING: Unable to adapt shebang line for Jython," - " the following script is NOT executable\n" - " see http://bugs.jython.org/issue1112 for" - " more information.") - else: - return '/usr/bin/env %s' % executable - return executable + cmd = CommandSpec.best().from_param(executable) + cmd.install_options(options) + return cmd.as_header().lstrip('#!').rstrip('\n') class CommandSpec(list): @@ -1859,6 +1854,14 @@ class CommandSpec(list): """ options = [] + split_args = dict() + + @classmethod + def best(cls): + """ + Choose the best CommandSpec class based on environmental conditions. + """ + return cls if not JythonCommandSpec.relevant() else JythonCommandSpec @classmethod def _sys_executable(cls): @@ -1882,7 +1885,7 @@ class CommandSpec(list): @classmethod def from_environment(cls): - return cls.from_string('"' + cls._sys_executable() + '"') + return cls([cls._sys_executable()]) @classmethod def from_string(cls, string): @@ -1890,8 +1893,8 @@ class CommandSpec(list): Construct a command spec from a simple string representing a command line parseable by shlex.split. """ - items = shlex.split(string) - return JythonCommandSpec.from_string(string) or cls(items) + items = shlex.split(string, **cls.split_args) + return cls(items) def install_options(self, script_text): self.options = shlex.split(self._extract_options(script_text)) @@ -1917,20 +1920,31 @@ class CommandSpec(list): cmdline = subprocess.list2cmdline(items) return '#!' + cmdline + '\n' +# For pbr compat; will be removed in a future version. +sys_executable = CommandSpec._sys_executable() + + +class WindowsCommandSpec(CommandSpec): + split_args = dict(posix=False) + class JythonCommandSpec(CommandSpec): @classmethod - def from_string(cls, string): - """ - On Jython, construct an instance of this class. - On platforms other than Jython, return None. - """ - needs_jython_spec = ( + def relevant(cls): + return ( sys.platform.startswith('java') and __import__('java').lang.System.getProperty('os.name') != 'Linux' ) - return cls([string]) if needs_jython_spec else None + + @classmethod + def from_environment(cls): + string = '"' + cls._sys_executable() + '"' + return cls.from_string(string) + + @classmethod + def from_string(cls, string): + return cls([string]) def as_header(self): """ @@ -1971,11 +1985,13 @@ class ScriptWriter(object): ) """).lstrip() + command_spec_class = CommandSpec + @classmethod def get_script_args(cls, dist, executable=None, wininst=False): # for backward compatibility warnings.warn("Use get_args", DeprecationWarning) - writer = cls.get_writer(wininst) + writer = (WindowsScriptWriter if wininst else ScriptWriter).best() header = cls.get_script_header("", executable, wininst) return writer.get_args(dist, header) @@ -1985,7 +2001,7 @@ class ScriptWriter(object): warnings.warn("Use get_header", DeprecationWarning) if wininst: executable = "python.exe" - cmd = CommandSpec.from_param(executable) + cmd = cls.command_spec_class.best().from_param(executable) cmd.install_options(script_text) return cmd.as_header() @@ -2007,9 +2023,16 @@ class ScriptWriter(object): @classmethod def get_writer(cls, force_windows): - if force_windows or sys.platform == 'win32': - return WindowsScriptWriter.get_writer() - return cls + # for backward compatibility + warnings.warn("Use best", DeprecationWarning) + return WindowsScriptWriter.best() if force_windows else cls.best() + + @classmethod + def best(cls): + """ + Select the best ScriptWriter for this environment. + """ + return WindowsScriptWriter.best() if sys.platform == 'win32' else cls @classmethod def _get_script_args(cls, type_, name, header, script_text): @@ -2019,16 +2042,24 @@ class ScriptWriter(object): @classmethod def get_header(cls, script_text="", executable=None): """Create a #! line, getting options (if any) from script_text""" - cmd = CommandSpec.from_param(executable) + cmd = cls.command_spec_class.best().from_param(executable) cmd.install_options(script_text) return cmd.as_header() class WindowsScriptWriter(ScriptWriter): + command_spec_class = WindowsCommandSpec + @classmethod def get_writer(cls): + # for backward compatibility + warnings.warn("Use best", DeprecationWarning) + return cls.best() + + @classmethod + def best(cls): """ - Get a script writer suitable for Windows + Select the best ScriptWriter suitable for Windows """ writer_lookup = dict( executable=WindowsExecutableLauncherWriter, @@ -2225,4 +2256,3 @@ def _patch_usage(): yield finally: distutils.core.gen_usage = saved - diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py index 722b0566..be66cb22 100755 --- a/setuptools/command/install_scripts.py +++ b/setuptools/command/install_scripts.py @@ -13,7 +13,7 @@ class install_scripts(orig.install_scripts): self.no_ep = False def run(self): - from setuptools.command.easy_install import ScriptWriter, CommandSpec + import setuptools.command.easy_install as ei self.run_command("egg_info") if self.distribution.scripts: @@ -30,13 +30,16 @@ class install_scripts(orig.install_scripts): ei_cmd.egg_name, ei_cmd.egg_version, ) bs_cmd = self.get_finalized_command('build_scripts') - cmd = CommandSpec.from_param(getattr(bs_cmd, 'executable', None)) - is_wininst = getattr( - self.get_finalized_command("bdist_wininst"), '_is_running', False - ) + exec_param = getattr(bs_cmd, 'executable', None) + bw_cmd = self.get_finalized_command("bdist_wininst") + is_wininst = getattr(bw_cmd, '_is_running', False) + writer = ei.ScriptWriter if is_wininst: - cmd = CommandSpec.from_string("python.exe") - writer = ScriptWriter.get_writer(force_windows=is_wininst) + exec_param = "python.exe" + writer = ei.WindowsScriptWriter + # resolve the writer to the environment + writer = writer.best() + cmd = writer.command_spec_class.best().from_param(exec_param) for args in writer.get_args(dist, cmd.as_header()): self.write_script(*args) diff --git a/setuptools/dist.py b/setuptools/dist.py index bc29b131..ffbc7c48 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -277,10 +277,9 @@ class Distribution(_Distribution): normalized_version = str(ver) if self.metadata.version != normalized_version: warnings.warn( - "The version specified requires normalization, " - "consider using '%s' instead of '%s'." % ( - normalized_version, + "Normalizing '%s' to '%s'" % ( self.metadata.version, + normalized_version, ) ) self.metadata.version = normalized_version diff --git a/setuptools/msvc9_support.py b/setuptools/msvc9_support.py index e76d70f0..a69c7474 100644 --- a/setuptools/msvc9_support.py +++ b/setuptools/msvc9_support.py @@ -1,5 +1,3 @@ -import sys - try: import distutils.msvc9compiler except ImportError: @@ -29,13 +27,15 @@ def patch_for_specialized_compiler(): def find_vcvarsall(version): Reg = distutils.msvc9compiler.Reg VC_BASE = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f' + key = VC_BASE % ('', version) try: # Per-user installs register the compiler path here - productdir = Reg.get_value(VC_BASE % ('', version), "installdir") + productdir = Reg.get_value(key, "installdir") except KeyError: try: # All-user installs on a 64-bit system register here - productdir = Reg.get_value(VC_BASE % ('Wow6432Node\\', version), "installdir") + key = VC_BASE % ('Wow6432Node\\', version) + productdir = Reg.get_value(key, "installdir") except KeyError: productdir = None diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 7971f42c..67255123 100755 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -47,8 +47,10 @@ def _execfile(filename, globals, locals=None): @contextlib.contextmanager -def save_argv(): +def save_argv(repl=None): saved = sys.argv[:] + if repl is not None: + sys.argv[:] = repl try: yield saved finally: @@ -92,6 +94,51 @@ def pushd(target): os.chdir(saved) +class UnpickleableException(Exception): + """ + An exception representing another Exception that could not be pickled. + """ + @classmethod + def dump(cls, type, exc): + """ + Always return a dumped (pickled) type and exc. If exc can't be pickled, + wrap it in UnpickleableException first. + """ + try: + return pickle.dumps(type), pickle.dumps(exc) + except Exception: + return cls.dump(cls, cls(repr(exc))) + + +class ExceptionSaver: + """ + A Context Manager that will save an exception, serialized, and restore it + later. + """ + def __enter__(self): + return self + + def __exit__(self, type, exc, tb): + if not exc: + return + + # dump the exception + self._saved = UnpickleableException.dump(type, exc) + self._tb = tb + + # suppress the exception + return True + + def resume(self): + "restore and re-raise any exception" + + if '_saved' not in vars(self): + return + + type, exc = map(pickle.loads, self._saved) + compat.reraise(type, exc, self._tb) + + @contextlib.contextmanager def save_modules(): """ @@ -101,31 +148,20 @@ def save_modules(): outside the context. """ saved = sys.modules.copy() - try: - try: - yield saved - except: - # dump any exception - class_, exc, tb = sys.exc_info() - saved_cls = pickle.dumps(class_) - saved_exc = pickle.dumps(exc) - raise - finally: - sys.modules.update(saved) - # remove any modules imported since - del_modules = ( - mod_name for mod_name in sys.modules - if mod_name not in saved - # exclude any encodings modules. See #285 - and not mod_name.startswith('encodings.') - ) - _clear_modules(del_modules) - except: - # reload and re-raise any exception, using restored modules - class_, exc, tb = sys.exc_info() - new_cls = pickle.loads(saved_cls) - new_exc = pickle.loads(saved_exc) - compat.reraise(new_cls, new_exc, tb) + with ExceptionSaver() as saved_exc: + yield saved + + sys.modules.update(saved) + # remove any modules imported since + del_modules = ( + mod_name for mod_name in sys.modules + if mod_name not in saved + # exclude any encodings modules. See #285 + and not mod_name.startswith('encodings.') + ) + _clear_modules(del_modules) + + saved_exc.resume() def _clear_modules(module_names): diff --git a/setuptools/tests/contexts.py b/setuptools/tests/contexts.py index d06a333f..1d29284b 100644 --- a/setuptools/tests/contexts.py +++ b/setuptools/tests/contexts.py @@ -27,7 +27,7 @@ def environment(**replacements): to clear the values. """ saved = dict( - (key, os.environ['key']) + (key, os.environ[key]) for key in replacements if key in os.environ ) @@ -49,14 +49,6 @@ def environment(**replacements): @contextlib.contextmanager -def argv(repl): - old_argv = sys.argv[:] - sys.argv[:] = repl - yield - sys.argv[:] = old_argv - - -@contextlib.contextmanager def quiet(): """ Redirect stdout/stderr to StringIO objects to prevent console output from diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index 0b1eaf5f..c70c38cb 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -1,4 +1,7 @@ -import mock +try: + from unittest import mock +except ImportError: + import mock import pytest from . import contexts diff --git a/setuptools/tests/py26compat.py b/setuptools/tests/py26compat.py index c53b4809..c5680881 100644 --- a/setuptools/tests/py26compat.py +++ b/setuptools/tests/py26compat.py @@ -8,4 +8,7 @@ def _tarfile_open_ex(*args, **kwargs): """ return contextlib.closing(tarfile.open(*args, **kwargs)) -tarfile_open = _tarfile_open_ex if sys.version_info < (2,7) else tarfile.open +if sys.version_info[:2] < (2, 7) or (3, 0) <= sys.version_info[:2] < (3, 2): + tarfile_open = _tarfile_open_ex +else: + tarfile_open = tarfile.open diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 72b040e1..7d61fb83 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -1,4 +1,4 @@ -#! -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- """Easy install Tests """ @@ -13,18 +13,19 @@ import contextlib import tarfile import logging import itertools +import distutils.errors import pytest -import mock +try: + from unittest import mock +except ImportError: + import mock from setuptools import sandbox from setuptools import compat from setuptools.compat import StringIO, BytesIO, urlparse from setuptools.sandbox import run_setup -from setuptools.command.easy_install import ( - easy_install, fix_jython_executable, nt_quote_arg, - is_sh, ScriptWriter, CommandSpec, -) +import setuptools.command.easy_install as ei from setuptools.command.easy_install import PthDistributions from setuptools.command import easy_install as easy_install_pkg from setuptools.dist import Distribution @@ -47,19 +48,6 @@ class FakeDist(object): def as_requirement(self): return 'spec' -WANTED = DALS(""" - #!%s - # EASY-INSTALL-ENTRY-SCRIPT: 'spec','console_scripts','name' - __requires__ = 'spec' - import sys - from pkg_resources import load_entry_point - - if __name__ == '__main__': - sys.exit( - load_entry_point('spec', 'console_scripts', 'name')() - ) - """) % nt_quote_arg(fix_jython_executable(sys.executable, "")) - SETUP_PY = DALS(""" from setuptools import setup @@ -70,7 +58,7 @@ class TestEasyInstallTest: def test_install_site_py(self): dist = Distribution() - cmd = easy_install(dist) + cmd = ei.easy_install(dist) cmd.sitepy_installed = False cmd.install_dir = tempfile.mkdtemp() try: @@ -81,18 +69,30 @@ class TestEasyInstallTest: shutil.rmtree(cmd.install_dir) def test_get_script_args(self): + header = ei.CommandSpec.best().from_environment().as_header() + expected = header + DALS(""" + # EASY-INSTALL-ENTRY-SCRIPT: 'spec','console_scripts','name' + __requires__ = 'spec' + import sys + from pkg_resources import load_entry_point + + if __name__ == '__main__': + sys.exit( + load_entry_point('spec', 'console_scripts', 'name')() + ) + """) dist = FakeDist() - args = next(ScriptWriter.get_args(dist)) + args = next(ei.ScriptWriter.get_args(dist)) name, script = itertools.islice(args, 2) - assert script == WANTED + assert script == expected def test_no_find_links(self): # new option '--no-find-links', that blocks find-links added at # the project level dist = Distribution() - cmd = easy_install(dist) + cmd = ei.easy_install(dist) cmd.check_pth_processing = lambda: True cmd.no_find_links = True cmd.find_links = ['link1', 'link2'] @@ -102,7 +102,7 @@ class TestEasyInstallTest: assert cmd.package_index.scanned_urls == {} # let's try without it (default behavior) - cmd = easy_install(dist) + cmd = ei.easy_install(dist) cmd.check_pth_processing = lambda: True cmd.find_links = ['link1', 'link2'] cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok') @@ -111,6 +111,16 @@ class TestEasyInstallTest: keys = sorted(cmd.package_index.scanned_urls.keys()) assert keys == ['link1', 'link2'] + def test_write_exception(self): + """ + Test that `cant_write_to_target` is rendered as a DistutilsError. + """ + dist = Distribution() + cmd = ei.easy_install(dist) + cmd.install_dir = os.getcwd() + with pytest.raises(distutils.errors.DistutilsError): + cmd.cant_write_to_target() + class TestPTHFileWriter: def test_add_from_cwd_site_sets_dirty(self): @@ -144,77 +154,64 @@ def setup_context(tmpdir): @pytest.mark.usefixtures("setup_context") class TestUserInstallTest: - @mock.patch('setuptools.command.easy_install.__file__', None) - def test_user_install_implied(self): - easy_install_pkg.__file__ = site.USER_SITE - site.ENABLE_USER_SITE = True # disabled sometimes - #XXX: replace with something meaningfull + # simulate setuptools installed in user site packages + @mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE) + @mock.patch('site.ENABLE_USER_SITE', True) + def test_user_install_not_implied_user_site_enabled(self): + self.assert_not_user_site() + + @mock.patch('site.ENABLE_USER_SITE', False) + def test_user_install_not_implied_user_site_disabled(self): + self.assert_not_user_site() + + @staticmethod + def assert_not_user_site(): + # create a finalized easy_install command dist = Distribution() dist.script_name = 'setup.py' - cmd = easy_install(dist) + cmd = ei.easy_install(dist) cmd.args = ['py'] cmd.ensure_finalized() - assert cmd.user, 'user should be implied' + assert not cmd.user, 'user should not be implied' def test_multiproc_atexit(self): - try: - __import__('multiprocessing') - except ImportError: - # skip the test if multiprocessing is not available - return + pytest.importorskip('multiprocessing') log = logging.getLogger('test_easy_install') logging.basicConfig(level=logging.INFO, stream=sys.stderr) log.info('this should not break') - def test_user_install_not_implied_without_usersite_enabled(self): - site.ENABLE_USER_SITE = False # usually enabled - #XXX: replace with something meaningfull - dist = Distribution() - dist.script_name = 'setup.py' - cmd = easy_install(dist) - cmd.args = ['py'] - cmd.initialize_options() - assert not cmd.user, 'NOT user should be implied' - - def test_local_index(self): - # make sure the local index is used - # when easy_install looks for installed - # packages - new_location = tempfile.mkdtemp() - target = tempfile.mkdtemp() - egg_file = os.path.join(new_location, 'foo-1.0.egg-info') - with open(egg_file, 'w') as f: + @pytest.fixture() + def foo_package(self, tmpdir): + egg_file = tmpdir / 'foo-1.0.egg-info' + with egg_file.open('w') as f: f.write('Name: foo\n') + return str(tmpdir) - sys.path.append(target) - old_ppath = os.environ.get('PYTHONPATH') - os.environ['PYTHONPATH'] = os.path.pathsep.join(sys.path) - try: - dist = Distribution() - dist.script_name = 'setup.py' - cmd = easy_install(dist) - cmd.install_dir = target - cmd.args = ['foo'] - cmd.ensure_finalized() - cmd.local_index.scan([new_location]) - res = cmd.easy_install('foo') - actual = os.path.normcase(os.path.realpath(res.location)) - expected = os.path.normcase(os.path.realpath(new_location)) - assert actual == expected - finally: - sys.path.remove(target) - for basedir in [new_location, target, ]: - if not os.path.exists(basedir) or not os.path.isdir(basedir): - continue - try: - shutil.rmtree(basedir) - except: - pass - if old_ppath is not None: - os.environ['PYTHONPATH'] = old_ppath - else: - del os.environ['PYTHONPATH'] + @pytest.yield_fixture() + def install_target(self, tmpdir): + target = str(tmpdir) + with mock.patch('sys.path', sys.path + [target]): + python_path = os.path.pathsep.join(sys.path) + with mock.patch.dict(os.environ, PYTHONPATH=python_path): + yield target + + def test_local_index(self, foo_package, install_target): + """ + The local index must be used when easy_install locates installed + packages. + """ + dist = Distribution() + dist.script_name = 'setup.py' + cmd = ei.easy_install(dist) + cmd.install_dir = install_target + cmd.args = ['foo'] + cmd.ensure_finalized() + cmd.local_index.scan([foo_package]) + res = cmd.easy_install('foo') + actual = os.path.normcase(os.path.realpath(res.location)) + expected = os.path.normcase(os.path.realpath(foo_package)) + assert actual == expected @contextlib.contextmanager def user_install_setup_context(self, *args, **kwargs): @@ -302,7 +299,7 @@ class TestSetupRequires: '--install-dir', temp_install_dir, dist_file, ] - with contexts.argv(['easy_install']): + with sandbox.save_argv(['easy_install']): # attempt to install the dist. It should fail because # it doesn't exist. with pytest.raises(SystemExit): @@ -420,24 +417,25 @@ class TestScriptHeader: exe_with_spaces = r'C:\Program Files\Python33\python.exe' @pytest.mark.skipif( - sys.platform.startswith('java') and is_sh(sys.executable), + sys.platform.startswith('java') and ei.is_sh(sys.executable), reason="Test cannot run under java when executable is sh" ) def test_get_script_header(self): - expected = '#!%s\n' % nt_quote_arg(os.path.normpath(sys.executable)) - actual = ScriptWriter.get_script_header('#!/usr/local/bin/python') + expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable)) + actual = ei.ScriptWriter.get_script_header('#!/usr/local/bin/python') assert actual == expected - expected = '#!%s -x\n' % nt_quote_arg(os.path.normpath(sys.executable)) - actual = ScriptWriter.get_script_header('#!/usr/bin/python -x') + expected = '#!%s -x\n' % ei.nt_quote_arg(os.path.normpath + (sys.executable)) + actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python -x') assert actual == expected - actual = ScriptWriter.get_script_header('#!/usr/bin/python', + actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python', executable=self.non_ascii_exe) expected = '#!%s -x\n' % self.non_ascii_exe assert actual == expected - actual = ScriptWriter.get_script_header('#!/usr/bin/python', + actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python', executable='"'+self.exe_with_spaces+'"') expected = '#!"%s"\n' % self.exe_with_spaces assert actual == expected @@ -460,7 +458,7 @@ class TestScriptHeader: f.write(header) exe = str(exe) - header = ScriptWriter.get_script_header('#!/usr/local/bin/python', + header = ei.ScriptWriter.get_script_header('#!/usr/local/bin/python', executable=exe) assert header == '#!/usr/bin/env %s\n' % exe @@ -469,14 +467,14 @@ class TestScriptHeader: with contexts.quiet() as (stdout, stderr): # When options are included, generate a broken shebang line # with a warning emitted - candidate = ScriptWriter.get_script_header('#!/usr/bin/python -x', + candidate = ei.ScriptWriter.get_script_header('#!/usr/bin/python -x', executable=exe) assert candidate == '#!%s -x\n' % exe output = locals()[expect_out] assert 'Unable to adapt shebang line' in output.getvalue() with contexts.quiet() as (stdout, stderr): - candidate = ScriptWriter.get_script_header('#!/usr/bin/python', + candidate = ei.ScriptWriter.get_script_header('#!/usr/bin/python', executable=self.non_ascii_exe) assert candidate == '#!%s -x\n' % self.non_ascii_exe output = locals()[expect_out] @@ -489,20 +487,20 @@ class TestCommandSpec: Show how a custom CommandSpec could be used to specify a #! executable which takes parameters. """ - cmd = CommandSpec(['/usr/bin/env', 'python3']) + cmd = ei.CommandSpec(['/usr/bin/env', 'python3']) assert cmd.as_header() == '#!/usr/bin/env python3\n' def test_from_param_for_CommandSpec_is_passthrough(self): """ from_param should return an instance of a CommandSpec """ - cmd = CommandSpec(['python']) - cmd_new = CommandSpec.from_param(cmd) + cmd = ei.CommandSpec(['python']) + cmd_new = ei.CommandSpec.from_param(cmd) assert cmd is cmd_new def test_from_environment_with_spaces_in_executable(self): with mock.patch('sys.executable', TestScriptHeader.exe_with_spaces): - cmd = CommandSpec.from_environment() + cmd = ei.CommandSpec.from_environment() assert len(cmd) == 1 assert cmd.as_header().startswith('#!"') @@ -511,6 +509,26 @@ class TestCommandSpec: In order to support `executable = /usr/bin/env my-python`, make sure from_param invokes shlex on that input. """ - cmd = CommandSpec.from_param('/usr/bin/env my-python') + cmd = ei.CommandSpec.from_param('/usr/bin/env my-python') assert len(cmd) == 2 assert '"' not in cmd.as_header() + + def test_sys_executable(self): + """ + CommandSpec.from_string(sys.executable) should contain just that param. + """ + writer = ei.ScriptWriter.best() + cmd = writer.command_spec_class.from_string(sys.executable) + assert len(cmd) == 1 + assert cmd[0] == sys.executable + + +class TestWindowsScriptWriter: + def test_header(self): + hdr = ei.WindowsScriptWriter.get_script_header('') + assert hdr.startswith('#!') + assert hdr.endswith('\n') + hdr = hdr.lstrip('#!') + hdr = hdr.rstrip('\n') + # header should not start with an escaped quote + assert not hdr.startswith('\\"') diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 92a27080..90bb4313 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -12,6 +12,7 @@ import pytest from setuptools.command.easy_install import easy_install from setuptools.command import easy_install as easy_install_pkg from setuptools.dist import Distribution +from setuptools.compat import urlopen def setup_module(module): @@ -24,6 +25,11 @@ def setup_module(module): except ImportError: pass + try: + urlopen('https://pypi.python.org/pypi') + except Exception as exc: + pytest.skip(reason=str(exc)) + @pytest.fixture def install_context(request, tmpdir, monkeypatch): diff --git a/setuptools/tests/test_msvc9compiler.py b/setuptools/tests/test_msvc9compiler.py index a0820fff..09e0460c 100644 --- a/setuptools/tests/test_msvc9compiler.py +++ b/setuptools/tests/test_msvc9compiler.py @@ -7,7 +7,10 @@ import contextlib import distutils.errors import pytest -import mock +try: + from unittest import mock +except ImportError: + import mock from . import contexts @@ -110,7 +113,8 @@ class TestModulePatch: Ensure user's settings are preferred. """ result = distutils.msvc9compiler.find_vcvarsall(9.0) - assert user_preferred_setting == result + expected = os.path.join(user_preferred_setting, 'vcvarsall.bat') + assert expected == result @pytest.yield_fixture def local_machine_setting(self): @@ -131,13 +135,14 @@ class TestModulePatch: Ensure machine setting is honored if user settings are not present. """ result = distutils.msvc9compiler.find_vcvarsall(9.0) - assert local_machine_setting == result + expected = os.path.join(local_machine_setting, 'vcvarsall.bat') + assert expected == result @pytest.yield_fixture def x64_preferred_setting(self): """ Set up environment with 64-bit and 32-bit system settings configured - and yield the 64-bit location. + and yield the canonical location. """ with self.mock_install_dir() as x32_dir: with self.mock_install_dir() as x64_dir: @@ -150,14 +155,15 @@ class TestModulePatch: }, ) with reg: - yield x64_dir + yield x32_dir def test_ensure_64_bit_preferred(self, x64_preferred_setting): """ Ensure 64-bit system key is preferred. """ result = distutils.msvc9compiler.find_vcvarsall(9.0) - assert x64_preferred_setting == result + expected = os.path.join(x64_preferred_setting, 'vcvarsall.bat') + assert expected == result @staticmethod @contextlib.contextmanager @@ -170,4 +176,4 @@ class TestModulePatch: vcvarsall = os.path.join(result, 'vcvarsall.bat') with open(vcvarsall, 'w'): pass - yield + yield result diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index cadc4812..6e1e9e1c 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -7,7 +7,7 @@ import pytest import pkg_resources import setuptools.sandbox -from setuptools.sandbox import DirectorySandbox, SandboxViolation +from setuptools.sandbox import DirectorySandbox class TestSandbox: @@ -54,3 +54,49 @@ class TestSandbox: with setup_py.open('wb') as stream: stream.write(b'"degenerate script"\r\n') setuptools.sandbox._execfile(str(setup_py), globals()) + + +class TestExceptionSaver: + def test_exception_trapped(self): + with setuptools.sandbox.ExceptionSaver(): + raise ValueError("details") + + def test_exception_resumed(self): + with setuptools.sandbox.ExceptionSaver() as saved_exc: + raise ValueError("details") + + with pytest.raises(ValueError) as caught: + saved_exc.resume() + + assert isinstance(caught.value, ValueError) + assert str(caught.value) == 'details' + + def test_exception_reconstructed(self): + orig_exc = ValueError("details") + + with setuptools.sandbox.ExceptionSaver() as saved_exc: + raise orig_exc + + with pytest.raises(ValueError) as caught: + saved_exc.resume() + + assert isinstance(caught.value, ValueError) + assert caught.value is not orig_exc + + def test_no_exception_passes_quietly(self): + with setuptools.sandbox.ExceptionSaver() as saved_exc: + pass + + saved_exc.resume() + + def test_unpickleable_exception(self): + class CantPickleThis(Exception): + "This Exception is unpickleable because it's not in globals" + + with setuptools.sandbox.ExceptionSaver() as saved_exc: + raise CantPickleThis('detail') + + with pytest.raises(setuptools.sandbox.UnpickleableException) as caught: + saved_exc.resume() + + assert str(caught.value) == "CantPickleThis('detail',)" diff --git a/setuptools/version.py b/setuptools/version.py index 1e71f92d..525a47ea 100644 --- a/setuptools/version.py +++ b/setuptools/version.py @@ -1 +1 @@ -__version__ = '11.3.2' +__version__ = '13.0.2' |
