diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2016-06-06 07:23:42 -0400 |
---|---|---|
committer | Jason R. Coombs <jaraco@jaraco.com> | 2016-06-06 07:23:42 -0400 |
commit | df3905616933c90af95e99f705b800a2f5c1c921 (patch) | |
tree | f9def47225ce6dd5d88cd51bd7442f3c78aab877 /setuptools/command | |
parent | 48b63f309650af9e43368cf0d6792ea247ad8663 (diff) | |
parent | f43c0f0651edfe1f52b0a178cfe2a5234a008af0 (diff) | |
download | external_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.py | 48 | ||||
-rw-r--r-- | setuptools/command/build_py.py | 93 | ||||
-rwxr-xr-x | setuptools/command/easy_install.py | 171 | ||||
-rwxr-xr-x | setuptools/command/egg_info.py | 23 | ||||
-rwxr-xr-x | setuptools/command/rotate.py | 6 | ||||
-rw-r--r-- | setuptools/command/test.py | 21 | ||||
-rw-r--r-- | setuptools/command/upload.py | 25 | ||||
-rw-r--r-- | setuptools/command/upload_docs.py | 88 |
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) |