diff options
| author | FELD Boris <lothiraldan@gmail.com> | 2011-01-30 15:09:13 +0100 |
|---|---|---|
| committer | FELD Boris <lothiraldan@gmail.com> | 2011-01-30 15:09:13 +0100 |
| commit | abf788d6a5451e400e1a6d8e17defff20009f2c2 (patch) | |
| tree | 628c2b0eb5010e75dc19c069fd2fc1d16306c675 | |
| parent | 57b7580dada83b93bcea4f77b3c50069399fc488 (diff) | |
| parent | 91bf3f123bf8c3190cef98ccbe9eff1149b287ac (diff) | |
| download | disutils2-abf788d6a5451e400e1a6d8e17defff20009f2c2.tar.gz | |
Merge
| -rw-r--r-- | distutils2/_backport/pkgutil.py | 4 | ||||
| -rw-r--r-- | distutils2/_backport/shutil.py | 9 | ||||
| -rw-r--r-- | distutils2/_backport/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO | 5 | ||||
| -rw-r--r-- | distutils2/_backport/tests/test_pkgutil.py | 13 | ||||
| -rw-r--r-- | distutils2/config.py | 11 | ||||
| -rw-r--r-- | distutils2/index/dist.py | 17 | ||||
| -rw-r--r-- | distutils2/install.py | 119 | ||||
| -rw-r--r-- | distutils2/mkcfg.py | 36 | ||||
| -rw-r--r-- | distutils2/run.py | 48 | ||||
| -rw-r--r-- | distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz | bin | 0 -> 110 bytes | |||
| -rw-r--r-- | distutils2/tests/test_config.py | 2 | ||||
| -rw-r--r-- | distutils2/tests/test_index_dist.py | 20 | ||||
| -rw-r--r-- | distutils2/tests/test_install.py | 58 | ||||
| -rw-r--r-- | docs/design/configfile.rst | 132 | ||||
| -rw-r--r-- | docs/source/distutils/sourcedist.rst | 7 | ||||
| -rw-r--r-- | docs/source/setupcfg.rst | 79 |
16 files changed, 385 insertions, 175 deletions
diff --git a/distutils2/_backport/pkgutil.py b/distutils2/_backport/pkgutil.py index a81f124..aca87b0 100644 --- a/distutils2/_backport/pkgutil.py +++ b/distutils2/_backport/pkgutil.py @@ -922,10 +922,6 @@ class EggInfoDistribution(object): for field in ('Obsoletes', 'Requires', 'Provides'): del self.metadata[field] - provides = "%s (%s)" % (self.metadata['name'], - self.metadata['version']) - self.metadata['Provides-Dist'] += (provides,) - reqs = [] if requires is not None: diff --git a/distutils2/_backport/shutil.py b/distutils2/_backport/shutil.py index 802d089..17d6728 100644 --- a/distutils2/_backport/shutil.py +++ b/distutils2/_backport/shutil.py @@ -742,6 +742,8 @@ def unpack_archive(filename, extract_dir=None, format=None): if extract_dir is None: extract_dir = os.getcwd() + func = None + if format is not None: try: format_info = _UNPACK_FORMATS[format] @@ -758,4 +760,9 @@ def unpack_archive(filename, extract_dir=None, format=None): func = _UNPACK_FORMATS[format][1] kwargs = dict(_UNPACK_FORMATS[format][2]) - raise ValueError('Unknown archive format: %s' % filename) + func(filename, extract_dir, **kwargs) + + if func is None: + raise ValueError('Unknown archive format: %s' % filename) + + return extract_dir diff --git a/distutils2/_backport/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO b/distutils2/_backport/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO new file mode 100644 index 0000000..499a083 --- /dev/null +++ b/distutils2/_backport/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO @@ -0,0 +1,5 @@ +Metadata-Version: 1.2 +Name: coconuts-aster +Version: 10.3 +Provides-Dist: strawberry (0.6) +Provides-Dist: banana (0.4) diff --git a/distutils2/_backport/tests/test_pkgutil.py b/distutils2/_backport/tests/test_pkgutil.py index ed27aee..09857e0 100644 --- a/distutils2/_backport/tests/test_pkgutil.py +++ b/distutils2/_backport/tests/test_pkgutil.py @@ -389,6 +389,7 @@ class TestPkgUtilPEP376(support.LoggingCatcher, support.WarningsCatcher, # Now, test if the egg-info distributions are found correctly as well fake_dists += [('bacon', '0.1'), ('cheese', '2.0.2'), + ('coconuts-aster', '10.3'), ('banana', '0.4'), ('strawberry', '0.6'), ('truffles', '5.0'), ('nut', 'funkyversion')] found_dists = [] @@ -494,18 +495,18 @@ class TestPkgUtilPEP376(support.LoggingCatcher, support.WarningsCatcher, l = [dist.name for dist in provides_distribution('truffles', '>1.5', use_egg_info=True)] - checkLists(l, ['bacon', 'truffles']) + checkLists(l, ['bacon']) l = [dist.name for dist in provides_distribution('truffles', '>=1.0')] checkLists(l, ['choxie', 'towel-stuff']) l = [dist.name for dist in provides_distribution('strawberry', '0.6', use_egg_info=True)] - checkLists(l, ['strawberry']) + checkLists(l, ['coconuts-aster']) l = [dist.name for dist in provides_distribution('strawberry', '>=0.5', use_egg_info=True)] - checkLists(l, ['strawberry']) + checkLists(l, ['coconuts-aster']) l = [dist.name for dist in provides_distribution('strawberry', '>0.6', use_egg_info=True)] @@ -513,11 +514,11 @@ class TestPkgUtilPEP376(support.LoggingCatcher, support.WarningsCatcher, l = [dist.name for dist in provides_distribution('banana', '0.4', use_egg_info=True)] - checkLists(l, ['banana']) + checkLists(l, ['coconuts-aster']) l = [dist.name for dist in provides_distribution('banana', '>=0.3', use_egg_info=True)] - checkLists(l, ['banana']) + checkLists(l, ['coconuts-aster']) l = [dist.name for dist in provides_distribution('banana', '!=0.4', use_egg_info=True)] @@ -557,7 +558,7 @@ class TestPkgUtilPEP376(support.LoggingCatcher, support.WarningsCatcher, eggs = [('bacon', '0.1'), ('banana', '0.4'), ('strawberry', '0.6'), ('truffles', '5.0'), ('cheese', '2.0.2'), - ('nut', 'funkyversion')] + ('coconuts-aster', '10.3'), ('nut', 'funkyversion')] dists = [('choxie', '2.0.0.9'), ('grammar', '1.0a4'), ('towel-stuff', '0.1')] diff --git a/distutils2/config.py b/distutils2/config.py index 0467dab..2715f5e 100644 --- a/distutils2/config.py +++ b/distutils2/config.py @@ -8,6 +8,7 @@ import re import sys import re from ConfigParser import RawConfigParser +from shlex import split from distutils2 import logger from distutils2.errors import DistutilsOptionError @@ -20,15 +21,9 @@ from distutils2.datafiles import resources_dests def _pop_values(values_dct, key): """Remove values from the dictionary and convert them as a list""" - vals_str = values_dct.pop(key, None) - if not vals_str: - return + vals_str = values_dct.pop(key, '') # Get bash options like `gcc -print-file-name=libgcc.a` - vals = re.search('(`.*?`)', vals_str) or [] - if vals: - vals = list(vals.groups()) - vals_str = re.sub('`.*?`', '', vals_str) - vals.extend(vals_str.split()) + vals = split(vals_str) if vals: return vals diff --git a/distutils2/index/dist.py b/distutils2/index/dist.py index eeb5809..b03a61d 100644 --- a/distutils2/index/dist.py +++ b/distutils2/index/dist.py @@ -149,6 +149,16 @@ class ReleaseInfo(IndexReference): dist = self.dists.values()[0] return dist + def unpack(self, path=None, prefer_source=True): + """Unpack the distribution to the given path. + + If not destination is given, creates a temporary location. + + Returns the location of the extracted files (root). + """ + return self.get_distribution(prefer_source=prefer_source)\ + .unpack(path=path) + def download(self, temp_path=None, prefer_source=True): """Download the distribution, using the requirements. @@ -312,7 +322,7 @@ class DistInfo(IndexReference): if path is None: path = tempfile.mkdtemp() - filename = self.download() + filename = self.download(path) content_type = mimetypes.guess_type(filename)[0] self._unpacked_dir = unpack_archive(filename) @@ -332,8 +342,11 @@ class DistInfo(IndexReference): % (hashval.hexdigest(), expected_hashval)) def __repr__(self): + if self.release is None: + return "<? ? %s>" % self.dist_type + return "<%s %s %s>" % ( - self.release.name, self.release.version, self.dist_type or "") + self.release.name, self.release.version, self.dist_type or "") class ReleasesList(IndexReference): diff --git a/distutils2/install.py b/distutils2/install.py index e7e97d2..84b75af 100644 --- a/distutils2/install.py +++ b/distutils2/install.py @@ -1,4 +1,11 @@ -from tempfile import mkdtemp +"""Provides installations scripts. + +The goal of this script is to install a release from the indexes (eg. +PyPI), including the dependencies of the releases if needed. + +It uses the work made in pkgutil and by the index crawlers to browse the +installed distributions, and rely on the instalation commands to install. +""" import shutil import os import sys @@ -17,14 +24,9 @@ from distutils2.index.errors import ProjectNotFound, ReleaseNotFound from distutils2.errors import DistutilsError from distutils2.version import get_version_predicate -"""Provides installations scripts. - -The goal of this script is to install a release from the indexes (eg. -PyPI), including the dependencies of the releases if needed. -It uses the work made in pkgutil and by the index crawlers to browse the -installed distributions, and rely on the instalation commands to install. -""" +__all__ = ['install_dists', 'install_from_infos', 'get_infos', 'remove', + 'install'] class InstallationException(Exception): @@ -35,7 +37,7 @@ class InstallationConflict(InstallationException): """Raised when a conflict is detected""" -def move_files(files, destination=None): +def _move_files(files, destination): """Move the list of files in the destination folder, keeping the same structure. @@ -43,13 +45,11 @@ def move_files(files, destination=None): :param files: a list of files to move. :param destination: the destination directory to put on the files. - if not defined, create a new one, using mkdtemp """ - if not destination: - destination = mkdtemp() - for old in files: - new = '%s%s' % (destination, old) + # not using os.path.join() because basename() might not be + # unique in destination + new = "%s%s" % (destination, old) # try to make the paths. try: @@ -60,7 +60,7 @@ def move_files(files, destination=None): else: raise e os.rename(old, new) - yield (old, new) + yield old, new def _run_d1_install(archive_dir, path): @@ -93,7 +93,7 @@ def _install_dist(dist, path): * copy the files in "path" * determine if the distribution is distutils2 or distutils1. """ - where = dist.unpack(archive) + where = dist.unpack(path) # get into the dir archive_dir = None @@ -119,7 +119,7 @@ def _install_dist(dist, path): os.chdir(old_dir) -def install_dists(dists, path=None): +def install_dists(dists, path, paths=sys.path): """Install all distributions provided in dists, with the given prefix. If an error occurs while installing one of the distributions, uninstall all @@ -129,27 +129,28 @@ def install_dists(dists, path=None): :param dists: distributions to install :param path: base path to install distribution in + :param paths: list of paths (defaults to sys.path) to look for info """ - if not path: - path = mkdtemp() installed_dists, installed_files = [], [] - for d in dists: - logger.info('Installing %s %s' % (d.name, d.version)) + for dist in dists: + logger.info('Installing %s %s' % (dist.name, dist.version)) try: - installed_files.extend(_install_dist(d, path)) - installed_dists.append(d) - except Exception, e : + installed_files.extend(_install_dist(dist, path)) + installed_dists.append(dist) + except Exception, e: logger.info('Failed. %s' % str(e)) # reverting - for d in installed_dists: - uninstall(d) + for installed_dist in installed_dists: + _remove_dist(installed_dist, paths) raise e + return installed_files -def install_from_infos(install=[], remove=[], conflicts=[], install_path=None): +def install_from_infos(install_path=None, install=[], remove=[], conflicts=[], + paths=sys.path): """Install and remove the given distributions. The function signature is made to be compatible with the one of get_infos. @@ -168,35 +169,43 @@ def install_from_infos(install=[], remove=[], conflicts=[], install_path=None): 4. Else, move the distributions to the right locations, and remove for real the distributions thats need to be removed. - :param install: list of distributions that will be installed. + :param install_path: the installation path where we want to install the + distributions. + :param install: list of distributions that will be installed; install_path + must be provided if this list is not empty. :param remove: list of distributions that will be removed. :param conflicts: list of conflicting distributions, eg. that will be in conflict once the install and remove distribution will be processed. - :param install_path: the installation path where we want to install the - distributions. + :param paths: list of paths (defaults to sys.path) to look for info """ # first of all, if we have conflicts, stop here. if conflicts: raise InstallationConflict(conflicts) + if install and not install_path: + raise ValueError("Distributions are to be installed but `install_path`" + " is not provided.") + # before removing the files, we will start by moving them away # then, if any error occurs, we could replace them in the good place. temp_files = {} # contains lists of {dist: (old, new)} paths + temp_dir = None if remove: + temp_dir = tempfile.mkdtemp() for dist in remove: files = dist.get_installed_files() - temp_files[dist] = move_files(files) + temp_files[dist] = _move_files(files, temp_dir) try: if install: - installed_files = install_dists(install, install_path) # install to tmp first - + install_dists(install, install_path, paths) except: - # if an error occurs, put back the files in the good place. + # if an error occurs, put back the files in the right place. for files in temp_files.values(): for old, new in files: shutil.move(new, old) - + if temp_dir: + shutil.rmtree(temp_dir) # now re-raising raise @@ -204,6 +213,8 @@ def install_from_infos(install=[], remove=[], conflicts=[], install_path=None): for files in temp_files.values(): for old, new in files: os.remove(new) + if temp_dir: + shutil.rmtree(temp_dir) def _get_setuptools_deps(release): @@ -265,9 +276,9 @@ def get_infos(requirements, index=None, installed=None, prefer_final=True): # Get all the releases that match the requirements try: releases = index.get_releases(requirements) - except (ReleaseNotFound, ProjectNotFound), e: + except (ReleaseNotFound, ProjectNotFound): raise InstallationException('Release not found: "%s"' % requirements) - + # Pick up a release, and try to get the dependency tree release = releases.get_last(requirements, prefer_final=prefer_final) @@ -284,6 +295,8 @@ def get_infos(requirements, index=None, installed=None, prefer_final=True): else: deps = metadata['requires_dist'] + # XXX deps not used + distributions = itertools.chain(installed, [release]) depgraph = generate_graph(distributions) @@ -315,16 +328,19 @@ def _update_infos(infos, new_infos): infos[key].extend(new_infos[key]) +def _remove_dist(dist, paths=sys.path): + remove(dist.name, paths) + + def remove(project_name, paths=sys.path): """Removes a single project from the installation""" - tmp = tempfile.mkdtemp(prefix=project_name+'-uninstall') dist = get_distribution(project_name, paths=paths) if dist is None: raise DistutilsError('Distribution %s not found' % project_name) files = dist.get_installed_files(local=True) rmdirs = [] rmfiles = [] - + tmp = tempfile.mkdtemp(prefix=project_name + '-uninstall') try: for file, md5, size in files: if os.path.isfile(file): @@ -339,8 +355,8 @@ def remove(project_name, paths=sys.path): rmfiles.append(file) if dirname not in rmdirs: rmdirs.append(dirname) - except OSError: - os.rmdir(tmp) + finally: + shutil.rmtree(tmp) for file in rmfiles: os.remove(file) @@ -351,14 +367,6 @@ def remove(project_name, paths=sys.path): os.rmdir(dirname) - -def main(**attrs): - if 'script_args' not in attrs: - import sys - attrs['requirements'] = sys.argv[1] - get_infos(**attrs) - - def install(project): logger.info('Getting information about "%s".' % project) try: @@ -373,13 +381,20 @@ def install(project): install_path = get_config_var('base') try: - install_from_infos(info['install'], info['remove'], info['conflict'], - install_path=install_path) + install_from_infos(install_path, + info['install'], info['remove'], info['conflict']) except InstallationConflict, e: projects = ['%s %s' % (p.name, p.version) for p in e.args[0]] logger.info('"%s" conflicts with "%s"' % (project, ','.join(projects))) +def _main(**attrs): + if 'script_args' not in attrs: + import sys + attrs['requirements'] = sys.argv[1] + get_infos(**attrs) + + if __name__ == '__main__': - main() + _main() diff --git a/distutils2/mkcfg.py b/distutils2/mkcfg.py index 6bf0bb3..3a37369 100644 --- a/distutils2/mkcfg.py +++ b/distutils2/mkcfg.py @@ -266,23 +266,25 @@ class MainProgram(object): data['packages'].extend(dist.packages or []) data['modules'].extend(dist.py_modules or []) # 2.1 data_files -> resources. - if len(dist.data_files) < 2 or isinstance(dist.data_files[1], str): - dist.data_files = [('', dist.data_files)] - #add tokens in the destination paths - vars = {'distribution.name':data['name']} - path_tokens = sysconfig.get_paths(vars=vars).items() - #sort tokens to use the longest one first - path_tokens.sort(cmp=lambda x,y: cmp(len(y), len(x)), - key=lambda x: x[1]) - for dest, srcs in (dist.data_files or []): - dest = os.path.join(sys.prefix, dest) - for tok, path in path_tokens: - if dest.startswith(path): - dest = ('{%s}' % tok) + dest[len(path):] - files = [('/ '.join(src.rsplit('/', 1)), dest) - for src in srcs] - data['resources'].extend(files) - continue + if dist.data_files: + if len(dist.data_files) < 2 or \ + isinstance(dist.data_files[1], str): + dist.data_files = [('', dist.data_files)] + #add tokens in the destination paths + vars = {'distribution.name':data['name']} + path_tokens = sysconfig.get_paths(vars=vars).items() + #sort tokens to use the longest one first + path_tokens.sort(cmp=lambda x,y: cmp(len(y), len(x)), + key=lambda x: x[1]) + for dest, srcs in (dist.data_files or []): + dest = os.path.join(sys.prefix, dest) + for tok, path in path_tokens: + if dest.startswith(path): + dest = ('{%s}' % tok) + dest[len(path):] + files = [('/ '.join(src.rsplit('/', 1)), dest) + for src in srcs] + data['resources'].extend(files) + continue # 2.2 package_data -> extra_files package_dirs = dist.package_dir or {} for package, extras in dist.package_data.iteritems() or []: diff --git a/distutils2/run.py b/distutils2/run.py index 01a654d..aa4f04f 100644 --- a/distutils2/run.py +++ b/distutils2/run.py @@ -83,10 +83,10 @@ def commands_main(**attrs): dist = distclass(attrs) except DistutilsSetupError, msg: if 'name' in attrs: - raise SystemExit, "error in %s setup command: %s" % \ - (attrs['name'], msg) + raise SystemExit("error in %s setup command: %s" % \ + (attrs['name'], msg)) else: - raise SystemExit, "error in setup command: %s" % msg + raise SystemExit("error in setup command: %s" % msg) # Find and parse the config file(s): they will override options from # the setup script, but be overridden by the command line. @@ -98,22 +98,21 @@ def commands_main(**attrs): try: res = dist.parse_command_line() except DistutilsArgError, msg: - raise SystemExit, gen_usage(dist.script_name) + "\nerror: %s" % msg + raise SystemExit(gen_usage(dist.script_name) + "\nerror: %s" % msg) # And finally, run all the commands found on the command line. if res: try: dist.run_commands() except KeyboardInterrupt: - raise SystemExit, "interrupted" + raise SystemExit("interrupted") except (IOError, os.error), exc: error = grok_environment_error(exc) - raise SystemExit, error + raise SystemExit(error) except (DistutilsError, CCompilerError), msg: - raise - raise SystemExit, "error: " + str(msg) + raise SystemExit("error: " + str(msg)) return dist @@ -127,7 +126,10 @@ def _set_logger(): def main(): - """Main entry point for Distutils2""" + """Main entry point for Distutils2 + + Execute an action or delegate to the commands system. + """ _set_logger() parser = OptionParser() parser.disable_interspersed_args() @@ -164,7 +166,7 @@ def main(): options, args = parser.parse_args() if options.version: print('Distutils2 %s' % __version__) -# sys.exit(0) + return 0 if len(options.metadata): from distutils2.dist import Distribution @@ -178,18 +180,18 @@ def main(): keys = options.metadata if len(keys) == 1: print metadata[keys[0]] - sys.exit(0) + return for key in keys: if key in metadata: - print(metadata._convert_name(key)+':') + print(metadata._convert_name(key) + ':') value = metadata[key] if isinstance(value, list): for v in value: - print(' '+v) + print(' ' + v) else: - print(' '+value.replace('\n', '\n ')) - sys.exit(0) + print(' ' + value.replace('\n', '\n ')) + return 0 if options.search is not None: search = options.search.lower() @@ -199,7 +201,7 @@ def main(): print('%s %s at %s' % (dist.name, dist.metadata['version'], dist.path)) - sys.exit(0) + return 0 if options.graph is not None: name = options.graph @@ -211,25 +213,25 @@ def main(): graph = generate_graph(dists) print(graph.repr_node(dist)) - sys.exit(0) + return 0 if options.fgraph: dists = get_distributions(use_egg_info=True) graph = generate_graph(dists) print(graph) - sys.exit(0) + return 0 if options.install is not None: install(options.install) - sys.exit(0) + return 0 if len(args) == 0: parser.print_help() - sys.exit(0) + return 0 - return commands_main() -# sys.exit(0) + commands_main() + return 0 if __name__ == '__main__': - main() + sys.exit(main()) diff --git a/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz b/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz Binary files differindex e69de29..333961e 100644 --- a/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz +++ b/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz diff --git a/distutils2/tests/test_config.py b/distutils2/tests/test_config.py index 79d0772..72e9274 100644 --- a/distutils2/tests/test_config.py +++ b/distutils2/tests/test_config.py @@ -103,7 +103,7 @@ packages = one [extension=speed_coconuts] name = one.speed_coconuts sources = c_src/speed_coconuts.c -extra_link_args = `gcc -print-file-name=libgcc.a` -shared +extra_link_args = "`gcc -print-file-name=libgcc.a`" -shared define_macros = HAVE_CAIRO HAVE_GTK2 [extension=fast_taunt] diff --git a/distutils2/tests/test_index_dist.py b/distutils2/tests/test_index_dist.py index c09e661..dcc8648 100644 --- a/distutils2/tests/test_index_dist.py +++ b/distutils2/tests/test_index_dist.py @@ -127,7 +127,7 @@ class TestDistInfo(TempdirManager, unittest.TestCase): url = "%s/simple/foobar/foobar-0.1.tar.gz" % server.full_address # check md5 if given dist = Dist(url=url, hashname="md5", - hashval="d41d8cd98f00b204e9800998ecf8427e") + hashval="fe18804c5b722ff024cabdf514924fc4") dist.download(self.mkdtemp()) # a wrong md5 fails @@ -157,6 +157,24 @@ class TestDistInfo(TempdirManager, unittest.TestCase): hashname="invalid_hashname", hashval="value") + @use_pypi_server('downloads_with_md5') + def test_unpack(self, server): + url = "%s/simple/foobar/foobar-0.1.tar.gz" % server.full_address + dist = Dist(url=url) + # doing an unpack + here = self.mkdtemp() + there = dist.unpack(here) + result = os.listdir(there) + self.assertIn('paf', result) + + def test_hashname(self): + # Invalid hashnames raises an exception on assignation + Dist(hashname="md5", hashval="value") + + self.assertRaises(UnsupportedHashName, Dist, + hashname="invalid_hashname", + hashval="value") + class TestReleasesList(unittest.TestCase): diff --git a/distutils2/tests/test_install.py b/distutils2/tests/test_install.py index 67c61c2..8ad5ac9 100644 --- a/distutils2/tests/test_install.py +++ b/distutils2/tests/test_install.py @@ -39,6 +39,11 @@ class ToInstallDist(object): for f in range(0,3): self._real_files.append(mkstemp()) + def _unlink_installed_files(self): + if self._files: + for f in self._real_files: + os.unlink(f[1]) + def get_installed_files(self, **args): if self._files: return [f[1] for f in self._real_files] @@ -54,14 +59,14 @@ class MagicMock(object): self._called_with = [] self._return_value = return_value self._raise = raise_exception - + def __call__(self, *args, **kwargs): self.called = True self._times_called = self._times_called + 1 self._called_with.append((args, kwargs)) iterable = hasattr(self._raise, '__iter__') if self._raise: - if ((not iterable and self._raise) + if ((not iterable and self._raise) or self._raise[self._times_called - 1]): raise Exception return self._return_value @@ -70,25 +75,8 @@ class MagicMock(object): return (args, kwargs) in self._called_with -def patch(parent, to_patch): - """monkey match a module""" - def wrapper(func): - print func - print dir(func) - old_func = getattr(parent, to_patch) - def wrapped(*args, **kwargs): - parent.__dict__[to_patch] = MagicMock() - try: - out = func(*args, **kwargs) - finally: - setattr(parent, to_patch, old_func) - return out - return wrapped - return wrapper - - def get_installed_dists(dists): - """Return a list of fake installed dists. + """Return a list of fake installed dists. The list is name, version, deps""" objects = [] for (name, version, deps) in dists: @@ -100,12 +88,6 @@ class TestInstall(TempdirManager, unittest.TestCase): def _get_client(self, server, *args, **kwargs): return Client(server.full_address, *args, **kwargs) - def _patch_run_install(self): - """Patch run install""" - - def _unpatch_run_install(self): - """Unpatch run install for d2 and d1""" - def _get_results(self, output): """return a list of results""" installed = [(o.name, '%s' % o.version) for o in output['install']] @@ -205,7 +187,7 @@ class TestInstall(TempdirManager, unittest.TestCase): ]) # name, version, deps. - already_installed = [('bacon', '0.1', []), + already_installed = [('bacon', '0.1', []), ('chicken', '1.1', ['bacon (0.1)'])] output = install.get_infos("choxie", index=client, installed= get_installed_dists(already_installed)) @@ -236,7 +218,7 @@ class TestInstall(TempdirManager, unittest.TestCase): files = [os.path.join(path, '%s' % x) for x in range(1, 20)] for f in files: file(f, 'a+') - output = [o for o in install.move_files(files, newpath)] + output = [o for o in install._move_files(files, newpath)] # check that output return the list of old/new places for f in files: @@ -265,19 +247,19 @@ class TestInstall(TempdirManager, unittest.TestCase): old_install_dist = install._install_dist old_uninstall = getattr(install, 'uninstall', None) - install._install_dist = MagicMock(return_value=[], + install._install_dist = MagicMock(return_value=[], raise_exception=(False, True)) - install.uninstall = MagicMock() + install.remove = MagicMock() try: d1 = ToInstallDist() d2 = ToInstallDist() path = self.mkdtemp() self.assertRaises(Exception, install.install_dists, [d1, d2], path) self.assertTrue(install._install_dist.called_with(d1, path)) - self.assertTrue(install.uninstall.called) + self.assertTrue(install.remove.called) finally: install._install_dist = old_install_dist - install.uninstall = old_uninstall + install.remove = old_uninstall def test_install_dists_success(self): @@ -322,7 +304,7 @@ class TestInstall(TempdirManager, unittest.TestCase): old_install_dist = install._install_dist old_uninstall = getattr(install, 'uninstall', None) - install._install_dist = MagicMock(return_value=[], + install._install_dist = MagicMock(return_value=[], raise_exception=(False, True)) install.uninstall = MagicMock() try: @@ -331,14 +313,17 @@ class TestInstall(TempdirManager, unittest.TestCase): for i in range(0,2): remove.append(ToInstallDist(files=True)) to_install = [ToInstallDist(), ToInstallDist()] + temp_dir = self.mkdtemp() - self.assertRaises(Exception, install.install_from_infos, - remove=remove, install=to_install) + self.assertRaises(Exception, install.install_from_infos, + install_path=temp_dir, install=to_install, + remove=remove) # assert that the files are in the same place # assert that the files have been removed for dist in remove: for f in dist.get_installed_files(): self.assertTrue(os.path.exists(f)) + dist._unlink_installed_files() finally: install.install_dist = old_install_dist install.uninstall = old_uninstall @@ -352,8 +337,7 @@ class TestInstall(TempdirManager, unittest.TestCase): install_path = "my_install_path" to_install = [ToInstallDist(), ToInstallDist()] - install.install_from_infos(install=to_install, - install_path=install_path) + install.install_from_infos(install_path, install=to_install) for dist in to_install: install._install_dist.called_with(install_path) finally: diff --git a/docs/design/configfile.rst b/docs/design/configfile.rst new file mode 100644 index 0000000..ec1e52f --- /dev/null +++ b/docs/design/configfile.rst @@ -0,0 +1,132 @@ +.. _setup-config: + +************************************ +Writing the Setup Configuration File +************************************ + +Often, it's not possible to write down everything needed to build a distribution +*a priori*: you may need to get some information from the user, or from the +user's system, in order to proceed. As long as that information is fairly +simple---a list of directories to search for C header files or libraries, for +example---then providing a configuration file, :file:`setup.cfg`, for users to +edit is a cheap and easy way to solicit it. Configuration files also let you +provide default values for any command option, which the installer can then +override either on the command line or by editing the config file. + +The setup configuration file is a useful middle-ground between the setup script +---which, ideally, would be opaque to installers [#]_---and the command line to +the setup script, which is outside of your control and entirely up to the +installer. In fact, :file:`setup.cfg` (and any other Distutils configuration +files present on the target system) are processed after the contents of the +setup script, but before the command line. This has several useful +consequences: + +.. If you have more advanced needs, such as determining which extensions to + build based on what capabilities are present on the target system, then you + need the Distutils auto-configuration facility. This started to appear in + Distutils 0.9 but, as of this writing, isn't mature or stable enough yet + for real-world use. + +* installers can override some of what you put in :file:`setup.py` by editing + :file:`setup.cfg` + +* you can provide non-standard defaults for options that are not easily set in + :file:`setup.py` + +* installers can override anything in :file:`setup.cfg` using the command-line + options to :file:`setup.py` + +The basic syntax of the configuration file is simple:: + + [command] + option=value + ... + +where *command* is one of the Distutils commands (e.g. :command:`build_py`, +:command:`install`), and *option* is one of the options that command supports. +Any number of options can be supplied for each command, and any number of +command sections can be included in the file. Blank lines are ignored, as are +comments, which run from a ``'#'`` character until the end of the line. Long +option values can be split across multiple lines simply by indenting the +continuation lines. + +You can find out the list of options supported by a particular command with the +universal :option:`--help` option, e.g. :: + + > python setup.py --help build_ext + [...] + Options for 'build_ext' command: + --build-lib (-b) directory for compiled extension modules + --build-temp (-t) directory for temporary files (build by-products) + --inplace (-i) ignore build-lib and put compiled extensions into the + source directory alongside your pure Python modules + --include-dirs (-I) list of directories to search for header files + --define (-D) C preprocessor macros to define + --undef (-U) C preprocessor macros to undefine + --swig-opts list of SWIG command-line options + [...] + +.. XXX do we want to support ``setup.py --help metadata``? + +Note that an option spelled :option:`--foo-bar` on the command line is spelled +:option:`foo_bar` in configuration files. + +For example, say you want your extensions to be built "in-place"---that is, you +have an extension :mod:`pkg.ext`, and you want the compiled extension file +(:file:`ext.so` on Unix, say) to be put in the same source directory as your +pure Python modules :mod:`pkg.mod1` and :mod:`pkg.mod2`. You can always use the +:option:`--inplace` option on the command line to ensure this:: + + python setup.py build_ext --inplace + +But this requires that you always specify the :command:`build_ext` command +explicitly, and remember to provide :option:`--inplace`. An easier way is to +"set and forget" this option, by encoding it in :file:`setup.cfg`, the +configuration file for this distribution:: + + [build_ext] + inplace=1 + +This will affect all builds of this module distribution, whether or not you +explicitly specify :command:`build_ext`. If you include :file:`setup.cfg` in +your source distribution, it will also affect end-user builds---which is +probably a bad idea for this option, since always building extensions in-place +would break installation of the module distribution. In certain peculiar cases, +though, modules are built right in their installation directory, so this is +conceivably a useful ability. (Distributing extensions that expect to be built +in their installation directory is almost always a bad idea, though.) + +Another example: certain commands take a lot of options that don't change from +run to run; for example, :command:`bdist_rpm` needs to know everything required +to generate a "spec" file for creating an RPM distribution. Some of this +information comes from the setup script, and some is automatically generated by +the Distutils (such as the list of files installed). But some of it has to be +supplied as options to :command:`bdist_rpm`, which would be very tedious to do +on the command line for every run. Hence, here is a snippet from the Distutils' +own :file:`setup.cfg`:: + + [bdist_rpm] + release = 1 + packager = Greg Ward <gward@python.net> + doc_files = CHANGES.txt + README.txt + USAGE.txt + doc/ + examples/ + +Note that the :option:`doc_files` option is simply a whitespace-separated string +split across multiple lines for readability. + + +.. seealso:: + + :ref:`inst-config-syntax` in "Installing Python Modules" + More information on the configuration files is available in the manual for + system administrators. + + +.. rubric:: Footnotes + +.. [#] This ideal probably won't be achieved until auto-configuration is fully + supported by the Distutils. + diff --git a/docs/source/distutils/sourcedist.rst b/docs/source/distutils/sourcedist.rst index 1aa722b..7ed9637 100644 --- a/docs/source/distutils/sourcedist.rst +++ b/docs/source/distutils/sourcedist.rst @@ -86,8 +86,7 @@ source distribution: distributions, but in the future there will be a standard for testing Python module distributions) -* :file:`README.txt` (or :file:`README`), :file:`setup.py` (or whatever you - called your setup script), and :file:`setup.cfg` +* The configuration file :file:`setup.cfg` * all files that matches the ``package_data`` metadata. See :ref:`distutils-installing-package-data`. @@ -95,6 +94,10 @@ source distribution: * all files that matches the ``data_files`` metadata. See :ref:`distutils-additional-files`. +.. Warning:: + In Distutils2, setup.py and README (or README.txt) files are not more + included in source distribution by default + Sometimes this is enough, but usually you will want to specify additional files to distribute. The typical way to do this is to write a *manifest template*, called :file:`MANIFEST.in` by default. The manifest template is just a list of diff --git a/docs/source/setupcfg.rst b/docs/source/setupcfg.rst index 68a37eb..af3adea 100644 --- a/docs/source/setupcfg.rst +++ b/docs/source/setupcfg.rst @@ -7,24 +7,35 @@ Distutils2 to replace the :file:`setup.py` file. Each section contains a description of its options. -- Options that are marked *\*multi* can have multiple values, one value - per line. +- Options that are marked *\*multi* can have multiple values, one value per + line. - Options that are marked *\*optional* can be omited. -- Options that are marked *\*environ* can use environement markes, as described - in PEP 345. +- Options that are marked *\*environ* can use environment markers, as described + in :PEP:`345`. + The sections are: -- global -- metadata -- files -- command sections +global + Global options for Distutils2. + +metadata + The metadata section contains the metadata for the project as described in + :PEP:`345`. + +files + Declaration of package files included in the project. + +`command` sections + Redefinition of user options for Distutils2 commands. global ====== -Contains global options for Distutils2. This section is shared with Distutils1. +Contains global options for Distutils2. This section is shared with Distutils1 +(legacy version distributed in python 2.X standard library). + - **commands**: Defined Distutils2 command. A command is defined by its fully qualified name. @@ -38,13 +49,13 @@ Contains global options for Distutils2. This section is shared with Distutils1. *\*optional* *\*multi* - **compilers**: Defined Distutils2 compiler. A compiler is defined by its fully - qualified name. + qualified name. Example:: [global] compiler = - package.compilers.CustomCCompiler + package.compiler.CustomCCompiler *\*optional* *\*multi* @@ -52,21 +63,29 @@ Contains global options for Distutils2. This section is shared with Distutils1. :file:`setup.cfg` file is read. The callable receives the configuration in form of a mapping and can make some changes to it. *\*optional* + Example:: + + [global] + setup_hook = + distutils2.tests.test_config.hook + metadata ======== The metadata section contains the metadata for the project as described in -PEP 345. +:PEP:`345`. +.. Note:: + Field names are case-insensitive. Fields: - **name**: Name of the project. -- **version**: Version of the project. Must comply with PEP 386. +- **version**: Version of the project. Must comply with :PEP:`386`. - **platform**: Platform specification describing an operating system supported by the distribution which is not listed in the "Operating System" Trove - classifiers. *\*multi* *\*optional* + classifiers (:PEP:`301`). *\*multi* *\*optional* - **supported-platform**: Binary distributions containing a PKG-INFO file will use the Supported-Platform field in their metadata to specify the OS and CPU for which the binary distribution was compiled. The semantics of @@ -113,14 +132,18 @@ Example:: name = pypi2rpm version = 0.1 author = Tarek Ziade - author_email = tarek@ziade.org + author-email = tarek@ziade.org summary = Script that transforms a sdist archive into a rpm archive description-file = README - home_page = http://bitbucket.org/tarek/pypi2rpm + home-page = http://bitbucket.org/tarek/pypi2rpm + project-url: RSS feed, https://bitbucket.org/tarek/pypi2rpm/rss classifier = Development Status :: 3 - Alpha License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1) +.. Note:: + Some metadata fields seen in :PEP:`345` are automatically generated + (for instance Metadata-Version value). files @@ -148,6 +171,10 @@ Example:: extra_files = setup.py + README + +.. Note:: + In Distutils2, setup.cfg will be implicitly included. data-files ========== @@ -369,17 +396,27 @@ Setup.cfg:: doc/ * = {doc} doc/ man = {man} -We use brace expansion syntax to place all the bash and batch scripts into {scripts} category. +We use brace expansion syntax to place all the bash and batch scripts into {scripts} category. -command sections -================ +.. Warning:: + In Distutils2, setup.py and README (or README.txt) files are not more + included in source distribution by default -Each command can have its options described in :file:`setup.cfg` +`command` sections +================== + +Each Distutils2 command can have its own user options defined in :file:`setup.cfg` Example:: [sdist] - manifest_makers = package.module.Maker + manifest-builders = package.module.Maker + + +To override the build class in order to generate Python3 code from your Python2 base:: + + [build_py] + use-2to3 = True |
