diff options
| -rw-r--r-- | .hgtags | 5 | ||||
| -rw-r--r-- | .travis.yml | 4 | ||||
| -rw-r--r-- | CHANGES.txt | 48 | ||||
| -rw-r--r-- | ez_setup.py | 32 | ||||
| -rw-r--r-- | pkg_resources/__init__.py | 6 | ||||
| -rw-r--r-- | release.py | 32 | ||||
| -rw-r--r-- | setuptools/command/build_py.py | 45 | ||||
| -rwxr-xr-x | setuptools/command/develop.py | 15 | ||||
| -rwxr-xr-x | setuptools/command/easy_install.py | 17 | ||||
| -rwxr-xr-x | setuptools/command/egg_info.py | 45 | ||||
| -rwxr-xr-x | setuptools/command/sdist.py | 6 | ||||
| -rwxr-xr-x | setuptools/command/setopt.py | 4 | ||||
| -rw-r--r-- | setuptools/compat.py | 8 | ||||
| -rw-r--r-- | setuptools/dist.py | 3 | ||||
| -rwxr-xr-x | setuptools/package_index.py | 65 | ||||
| -rwxr-xr-x | setuptools/sandbox.py | 7 | ||||
| -rw-r--r-- | setuptools/ssl_support.py | 6 | ||||
| -rw-r--r-- | setuptools/tests/test_easy_install.py | 11 | ||||
| -rw-r--r-- | setuptools/tests/test_packageindex.py | 21 | ||||
| -rw-r--r-- | setuptools/tests/test_sandbox.py | 39 | ||||
| -rw-r--r-- | setuptools/tests/test_sdist.py | 18 | ||||
| -rw-r--r-- | setuptools/tests/test_setuptools.py | 2 | ||||
| -rw-r--r-- | setuptools/version.py | 2 |
23 files changed, 292 insertions, 149 deletions
@@ -227,3 +227,8 @@ dfe190b09908f6b953209d13573063809de451b8 18.6 804f87045a901f1dc121cf9149143d654228dc13 18.6.1 67d07805606aead09349d5b91d7d26c68ddad2fc 18.7 3041e1fc409be90e885968b90faba405420fc161 18.7.1 +c811801ffa1de758cf01fbf6a86e4c04ff0c0935 18.8 +fbf06fa35f93a43f044b1645a7e4ff470edb462c 18.8.1 +cc41477ecf92f221c113736fac2830bf8079d40c 19.0 +834782ce49154e9744e499e00eb392c347f9e034 19.1 +0a2a3d89416e1642cf6f41d22dbc07b3d3c15a4d 19.1.1 diff --git a/.travis.yml b/.travis.yml index 0bfb11bb..54d9c395 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,4 +20,6 @@ script: - python bootstrap.py - python setup.py test --addopts='-rs' - - python ez_setup.py --version 18.6.1 + + # test the bootstrap script + - python ez_setup.py diff --git a/CHANGES.txt b/CHANGES.txt index 7f7ab9bd..48fc18a9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,54 @@ CHANGES ------ +19.1.1 +------ + +* Issue #476: Cast version to string (using default encoding) + to avoid creating Unicode types on Python 2 clients. +* Issue #477: In Powershell downloader, use explicit rendering + of strings, rather than rely on ``repr``, which can be + incorrect (especially on Python 2). + +---- +19.1 +---- + +* Issue #215: The bootstrap script ``ez_setup.py`` now + automatically detects + the latest version of setuptools (using PyPI JSON API) rather + than hard-coding a particular value. +* Issue #475: Fix incorrect usage in _translate_metadata2. + +---- +19.0 +---- + +* Issue #442: Use RawConfigParser for parsing .pypirc file. + Interpolated values are no longer honored in .pypirc files. + +------ +18.8.1 +------ + +* Issue #440: Prevent infinite recursion when a SandboxViolation + or other UnpickleableException occurs in a sandbox context + with setuptools hidden. Fixes regression introduced in Setuptools + 12.0. + +---- +18.8 +---- + +* Deprecated ``egg_info.get_pkg_info_revision``. +* Issue #471: Don't rely on repr for an HTML attribute value in + package_index. +* Issue #419: Avoid errors in FileMetadata when the metadata directory + is broken. +* Issue #472: Remove deprecated use of 'U' in mode parameter + when opening files. + +------ 18.7.1 ------ diff --git a/ez_setup.py b/ez_setup.py index 975b45c5..9715bdc7 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -16,7 +16,8 @@ import subprocess import platform import textwrap import contextlib -import warnings +import json +import codecs from distutils import log @@ -30,7 +31,8 @@ try: except ImportError: USER_SITE = None -DEFAULT_VERSION = "18.7.2" +LATEST = object() +DEFAULT_VERSION = LATEST DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" DEFAULT_SAVE_DIR = os.curdir @@ -140,6 +142,7 @@ def use_setuptools( Return None. Raise SystemExit if the requested version or later cannot be installed. """ + version = _resolve_version(version) to_dir = os.path.abspath(to_dir) # prior to importing, capture the module state for @@ -222,8 +225,8 @@ def download_file_powershell(url, target): ps_cmd = ( "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " "[System.Net.CredentialCache]::DefaultCredentials; " - "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" - % vars() + '(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")' + % locals() ) cmd = [ 'powershell', @@ -321,6 +324,7 @@ def download_setuptools( ``downloader_factory`` should be a function taking no arguments and returning a function for downloading a URL to a target. """ + version = _resolve_version(version) # making sure we use the absolute path to_dir = os.path.abspath(to_dir) zip_name = "setuptools-%s.zip" % version @@ -333,6 +337,26 @@ def download_setuptools( return os.path.realpath(saveto) +def _resolve_version(version): + """ + Resolve LATEST version + """ + if version is not LATEST: + return version + + resp = urlopen('https://pypi.python.org/pypi/setuptools/json') + with contextlib.closing(resp): + try: + charset = resp.info().get_content_charset() + except Exception: + # Python 2 compat; assume UTF-8 + charset = 'UTF-8' + reader = codecs.getreader(charset) + doc = json.load(reader(resp)) + + return str(doc['info']['version']) + + def _build_install_args(options): """ Build the arguments to 'python setup.py install' on the setuptools package. diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 3cd67fa0..b55e4127 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -1537,7 +1537,7 @@ class MarkerEvaluation(object): """ return dict( (key.replace('.', '_'), value) - for key, value in env + for key, value in env.items() ) @classmethod @@ -2017,11 +2017,11 @@ class FileMetadata(EmptyProvider): self.path = path def has_metadata(self, name): - return name=='PKG-INFO' + return name=='PKG-INFO' and os.path.isfile(self.path) def get_metadata(self, name): if name=='PKG-INFO': - with io.open(self.path, 'rU', encoding='utf-8') as f: + with io.open(self.path, encoding='utf-8') as f: metadata = f.read() return metadata raise KeyError("No metadata except PKG-INFO is available") @@ -4,24 +4,13 @@ install jaraco.packaging and run 'python -m jaraco.packaging.release' """ import os -import subprocess import pkg_resources pkg_resources.require('jaraco.packaging>=2.0') pkg_resources.require('wheel') - -def before_upload(): - BootstrapBookmark.add() - - -def after_push(): - BootstrapBookmark.push() - -files_with_versions = ( - 'ez_setup.py', 'setuptools/version.py', -) +files_with_versions = 'setuptools/version.py', # bdist_wheel must be included or pip will break dist_commands = 'sdist', 'bdist_wheel' @@ -29,22 +18,3 @@ dist_commands = 'sdist', 'bdist_wheel' test_info = "Travis-CI tests: http://travis-ci.org/#!/jaraco/setuptools" os.environ["SETUPTOOLS_INSTALL_WINDOWS_SPECIFIC_FILES"] = "1" - -class BootstrapBookmark: - name = 'bootstrap' - - @classmethod - def add(cls): - cmd = ['hg', 'bookmark', '-i', cls.name, '-f'] - subprocess.Popen(cmd) - - @classmethod - def push(cls): - """ - Push the bootstrap bookmark - """ - push_command = ['hg', 'push', '-B', cls.name] - # don't use check_call here because mercurial will return a non-zero - # code even if it succeeds at pushing the bookmark (because there are - # no changesets to be pushed). !dm mercurial - subprocess.call(push_command) diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py index a873d54b..8a50f032 100644 --- a/setuptools/command/build_py.py +++ b/setuptools/command/build_py.py @@ -2,9 +2,13 @@ from glob import glob from distutils.util import convert_path import distutils.command.build_py as orig import os -import sys import fnmatch import textwrap +import io +import distutils.errors +import collections +import itertools + try: from setuptools.lib2to3_ex import Mixin2to3 @@ -157,17 +161,15 @@ class build_py(orig.build_py, Mixin2to3): else: return init_py - f = open(init_py, 'rbU') - if 'declare_namespace'.encode() not in f.read(): - from distutils.errors import DistutilsError - - raise DistutilsError( + with io.open(init_py, 'rb') as f: + contents = f.read() + if b'declare_namespace' not in contents: + raise distutils.errors.DistutilsError( "Namespace package problem: %s is a namespace package, but " "its\n__init__.py does not call declare_namespace()! Please " 'fix it.\n(See the setuptools manual under ' '"Namespace Packages" for details.)\n"' % (package,) ) - f.close() return init_py def initialize_options(self): @@ -182,20 +184,25 @@ class build_py(orig.build_py, Mixin2to3): def exclude_data_files(self, package, src_dir, files): """Filter filenames for package's data files in 'src_dir'""" - globs = (self.exclude_package_data.get('', []) - + self.exclude_package_data.get(package, [])) - bad = [] - for pattern in globs: - bad.extend( - fnmatch.filter( - files, os.path.join(src_dir, convert_path(pattern)) - ) + globs = ( + self.exclude_package_data.get('', []) + + self.exclude_package_data.get(package, []) + ) + bad = set( + item + for pattern in globs + for item in fnmatch.filter( + files, + os.path.join(src_dir, convert_path(pattern)), ) - bad = dict.fromkeys(bad) - seen = {} + ) + seen = collections.defaultdict(itertools.count) return [ - f for f in files if f not in bad - and f not in seen and seen.setdefault(f, 1) # ditch dupes + fn + for fn in files + if fn not in bad + # ditch dupes + and not next(seen[fn]) ] diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 5ae25d71..07b66ccb 100755 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -3,6 +3,7 @@ from distutils import log from distutils.errors import DistutilsError, DistutilsOptionError import os import glob +import io from pkg_resources import Distribution, PathMetadata, normalize_path from setuptools.command.easy_install import easy_install @@ -53,8 +54,8 @@ class develop(easy_install): # pick up setup-dir .egg files only: no .egg-info self.package_index.scan(glob.glob('*.egg')) - self.egg_link = os.path.join(self.install_dir, ei.egg_name + - '.egg-link') + egg_link_fn = ei.egg_name + '.egg-link' + self.egg_link = os.path.join(self.install_dir, egg_link_fn) self.egg_base = ei.egg_base if self.egg_path is None: self.egg_path = os.path.abspath(ei.egg_base) @@ -124,9 +125,8 @@ class develop(easy_install): # create an .egg-link in the installation dir, pointing to our egg log.info("Creating %s (link to %s)", self.egg_link, self.egg_base) if not self.dry_run: - f = open(self.egg_link, "w") - f.write(self.egg_path + "\n" + self.setup_path) - f.close() + with open(self.egg_link, "w") as f: + f.write(self.egg_path + "\n" + self.setup_path) # postprocess the installed distro, fixing up .pth, installing scripts, # and handling requirements self.process_distribution(None, self.dist, not self.no_deps) @@ -163,9 +163,8 @@ class develop(easy_install): for script_name in self.distribution.scripts or []: script_path = os.path.abspath(convert_path(script_name)) script_name = os.path.basename(script_path) - f = open(script_path, 'rU') - script_text = f.read() - f.close() + with io.open(script_path) as strm: + script_text = strm.read() self.install_script(dist, script_name, script_text, script_path) def install_wrapper_scripts(self, dist): diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 9e9c5e54..9ca3b515 100755 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -772,8 +772,8 @@ class easy_install(Command): is_script = is_python_script(script_text, script_name) if is_script: - script_text = (ScriptWriter.get_header(script_text) + - self._load_template(dev_path) % locals()) + body = self._load_template(dev_path) % locals() + script_text = ScriptWriter.get_header(script_text) + body self.write_script(script_name, _to_ascii(script_text), 'b') @staticmethod @@ -805,9 +805,8 @@ class easy_install(Command): ensure_directory(target) if os.path.exists(target): os.unlink(target) - f = open(target, "w" + mode) - f.write(contents) - f.close() + with open(target, "w" + mode) as f: + f.write(contents) chmod(target, 0o777 - mask) def install_eggs(self, spec, dist_filename, tmpdir): @@ -1403,7 +1402,7 @@ def expand_paths(inputs): def extract_wininst_cfg(dist_filename): """Extract configuration data from a bdist_wininst .exe - Returns a ConfigParser.RawConfigParser, or None + Returns a configparser.RawConfigParser, or None """ f = open(dist_filename, 'rb') try: @@ -1416,7 +1415,7 @@ def extract_wininst_cfg(dist_filename): return None f.seek(prepended - 12) - from setuptools.compat import StringIO, ConfigParser + from setuptools.compat import StringIO, configparser import struct tag, cfglen, bmlen = struct.unpack("<iii", f.read(12)) @@ -1424,7 +1423,7 @@ def extract_wininst_cfg(dist_filename): return None # not a valid tag f.seek(prepended - (12 + cfglen)) - cfg = ConfigParser.RawConfigParser( + cfg = configparser.RawConfigParser( {'version': '', 'target_version': ''}) try: part = f.read(cfglen) @@ -1434,7 +1433,7 @@ def extract_wininst_cfg(dist_filename): # be text, so decode it. config = config.decode(sys.getfilesystemencoding()) cfg.readfp(StringIO(config)) - except ConfigParser.Error: + except configparser.Error: return None if not cfg.has_section('metadata') or not cfg.has_section('Setup'): return None diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 50f3d5c0..1301bd84 100755 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -10,16 +10,17 @@ import distutils.filelist import os import re import sys +import io +import warnings +import time -try: - from setuptools_svn import svn_utils -except ImportError: - pass +from setuptools.compat import basestring, PY3, StringIO from setuptools import Command from setuptools.command.sdist import sdist -from setuptools.compat import basestring, PY3, StringIO from setuptools.command.sdist import walk_revctrl +from setuptools.command.setopt import edit_config +from setuptools.command import bdist_egg from pkg_resources import ( parse_requirements, safe_name, parse_version, safe_version, yield_lines, EntryPoint, iter_entry_points, to_filename) @@ -27,6 +28,12 @@ import setuptools.unicode_utils as unicode_utils from pkg_resources import packaging +try: + from setuptools_svn import svn_utils +except ImportError: + pass + + class egg_info(Command): description = "create a distribution's .egg-info directory" @@ -58,8 +65,6 @@ class egg_info(Command): self.vtags = None def save_version_info(self, filename): - from setuptools.command.setopt import edit_config - values = dict( egg_info=dict( tag_svn_revision=0, @@ -184,12 +189,8 @@ class egg_info(Command): if self.tag_build: version += self.tag_build if self.tag_svn_revision: - rev = self.get_svn_revision() - if rev: # is 0 if it's not an svn working copy - version += '-r%s' % rev + version += '-r%s' % self.get_svn_revision() if self.tag_date: - import time - version += time.strftime("-%Y%m%d") return version @@ -390,7 +391,6 @@ def write_pkg_info(cmd, basename, filename): metadata.name, metadata.version = oldname, oldver safe = getattr(cmd.distribution, 'zip_safe', None) - from setuptools.command import bdist_egg bdist_egg.write_safety_flag(cmd.egg_info, safe) @@ -467,14 +467,15 @@ def write_entries(cmd, basename, filename): def get_pkg_info_revision(): - # See if we can get a -r### off of PKG-INFO, in case this is an sdist of - # a subversion revision - # + """ + Get a -r### off of PKG-INFO Version in case this is an sdist of + a subversion revision. + """ + warnings.warn("get_pkg_info_revision is deprecated.", DeprecationWarning) if os.path.exists('PKG-INFO'): - f = open('PKG-INFO', 'rU') - for line in f: - match = re.match(r"Version:.*-r(\d+)\s*$", line) - if match: - return int(match.group(1)) - f.close() + with io.open('PKG-INFO') as f: + for line in f: + match = re.match(r"Version:.*-r(\d+)\s*$", line) + if match: + return int(match.group(1)) return 0 diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 851a1775..71196512 100755 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -3,6 +3,7 @@ from distutils import log import distutils.command.sdist as orig import os import sys +import io from setuptools.compat import PY3 from setuptools.utils import cs_path_exists @@ -166,11 +167,8 @@ class sdist(orig.sdist): if not os.path.isfile(self.manifest): return False - fp = open(self.manifest, 'rbU') - try: + with io.open(self.manifest, 'rb') as fp: first_line = fp.readline() - finally: - fp.close() return (first_line != '# file GENERATED by distutils, do NOT edit\n'.encode()) diff --git a/setuptools/command/setopt.py b/setuptools/command/setopt.py index a04d6032..74c7cad8 100755 --- a/setuptools/command/setopt.py +++ b/setuptools/command/setopt.py @@ -37,10 +37,10 @@ def edit_config(filename, settings, dry_run=False): while a dictionary lists settings to be changed or deleted in that section. A setting of ``None`` means to delete that setting. """ - from setuptools.compat import ConfigParser + from setuptools.compat import configparser log.debug("Reading configuration from %s", filename) - opts = ConfigParser.RawConfigParser() + opts = configparser.RawConfigParser() opts.read([filename]) for section, options in settings.items(): if options is None: diff --git a/setuptools/compat.py b/setuptools/compat.py index 73e6e4aa..f0175a5d 100644 --- a/setuptools/compat.py +++ b/setuptools/compat.py @@ -7,7 +7,7 @@ PY2 = not PY3 if PY2: basestring = basestring import __builtin__ as builtins - import ConfigParser + import ConfigParser as configparser from StringIO import StringIO BytesIO = StringIO func_code = lambda o: o.func_code @@ -29,6 +29,8 @@ if PY2: from urllib2 import urlopen, HTTPError, URLError, unquote, splituser from urlparse import urlparse, urlunparse, urljoin, urlsplit, urlunsplit filterfalse = itertools.ifilterfalse + filter = itertools.ifilter + map = itertools.imap exec("""def reraise(tp, value, tb=None): raise tp, value, tb""") @@ -36,7 +38,7 @@ if PY2: if PY3: basestring = str import builtins - import configparser as ConfigParser + import configparser from io import StringIO, BytesIO func_code = lambda o: o.__code__ func_globals = lambda o: o.__globals__ @@ -59,6 +61,8 @@ if PY3: urlunsplit, splittag, ) filterfalse = itertools.filterfalse + filter = filter + map = map def reraise(tp, value, tb=None): if value.__traceback__ is not tb: diff --git a/setuptools/dist.py b/setuptools/dist.py index c5f04b33..c1421b0f 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -267,8 +267,7 @@ class Distribution(_Distribution): if attrs and 'setup_requires' in attrs: self.fetch_build_eggs(attrs['setup_requires']) for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): - if not hasattr(self,ep.name): - setattr(self,ep.name,None) + vars(self).setdefault(ep.name, None) _Distribution.__init__(self,attrs) if isinstance(self.metadata.version, numbers.Number): # Some people apparently take "version number" too literally :) diff --git a/setuptools/package_index.py b/setuptools/package_index.py index cabf1039..2c565e88 100755 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -6,6 +6,7 @@ import shutil import socket import base64 import hashlib +import itertools from functools import wraps from pkg_resources import ( @@ -16,11 +17,11 @@ from pkg_resources import ( from setuptools import ssl_support from distutils import log from distutils.errors import DistutilsError -from setuptools.compat import (urllib2, httplib, StringIO, HTTPError, - urlparse, urlunparse, unquote, splituser, - url2pathname, name2codepoint, - unichr, urljoin, urlsplit, urlunsplit, - ConfigParser) +from setuptools.compat import ( + urllib2, httplib, StringIO, HTTPError, urlparse, urlunparse, unquote, + splituser, url2pathname, name2codepoint, unichr, urljoin, urlsplit, + urlunsplit, configparser, filter, map, +) from setuptools.compat import filterfalse from fnmatch import translate from setuptools.py26compat import strip_fragment @@ -352,20 +353,30 @@ class PackageIndex(Environment): self.warn(msg, url) def scan_egg_links(self, search_path): - for item in search_path: - if os.path.isdir(item): - for entry in os.listdir(item): - if entry.endswith('.egg-link'): - self.scan_egg_link(item, entry) + dirs = filter(os.path.isdir, search_path) + egg_links = ( + (path, entry) + for path in dirs + for entry in os.listdir(path) + if entry.endswith('.egg-link') + ) + list(itertools.starmap(self.scan_egg_link, egg_links)) def scan_egg_link(self, path, entry): - lines = [_f for _f in map(str.strip, - open(os.path.join(path, entry))) if _f] - if len(lines)==2: - for dist in find_distributions(os.path.join(path, lines[0])): - dist.location = os.path.join(path, *lines) - dist.precedence = SOURCE_DIST - self.add(dist) + with open(os.path.join(path, entry)) as raw_lines: + # filter non-empty lines + lines = list(filter(None, map(str.strip, raw_lines))) + + if len(lines) != 2: + # format is not recognized; punt + return + + egg_path, setup_path = lines + + for dist in find_distributions(os.path.join(path, egg_path)): + dist.location = os.path.join(path, *lines) + dist.precedence = SOURCE_DIST + self.add(dist) def process_index(self,url,page): """Process the contents of a PyPI page""" @@ -934,14 +945,14 @@ class Credential(object): def __str__(self): return '%(username)s:%(password)s' % vars(self) -class PyPIConfig(ConfigParser.ConfigParser): +class PyPIConfig(configparser.RawConfigParser): def __init__(self): """ Load from ~/.pypirc """ defaults = dict.fromkeys(['username', 'password', 'repository'], '') - ConfigParser.ConfigParser.__init__(self, defaults) + configparser.RawConfigParser.__init__(self, defaults) rc = os.path.join(os.path.expanduser('~'), '.pypirc') if os.path.exists(rc): @@ -1031,16 +1042,18 @@ def local_open(url): elif path.endswith('/') and os.path.isdir(filename): files = [] for f in os.listdir(filename): - if f=='index.html': - with open(os.path.join(filename,f),'r') as fp: + filepath = os.path.join(filename, f) + if f == 'index.html': + with open(filepath, 'r') as fp: body = fp.read() break - elif os.path.isdir(os.path.join(filename,f)): - f+='/' - files.append("<a href=%r>%s</a>" % (f,f)) + elif os.path.isdir(filepath): + f += '/' + files.append('<a href="{name}">{name}</a>'.format(name=f)) else: - body = ("<html><head><title>%s</title>" % url) + \ - "</head><body>%s</body></html>" % '\n'.join(files) + tmpl = ("<html><head><title>{url}</title>" + "</head><body>{files}</body></html>") + body = tmpl.format(url=url, files='\n'.join(files)) status, message = 200, "OK" else: status, message, body = 404, "Path not found", "Not found" diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 213cebff..85de85ff 100755 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -98,8 +98,8 @@ class UnpickleableException(Exception): """ An exception representing another Exception that could not be pickled. """ - @classmethod - def dump(cls, type, exc): + @staticmethod + def dump(type, exc): """ Always return a dumped (pickled) type and exc. If exc can't be pickled, wrap it in UnpickleableException first. @@ -107,6 +107,8 @@ class UnpickleableException(Exception): try: return pickle.dumps(type), pickle.dumps(exc) except Exception: + # get UnpickleableException inside the sandbox + from setuptools.sandbox import UnpickleableException as cls return cls.dump(cls, cls(repr(exc))) @@ -382,6 +384,7 @@ class DirectorySandbox(AbstractSandbox): AbstractSandbox.__init__(self) def _violation(self, operation, *args, **kw): + from setuptools.sandbox import SandboxViolation raise SandboxViolation(operation, args, kw) if _file: diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py index cc7db067..7394f4f5 100644 --- a/setuptools/ssl_support.py +++ b/setuptools/ssl_support.py @@ -223,6 +223,12 @@ def get_win_certfile(): self.addcerts(certs) atexit.register(self.close) + def close(self): + try: + super(MyCertFile, self).close() + except OSError: + pass + _wincerts = MyCertFile(stores=['CA', 'ROOT']) return _wincerts.name diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 00e16b63..f0330f17 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -446,10 +446,15 @@ class TestScriptHeader: exe = tmpdir / 'exe.py' with exe.open('w') as f: f.write(header) - exe = str(exe) + + exe = ei.nt_quote_arg(os.path.normpath(str(exe))) + + # Make sure Windows paths are quoted properly before they're sent + # through shlex.split by get_script_header + executable = '"%s"' % exe if os.path.splitdrive(exe)[0] else exe header = ei.ScriptWriter.get_script_header('#!/usr/local/bin/python', - executable=exe) + executable=executable) assert header == '#!/usr/bin/env %s\n' % exe expect_out = 'stdout' if sys.version_info < (2,7) else 'stderr' @@ -458,7 +463,7 @@ class TestScriptHeader: # When options are included, generate a broken shebang line # with a warning emitted candidate = ei.ScriptWriter.get_script_header('#!/usr/bin/python -x', - executable=exe) + executable=executable) assert candidate == '#!%s -x\n' % exe output = locals()[expect_out] assert 'Unable to adapt shebang line' in output.getvalue() diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index dcd90d6f..746860d5 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -1,7 +1,11 @@ +from __future__ import absolute_import + import sys +import os import distutils.errors from setuptools.compat import httplib, HTTPError, unicode, pathname2url +from .textwrap import DALS import pkg_resources import setuptools.package_index @@ -201,3 +205,20 @@ class TestContentCheckers: 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478') rep = checker.report(lambda x: x, 'My message about %s') assert rep == 'My message about md5' + + +class TestPyPIConfig: + def test_percent_in_password(self, tmpdir, monkeypatch): + monkeypatch.setitem(os.environ, 'HOME', str(tmpdir)) + pypirc = tmpdir / '.pypirc' + with pypirc.open('w') as strm: + strm.write(DALS(""" + [pypi] + repository=https://pypi.python.org + username=jaraco + password=pity% + """)) + cfg = setuptools.package_index.PyPIConfig() + cred = cfg.creds_by_repository['https://pypi.python.org'] + assert cred.username == 'jaraco' + assert cred.password == 'pity%' diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index 6e1e9e1c..fefd46f7 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -100,3 +100,42 @@ class TestExceptionSaver: saved_exc.resume() assert str(caught.value) == "CantPickleThis('detail',)" + + def test_unpickleable_exception_when_hiding_setuptools(self): + """ + As revealed in #440, an infinite recursion can occur if an unpickleable + exception while setuptools is hidden. Ensure this doesn't happen. + """ + class ExceptionUnderTest(Exception): + """ + An unpickleable exception (not in globals). + """ + + with pytest.raises(setuptools.sandbox.UnpickleableException) as caught: + with setuptools.sandbox.save_modules(): + setuptools.sandbox.hide_setuptools() + raise ExceptionUnderTest() + + msg, = caught.value.args + assert msg == 'ExceptionUnderTest()' + + def test_sandbox_violation_raised_hiding_setuptools(self, tmpdir): + """ + When in a sandbox with setuptools hidden, a SandboxViolation + should reflect a proper exception and not be wrapped in + an UnpickleableException. + """ + def write_file(): + "Trigger a SandboxViolation by writing outside the sandbox" + with open('/etc/foo', 'w'): + pass + sandbox = DirectorySandbox(str(tmpdir)) + with pytest.raises(setuptools.sandbox.SandboxViolation) as caught: + with setuptools.sandbox.save_modules(): + setuptools.sandbox.hide_setuptools() + sandbox.run(write_file) + + cmd, args, kwargs = caught.value.args + assert cmd == 'open' + assert args == ('/etc/foo', 'w') + assert kwargs == {} diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index ec3c8aa9..8ec9a4cb 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -7,6 +7,7 @@ import sys import tempfile import unicodedata import contextlib +import io import pytest @@ -81,6 +82,11 @@ def decompose(path): return path +def read_all_bytes(filename): + with io.open(filename, 'rb') as fp: + return fp.read() + + class TestSdistTest: def setup_method(self, method): @@ -172,9 +178,7 @@ class TestSdistTest: mm.filelist.append(filename) mm.write_manifest() - manifest = open(mm.manifest, 'rbU') - contents = manifest.read() - manifest.close() + contents = read_all_bytes(mm.manifest) # The manifest should be UTF-8 encoded u_contents = contents.decode('UTF-8') @@ -210,9 +214,7 @@ class TestSdistTest: # Re-write manifest mm.write_manifest() - manifest = open(mm.manifest, 'rbU') - contents = manifest.read() - manifest.close() + contents = read_all_bytes(mm.manifest) # The manifest should be UTF-8 encoded contents.decode('UTF-8') @@ -248,9 +250,7 @@ class TestSdistTest: # Re-write manifest mm.write_manifest() - manifest = open(mm.manifest, 'rbU') - contents = manifest.read() - manifest.close() + contents = read_all_bytes(mm.manifest) # The manifest should be UTF-8 encoded contents.decode('UTF-8') diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 8aca593a..e59800d2 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -23,7 +23,7 @@ def test_findall(example_source): def test_findall_curdir(example_source): with example_source.as_cwd(): found = list(setuptools.findall()) - expected = ['readme.txt', 'foo/bar.py'] + expected = ['readme.txt', os.path.join('foo', 'bar.py')] assert found == expected diff --git a/setuptools/version.py b/setuptools/version.py index 5e40c658..961ff96e 100644 --- a/setuptools/version.py +++ b/setuptools/version.py @@ -1 +1 @@ -__version__ = '18.7.2' +__version__ = '19.1.2' |
