aboutsummaryrefslogtreecommitdiffstats
path: root/setuptools/command
diff options
context:
space:
mode:
authorJason R. Coombs <jaraco@jaraco.com>2016-06-06 07:23:42 -0400
committerJason R. Coombs <jaraco@jaraco.com>2016-06-06 07:23:42 -0400
commitdf3905616933c90af95e99f705b800a2f5c1c921 (patch)
treef9def47225ce6dd5d88cd51bd7442f3c78aab877 /setuptools/command
parent48b63f309650af9e43368cf0d6792ea247ad8663 (diff)
parentf43c0f0651edfe1f52b0a178cfe2a5234a008af0 (diff)
downloadexternal_python_setuptools-df3905616933c90af95e99f705b800a2f5c1c921.tar.gz
external_python_setuptools-df3905616933c90af95e99f705b800a2f5c1c921.tar.bz2
external_python_setuptools-df3905616933c90af95e99f705b800a2f5c1c921.zip
Merge with master
Diffstat (limited to 'setuptools/command')
-rw-r--r--setuptools/command/build_ext.py48
-rw-r--r--setuptools/command/build_py.py93
-rwxr-xr-xsetuptools/command/easy_install.py171
-rwxr-xr-xsetuptools/command/egg_info.py23
-rwxr-xr-xsetuptools/command/rotate.py6
-rw-r--r--setuptools/command/test.py21
-rw-r--r--setuptools/command/upload.py25
-rw-r--r--setuptools/command/upload_docs.py88
8 files changed, 313 insertions, 162 deletions
diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py
index 92e4a189..1caf8c81 100644
--- a/setuptools/command/build_ext.py
+++ b/setuptools/command/build_ext.py
@@ -16,15 +16,32 @@ try:
except ImportError:
_build_ext = _du_build_ext
-try:
- # Python 2.7 or >=3.2
- from sysconfig import _CONFIG_VARS
-except ImportError:
- from distutils.sysconfig import get_config_var
+from distutils.sysconfig import get_config_var
+
+get_config_var("LDSHARED") # make sure _config_vars is initialized
+del get_config_var
+from distutils.sysconfig import _config_vars as _CONFIG_VARS
+
+
+def _customize_compiler_for_shlib(compiler):
+ if sys.platform == "darwin":
+ # building .dylib requires additional compiler flags on OSX; here we
+ # temporarily substitute the pyconfig.h variables so that distutils'
+ # 'customize_compiler' uses them before we build the shared libraries.
+ tmp = _CONFIG_VARS.copy()
+ try:
+ # XXX Help! I don't have any idea whether these are right...
+ _CONFIG_VARS['LDSHARED'] = (
+ "gcc -Wl,-x -dynamiclib -undefined dynamic_lookup")
+ _CONFIG_VARS['CCSHARED'] = " -dynamiclib"
+ _CONFIG_VARS['SO'] = ".dylib"
+ customize_compiler(compiler)
+ finally:
+ _CONFIG_VARS.clear()
+ _CONFIG_VARS.update(tmp)
+ else:
+ customize_compiler(compiler)
- get_config_var("LDSHARED") # make sure _config_vars is initialized
- del get_config_var
- from distutils.sysconfig import _config_vars as _CONFIG_VARS
have_rtld = False
use_stubs = False
@@ -124,20 +141,7 @@ class build_ext(_build_ext):
compiler = self.shlib_compiler = new_compiler(
compiler=self.compiler, dry_run=self.dry_run, force=self.force
)
- if sys.platform == "darwin":
- tmp = _CONFIG_VARS.copy()
- try:
- # XXX Help! I don't have any idea whether these are right...
- _CONFIG_VARS['LDSHARED'] = (
- "gcc -Wl,-x -dynamiclib -undefined dynamic_lookup")
- _CONFIG_VARS['CCSHARED'] = " -dynamiclib"
- _CONFIG_VARS['SO'] = ".dylib"
- customize_compiler(compiler)
- finally:
- _CONFIG_VARS.clear()
- _CONFIG_VARS.update(tmp)
- else:
- customize_compiler(compiler)
+ _customize_compiler_for_shlib(compiler)
if self.include_dirs is not None:
compiler.set_include_dirs(self.include_dirs)
diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py
index 8623c777..0bad8295 100644
--- a/setuptools/command/build_py.py
+++ b/setuptools/command/build_py.py
@@ -6,10 +6,10 @@ import fnmatch
import textwrap
import io
import distutils.errors
-import collections
import itertools
-from setuptools.extern.six.moves import map
+from setuptools.extern import six
+from setuptools.extern.six.moves import map, filter, filterfalse
try:
from setuptools.lib2to3_ex import Mixin2to3
@@ -67,6 +67,9 @@ class build_py(orig.build_py, Mixin2to3):
return orig.build_py.__getattr__(self, attr)
def build_module(self, module, module_file, package):
+ if six.PY2 and isinstance(package, six.string_types):
+ # avoid errors on Python 2 when unicode is passed (#190)
+ package = package.split('.')
outfile, copied = orig.build_py.build_module(self, module, module_file,
package)
if copied:
@@ -94,12 +97,19 @@ class build_py(orig.build_py, Mixin2to3):
def find_data_files(self, package, src_dir):
"""Return filenames for package's data files in 'src_dir'"""
- globs = (self.package_data.get('', [])
- + self.package_data.get(package, []))
- files = self.manifest_files.get(package, [])[:]
- for pattern in globs:
- # Each pattern has to be converted to a platform-specific path
- files.extend(glob(os.path.join(src_dir, convert_path(pattern))))
+ patterns = self._get_platform_patterns(
+ self.package_data,
+ package,
+ src_dir,
+ )
+ globs_expanded = map(glob, patterns)
+ # flatten the expanded globs into an iterable of matches
+ globs_matches = itertools.chain.from_iterable(globs_expanded)
+ glob_files = filter(os.path.isfile, globs_matches)
+ files = itertools.chain(
+ self.manifest_files.get(package, []),
+ glob_files,
+ )
return self.exclude_data_files(package, src_dir, files)
def build_package_data(self):
@@ -184,26 +194,63 @@ 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, [])
+ files = list(files)
+ patterns = self._get_platform_patterns(
+ self.exclude_package_data,
+ package,
+ src_dir,
)
- bad = set(
- item
- for pattern in globs
- for item in fnmatch.filter(
- files,
- os.path.join(src_dir, convert_path(pattern)),
- )
+ match_groups = (
+ fnmatch.filter(files, pattern)
+ for pattern in patterns
)
- seen = collections.defaultdict(itertools.count)
- return [
+ # flatten the groups of matches into an iterable of matches
+ matches = itertools.chain.from_iterable(match_groups)
+ bad = set(matches)
+ keepers = (
fn
for fn in files
if fn not in bad
- # ditch dupes
- and not next(seen[fn])
- ]
+ )
+ # ditch dupes
+ return list(_unique_everseen(keepers))
+
+ @staticmethod
+ def _get_platform_patterns(spec, package, src_dir):
+ """
+ yield platfrom-specific path patterns (suitable for glob
+ or fn_match) from a glob-based spec (such as
+ self.package_data or self.exclude_package_data)
+ matching package in src_dir.
+ """
+ raw_patterns = itertools.chain(
+ spec.get('', []),
+ spec.get(package, []),
+ )
+ return (
+ # Each pattern has to be converted to a platform-specific path
+ os.path.join(src_dir, convert_path(pattern))
+ for pattern in raw_patterns
+ )
+
+
+# from Python docs
+def _unique_everseen(iterable, key=None):
+ "List unique elements, preserving order. Remember all elements ever seen."
+ # unique_everseen('AAAABBBCCDAABBB') --> A B C D
+ # unique_everseen('ABBCcAD', str.lower) --> A B C D
+ seen = set()
+ seen_add = seen.add
+ if key is None:
+ for element in filterfalse(seen.__contains__, iterable):
+ seen_add(element)
+ yield element
+ else:
+ for element in iterable:
+ k = key(element)
+ if k not in seen:
+ seen_add(k)
+ yield element
def assert_relative(path):
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index ea5cb028..ccc66cf7 100755
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -15,8 +15,10 @@ __ https://pythonhosted.org/setuptools/easy_install.html
from glob import glob
from distutils.util import get_platform
from distutils.util import convert_path, subst_vars
-from distutils.errors import DistutilsArgError, DistutilsOptionError, \
- DistutilsError, DistutilsPlatformError
+from distutils.errors import (
+ DistutilsArgError, DistutilsOptionError,
+ DistutilsError, DistutilsPlatformError,
+)
from distutils.command.install import INSTALL_SCHEMES, SCHEME_KEYS
from distutils import log, dir_util
from distutils.command.build_scripts import first_line_re
@@ -74,6 +76,12 @@ def is_64bit():
def samefile(p1, p2):
+ """
+ Determine if two paths reference the same file.
+
+ Augments os.path.samefile to work on Windows and
+ suppresses errors if the path doesn't exist.
+ """
both_exist = os.path.exists(p1) and os.path.exists(p2)
use_samefile = hasattr(os.path, 'samefile') and both_exist
if use_samefile:
@@ -105,6 +113,9 @@ else:
return False
+_one_liner = lambda text: textwrap.dedent(text).strip().replace('\n', '; ')
+
+
class easy_install(Command):
"""Manage a download/build/install process"""
description = "Find/get/install Python packages"
@@ -258,8 +269,10 @@ class easy_install(Command):
self.expand_basedirs()
self.expand_dirs()
- self._expand('install_dir', 'script_dir', 'build_directory',
- 'site_dirs')
+ self._expand(
+ 'install_dir', 'script_dir', 'build_directory',
+ 'site_dirs',
+ )
# If a non-default installation directory was specified, default the
# script directory to match it.
if self.script_dir is None:
@@ -379,9 +392,15 @@ class easy_install(Command):
def expand_dirs(self):
"""Calls `os.path.expanduser` on install dirs."""
- self._expand_attrs(['install_purelib', 'install_platlib',
- 'install_lib', 'install_headers',
- 'install_scripts', 'install_data', ])
+ dirs = [
+ 'install_purelib',
+ 'install_platlib',
+ 'install_lib',
+ 'install_headers',
+ 'install_scripts',
+ 'install_data',
+ ]
+ self._expand_attrs(dirs)
def run(self):
if self.verbose != self.distribution.verbose:
@@ -515,6 +534,12 @@ class easy_install(Command):
pth_file = self.pseudo_tempname() + ".pth"
ok_file = pth_file + '.ok'
ok_exists = os.path.exists(ok_file)
+ tmpl = _one_liner("""
+ import os
+ f = open({ok_file!r}, 'w')
+ f.write('OK')
+ f.close()
+ """) + '\n'
try:
if ok_exists:
os.unlink(ok_file)
@@ -526,16 +551,18 @@ class easy_install(Command):
self.cant_write_to_target()
else:
try:
- f.write("import os; f = open(%r, 'w'); f.write('OK'); "
- "f.close()\n" % (ok_file,))
+ f.write(tmpl.format(**locals()))
f.close()
f = None
executable = sys.executable
if os.name == 'nt':
dirname, basename = os.path.split(executable)
alt = os.path.join(dirname, 'pythonw.exe')
- if (basename.lower() == 'python.exe' and
- os.path.exists(alt)):
+ use_alt = (
+ basename.lower() == 'python.exe' and
+ os.path.exists(alt)
+ )
+ if use_alt:
# use pythonw.exe to avoid opening a console window
executable = alt
@@ -602,7 +629,6 @@ class easy_install(Command):
def easy_install(self, spec, deps=False):
tmpdir = tempfile.mkdtemp(prefix="easy_install-")
- download = None
if not self.editable:
self.install_site_py()
@@ -611,9 +637,8 @@ class easy_install(Command):
if URL_SCHEME(spec):
# It's a url, download it to tmpdir and process
self.not_editable(spec)
- download = self.package_index.download(spec, tmpdir)
- return self.install_item(None, download, tmpdir, deps,
- True)
+ dl = self.package_index.download(spec, tmpdir)
+ return self.install_item(None, dl, tmpdir, deps, True)
elif os.path.exists(spec):
# Existing file or directory, just process it directly
@@ -739,8 +764,9 @@ class easy_install(Command):
def maybe_move(self, spec, dist_filename, setup_base):
dst = os.path.join(self.build_directory, spec.key)
if os.path.exists(dst):
- msg = ("%r already exists in %s; build directory %s will not be "
- "kept")
+ msg = (
+ "%r already exists in %s; build directory %s will not be kept"
+ )
log.warn(msg, spec.key, self.build_directory, setup_base)
return setup_base
if os.path.isdir(dist_filename):
@@ -858,8 +884,10 @@ class easy_install(Command):
return Distribution.from_filename(egg_path, metadata=metadata)
def install_egg(self, egg_path, tmpdir):
- destination = os.path.join(self.install_dir,
- os.path.basename(egg_path))
+ destination = os.path.join(
+ self.install_dir,
+ os.path.basename(egg_path),
+ )
destination = os.path.abspath(destination)
if not self.dry_run:
ensure_directory(destination)
@@ -869,8 +897,11 @@ class easy_install(Command):
if os.path.isdir(destination) and not os.path.islink(destination):
dir_util.remove_tree(destination, dry_run=self.dry_run)
elif os.path.exists(destination):
- self.execute(os.unlink, (destination,), "Removing " +
- destination)
+ self.execute(
+ os.unlink,
+ (destination,),
+ "Removing " + destination,
+ )
try:
new_dist_is_zipped = False
if os.path.isdir(egg_path):
@@ -887,12 +918,18 @@ class easy_install(Command):
f, m = shutil.move, "Moving"
else:
f, m = shutil.copy2, "Copying"
- self.execute(f, (egg_path, destination),
- (m + " %s to %s") %
- (os.path.basename(egg_path),
- os.path.dirname(destination)))
- update_dist_caches(destination,
- fix_zipimporter_caches=new_dist_is_zipped)
+ self.execute(
+ f,
+ (egg_path, destination),
+ (m + " %s to %s") % (
+ os.path.basename(egg_path),
+ os.path.dirname(destination)
+ ),
+ )
+ update_dist_caches(
+ destination,
+ fix_zipimporter_caches=new_dist_is_zipped,
+ )
except:
update_dist_caches(destination, fix_zipimporter_caches=False)
raise
@@ -915,8 +952,8 @@ class easy_install(Command):
)
# Convert the .exe to an unpacked egg
- egg_path = dist.location = os.path.join(tmpdir, dist.egg_name() +
- '.egg')
+ egg_path = os.path.join(tmpdir, dist.egg_name() + '.egg')
+ dist.location = egg_path
egg_tmp = egg_path + '.tmp'
_egg_info = os.path.join(egg_tmp, 'EGG-INFO')
pkg_inf = os.path.join(_egg_info, 'PKG-INFO')
@@ -934,13 +971,13 @@ class easy_install(Command):
f.close()
script_dir = os.path.join(_egg_info, 'scripts')
# delete entry-point scripts to avoid duping
- self.delete_blockers(
- [os.path.join(script_dir, args[0]) for args in
- ScriptWriter.get_args(dist)]
- )
+ self.delete_blockers([
+ os.path.join(script_dir, args[0])
+ for args in ScriptWriter.get_args(dist)
+ ])
# Build .egg file from tmpdir
bdist_egg.make_zipfile(
- egg_path, egg_tmp, verbose=self.verbose, dry_run=self.dry_run
+ egg_path, egg_tmp, verbose=self.verbose, dry_run=self.dry_run,
)
# install the .egg
return self.install_egg(egg_path, tmpdir)
@@ -1128,7 +1165,7 @@ class easy_install(Command):
if dist.location in self.pth_file.paths:
log.info(
"%s is already the active version in easy-install.pth",
- dist
+ dist,
)
else:
log.info("Adding %s to easy-install.pth file", dist)
@@ -1189,7 +1226,7 @@ class easy_install(Command):
if self.optimize:
byte_compile(
to_compile, optimize=self.optimize, force=1,
- dry_run=self.dry_run
+ dry_run=self.dry_run,
)
finally:
log.set_verbosity(self.verbose) # restore original verbosity
@@ -1317,15 +1354,20 @@ def get_site_dirs():
if sys.platform in ('os2emx', 'riscos'):
sitedirs.append(os.path.join(prefix, "Lib", "site-packages"))
elif os.sep == '/':
- sitedirs.extend([os.path.join(prefix,
- "lib",
- "python" + sys.version[:3],
- "site-packages"),
- os.path.join(prefix, "lib", "site-python")])
+ sitedirs.extend([
+ os.path.join(
+ prefix,
+ "lib",
+ "python" + sys.version[:3],
+ "site-packages",
+ ),
+ os.path.join(prefix, "lib", "site-python"),
+ ])
else:
- sitedirs.extend(
- [prefix, os.path.join(prefix, "lib", "site-packages")]
- )
+ sitedirs.extend([
+ prefix,
+ os.path.join(prefix, "lib", "site-packages"),
+ ])
if sys.platform == 'darwin':
# for framework builds *only* we add the standard Apple
# locations. Currently only per-user, but /Library and
@@ -1333,12 +1375,14 @@ def get_site_dirs():
if 'Python.framework' in prefix:
home = os.environ.get('HOME')
if home:
- sitedirs.append(
- os.path.join(home,
- 'Library',
- 'Python',
- sys.version[:3],
- 'site-packages'))
+ home_sp = os.path.join(
+ home,
+ 'Library',
+ 'Python',
+ sys.version[:3],
+ 'site-packages',
+ )
+ sitedirs.append(home_sp)
lib_paths = get_path('purelib'), get_path('platlib')
for site_lib in lib_paths:
if site_lib not in sitedirs:
@@ -1347,6 +1391,11 @@ def get_site_dirs():
if site.ENABLE_USER_SITE:
sitedirs.append(site.USER_SITE)
+ try:
+ sitedirs.extend(site.getsitepackages())
+ except AttributeError:
+ pass
+
sitedirs = list(map(normalize_path, sitedirs))
return sitedirs
@@ -1414,8 +1463,8 @@ def extract_wininst_cfg(dist_filename):
return None # not a valid tag
f.seek(prepended - (12 + cfglen))
- cfg = configparser.RawConfigParser(
- {'version': '', 'target_version': ''})
+ init = {'version': '', 'target_version': ''}
+ cfg = configparser.RawConfigParser(init)
try:
part = f.read(cfglen)
# Read up to the first null byte.
@@ -1438,7 +1487,8 @@ def get_exe_prefixes(exe_filename):
"""Get exe->egg path translations for a given .exe file"""
prefixes = [
- ('PURELIB/', ''), ('PLATLIB/pywin32_system32', ''),
+ ('PURELIB/', ''),
+ ('PLATLIB/pywin32_system32', ''),
('PLATLIB/', ''),
('SCRIPTS/', 'EGG-INFO/scripts/'),
('DATA/lib/site-packages', ''),
@@ -1598,12 +1648,11 @@ class RewritePthDistributions(PthDistributions):
yield line
yield cls.postlude
- _inline = lambda text: textwrap.dedent(text).strip().replace('\n', '; ')
- prelude = _inline("""
+ prelude = _one_liner("""
import sys
sys.__plen = len(sys.path)
""")
- postlude = _inline("""
+ postlude = _one_liner("""
import sys
new = sys.path[sys.__plen:]
del sys.path[sys.__plen:]
@@ -2071,8 +2120,11 @@ class WindowsScriptWriter(ScriptWriter):
"For Windows, add a .py extension"
ext = dict(console='.pya', gui='.pyw')[type_]
if ext not in os.environ['PATHEXT'].lower().split(';'):
- warnings.warn("%s not listed in PATHEXT; scripts will not be "
- "recognized as executables." % ext, UserWarning)
+ msg = (
+ "{ext} not listed in PATHEXT; scripts will not be "
+ "recognized as executables."
+ ).format(**locals())
+ warnings.warn(msg, UserWarning)
old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
old.remove(ext)
header = cls._adjust_header(type_, header)
@@ -2238,7 +2290,8 @@ def main(argv=None, **kw):
setup(
script_args=['-q', 'easy_install', '-v'] + argv,
script_name=sys.argv[0] or 'easy_install',
- distclass=DistributionWithoutHelpCommands, **kw
+ distclass=DistributionWithoutHelpCommands,
+ **kw
)
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index d1bd9b04..8e1502a5 100755
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -13,6 +13,7 @@ import sys
import io
import warnings
import time
+import collections
from setuptools.extern import six
from setuptools.extern.six.moves import map
@@ -66,14 +67,20 @@ class egg_info(Command):
self.vtags = None
def save_version_info(self, filename):
- values = dict(
- egg_info=dict(
- tag_svn_revision=0,
- tag_date=0,
- tag_build=self.tags(),
- )
- )
- edit_config(filename, values)
+ """
+ Materialize the values of svn_revision and date into the
+ build tag. Install these keys in a deterministic order
+ to avoid arbitrary reordering on subsequent builds.
+ """
+ # python 2.6 compatibility
+ odict = getattr(collections, 'OrderedDict', dict)
+ egg_info = odict()
+ # follow the order these keys would have been added
+ # when PYTHONHASHSEED=0
+ egg_info['tag_build'] = self.tags()
+ egg_info['tag_date'] = 0
+ egg_info['tag_svn_revision'] = 0
+ edit_config(filename, dict(egg_info=egg_info))
def finalize_options(self):
self.egg_name = safe_name(self.distribution.get_name())
diff --git a/setuptools/command/rotate.py b/setuptools/command/rotate.py
index 804f962a..b89353f5 100755
--- a/setuptools/command/rotate.py
+++ b/setuptools/command/rotate.py
@@ -2,6 +2,7 @@ from distutils.util import convert_path
from distutils import log
from distutils.errors import DistutilsOptionError
import os
+import shutil
from setuptools.extern import six
@@ -59,4 +60,7 @@ class rotate(Command):
for (t, f) in files:
log.info("Deleting %s", f)
if not self.dry_run:
- os.unlink(f)
+ if os.path.isdir(f):
+ shutil.rmtree(f)
+ else:
+ os.unlink(f)
diff --git a/setuptools/command/test.py b/setuptools/command/test.py
index 371e913b..39746a02 100644
--- a/setuptools/command/test.py
+++ b/setuptools/command/test.py
@@ -1,6 +1,7 @@
+import sys
+import contextlib
from distutils.errors import DistutilsOptionError
from unittest import TestLoader
-import sys
from setuptools.extern import six
from setuptools.extern.six.moves import map
@@ -102,6 +103,14 @@ class test(Command):
yield self.test_suite
def with_project_on_sys_path(self, func):
+ """
+ Backward compatibility for project_on_sys_path context.
+ """
+ with self.project_on_sys_path():
+ func()
+
+ @contextlib.contextmanager
+ def project_on_sys_path(self):
with_2to3 = six.PY3 and getattr(self.distribution, 'use_2to3', False)
if with_2to3:
@@ -137,7 +146,7 @@ class test(Command):
working_set.__init__()
add_activation_listener(lambda dist: dist.activate())
require('%s==%s' % (ei_cmd.egg_name, ei_cmd.egg_version))
- func()
+ yield
finally:
sys.path[:] = old_path
sys.modules.clear()
@@ -154,9 +163,11 @@ class test(Command):
cmd = ' '.join(self._argv)
if self.dry_run:
self.announce('skipping "%s" (dry run)' % cmd)
- else:
- self.announce('running "%s"' % cmd)
- self.with_project_on_sys_path(self.run_tests)
+ return
+
+ self.announce('running "%s"' % cmd)
+ with self.project_on_sys_path():
+ self.run_tests()
def run_tests(self):
# Purge modules under test from sys.modules. The test loader will
diff --git a/setuptools/command/upload.py b/setuptools/command/upload.py
index 08c20ba8..484baa5a 100644
--- a/setuptools/command/upload.py
+++ b/setuptools/command/upload.py
@@ -1,15 +1,22 @@
+import getpass
from distutils.command import upload as orig
class upload(orig.upload):
"""
- Override default upload behavior to look up password
- in the keyring if available.
+ Override default upload behavior to obtain password
+ in a variety of different ways.
"""
def finalize_options(self):
orig.upload.finalize_options(self)
- self.password or self._load_password_from_keyring()
+ # Attempt to obtain password. Short circuit evaluation at the first
+ # sign of success.
+ self.password = (
+ self.password or
+ self._load_password_from_keyring() or
+ self._prompt_for_password()
+ )
def _load_password_from_keyring(self):
"""
@@ -17,7 +24,15 @@ class upload(orig.upload):
"""
try:
keyring = __import__('keyring')
- self.password = keyring.get_password(self.repository,
- self.username)
+ return keyring.get_password(self.repository, self.username)
except Exception:
pass
+
+ def _prompt_for_password(self):
+ """
+ Prompt for a password on the tty. Suppress Exceptions.
+ """
+ try:
+ return getpass.getpass()
+ except (Exception, KeyboardInterrupt):
+ pass
diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py
index f887b47e..01b49046 100644
--- a/setuptools/command/upload_docs.py
+++ b/setuptools/command/upload_docs.py
@@ -13,6 +13,8 @@ import socket
import zipfile
import tempfile
import shutil
+import itertools
+import functools
from setuptools.extern import six
from setuptools.extern.six.moves import http_client, urllib
@@ -21,15 +23,9 @@ from pkg_resources import iter_entry_points
from .upload import upload
-errors = 'surrogateescape' if six.PY3 else 'strict'
-
-
-# This is not just a replacement for byte literals
-# but works as a general purpose encoder
-def b(s, encoding='utf-8'):
- if isinstance(s, six.text_type):
- return s.encode(encoding, errors)
- return s
+def _encode(s):
+ errors = 'surrogateescape' if six.PY3 else 'strict'
+ return s.encode('utf-8', errors)
class upload_docs(upload):
@@ -101,10 +97,48 @@ class upload_docs(upload):
finally:
shutil.rmtree(tmp_dir)
+ @staticmethod
+ def _build_part(item, sep_boundary):
+ key, values = item
+ title = '\nContent-Disposition: form-data; name="%s"' % key
+ # handle multiple entries for the same name
+ if not isinstance(values, list):
+ values = [values]
+ for value in values:
+ if type(value) is tuple:
+ title += '; filename="%s"' % value[0]
+ value = value[1]
+ else:
+ value = _encode(value)
+ yield sep_boundary
+ yield _encode(title)
+ yield b"\n\n"
+ yield value
+ if value and value[-1:] == b'\r':
+ yield b'\n' # write an extra newline (lurve Macs)
+
+ @classmethod
+ def _build_multipart(cls, data):
+ """
+ Build up the MIME payload for the POST data
+ """
+ boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
+ sep_boundary = b'\n--' + boundary
+ end_boundary = sep_boundary + b'--'
+ end_items = end_boundary, b"\n",
+ builder = functools.partial(
+ cls._build_part,
+ sep_boundary=sep_boundary,
+ )
+ part_groups = map(builder, data.items())
+ parts = itertools.chain.from_iterable(part_groups)
+ body_items = itertools.chain(parts, end_items)
+ content_type = 'multipart/form-data; boundary=%s' % boundary
+ return b''.join(body_items), content_type
+
def upload_file(self, filename):
- f = open(filename, 'rb')
- content = f.read()
- f.close()
+ with open(filename, 'rb') as f:
+ content = f.read()
meta = self.distribution.metadata
data = {
':action': 'doc_upload',
@@ -112,37 +146,13 @@ class upload_docs(upload):
'content': (os.path.basename(filename), content),
}
# set up the authentication
- credentials = b(self.username + ':' + self.password)
+ credentials = _encode(self.username + ':' + self.password)
credentials = standard_b64encode(credentials)
if six.PY3:
credentials = credentials.decode('ascii')
auth = "Basic " + credentials
- # Build up the MIME payload for the POST data
- boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
- sep_boundary = b('\n--') + b(boundary)
- end_boundary = sep_boundary + b('--')
- body = []
- for key, values in six.iteritems(data):
- title = '\nContent-Disposition: form-data; name="%s"' % key
- # handle multiple entries for the same name
- if not isinstance(values, list):
- values = [values]
- for value in values:
- if type(value) is tuple:
- title += '; filename="%s"' % value[0]
- value = value[1]
- else:
- value = b(value)
- body.append(sep_boundary)
- body.append(b(title))
- body.append(b("\n\n"))
- body.append(value)
- if value and value[-1:] == b('\r'):
- body.append(b('\n')) # write an extra newline (lurve Macs)
- body.append(end_boundary)
- body.append(b("\n"))
- body = b('').join(body)
+ body, ct = self._build_multipart(data)
self.announce("Submitting documentation to %s" % (self.repository),
log.INFO)
@@ -164,7 +174,7 @@ class upload_docs(upload):
try:
conn.connect()
conn.putrequest("POST", url)
- content_type = 'multipart/form-data; boundary=%s' % boundary
+ content_type = ct
conn.putheader('Content-type', content_type)
conn.putheader('Content-length', str(len(body)))
conn.putheader('Authorization', auth)