diff options
Diffstat (limited to 'Lib/distutils')
-rw-r--r-- | Lib/distutils/__init__.py | 2 | ||||
-rw-r--r-- | Lib/distutils/command/build.py | 9 | ||||
-rw-r--r-- | Lib/distutils/command/build_ext.py | 68 | ||||
-rw-r--r-- | Lib/distutils/command/upload.py | 40 | ||||
-rw-r--r-- | Lib/distutils/dist.py | 69 | ||||
-rw-r--r-- | Lib/distutils/extension.py | 8 | ||||
-rw-r--r-- | Lib/distutils/tests/test_build_ext.py | 56 |
7 files changed, 159 insertions, 93 deletions
diff --git a/Lib/distutils/__init__.py b/Lib/distutils/__init__.py index 750a503..328bea6 100644 --- a/Lib/distutils/__init__.py +++ b/Lib/distutils/__init__.py @@ -13,5 +13,5 @@ used from a setup script as # Updated automatically by the Python release process. # #--start constants-- -__version__ = "3.4.2rc1" +__version__ = "3.5.0a0" #--end constants-- diff --git a/Lib/distutils/command/build.py b/Lib/distutils/command/build.py index cfc15cf..337dd0b 100644 --- a/Lib/distutils/command/build.py +++ b/Lib/distutils/command/build.py @@ -36,6 +36,8 @@ class build(Command): "(default: %s)" % get_platform()), ('compiler=', 'c', "specify the compiler type"), + ('parallel=', 'j', + "number of parallel build jobs"), ('debug', 'g', "compile extensions and libraries with debugging information"), ('force', 'f', @@ -65,6 +67,7 @@ class build(Command): self.debug = None self.force = 0 self.executable = None + self.parallel = None def finalize_options(self): if self.plat_name is None: @@ -116,6 +119,12 @@ class build(Command): if self.executable is None: self.executable = os.path.normpath(sys.executable) + if isinstance(self.parallel, str): + try: + self.parallel = int(self.parallel) + except ValueError: + raise DistutilsOptionError("parallel should be an integer") + def run(self): # Run all relevant sub-commands. This will be some subset of: # - build_py - pure Python modules diff --git a/Lib/distutils/command/build_ext.py b/Lib/distutils/command/build_ext.py index acbe648..54ce134 100644 --- a/Lib/distutils/command/build_ext.py +++ b/Lib/distutils/command/build_ext.py @@ -4,7 +4,10 @@ Implements the Distutils 'build_ext' command, for building extension modules (currently limited to C extensions, should accommodate C++ extensions ASAP).""" -import sys, os, re +import contextlib +import os +import re +import sys from distutils.core import Command from distutils.errors import * from distutils.sysconfig import customize_compiler, get_python_version @@ -85,6 +88,8 @@ class build_ext(Command): "forcibly build everything (ignore file timestamps)"), ('compiler=', 'c', "specify the compiler type"), + ('parallel=', 'j', + "number of parallel build jobs"), ('swig-cpp', None, "make SWIG create C++ files (default is C)"), ('swig-opts=', None, @@ -124,6 +129,7 @@ class build_ext(Command): self.swig_cpp = None self.swig_opts = None self.user = None + self.parallel = None def finalize_options(self): from distutils import sysconfig @@ -134,6 +140,7 @@ class build_ext(Command): ('compiler', 'compiler'), ('debug', 'debug'), ('force', 'force'), + ('parallel', 'parallel'), ('plat_name', 'plat_name'), ) @@ -274,6 +281,12 @@ class build_ext(Command): self.library_dirs.append(user_lib) self.rpath.append(user_lib) + if isinstance(self.parallel, str): + try: + self.parallel = int(self.parallel) + except ValueError: + raise DistutilsOptionError("parallel should be an integer") + def run(self): from distutils.ccompiler import new_compiler @@ -442,15 +455,45 @@ class build_ext(Command): def build_extensions(self): # First, sanity-check the 'extensions' list self.check_extensions_list(self.extensions) + if self.parallel: + self._build_extensions_parallel() + else: + self._build_extensions_serial() + + def _build_extensions_parallel(self): + workers = self.parallel + if self.parallel is True: + workers = os.cpu_count() # may return None + try: + from concurrent.futures import ThreadPoolExecutor + except ImportError: + workers = None + + if workers is None: + self._build_extensions_serial() + return + + with ThreadPoolExecutor(max_workers=workers) as executor: + futures = [executor.submit(self.build_extension, ext) + for ext in self.extensions] + for ext, fut in zip(self.extensions, futures): + with self._filter_build_errors(ext): + fut.result() + def _build_extensions_serial(self): for ext in self.extensions: - try: + with self._filter_build_errors(ext): self.build_extension(ext) - except (CCompilerError, DistutilsError, CompileError) as e: - if not ext.optional: - raise - self.warn('building extension "%s" failed: %s' % - (ext.name, e)) + + @contextlib.contextmanager + def _filter_build_errors(self, ext): + try: + yield + except (CCompilerError, DistutilsError, CompileError) as e: + if not ext.optional: + raise + self.warn('building extension "%s" failed: %s' % + (ext.name, e)) def build_extension(self, ext): sources = ext.sources @@ -502,15 +545,8 @@ class build_ext(Command): extra_postargs=extra_args, depends=ext.depends) - # XXX -- this is a Vile HACK! - # - # The setup.py script for Python on Unix needs to be able to - # get this list so it can perform all the clean up needed to - # avoid keeping object files around when cleaning out a failed - # build of an extension module. Since Distutils does not - # track dependencies, we have to get rid of intermediates to - # ensure all the intermediates will be properly re-built. - # + # XXX outdated variable, kept here in case third-part code + # needs it. self._built_objects = objects[:] # Now link the object files together into a "shared object" -- diff --git a/Lib/distutils/command/upload.py b/Lib/distutils/command/upload.py index 1a96e22..1c4fc48 100644 --- a/Lib/distutils/command/upload.py +++ b/Lib/distutils/command/upload.py @@ -1,11 +1,14 @@ -"""distutils.command.upload +""" +distutils.command.upload -Implements the Distutils 'upload' subcommand (upload package to PyPI).""" +Implements the Distutils 'upload' subcommand (upload package to a package +index). +""" -import sys -import os, io -import socket +import os +import io import platform +import hashlib from base64 import standard_b64encode from urllib.request import urlopen, Request, HTTPError from urllib.parse import urlparse @@ -14,12 +17,6 @@ from distutils.core import PyPIRCCommand from distutils.spawn import spawn from distutils import log -# this keeps compatibility for 2.3 and 2.4 -if sys.version < "2.5": - from md5 import md5 -else: - from hashlib import md5 - class upload(PyPIRCCommand): description = "upload binary package to PyPI" @@ -60,7 +57,8 @@ class upload(PyPIRCCommand): def run(self): if not self.distribution.dist_files: - raise DistutilsOptionError("No dist file created in earlier command") + msg = "No dist file created in earlier command" + raise DistutilsOptionError(msg) for command, pyversion, filename in self.distribution.dist_files: self.upload_file(command, pyversion, filename) @@ -103,10 +101,10 @@ class upload(PyPIRCCommand): 'content': (os.path.basename(filename),content), 'filetype': command, 'pyversion': pyversion, - 'md5_digest': md5(content).hexdigest(), + 'md5_digest': hashlib.md5(content).hexdigest(), # additional meta-data - 'metadata_version' : '1.0', + 'metadata_version': '1.0', 'summary': meta.get_description(), 'home_page': meta.get_url(), 'author': meta.get_contact(), @@ -149,7 +147,7 @@ class upload(PyPIRCCommand): for key, value in data.items(): title = '\r\nContent-Disposition: form-data; name="%s"' % key # handle multiple entries for the same name - if type(value) != type([]): + if not isinstance(value, list): value = [value] for value in value: if type(value) is tuple: @@ -166,13 +164,15 @@ class upload(PyPIRCCommand): body.write(end_boundary) body = body.getvalue() - self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO) + msg = "Submitting %s to %s" % (filename, self.repository) + self.announce(msg, log.INFO) # build the Request - headers = {'Content-type': - 'multipart/form-data; boundary=%s' % boundary, - 'Content-length': str(len(body)), - 'Authorization': auth} + headers = { + 'Content-type': 'multipart/form-data; boundary=%s' % boundary, + 'Content-length': str(len(body)), + 'Authorization': auth, + } request = Request(self.repository, data=body, headers=headers) diff --git a/Lib/distutils/dist.py b/Lib/distutils/dist.py index 7eb04bc..ffb33ff6 100644 --- a/Lib/distutils/dist.py +++ b/Lib/distutils/dist.py @@ -4,7 +4,9 @@ Provides the Distribution class, which represents the module distribution being built/installed/distributed. """ -import sys, os, re +import sys +import os +import re from email import message_from_file try: @@ -22,7 +24,7 @@ from distutils.debug import DEBUG # the same as a Python NAME -- I don't allow leading underscores. The fact # that they're very similar is no coincidence; the default naming scheme is # to look for a Python module named after the command. -command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$') +command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$') class Distribution: @@ -39,7 +41,6 @@ class Distribution: See the code for 'setup()', in core.py, for details. """ - # 'global_options' describes the command-line options that may be # supplied to the setup script prior to any actual commands. # Eg. "./setup.py -n" or "./setup.py --quiet" both take advantage of @@ -48,12 +49,13 @@ class Distribution: # don't want to pollute the commands with too many options that they # have minimal control over. # The fourth entry for verbose means that it can be repeated. - global_options = [('verbose', 'v', "run verbosely (default)", 1), - ('quiet', 'q', "run quietly (turns verbosity off)"), - ('dry-run', 'n', "don't actually do anything"), - ('help', 'h', "show detailed help message"), - ('no-user-cfg', None, - 'ignore pydistutils.cfg in your home directory'), + global_options = [ + ('verbose', 'v', "run verbosely (default)", 1), + ('quiet', 'q', "run quietly (turns verbosity off)"), + ('dry-run', 'n', "don't actually do anything"), + ('help', 'h', "show detailed help message"), + ('no-user-cfg', None, + 'ignore pydistutils.cfg in your home directory'), ] # 'common_usage' is a short (2-3 line) string describing the common @@ -115,10 +117,9 @@ Common commands: (see '--help-commands' for more) # negative options are options that exclude other options negative_opt = {'quiet': 'verbose'} - # -- Creation/initialization methods ------------------------------- - def __init__ (self, attrs=None): + def __init__(self, attrs=None): """Construct a new Distribution instance: initialize all the attributes of a Distribution, and then use 'attrs' (a dictionary mapping attribute names to values) to assign some of those @@ -532,15 +533,15 @@ Common commands: (see '--help-commands' for more) # to be sure that the basic "command" interface is implemented. if not issubclass(cmd_class, Command): raise DistutilsClassError( - "command class %s must subclass Command" % cmd_class) + "command class %s must subclass Command" % cmd_class) # Also make sure that the command object provides a list of its # known options. if not (hasattr(cmd_class, 'user_options') and isinstance(cmd_class.user_options, list)): - raise DistutilsClassError(("command class %s must provide " + - "'user_options' attribute (a list of tuples)") % \ - cmd_class) + msg = ("command class %s must provide " + "'user_options' attribute (a list of tuples)") + raise DistutilsClassError(msg % cmd_class) # If the command class has a list of negative alias options, # merge it in with the global negative aliases. @@ -552,12 +553,11 @@ Common commands: (see '--help-commands' for more) # Check for help_options in command class. They have a different # format (tuple of four) so we need to preprocess them here. if (hasattr(cmd_class, 'help_options') and - isinstance(cmd_class.help_options, list)): + isinstance(cmd_class.help_options, list)): help_options = fix_help_options(cmd_class.help_options) else: help_options = [] - # All commands support the global options too, just by adding # in 'global_options'. parser.set_option_table(self.global_options + @@ -570,7 +570,7 @@ Common commands: (see '--help-commands' for more) return if (hasattr(cmd_class, 'help_options') and - isinstance(cmd_class.help_options, list)): + isinstance(cmd_class.help_options, list)): help_option_found=0 for (help_option, short, desc, func) in cmd_class.help_options: if hasattr(opts, parser.get_attr_name(help_option)): @@ -647,7 +647,7 @@ Common commands: (see '--help-commands' for more) else: klass = self.get_command_class(command) if (hasattr(klass, 'help_options') and - isinstance(klass.help_options, list)): + isinstance(klass.help_options, list)): parser.set_option_table(klass.user_options + fix_help_options(klass.help_options)) else: @@ -814,7 +814,7 @@ Common commands: (see '--help-commands' for more) klass_name = command try: - __import__ (module_name) + __import__(module_name) module = sys.modules[module_name] except ImportError: continue @@ -823,8 +823,8 @@ Common commands: (see '--help-commands' for more) klass = getattr(module, klass_name) except AttributeError: raise DistutilsModuleError( - "invalid command '%s' (no class '%s' in module '%s')" - % (command, klass_name, module_name)) + "invalid command '%s' (no class '%s' in module '%s')" + % (command, klass_name, module_name)) self.cmdclass[command] = klass return klass @@ -840,7 +840,7 @@ Common commands: (see '--help-commands' for more) cmd_obj = self.command_obj.get(command) if not cmd_obj and create: if DEBUG: - self.announce("Distribution.get_command_obj(): " \ + self.announce("Distribution.get_command_obj(): " "creating '%s' command object" % command) klass = self.get_command_class(command) @@ -897,8 +897,8 @@ Common commands: (see '--help-commands' for more) setattr(command_obj, option, value) else: raise DistutilsOptionError( - "error in %s: command '%s' has no such option '%s'" - % (source, command_name, option)) + "error in %s: command '%s' has no such option '%s'" + % (source, command_name, option)) except ValueError as msg: raise DistutilsOptionError(msg) @@ -974,7 +974,6 @@ Common commands: (see '--help-commands' for more) cmd_obj.run() self.have_run[command] = 1 - # -- Distribution query methods ------------------------------------ def has_pure_modules(self): @@ -1112,17 +1111,17 @@ class DistributionMetadata: """ version = '1.0' if (self.provides or self.requires or self.obsoletes or - self.classifiers or self.download_url): + self.classifiers or self.download_url): version = '1.1' file.write('Metadata-Version: %s\n' % version) - file.write('Name: %s\n' % self.get_name() ) - file.write('Version: %s\n' % self.get_version() ) - file.write('Summary: %s\n' % self.get_description() ) - file.write('Home-page: %s\n' % self.get_url() ) - file.write('Author: %s\n' % self.get_contact() ) - file.write('Author-email: %s\n' % self.get_contact_email() ) - file.write('License: %s\n' % self.get_license() ) + file.write('Name: %s\n' % self.get_name()) + file.write('Version: %s\n' % self.get_version()) + file.write('Summary: %s\n' % self.get_description()) + file.write('Home-page: %s\n' % self.get_url()) + file.write('Author: %s\n' % self.get_contact()) + file.write('Author-email: %s\n' % self.get_contact_email()) + file.write('License: %s\n' % self.get_license()) if self.download_url: file.write('Download-URL: %s\n' % self.download_url) @@ -1131,7 +1130,7 @@ class DistributionMetadata: keywords = ','.join(self.get_keywords()) if keywords: - file.write('Keywords: %s\n' % keywords ) + file.write('Keywords: %s\n' % keywords) self._write_list(file, 'Platform', self.get_platforms()) self._write_list(file, 'Classifier', self.get_classifiers()) diff --git a/Lib/distutils/extension.py b/Lib/distutils/extension.py index a93655a..7efbb74 100644 --- a/Lib/distutils/extension.py +++ b/Lib/distutils/extension.py @@ -131,6 +131,14 @@ class Extension: msg = "Unknown Extension options: %s" % options warnings.warn(msg) + def __repr__(self): + return '<%s.%s(%r) at %#x>' % ( + self.__class__.__module__, + self.__class__.__qualname__, + self.name, + id(self)) + + def read_setup_file(filename): """Reads a Setup file and returns Extension instances.""" from distutils.sysconfig import (parse_makefile, expand_makefile_vars, diff --git a/Lib/distutils/tests/test_build_ext.py b/Lib/distutils/tests/test_build_ext.py index b9f407f..366ffbe 100644 --- a/Lib/distutils/tests/test_build_ext.py +++ b/Lib/distutils/tests/test_build_ext.py @@ -37,6 +37,9 @@ class BuildExtTestCase(TempdirManager, from distutils.command import build_ext build_ext.USER_BASE = site.USER_BASE + def build_ext(self, *args, **kwargs): + return build_ext(*args, **kwargs) + def test_build_ext(self): global ALREADY_TESTED copy_xxmodule_c(self.tmp_dir) @@ -44,7 +47,7 @@ class BuildExtTestCase(TempdirManager, xx_ext = Extension('xx', [xx_c]) dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]}) dist.package_dir = self.tmp_dir - cmd = build_ext(dist) + cmd = self.build_ext(dist) fixup_build_ext(cmd) cmd.build_lib = self.tmp_dir cmd.build_temp = self.tmp_dir @@ -91,7 +94,7 @@ class BuildExtTestCase(TempdirManager, def test_solaris_enable_shared(self): dist = Distribution({'name': 'xx'}) - cmd = build_ext(dist) + cmd = self.build_ext(dist) old = sys.platform sys.platform = 'sunos' # fooling finalize_options @@ -113,7 +116,7 @@ class BuildExtTestCase(TempdirManager, def test_user_site(self): import site dist = Distribution({'name': 'xx'}) - cmd = build_ext(dist) + cmd = self.build_ext(dist) # making sure the user option is there options = [name for name, short, lable in @@ -144,14 +147,14 @@ class BuildExtTestCase(TempdirManager, # with the optional argument. modules = [Extension('foo', ['xxx'], optional=False)] dist = Distribution({'name': 'xx', 'ext_modules': modules}) - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.ensure_finalized() self.assertRaises((UnknownFileError, CompileError), cmd.run) # should raise an error modules = [Extension('foo', ['xxx'], optional=True)] dist = Distribution({'name': 'xx', 'ext_modules': modules}) - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.ensure_finalized() cmd.run() # should pass @@ -160,7 +163,7 @@ class BuildExtTestCase(TempdirManager, # etc.) are in the include search path. modules = [Extension('foo', ['xxx'], optional=False)] dist = Distribution({'name': 'xx', 'ext_modules': modules}) - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.finalize_options() from distutils import sysconfig @@ -172,14 +175,14 @@ class BuildExtTestCase(TempdirManager, # make sure cmd.libraries is turned into a list # if it's a string - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.libraries = 'my_lib, other_lib lastlib' cmd.finalize_options() self.assertEqual(cmd.libraries, ['my_lib', 'other_lib', 'lastlib']) # make sure cmd.library_dirs is turned into a list # if it's a string - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.library_dirs = 'my_lib_dir%sother_lib_dir' % os.pathsep cmd.finalize_options() self.assertIn('my_lib_dir', cmd.library_dirs) @@ -187,7 +190,7 @@ class BuildExtTestCase(TempdirManager, # make sure rpath is turned into a list # if it's a string - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.rpath = 'one%stwo' % os.pathsep cmd.finalize_options() self.assertEqual(cmd.rpath, ['one', 'two']) @@ -196,32 +199,32 @@ class BuildExtTestCase(TempdirManager, # make sure define is turned into 2-tuples # strings if they are ','-separated strings - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.define = 'one,two' cmd.finalize_options() self.assertEqual(cmd.define, [('one', '1'), ('two', '1')]) # make sure undef is turned into a list of # strings if they are ','-separated strings - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.undef = 'one,two' cmd.finalize_options() self.assertEqual(cmd.undef, ['one', 'two']) # make sure swig_opts is turned into a list - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.swig_opts = None cmd.finalize_options() self.assertEqual(cmd.swig_opts, []) - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.swig_opts = '1 2' cmd.finalize_options() self.assertEqual(cmd.swig_opts, ['1', '2']) def test_check_extensions_list(self): dist = Distribution() - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.finalize_options() #'extensions' option must be a list of Extension instances @@ -270,7 +273,7 @@ class BuildExtTestCase(TempdirManager, def test_get_source_files(self): modules = [Extension('foo', ['xxx'], optional=False)] dist = Distribution({'name': 'xx', 'ext_modules': modules}) - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.ensure_finalized() self.assertEqual(cmd.get_source_files(), ['xxx']) @@ -279,7 +282,7 @@ class BuildExtTestCase(TempdirManager, # should not be overriden by a compiler instance # when the command is run dist = Distribution() - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.compiler = 'unix' cmd.ensure_finalized() cmd.run() @@ -292,7 +295,7 @@ class BuildExtTestCase(TempdirManager, ext = Extension('foo', [c_file], optional=False) dist = Distribution({'name': 'xx', 'ext_modules': [ext]}) - cmd = build_ext(dist) + cmd = self.build_ext(dist) fixup_build_ext(cmd) cmd.ensure_finalized() self.assertEqual(len(cmd.get_outputs()), 1) @@ -355,7 +358,7 @@ class BuildExtTestCase(TempdirManager, #etree_ext = Extension('lxml.etree', [etree_c]) #dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]}) dist = Distribution() - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.inplace = 1 cmd.distribution.package_dir = {'': 'src'} cmd.distribution.packages = ['lxml', 'lxml.html'] @@ -462,7 +465,7 @@ class BuildExtTestCase(TempdirManager, 'ext_modules': [deptarget_ext] }) dist.package_dir = self.tmp_dir - cmd = build_ext(dist) + cmd = self.build_ext(dist) cmd.build_lib = self.tmp_dir cmd.build_temp = self.tmp_dir @@ -481,8 +484,19 @@ class BuildExtTestCase(TempdirManager, self.fail("Wrong deployment target during compilation") +class ParallelBuildExtTestCase(BuildExtTestCase): + + def build_ext(self, *args, **kwargs): + build_ext = super().build_ext(*args, **kwargs) + build_ext.parallel = True + return build_ext + + def test_suite(): - return unittest.makeSuite(BuildExtTestCase) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BuildExtTestCase)) + suite.addTest(unittest.makeSuite(ParallelBuildExtTestCase)) + return suite if __name__ == '__main__': - support.run_unittest(test_suite()) + support.run_unittest(__name__) |