[Python-checkins] distutils2: merge

tarek.ziade python-checkins at python.org
Sun Sep 19 10:20:21 CEST 2010


tarek.ziade pushed bc408018ca27 to distutils2:
http://hg.python.org/distutils2/rev/bc408018ca27
changeset: 609:bc408018ca27
parent: 608:10da588ede3e
parent: 521:e51ba85f26cd
user: Konrad Delong <konryd at gmail.com>
date: Mon Aug 09 18:22:43 2010 +0200
summary: merge
files: src/distutils2/dist.py
diff --git a/docs/source/projects-index.dist.rst b/docs/source/projects-index.dist.rst
--- a/docs/source/projects-index.dist.rst
+++ b/docs/source/projects-index.dist.rst
@@ -84,26 +84,27 @@
 {'url': 'http://example.org/foobar-1.0.tar.gz', 'hashname': None, 'hashval':
 None, 'is_external': True}
 
-Attributes Lazy loading
------------------------
+Getting attributes from the dist objects 
+-----------------------------------------
 
 To abstract a maximum the way of querying informations to the indexes,
-attributes and releases informations can be retrieved "on demand", in a "lazy"
-way.
+attributes and releases informations can be retrieved directly from the objects
+returned by the indexes. 
 
 For instance, if you have a release instance that does not contain the metadata
-attribute, it can be build directly when accedded::
+attribute, it can be fetched by using the "fetch_metadata" method::
 
 >>> r = Release("FooBar", "1.1")
- >>> print r._metadata 
+ >>> print r.metadata 
 None # metadata field is actually set to "None"
- >>> r.metadata
+ >>> r.fetch_metadata()
 <Metadata for FooBar 1.1>
 
-Like this, it's possible to retrieve project's releases, releases metadata and 
-releases distributions informations. 
+Like this, it's possible to retrieve project's releases (`fetch_releases`), 
+releases metadata (`fetch_metadata` and releases distributions
+(`fetch_distributions` informations). 
 
 Internally, this is possible because while retrieving for the first time
 informations about projects, releases or distributions, a reference to the
-client used is stored in the objects. Then, while trying to access undefined
-fields, it will be used if necessary.
+client used is stored in the objects (can be accessed using the object 
+`_index` attribute. 
diff --git a/docs/source/projects-index.xmlrpc.rst b/docs/source/projects-index.xmlrpc.rst
--- a/docs/source/projects-index.xmlrpc.rst
+++ b/docs/source/projects-index.xmlrpc.rst
@@ -126,24 +126,3 @@
 
 As you see, this does not return a list of distributions, but a release, 
 because a release can be used like a list of distributions. 
-
-Lazy load information from project, releases and distributions.
-----------------------------------------------------------------
-
-.. note:: The lazy loading feature is not currently available !
-
-As :mod:`distutils2.index.dist` classes support "lazy" loading of 
-informations, you can use it while retrieving informations from XML-RPC.
-
-For instance, it's possible to get all the releases for a project, and to access
-directly the metadata of each release, without making
-:class:`distutils2.index.xmlrpc.Client` directly (they will be made, but they're
-invisible to the you)::
-
- >>> client = xmlrpc.Client()
- >>> releases = client.get_releases("FooBar")
- >>> releases.get_release("1.1").metadata
- <Metadata for FooBar 1.1>
-
-Refer to the :mod:`distutils2.index.dist` documentation for more information
-about attributes lazy loading.
diff --git a/src/DEVNOTES.txt b/src/DEVNOTES.txt
--- a/src/DEVNOTES.txt
+++ b/src/DEVNOTES.txt
@@ -17,6 +17,5 @@
 
 - DistributionMetadata > Metadata or ReleaseMetadata
 - pkgutil > pkgutil.__init__ + new pkgutil.database (or better name)
- - pypi > index
 - RationalizedVersion > Version
 - suggest_rationalized_version > suggest
diff --git a/src/distutils2/__init__.py b/src/distutils2/__init__.py
--- a/src/distutils2/__init__.py
+++ b/src/distutils2/__init__.py
@@ -20,7 +20,7 @@
 __version__ = "1.0a2"
 
 
-# when set to True, converts doctests by default too 
+# when set to True, converts doctests by default too
 run_2to3_on_doctests = True
 # Standard package names for fixer packages
 lib2to3_fixer_packages = ['lib2to3.fixes']
diff --git a/src/distutils2/command/__init__.py b/src/distutils2/command/__init__.py
--- a/src/distutils2/command/__init__.py
+++ b/src/distutils2/command/__init__.py
@@ -5,7 +5,8 @@
 
 __revision__ = "$Id: __init__.py 71473 2009年04月11日 14:55:07Z tarek.ziade $"
 
-__all__ = ['build',
+__all__ = ['check',
+ 'build',
 'build_py',
 'build_ext',
 'build_clib',
@@ -16,17 +17,11 @@
 'install_headers',
 'install_scripts',
 'install_data',
+ 'install_distinfo',
 'sdist',
- 'register',
 'bdist',
 'bdist_dumb',
 'bdist_wininst',
+ 'register',
 'upload',
- 'check',
- # These two are reserved for future use:
- #'bdist_sdux',
- #'bdist_pkgtool',
- # Note:
- # bdist_packager is not included because it only provides
- # an abstract base class
 ]
diff --git a/src/distutils2/command/build_py.py b/src/distutils2/command/build_py.py
--- a/src/distutils2/command/build_py.py
+++ b/src/distutils2/command/build_py.py
@@ -371,7 +371,12 @@
 return modules
 
 def get_source_files(self):
- return [module[-1] for module in self.find_all_modules()]
+ sources = [module[-1] for module in self.find_all_modules()]
+ sources += [
+ os.path.join(src_dir, filename)
+ for package, src_dir, build_dir, filenames in self.data_files
+ for filename in filenames]
+ return sources
 
 def get_module_outfile(self, build_dir, package, module):
 outfile_path = [build_dir] + list(package) + [module + ".py"]
@@ -393,8 +398,7 @@
 outputs += [
 os.path.join(build_dir, filename)
 for package, src_dir, build_dir, filenames in self.data_files
- for filename in filenames
- ]
+ for filename in filenames]
 
 return outputs
 
diff --git a/src/distutils2/command/install.py b/src/distutils2/command/install.py
--- a/src/distutils2/command/install.py
+++ b/src/distutils2/command/install.py
@@ -18,6 +18,7 @@
 from distutils2.util import convert_path, change_root, get_platform
 from distutils2.errors import DistutilsOptionError
 
+
 class install(Command):
 
 description = "install everything from build directory"
@@ -31,7 +32,7 @@
 ('home=', None,
 "(Unix only) home directory to install under"),
 
- # Or, just set the base director(y|ies)
+ # Or just set the base director(y|ies)
 ('install-base=', None,
 "base installation directory (instead of --prefix or --home)"),
 ('install-platbase=', None,
@@ -40,7 +41,7 @@
 ('root=', None,
 "install everything relative to this alternate root directory"),
 
- # Or, explicitly set the installation scheme
+ # Or explicitly set the installation scheme
 ('install-purelib=', None,
 "installation directory for pure Python module distributions"),
 ('install-platlib=', None,
@@ -62,8 +63,8 @@
 ('compile', 'c', "compile .py to .pyc [default]"),
 ('no-compile', None, "don't compile .py files"),
 ('optimize=', 'O',
- "also compile with optimization: -O1 for \"python -O\", "
- "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
+ 'also compile with optimization: -O1 for "python -O", '
+ '-O2 for "python -OO", and -O0 to disable [default: -O0]'),
 
 # Miscellaneous control options
 ('force', 'f',
@@ -77,34 +78,45 @@
 #('install-html=', None, "directory for HTML documentation"),
 #('install-info=', None, "directory for GNU info files"),
 
+ # XXX use a name that makes clear this is the old format
 ('record=', None,
- "filename in which to record list of installed files"),
+ "filename in which to record a list of installed files "
+ "(not PEP 376-compliant)"),
 
 # .dist-info related arguments, read by install_dist_info
- ('no-distinfo', None, 'do not create a .dist-info directory'),
- ('installer=', None, 'the name of the installer'),
- ('requested', None, 'generate a REQUESTED file'),
- ('no-requested', None, 'do not generate a REQUESTED file'),
- ('no-record', None, 'do not generate a RECORD file'),
+ ('no-distinfo', None,
+ "do not create a .dist-info directory"),
+ ('installer=', None,
+ "the name of the installer"),
+ ('requested', None,
+ "generate a REQUESTED file (i.e."),
+ ('no-requested', None,
+ "do not generate a REQUESTED file"),
+ ('no-record', None,
+ "do not generate a RECORD file"),
 ]
 
- boolean_options = ['compile', 'force', 'skip-build', 'no-dist-info',
- 'requested', 'no-dist-record',]
+ boolean_options = ['compile', 'force', 'skip-build', 'no-distinfo',
+ 'requested', 'no-record']
 
- user_options.append(('user', None,
- "install in user site-package '%s'" % \
- get_path('purelib', '%s_user' % os.name)))
- boolean_options.append('user')
- negative_opt = {'no-compile' : 'compile', 'no-requested': 'requested'}
+ if sys.version >= '2.6':
+ user_options.append(
+ ('user', None,
+ "install in user site-package [%s]" %
+ get_path('purelib', '%s_user' % os.name)))
 
+ boolean_options.append('user')
+
+ negative_opt = {'no-compile': 'compile', 'no-requested': 'requested'}
 
 def initialize_options(self):
- """Initializes options."""
 # High-level options: these select both an installation base
 # and scheme.
 self.prefix = None
 self.exec_prefix = None
 self.home = None
+ # This attribute is used all over the place, so it's best to
+ # define it even in < 2.6
 self.user = 0
 
 # These select only the installation base; it's up to the user to
@@ -174,7 +186,6 @@
 self.requested = None
 self.no_record = None
 
-
 # -- Option finalizing methods -------------------------------------
 # (This is rather more involved than for most commands,
 # because this is where the policy for installing third-
@@ -182,7 +193,6 @@
 # array of user input is decided. Yes, it's quite complex!)
 
 def finalize_options(self):
- """Finalizes options."""
 # This method (and its pliant slaves, like 'finalize_unix()',
 # 'finalize_other()', and 'select_scheme()') is where the default
 # installation directories for modules, extension modules, and
@@ -199,18 +209,19 @@
 
 if ((self.prefix or self.exec_prefix or self.home) and
 (self.install_base or self.install_platbase)):
- raise DistutilsOptionError, \
- ("must supply either prefix/exec-prefix/home or " +
- "install-base/install-platbase -- not both")
+ raise DistutilsOptionError(
+ "must supply either prefix/exec-prefix/home or "
+ "install-base/install-platbase -- not both")
 
 if self.home and (self.prefix or self.exec_prefix):
- raise DistutilsOptionError, \
- "must supply either home or prefix/exec-prefix -- not both"
+ raise DistutilsOptionError(
+ "must supply either home or prefix/exec-prefix -- not both")
 
 if self.user and (self.prefix or self.exec_prefix or self.home or
- self.install_base or self.install_platbase):
- raise DistutilsOptionError("can't combine user with with prefix/"
- "exec_prefix/home or install_(plat)base")
+ self.install_base or self.install_platbase):
+ raise DistutilsOptionError(
+ "can't combine user with prefix/exec_prefix/home or "
+ "install_base/install_platbase")
 
 # Next, stuff that's wrong (or dubious) only on certain platforms.
 if os.name != "posix":
@@ -245,18 +256,19 @@
 'srcdir')
 
 metadata = self.distribution.metadata
- self.config_vars = {'dist_name': metadata['Name'],
- 'dist_version': metadata['Version'],
- 'dist_fullname': metadata.get_fullname(),
- 'py_version': py_version,
- 'py_version_short': py_version[0:3],
- 'py_version_nodot': py_version[0] + py_version[2],
- 'sys_prefix': prefix,
- 'prefix': prefix,
- 'sys_exec_prefix': exec_prefix,
- 'exec_prefix': exec_prefix,
- 'srcdir': srcdir,
- }
+ self.config_vars = {
+ 'dist_name': metadata['Name'],
+ 'dist_version': metadata['Version'],
+ 'dist_fullname': metadata.get_fullname(),
+ 'py_version': py_version,
+ 'py_version_short': py_version[:3],
+ 'py_version_nodot': py_version[:3:2],
+ 'sys_prefix': prefix,
+ 'prefix': prefix,
+ 'sys_exec_prefix': exec_prefix,
+ 'exec_prefix': exec_prefix,
+ 'srcdir': srcdir,
+ }
 
 self.config_vars['userbase'] = self.install_userbase
 self.config_vars['usersite'] = self.install_usersite
@@ -284,12 +296,11 @@
 # module distribution is pure or not. Of course, if the user
 # already specified install_lib, use their selection.
 if self.install_lib is None:
- if self.distribution.ext_modules: # has extensions: non-pure
+ if self.distribution.ext_modules: # has extensions: non-pure
 self.install_lib = self.install_platlib
 else:
 self.install_lib = self.install_purelib
 
-
 # Convert directories from Unix /-separated syntax to the local
 # convention.
 self.convert_paths('lib', 'purelib', 'platlib',
@@ -301,7 +312,7 @@
 # non-packagized module distributions (hello, Numerical Python!) to
 # get their own directories.
 self.handle_extra_path()
- self.install_libbase = self.install_lib # needed for .pth file
+ self.install_libbase = self.install_lib # needed for .pth file
 self.install_lib = os.path.join(self.install_lib, self.extra_dirs)
 
 # If a new root directory was supplied, make all the installation
@@ -321,25 +332,8 @@
 if self.no_distinfo is None:
 self.no_distinfo = False
 
- def dump_dirs(self, msg):
- """Dumps the list of user options."""
- from distutils2.fancy_getopt import longopt_xlate
- log.debug(msg + ":")
- for opt in self.user_options:
- opt_name = opt[0]
- if opt_name[-1] == "=":
- opt_name = opt_name[0:-1]
- if opt_name in self.negative_opt:
- opt_name = self.negative_opt[opt_name]
- opt_name = opt_name.translate(longopt_xlate)
- val = not getattr(self, opt_name)
- else:
- opt_name = opt_name.translate(longopt_xlate)
- val = getattr(self, opt_name)
- log.debug(" %s: %s" % (opt_name, val))
-
 def finalize_unix(self):
- """Finalizes options for posix platforms."""
+ """Finalize options for posix platforms."""
 if self.install_base is not None or self.install_platbase is not None:
 if ((self.install_lib is None and
 self.install_purelib is None and
@@ -347,15 +341,15 @@
 self.install_headers is None or
 self.install_scripts is None or
 self.install_data is None):
- raise DistutilsOptionError, \
- ("install-base or install-platbase supplied, but "
- "installation scheme is incomplete")
+ raise DistutilsOptionError(
+ "install-base or install-platbase supplied, but "
+ "installation scheme is incomplete")
 return
 
 if self.user:
 if self.install_userbase is None:
 raise DistutilsPlatformError(
- "User base directory is not specified")
+ "user base directory is not specified")
 self.install_base = self.install_platbase = self.install_userbase
 self.select_scheme("posix_user")
 elif self.home is not None:
@@ -364,8 +358,8 @@
 else:
 if self.prefix is None:
 if self.exec_prefix is not None:
- raise DistutilsOptionError, \
- "must not supply exec-prefix without prefix"
+ raise DistutilsOptionError(
+ "must not supply exec-prefix without prefix")
 
 self.prefix = os.path.normpath(sys.prefix)
 self.exec_prefix = os.path.normpath(sys.exec_prefix)
@@ -379,11 +373,11 @@
 self.select_scheme("posix_prefix")
 
 def finalize_other(self):
- """Finalizes options for non-posix platforms"""
+ """Finalize options for non-posix platforms"""
 if self.user:
 if self.install_userbase is None:
 raise DistutilsPlatformError(
- "User base directory is not specified")
+ "user base directory is not specified")
 self.install_base = self.install_platbase = self.install_userbase
 self.select_scheme(os.name + "_user")
 elif self.home is not None:
@@ -397,11 +391,27 @@
 try:
 self.select_scheme(os.name)
 except KeyError:
- raise DistutilsPlatformError, \
- "I don't know how to install stuff on '%s'" % os.name
+ raise DistutilsPlatformError(
+ "no support for installation on '%s'" % os.name)
+
+ def dump_dirs(self, msg):
+ """Dump the list of user options."""
+ log.debug(msg + ":")
+ for opt in self.user_options:
+ opt_name = opt[0]
+ if opt_name[-1] == "=":
+ opt_name = opt_name[0:-1]
+ if opt_name in self.negative_opt:
+ opt_name = self.negative_opt[opt_name]
+ opt_name = opt_name.replace('-', '_')
+ val = not getattr(self, opt_name)
+ else:
+ opt_name = opt_name.replace('-', '_')
+ val = getattr(self, opt_name)
+ log.debug(" %s: %s" % (opt_name, val))
 
 def select_scheme(self, name):
- """Sets the install directories by applying the install schemes."""
+ """Set the install directories by applying the install schemes."""
 # it's the caller's problem if they supply a bad name!
 scheme = get_paths(name, expand=False)
 for key, value in scheme.items():
@@ -424,15 +434,14 @@
 setattr(self, attr, val)
 
 def expand_basedirs(self):
- """Calls `os.path.expanduser` on install_base, install_platbase and
- root."""
+ """Call `os.path.expanduser` on install_{base,platbase} and root."""
 self._expand_attrs(['install_base', 'install_platbase', 'root'])
 
 def expand_dirs(self):
- """Calls `os.path.expanduser` on install dirs."""
+ """Call `os.path.expanduser` on install dirs."""
 self._expand_attrs(['install_purelib', 'install_platlib',
 'install_lib', 'install_headers',
- 'install_scripts', 'install_data',])
+ 'install_scripts', 'install_data'])
 
 def convert_paths(self, *names):
 """Call `convert_path` over `names`."""
@@ -454,9 +463,9 @@
 elif len(self.extra_path) == 2:
 path_file, extra_dirs = self.extra_path
 else:
- raise DistutilsOptionError, \
- ("'extra_path' option must be a list, tuple, or "
- "comma-separated string with 1 or 2 elements")
+ raise DistutilsOptionError(
+ "'extra_path' option must be a list, tuple, or "
+ "comma-separated string with 1 or 2 elements")
 
 # convert to local form in case Unix notation used (as it
 # should be in setup scripts)
@@ -542,7 +551,6 @@
 else:
 self.warn("path file '%s' not created" % filename)
 
-
 # -- Reporting methods ---------------------------------------------
 
 def get_outputs(self):
@@ -597,10 +605,10 @@
 
 # 'sub_commands': a list of commands this command might have to run to
 # get its work done. See cmd.py for more info.
- sub_commands = [('install_lib', has_lib),
+ sub_commands = [('install_lib', has_lib),
 ('install_headers', has_headers),
 ('install_scripts', has_scripts),
- ('install_data', has_data),
+ ('install_data', has_data),
 # keep install_distinfo last, as it needs the record
 # with files to be completely generated
 ('install_distinfo', lambda self: not self.no_distinfo),
diff --git a/src/distutils2/command/install_data.py b/src/distutils2/command/install_data.py
--- a/src/distutils2/command/install_data.py
+++ b/src/distutils2/command/install_data.py
@@ -72,6 +72,21 @@
 (out, _) = self.copy_file(data, dir)
 self.outfiles.append(out)
 
+ def get_source_files(self):
+ sources = []
+ for item in self.data_files:
+ if isinstance(item, str): # plain file
+ item = convert_path(item)
+ if os.path.isfile(item):
+ sources.append(item)
+ else: # a (dirname, filenames) tuple
+ dirname, filenames = item
+ for f in filenames:
+ f = convert_path(f)
+ if os.path.isfile(f):
+ sources.append(f)
+ return sources
+
 def get_inputs(self):
 return self.data_files or []
 
diff --git a/src/distutils2/command/install_distinfo.py b/src/distutils2/command/install_distinfo.py
--- a/src/distutils2/command/install_distinfo.py
+++ b/src/distutils2/command/install_distinfo.py
@@ -31,18 +31,18 @@
 
 user_options = [
 ('distinfo-dir=', None,
- 'directory where the the .dist-info directory will '
- 'be installed'),
- ('installer=', None, 'the name of the installer'),
- ('requested', None, 'generate a REQUESTED file'),
- ('no-requested', None, 'do not generate a REQUESTED file'),
- ('no-record', None, 'do not generate a RECORD file'),
+ "directory where the the .dist-info directory will be installed"),
+ ('installer=', None,
+ "the name of the installer"),
+ ('requested', None,
+ "generate a REQUESTED file"),
+ ('no-requested', None,
+ "do not generate a REQUESTED file"),
+ ('no-record', None,
+ "do not generate a RECORD file"),
 ]
 
- boolean_options = [
- 'requested',
- 'no-record',
- ]
+ boolean_options = ['requested', 'no-record']
 
 negative_opt = {'no-requested': 'requested'}
 
@@ -54,14 +54,14 @@
 
 def finalize_options(self):
 self.set_undefined_options('install',
- ('installer', 'installer'),
- ('requested', 'requested'),
- ('no_record', 'no_record'))
+ 'installer', 'requested', 'no_record')
 
 self.set_undefined_options('install_lib',
 ('install_dir', 'distinfo_dir'))
 
 if self.installer is None:
+ # FIXME distutils or distutils2?
+ # + document default in the option help text above and in install
 self.installer = 'distutils'
 if self.requested is None:
 self.requested = True
@@ -144,10 +144,7 @@
 return self.outputs
 
 
-# The following routines are taken from setuptools' pkg_resources module and
-# can be replaced by importing them from pkg_resources once it is included
-# in the stdlib.
-
+# The following functions are taken from setuptools' pkg_resources module.
 
 def safe_name(name):
 """Convert an arbitrary string to a standard distribution name
diff --git a/src/distutils2/command/sdist.py b/src/distutils2/command/sdist.py
--- a/src/distutils2/command/sdist.py
+++ b/src/distutils2/command/sdist.py
@@ -170,14 +170,6 @@
 # or zipfile, or whatever.
 self.make_distribution()
 
- def check_metadata(self):
- """Deprecated API."""
- warn("distutils.command.sdist.check_metadata is deprecated, \
- use the check command instead", PendingDeprecationWarning)
- check = self.distribution.get_command_obj('check')
- check.ensure_finalized()
- check.run()
-
 def get_file_list(self):
 """Figure out the list of files to include in the source
 distribution, and put it in 'self.filelist'. This might involve
@@ -243,47 +235,9 @@
 if files:
 self.filelist.extend(files)
 
- # build_py is used to get:
- # - python modules
- # - files defined in package_data
- build_py = self.get_finalized_command('build_py')
-
- # getting python files
- if self.distribution.has_pure_modules():
- self.filelist.extend(build_py.get_source_files())
-
- # getting package_data files
- # (computed in build_py.data_files by build_py.finalize_options)
- for pkg, src_dir, build_dir, filenames in build_py.data_files:
- for filename in filenames:
- self.filelist.append(os.path.join(src_dir, filename))
-
- # getting distribution.data_files
- if self.distribution.has_data_files():
- for item in self.distribution.data_files:
- if isinstance(item, str): # plain file
- item = convert_path(item)
- if os.path.isfile(item):
- self.filelist.append(item)
- else: # a (dirname, filenames) tuple
- dirname, filenames = item
- for f in filenames:
- f = convert_path(f)
- if os.path.isfile(f):
- self.filelist.append(f)
-
- if self.distribution.has_ext_modules():
- build_ext = self.get_finalized_command('build_ext')
- self.filelist.extend(build_ext.get_source_files())
-
- if self.distribution.has_c_libraries():
- build_clib = self.get_finalized_command('build_clib')
- self.filelist.extend(build_clib.get_source_files())
-
- if self.distribution.has_scripts():
- build_scripts = self.get_finalized_command('build_scripts')
- self.filelist.extend(build_scripts.get_source_files())
-
+ for cmd_name in self.distribution.get_command_names():
+ cmd_obj = self.get_finalized_command(cmd_name)
+ self.filelist.extend(cmd_obj.get_source_files())
 
 def prune_file_list(self):
 """Prune off branches that might slip into the file list as created
diff --git a/src/distutils2/core.py b/src/distutils2/core.py
--- a/src/distutils2/core.py
+++ b/src/distutils2/core.py
@@ -33,6 +33,7 @@
 or: %(script)s cmd --help
 """
 
+
 def gen_usage(script_name):
 script = os.path.basename(script_name)
 return USAGE % {'script': script}
@@ -59,6 +60,7 @@
 'extra_objects', 'extra_compile_args', 'extra_link_args',
 'swig_opts', 'export_symbols', 'depends', 'language')
 
+
 def setup(**attrs):
 """The gateway to the Distutils: do everything your setup script needs
 to do, in a highly flexible and user-driven way. Briefly: create a
diff --git a/src/distutils2/depgraph.py b/src/distutils2/depgraph.py
--- a/src/distutils2/depgraph.py
+++ b/src/distutils2/depgraph.py
@@ -1,5 +1,5 @@
-"""Analyse the relationships between the distributions in the system and generate
-a dependency graph.
+"""Analyse the relationships between the distributions in the system
+and generate a dependency graph.
 """
 
 from distutils2.errors import DistutilsError
@@ -135,8 +135,7 @@
 requires = dist.metadata['Requires-Dist'] + dist.metadata['Requires']
 for req in requires:
 predicate = VersionPredicate(req)
- comps = req.strip().rsplit(" ", 1)
- name = comps[0]
+ name = predicate.name
 
 if not name in provided:
 graph.add_missing(dist, req)
diff --git a/src/distutils2/dist.py b/src/distutils2/dist.py
--- a/src/distutils2/dist.py
+++ b/src/distutils2/dist.py
@@ -6,12 +6,10 @@
 
 __revision__ = "$Id: dist.py 77717 2010年01月24日 00:33:32Z tarek.ziade $"
 
-import sys, os, re
-
-try:
- import warnings
-except ImportError:
- warnings = None
+import sys
+import os
+import re
+import warnings
 
 from ConfigParser import RawConfigParser
 
@@ -26,7 +24,8 @@
 # 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(object):
 """The core of the Distutils. Most of the work hiding behind 'setup'
@@ -42,7 +41,6 @@
 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
@@ -125,10 +123,8 @@
 # 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
@@ -193,18 +189,18 @@
 # These options are really the business of various commands, rather
 # than of the Distribution itself. We provide aliases for them in
 # Distribution as a convenience to the developer.
- self.packages = None
+ self.packages = []
 self.package_data = {}
 self.package_dir = None
- self.py_modules = None
- self.libraries = None
- self.headers = None
- self.ext_modules = None
+ self.py_modules = []
+ self.libraries = []
+ self.headers = []
+ self.ext_modules = []
 self.ext_package = None
- self.include_dirs = None
+ self.include_dirs = []
 self.extra_path = None
- self.scripts = None
- self.data_files = None
+ self.scripts = []
+ self.data_files = []
 self.password = ''
 self.use_2to3 = False
 self.convert_2to3_doctests = []
@@ -253,10 +249,7 @@
 setattr(self, key, val)
 else:
 msg = "Unknown distribution option: %r" % key
- if warnings is not None:
- warnings.warn(msg)
- else:
- sys.stderr.write(msg + "\n")
+ warnings.warn(msg)
 
 # no-user-cfg is handled before other command line args
 # because other args override the config files, and this
@@ -282,10 +275,10 @@
 and return the new dictionary; otherwise, return the existing
 option dictionary.
 """
- dict = self.command_options.get(command)
- if dict is None:
- dict = self.command_options[command] = {}
- return dict
+ d = self.command_options.get(command)
+ if d is None:
+ d = self.command_options[command] = {}
+ return d
 
 def get_fullname(self):
 return self.metadata.get_fullname()
@@ -381,7 +374,7 @@
 
 for opt in options:
 if opt != '__name__':
- val = parser.get(section,opt)
+ val = parser.get(section, opt)
 opt = opt.replace('-', '_')
 
 # ... although practicality beats purity :(
@@ -410,12 +403,12 @@
 try:
 if alias:
 setattr(self, alias, not strtobool(val))
- elif opt in ('verbose', 'dry_run'): # ugh!
+ elif opt in ('verbose', 'dry_run'): # ugh!
 setattr(self, opt, strtobool(val))
 else:
 setattr(self, opt, val)
 except ValueError, msg:
- raise DistutilsOptionError, msg
+ raise DistutilsOptionError(msg)
 
 # -- Command-line parsing methods ----------------------------------
 
@@ -481,7 +474,7 @@
 
 # Oops, no commands found -- an end-user error
 if not self.commands:
- raise DistutilsArgError, "no commands supplied"
+ raise DistutilsArgError("no commands supplied")
 
 # All is well: return true
 return 1
@@ -512,7 +505,7 @@
 # Pull the current command from the head of the command line
 command = args[0]
 if not command_re.match(command):
- raise SystemExit, "invalid command name '%s'" % command
+ raise SystemExit("invalid command name '%s'" % command)
 self.commands.append(command)
 
 # Dig up the command class that implements this command, so we
@@ -521,22 +514,21 @@
 try:
 cmd_class = self.get_command_class(command)
 except DistutilsModuleError, msg:
- raise DistutilsArgError, msg
+ raise DistutilsArgError(msg)
 
 # Require that the command class be derived from Command -- want
 # 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
+ raise DistutilsClassError(
+ "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
+ raise DistutilsClassError(
+ ("command class %s must provide "
+ "'user_options' attribute (a list of tuples)") % cmd_class)
 
 # If the command class has a list of negative alias options,
 # merge it in with the global negative aliases.
@@ -553,7 +545,6 @@
 else:
 help_options = []
 
-
 # All commands support the global options too, just by adding
 # in 'global_options'.
 parser.set_option_table(self.global_options +
@@ -567,10 +558,10 @@
 
 if (hasattr(cmd_class, 'help_options') and
 isinstance(cmd_class.help_options, list)):
- help_option_found=0
+ help_option_found = 0
 for (help_option, short, desc, func) in cmd_class.help_options:
 if hasattr(opts, parser.get_attr_name(help_option)):
- help_option_found=1
+ help_option_found = 1
 if hasattr(func, '__call__'):
 func()
 else:
@@ -708,25 +699,26 @@
 
 print(" %-*s %s" % (max_length, cmd, description))
 
+ def _get_command_groups(self):
+ """Helper function to retrieve all the command class names divided
+ into standard commands (listed in distutils2.command.__all__)
+ and extra commands (given in self.cmdclass and not standard
+ commands).
+ """
+ from distutils2.command import __all__ as std_commands
+ extra_commands = [cmd for cmd in self.cmdclass
+ if cmd not in std_commands]
+ return std_commands, extra_commands
+
 def print_commands(self):
 """Print out a help message listing all available commands with a
- description of each. The list is divided into "standard commands"
- (listed in distutils2.command.__all__) and "extra commands"
- (mentioned in self.cmdclass, but not a standard command). The
+ description of each. The list is divided into standard commands
+ (listed in distutils2.command.__all__) and extra commands
+ (given in self.cmdclass and not standard commands). The
 descriptions come from the command class attribute
 'description'.
 """
- import distutils2.command
- std_commands = distutils2.command.__all__
- is_std = {}
- for cmd in std_commands:
- is_std[cmd] = 1
-
- extra_commands = []
- for cmd in self.cmdclass.keys():
- if not is_std.get(cmd):
- extra_commands.append(cmd)
-
+ std_commands, extra_commands = self._get_command_groups()
 max_length = 0
 for cmd in (std_commands + extra_commands):
 if len(cmd) > max_length:
@@ -743,30 +735,17 @@
 
 def get_command_list(self):
 """Get a list of (command, description) tuples.
- The list is divided into "standard commands" (listed in
- distutils2.command.__all__) and "extra commands" (mentioned in
- self.cmdclass, but not a standard command). The descriptions come
+
+ The list is divided into standard commands (listed in
+ distutils2.command.__all__) and extra commands (given in
+ self.cmdclass and not standard commands). The descriptions come
 from the command class attribute 'description'.
 """
 # Currently this is only used on Mac OS, for the Mac-only GUI
 # Distutils interface (by Jack Jansen)
 
- import distutils2.command
- std_commands = distutils2.command.__all__
- is_std = {}
- for cmd in std_commands:
- is_std[cmd] = 1
-
- extra_commands = []
- for cmd in self.cmdclass.keys():
- if not is_std.get(cmd):
- extra_commands.append(cmd)
-
 rv = []
- for cmd in (std_commands + extra_commands):
- cls = self.cmdclass.get(cmd)
- if not cls:
- cls = self.get_command_class(cmd)
+ for cls in self.get_command_classes():
 try:
 description = cls.description
 except AttributeError:
@@ -788,6 +767,23 @@
 self.command_packages = pkgs
 return pkgs
 
+ def get_command_names(self):
+ """Return a list of all command names."""
+ return [getattr(cls, 'command_name', cls.__name__)
+ for cls in self.get_command_classes()]
+
+ def get_command_classes(self):
+ """Return a list of all command classes."""
+ std_commands, extra_commands = self._get_command_groups()
+ classes = []
+ for cmd in (std_commands + extra_commands):
+ try:
+ cls = self.cmdclass[cmd]
+ except KeyError:
+ cls = self.get_command_class(cmd)
+ classes.append(cls)
+ return classes
+
 def get_command_class(self, command):
 """Return the class that implements the Distutils command named by
 'command'. First we check the 'cmdclass' dictionary; if the
@@ -809,7 +805,7 @@
 class_name = command
 
 try:
- __import__ (module_name)
+ __import__(module_name)
 module = sys.modules[module_name]
 except ImportError:
 continue
@@ -817,16 +813,15 @@
 try:
 cls = getattr(module, class_name)
 except AttributeError:
- raise DistutilsModuleError, \
- "invalid command '%s' (no class '%s' in module '%s')" \
- % (command, class_name, module_name)
+ raise DistutilsModuleError(
+ "invalid command '%s' (no class '%s' in module '%s')" %
+ (command, class_name, module_name))
 
 self.cmdclass[command] = cls
 return cls
 
 raise DistutilsModuleError("invalid command '%s'" % command)
 
-
 def get_command_obj(self, command, create=1):
 """Return the command object for 'command'. Normally this object
 is cached on a previous call to 'get_command_obj()'; if no command
@@ -889,11 +884,11 @@
 elif hasattr(command_obj, option):
 setattr(command_obj, option, value)
 else:
- raise DistutilsOptionError, \
- ("error in %s: command '%s' has no such option '%s'"
- % (source, command_name, option))
+ raise DistutilsOptionError(
+ "error in %s: command '%s' has no such option '%s'" %
+ (source, command_name, option))
 except ValueError, msg:
- raise DistutilsOptionError, msg
+ raise DistutilsOptionError(msg)
 
 def get_reinitialized_command(self, command, reinit_subcommands=0):
 """Reinitializes a command to the state it was in when first
@@ -969,7 +964,6 @@
 self.run_command_hooks(cmd_obj, 'post_hook')
 self.have_run[command] = 1
 
-
 def run_command_hooks(self, cmd_obj, hook_kind):
 hooks = getattr(cmd_obj, hook_kind)
 if hooks is None:
@@ -979,7 +973,6 @@
 hook_func(cmd_obj)
 
 # -- Distribution query methods ------------------------------------
-
 def has_pure_modules(self):
 return len(self.packages or self.py_modules or []) > 0
 
@@ -1006,13 +999,8 @@
 not self.has_ext_modules() and
 not self.has_c_libraries())
 
- # -- Metadata query methods ----------------------------------------
 
- # If you're looking for 'get_name()', 'get_version()', and so forth,
- # they are defined in a sneaky way: the constructor binds self.get_XXX
- # to self.metadata.get_XXX. The actual code is in the
- # DistributionMetadata class, below.
-
+# XXX keep for compat or remove?
 
 def fix_help_options(options):
 """Convert a 4-tuple 'help_options' list as found in various command
diff --git a/src/distutils2/errors.py b/src/distutils2/errors.py
--- a/src/distutils2/errors.py
+++ b/src/distutils2/errors.py
@@ -10,31 +10,38 @@
 
 __revision__ = "$Id: errors.py 75901 2009年10月28日 06:45:18Z tarek.ziade $"
 
+
 class DistutilsError(Exception):
 """The root of all Distutils evil."""
 
+
 class DistutilsModuleError(DistutilsError):
 """Unable to load an expected module, or to find an expected class
 within some module (in particular, command modules and classes)."""
 
+
 class DistutilsClassError(DistutilsError):
 """Some command class (or possibly distribution class, if anyone
 feels a need to subclass Distribution) is found not to be holding
 up its end of the bargain, ie. implementing some part of the
 "command "interface."""
 
+
 class DistutilsGetoptError(DistutilsError):
 """The option table provided to 'fancy_getopt()' is bogus."""
 
+
 class DistutilsArgError(DistutilsError):
 """Raised by fancy_getopt in response to getopt.error -- ie. an
 error in the command line usage."""
 
+
 class DistutilsFileError(DistutilsError):
 """Any problems in the filesystem: expected file not found, etc.
 Typically this is for problems that we detect before IOError or
 OSError could be raised."""
 
+
 class DistutilsOptionError(DistutilsError):
 """Syntactic/semantic errors in command options, such as use of
 mutually conflicting options, or inconsistent options,
@@ -43,60 +50,76 @@
 files, or what-have-you -- but if we *know* something originated in
 the setup script, we'll raise DistutilsSetupError instead."""
 
+
 class DistutilsSetupError(DistutilsError):
 """For errors that can be definitely blamed on the setup script,
 such as invalid keyword arguments to 'setup()'."""
 
+
 class DistutilsPlatformError(DistutilsError):
 """We don't know how to do something on the current platform (but
 we do know how to do it on some platform) -- eg. trying to compile
 C files on a platform not supported by a CCompiler subclass."""
 
+
 class DistutilsExecError(DistutilsError):
 """Any problems executing an external program (such as the C
 compiler, when compiling C files)."""
 
+
 class DistutilsInternalError(DistutilsError):
 """Internal inconsistencies or impossibilities (obviously, this
 should never be seen if the code is working!)."""
 
+
 class DistutilsTemplateError(DistutilsError):
 """Syntax error in a file list template."""
 
+
 class DistutilsByteCompileError(DistutilsError):
 """Byte compile error."""
 
+
 # Exception classes used by the CCompiler implementation classes
 class CCompilerError(Exception):
 """Some compile/link operation failed."""
 
+
 class PreprocessError(CCompilerError):
 """Failure to preprocess one or more C/C++ files."""
 
+
 class CompileError(CCompilerError):
 """Failure to compile one or more C/C++ source files."""
 
+
 class LibError(CCompilerError):
 """Failure to create a static library from one or more C/C++ object
 files."""
 
+
 class LinkError(CCompilerError):
 """Failure to link one or more C/C++ object files into an executable
 or shared library file."""
 
+
 class UnknownFileError(CCompilerError):
 """Attempt to process an unknown file type."""
 
+
 class MetadataConflictError(DistutilsError):
 """Attempt to read or write metadata fields that are conflictual."""
 
+
 class MetadataUnrecognizedVersionError(DistutilsError):
 """Unknown metadata version number."""
 
+
 class IrrationalVersionError(Exception):
 """This is an irrational version."""
 pass
 
+
 class HugeMajorVersionNumError(IrrationalVersionError):
 """An irrational version because the major version number is huge
 (often because a year or date was used).
@@ -105,4 +128,3 @@
 This guard can be disabled by setting that option False.
 """
 pass
-
diff --git a/src/distutils2/extension.py b/src/distutils2/extension.py
--- a/src/distutils2/extension.py
+++ b/src/distutils2/extension.py
@@ -17,6 +17,7 @@
 # import that large-ish module (indirectly, through distutils.core) in
 # order to do anything.
 
+
 class Extension(object):
 """Just a collection of attributes that describes an extension
 module and everything needed to build it (hopefully in a portable
@@ -84,7 +85,7 @@
 
 # When adding arguments to this constructor, be sure to update
 # setup_keywords in core.py.
- def __init__ (self, name, sources,
+ def __init__(self, name, sources,
 include_dirs=None,
 define_macros=None,
 undef_macros=None,
@@ -95,11 +96,11 @@
 extra_compile_args=None,
 extra_link_args=None,
 export_symbols=None,
- swig_opts = None,
+ swig_opts=None,
 depends=None,
 language=None,
 optional=None,
- **kw # To catch unknown keywords
+ **kw # To catch unknown keywords
 ):
 if not isinstance(name, str):
 raise AssertionError("'name' must be a string")
@@ -134,4 +135,3 @@
 options = ', '.join(sorted(options))
 msg = "Unknown Extension options: %s" % options
 warnings.warn(msg)
-
diff --git a/src/distutils2/fancy_getopt.py b/src/distutils2/fancy_getopt.py
--- a/src/distutils2/fancy_getopt.py
+++ b/src/distutils2/fancy_getopt.py
@@ -30,6 +30,7 @@
 # (for use as attributes of some object).
 longopt_xlate = string.maketrans('-', '_')
 
+
 class FancyGetopt(object):
 """Wrapper around the standard 'getopt()' module that provides some
 handy extra functionality:
@@ -42,7 +43,7 @@
 on the command line sets 'verbose' to false
 """
 
- def __init__ (self, option_table=None):
+ def __init__(self, option_table=None):
 
 # The option table is (currently) a list of tuples. The
 # tuples may have 3 or four values:
@@ -180,7 +181,8 @@
 self.long_opts.append(long)
 
 if long[-1] == '=': # option takes an argument?
- if short: short = short + ':'
+ if short:
+ short = short + ':'
 long = long[0:-1]
 self.takes_arg[long] = 1
 else:
diff --git a/src/distutils2/index/base.py b/src/distutils2/index/base.py
--- a/src/distutils2/index/base.py
+++ b/src/distutils2/index/base.py
@@ -1,4 +1,3 @@
-from distutils2.version import VersionPredicate
 from distutils2.index.dist import ReleasesList
 
 
@@ -10,14 +9,6 @@
 self._prefer_source = prefer_source
 self._index = self
 
- def _get_version_predicate(self, requirements):
- """Return a VersionPredicate object, from a string or an already
- existing object.
- """
- if isinstance(requirements, str):
- requirements = VersionPredicate(requirements)
- return requirements
-
 def _get_prefer_final(self, prefer_final=None):
 """Return the prefer_final internal parameter or the specified one if
 provided"""
diff --git a/src/distutils2/index/dist.py b/src/distutils2/index/dist.py
--- a/src/distutils2/index/dist.py
+++ b/src/distutils2/index/dist.py
@@ -26,7 +26,8 @@
 from distutils2.errors import IrrationalVersionError
 from distutils2.index.errors import (HashDoesNotMatch, UnsupportedHashName,
 CantParseArchiveName)
-from distutils2.version import suggest_normalized_version, NormalizedVersion
+from distutils2.version import (suggest_normalized_version, NormalizedVersion,
+ get_version_predicate)
 from distutils2.metadata import DistributionMetadata
 from distutils2.util import untar_file, unzip_file, splitext
 
@@ -38,6 +39,7 @@
 
 
 class IndexReference(object):
+ """Mixin used to store the index reference"""
 def set_index(self, index=None):
 self._index = index
 
@@ -64,10 +66,10 @@
 self._version = None
 self.version = version
 if metadata:
- self._metadata = DistributionMetadata(mapping=metadata)
+ self.metadata = DistributionMetadata(mapping=metadata)
 else:
- self._metadata = None
- self._dists = {}
+ self.metadata = None
+ self.dists = {}
 self.hidden = hidden
 
 if 'dist_type' in kwargs:
@@ -89,25 +91,23 @@
 
 version = property(get_version, set_version)
 
- @property
- def metadata(self):
+ def fetch_metadata(self):
 """If the metadata is not set, use the indexes to get it"""
- if not self._metadata:
+ if not self.metadata:
 self._index.get_metadata(self.name, '%s' % self.version)
- return self._metadata
+ return self.metadata
 
 @property
 def is_final(self):
 """proxy to version.is_final"""
 return self.version.is_final
 
- @property
- def dists(self):
- if self._dists is None:
+ def fetch_distributions(self):
+ if self.dists is None:
 self._index.get_distributions(self.name, '%s' % self.version)
- if self._dists is None:
- self._dists = {}
- return self._dists
+ if self.dists is None:
+ self.dists = {}
+ return self.dists
 
 def add_distribution(self, dist_type='sdist', python_version=None, **params):
 """Add distribution informations to this release.
@@ -122,12 +122,12 @@
 if dist_type not in DIST_TYPES:
 raise ValueError(dist_type)
 if dist_type in self.dists:
- self._dists[dist_type].add_url(**params)
+ self.dists[dist_type].add_url(**params)
 else:
- self._dists[dist_type] = DistInfo(self, dist_type,
+ self.dists[dist_type] = DistInfo(self, dist_type,
 index=self._index, **params)
 if python_version:
- self._dists[dist_type].python_version = python_version
+ self.dists[dist_type].python_version = python_version 
 
 def get_distribution(self, dist_type=None, prefer_source=True):
 """Return a distribution.
@@ -163,9 +163,9 @@
 .download(path=temp_path)
 
 def set_metadata(self, metadata):
- if not self._metadata:
- self._metadata = DistributionMetadata()
- self._metadata.update(metadata)
+ if not self.metadata:
+ self.metadata = DistributionMetadata()
+ self.metadata.update(metadata)
 
 def __getitem__(self, item):
 """distributions are available using release["sdist"]"""
@@ -351,18 +351,12 @@
 """
 def __init__(self, name, releases=None, contains_hidden=False, index=None):
 self.set_index(index)
- self._releases = []
+ self.releases = [] 
 self.name = name
 self.contains_hidden = contains_hidden
 if releases:
 self.add_releases(releases)
 
- @property
- def releases(self):
- if not self._releases:
- self.fetch_releases()
- return self._releases
- 
 def fetch_releases(self):
 self._index.get_releases(self.name)
 return self.releases
@@ -374,12 +368,13 @@
 if predicate.match(release.version)],
 index=self._index)
 
- def get_last(self, predicate, prefer_final=None):
+ def get_last(self, requirements, prefer_final=None):
 """Return the "last" release, that satisfy the given predicates.
 
 "last" is defined by the version number of the releases, you also could
 set prefer_final parameter to True or False to change the order results
 """
+ predicate = get_version_predicate(requirements)
 releases = self.filter(predicate)
 releases.sort_releases(prefer_final, reverse=True)
 return releases[0]
@@ -412,16 +407,16 @@
 
 if not version in self.get_versions():
 # append only if not already exists
- self._releases.append(release)
+ self.releases.append(release)
 for dist in release.dists.values():
 for url in dist.urls:
 self.add_release(version, dist.dist_type, **url)
 else:
- matches = [r for r in self._releases if '%s' % r.version == version
+ matches = [r for r in self.releases if '%s' % r.version == version
 and r.name == self.name]
 if not matches:
 release = ReleaseInfo(self.name, version, index=self._index)
- self._releases.append(release)
+ self.releases.append(release)
 else:
 release = matches[0]
 
@@ -459,7 +454,7 @@
 
 def get_versions(self):
 """Return a list of releases versions contained"""
- return ["%s" % r.version for r in self._releases]
+ return ["%s" % r.version for r in self.releases]
 
 def __getitem__(self, key):
 return self.releases[key]
diff --git a/src/distutils2/index/simple.py b/src/distutils2/index/simple.py
--- a/src/distutils2/index/simple.py
+++ b/src/distutils2/index/simple.py
@@ -22,6 +22,7 @@
 ReleaseNotFound, ProjectNotFound)
 from distutils2.index.mirrors import get_mirrors
 from distutils2.metadata import DistributionMetadata
+from distutils2.version import get_version_predicate
 from distutils2 import __version__ as __distutils2_version__
 
 __all__ = ['Crawler', 'DEFAULT_SIMPLE_INDEX_URL']
@@ -158,7 +159,7 @@
 """Search for releases and return a ReleaseList object containing
 the results.
 """
- predicate = self._get_version_predicate(requirements)
+ predicate = get_version_predicate(requirements)
 if self._projects.has_key(predicate.name.lower()) and not force_update:
 return self._projects.get(predicate.name.lower())
 prefer_final = self._get_prefer_final(prefer_final)
@@ -173,7 +174,7 @@
 
 def get_release(self, requirements, prefer_final=None):
 """Return only one release that fulfill the given requirements"""
- predicate = self._get_version_predicate(requirements)
+ predicate = get_version_predicate(requirements)
 release = self.get_releases(predicate, prefer_final)\
 .get_last(predicate)
 if not release:
diff --git a/src/distutils2/index/xmlrpc.py b/src/distutils2/index/xmlrpc.py
--- a/src/distutils2/index/xmlrpc.py
+++ b/src/distutils2/index/xmlrpc.py
@@ -3,8 +3,10 @@
 
 from distutils2.errors import IrrationalVersionError
 from distutils2.index.base import BaseClient
-from distutils2.index.errors import ProjectNotFound, InvalidSearchField
+from distutils2.index.errors import (ProjectNotFound, InvalidSearchField,
+ ReleaseNotFound)
 from distutils2.index.dist import ReleaseInfo
+from distutils2.version import get_version_predicate
 
 __all__ = ['Client', 'DEFAULT_XMLRPC_INDEX_URL']
 
@@ -41,7 +43,7 @@
 related informations.
 """
 prefer_final = self._get_prefer_final(prefer_final)
- predicate = self._get_version_predicate(requirements)
+ predicate = get_version_predicate(requirements)
 releases = self.get_releases(predicate.name)
 release = releases.get_last(predicate, prefer_final)
 self.get_metadata(release.name, "%s" % release.version)
@@ -72,7 +74,7 @@
 def get_versions(project_name, show_hidden):
 return self.proxy.package_releases(project_name, show_hidden)
 
- predicate = self._get_version_predicate(requirements)
+ predicate = get_version_predicate(requirements)
 prefer_final = self._get_prefer_final(prefer_final)
 project_name = predicate.name
 if not force_update and (project_name.lower() in self._projects):
@@ -96,6 +98,8 @@
 index=self._index)
 for version in versions])
 project = project.filter(predicate)
+ if len(project) == 0:
+ raise ReleaseNotFound("%s" % predicate)
 project.sort_releases(prefer_final)
 return project
 
diff --git a/src/distutils2/install_with_deps.py b/src/distutils2/install_with_deps.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/install_with_deps.py
@@ -0,0 +1,171 @@
+import logging
+from distutils2.index import wrapper
+from distutils2.index.errors import ProjectNotFound, ReleaseNotFound
+from distutils2.depgraph import generate_graph
+from distutils2._backport.pkgutil import get_distributions
+
+
+"""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.
+"""
+
+
+def get_deps(requirements, index):
+ """Return the dependencies of a project, as a depgraph object.
+
+ Build a :class depgraph.DependencyGraph: for the given requirements
+
+ If the project does not uses Metadata < 1.1, dependencies can't be handled
+ from here, so it returns an empty list of dependencies.
+
+ :param requirements: is a string containing the version predicate to take
+ the project name and version specifier from.
+ :param index: the index to use for making searches.
+ """
+ deps = []
+ release = index.get_release(requirements)
+ requires = release.metadata['Requires-Dist'] + release.metadata['Requires']
+ deps.append(release) # include the release we are computing deps.
+ for req in requires:
+ deps.extend(get_deps(req, index))
+ return deps
+
+
+def install(requirements, index=None, interactive=True, upgrade=True,
+ prefer_source=True, prefer_final=True):
+ """Given a list of distributions to install, a list of distributions to
+ remove, and a list of conflicts, proceed and do what's needed to be done.
+
+ :param requirements: is a *string* containing the requirements for this
+ project (for instance "FooBar 1.1" or "BarBaz (<1.2)
+ :param index: If an index is specified, use this one, otherwise, use
+ :class index.ClientWrapper: to get project metadatas.
+ :param interactive: if set to True, will prompt the user for interactions
+ of needed. If false, use the default values.
+ :param upgrade: If a project exists in a newer version, does the script
+ need to install the new one, or keep the already installed
+ version.
+ :param prefer_source: used to tell if the user prefer source distributions
+ over built dists.
+ :param prefer_final: if set to true, pick up the "final" versions (eg.
+ stable) over the beta, alpha (not final) ones.
+ """
+ # get the default index if none is specified
+ if not index:
+ index = wrapper.WrapperClient()
+
+ # check if the project is already installed.
+ installed_release = get_installed_release(requirements)
+
+ # if a version that satisfy the requirements is already installed
+ if installed_release and (interactive or upgrade):
+ new_releases = index.get_releases(requirements)
+ if (new_releases.get_last(requirements).version >
+ installed_release.version):
+ if interactive:
+ # prompt the user to install the last version of the package.
+ # set upgrade here.
+ print "You want to install a package already installed on your"
+ "system. A new version exists, you could just use the version"
+ "you have, or upgrade to the latest version"
+
+ upgrade = raw_input("Do you want to install the most recent one ? (Y/n)") or "Y"
+ if upgrade in ('Y', 'y'):
+ upgrade = True
+ else:
+ upgrade = False
+ if not upgrade:
+ return
+
+ # create the depgraph from the dependencies of the release we want to
+ # install
+ graph = generate_graph(get_deps(requirements, index))
+ from ipdb import set_trace
+ set_trace()
+ installed = [] # to uninstall on errors
+ try:
+ for release in graph.adjacency_list:
+ dist = release.get_distribution()
+ install(dist)
+ installed.append(dist)
+ print "%s have been installed on your system" % requirements
+ except:
+ print "an error has occured, uninstalling"
+ for dist in installed:
+ uninstall_dist(dist)
+
+class InstallationException(Exception):
+ pass
+
+def get_install_info(requirements, index=None, already_installed=None):
+ """Return the informations on what's going to be installed and upgraded.
+
+ :param requirements: is a *string* containing the requirements for this
+ project (for instance "FooBar 1.1" or "BarBaz (<1.2)")
+ :param index: If an index is specified, use this one, otherwise, use
+ :class index.ClientWrapper: to get project metadatas.
+ :param already_installed: a list of already installed distributions.
+
+ The results are returned in a dict. For instance::
+
+ >>> get_install_info("FooBar (<=1.2)")
+ {'install': [<FooBar 1.1>], 'remove': [], 'conflict': []}
+
+ Conflict contains all the conflicting distributions, if there is a
+ conflict.
+
+ """
+ def update_infos(new_infos, infos):
+ for key, value in infos.items():
+ if key in new_infos:
+ infos[key].extend(new_infos[key])
+ return new_infos
+
+ if not index:
+ index = wrapper.ClientWrapper()
+ logging.info("searching releases for %s" % requirements)
+
+ # 1. get all the releases that match the requirements
+ try:
+ releases = index.get_releases(requirements)
+ except (ReleaseNotFound, ProjectNotFound), e:
+ raise InstallationException('Release not found: "%s"' % requirements)
+
+ # 2. pick up a release, and try to get the dependency tree
+ release = releases.get_last(requirements)
+ metadata = release.fetch_metadata()
+
+ # 3. get the distributions already_installed on the system
+ # 4. and add the one we want to install
+ if not already_installed:
+ already_installed = get_distributions()
+
+ logging.info("fetching %s %s dependencies" % (
+ release.name, release.version))
+ distributions = already_installed + [release]
+ depgraph = generate_graph(distributions)
+
+ # store all the already_installed packages in a list, in case of rollback.
+ infos = {'install':[], 'remove': [], 'conflict': []}
+
+ # 5. get what the missing deps are
+ for dists in depgraph.missing.values():
+ if dists:
+ logging.info("missing dependencies found, installing them")
+ # we have missing deps
+ for dist in dists:
+ update_infos(get_install_info(dist, index, already_installed),
+ infos)
+
+ # 6. fill in the infos
+ existing = [d for d in already_installed if d.name == release.name]
+ if existing:
+ infos['remove'].append(existing[0])
+ infos['conflict'].extend(depgraph.reverse_list[existing[0]])
+ infos['install'].append(release)
+ return infos
diff --git a/src/distutils2/manifest.py b/src/distutils2/manifest.py
--- a/src/distutils2/manifest.py
+++ b/src/distutils2/manifest.py
@@ -22,7 +22,7 @@
 
 # a \ followed by some spaces + EOL
 _COLLAPSE_PATTERN = re.compile('\\\w*\n', re.M)
-_COMMENTED_LINE = re.compile('#.*?(?=\n)|^\w*\n|\n(?=$)', re.M|re.S)
+_COMMENTED_LINE = re.compile('#.*?(?=\n)|^\w*\n|\n(?=$)', re.M | re.S)
 
 class Manifest(object):
 """A list of files built by on exploring the filesystem and filtered by
diff --git a/src/distutils2/metadata.py b/src/distutils2/metadata.py
--- a/src/distutils2/metadata.py
+++ b/src/distutils2/metadata.py
@@ -3,10 +3,10 @@
 Supports all metadata formats (1.0, 1.1, 1.2).
 """
 
-import re
 import os
 import sys
 import platform
+import re
 from StringIO import StringIO
 from email import message_from_file
 from tokenize import tokenize, NAME, OP, STRING, ENDMARKER
@@ -52,12 +52,12 @@
 PKG_INFO_PREFERRED_VERSION = '1.0'
 
 _LINE_PREFIX = re.compile('\n \|')
-_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
+_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
 'Summary', 'Description',
 'Keywords', 'Home-page', 'Author', 'Author-email',
 'License')
 
-_314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
+_314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
 'Supported-Platform', 'Summary', 'Description',
 'Keywords', 'Home-page', 'Author', 'Author-email',
 'License', 'Classifier', 'Download-URL', 'Obsoletes',
@@ -65,7 +65,7 @@
 
 _314_MARKERS = ('Obsoletes', 'Provides', 'Requires')
 
-_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
+_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
 'Supported-Platform', 'Summary', 'Description',
 'Keywords', 'Home-page', 'Author', 'Author-email',
 'Maintainer', 'Maintainer-email', 'License',
@@ -92,6 +92,7 @@
 return _345_FIELDS
 raise MetadataUnrecognizedVersionError(version)
 
+
 def _best_version(fields):
 """Detect the best version depending on the fields used."""
 def _has_marker(keys, markers):
@@ -349,7 +350,7 @@
 Empty values (e.g. None and []) are not setted this way.
 """
 def _set(key, value):
- if value not in ([], None) and key in _ATTR2FIELD:
+ if value not in ([], None, '') and key in _ATTR2FIELD:
 self.set(self._convert_name(key), value)
 
 if other is None:
@@ -387,13 +388,16 @@
 for v in value:
 # check that the values are valid predicates
 if not is_valid_predicate(v.split(';')[0]):
- warn('"%s" is not a valid predicate' % v)
+ warn('"%s" is not a valid predicate (field "%s")' %
+ (v, name))
 elif name in _VERSIONS_FIELDS and value is not None:
 if not is_valid_versions(value):
- warn('"%s" is not a valid predicate' % value)
+ warn('"%s" is not a valid version (field "%s")' %
+ (value, name))
 elif name in _VERSION_FIELDS and value is not None:
 if not is_valid_version(value):
- warn('"%s" is not a valid version' % value)
+ warn('"%s" is not a valid version (field "%s")' %
+ (value, name))
 
 if name in _UNICODEFIELDS:
 value = self._encode_field(value)
@@ -448,7 +452,7 @@
 missing.append(attr)
 
 if _HAS_DOCUTILS:
- warnings = self._check_rst_data(self['Description'])
+ warnings.extend(self._check_rst_data(self['Description']))
 
 # checking metadata 1.2 (XXX needs to check 1.1, 1.0)
 if self['Metadata-Version'] != '1.2':
@@ -497,6 +501,7 @@
 'in': lambda x, y: x in y,
 'not in': lambda x, y: x not in y}
 
+
 def _operate(operation, x, y):
 return _OPERATORS[operation](x, y)
 
@@ -508,6 +513,7 @@
 'platform.version': platform.version(),
 'platform.machine': platform.machine()}
 
+
 class _Operation(object):
 
 def __init__(self, execution_context=None):
@@ -568,6 +574,7 @@
 right = self._convert(self.right)
 return _operate(self.op, left, right)
 
+
 class _OR(object):
 def __init__(self, left, right=None):
 self.left = left
@@ -597,6 +604,7 @@
 def __call__(self):
 return self.left() and self.right()
 
+
 class _CHAIN(object):
 
 def __init__(self, execution_context=None):
@@ -658,6 +666,7 @@
 return False
 return True
 
+
 def _interpret(marker, execution_context=None):
 """Interpret a marker and return a result depending on environment."""
 marker = marker.strip()
diff --git a/src/distutils2/mkpkg.py b/src/distutils2/mkpkg.py
--- a/src/distutils2/mkpkg.py
+++ b/src/distutils2/mkpkg.py
@@ -30,7 +30,11 @@
 #
 # Detect scripts (not sure how. #! outside of package?)
 
-import sys, os, re, shutil, ConfigParser
+import sys
+import os
+import re
+import shutil
+import ConfigParser
 
 
 helpText = {
@@ -629,19 +633,20 @@
 print '\nERROR: You must select "Y" or "N".\n'
 
 
-def ask(question, default = None, helptext = None, required = True,
- lengthy = False, multiline = False):
- prompt = '%s: ' % ( question, )
+def ask(question, default=None, helptext=None, required=True,
+ lengthy=False, multiline=False):
+ prompt = '%s: ' % (question,)
 if default:
- prompt = '%s [%s]: ' % ( question, default )
+ prompt = '%s [%s]: ' % (question, default)
 if default and len(question) + len(default) > 70:
- prompt = '%s\n [%s]: ' % ( question, default )
+ prompt = '%s\n [%s]: ' % (question, default)
 if lengthy or multiline:
 prompt += '\n >'
 
- if not helptext: helptext = 'No additional help available.'
- if helptext[0] == '\n': helptext = helptext[1:]
- if helptext[-1] == '\n': helptext = helptext[:-1]
+ if not helptext:
+ helptext = 'No additional help available.'
+
+ helptext = helptext.strip("\n")
 
 while True:
 sys.stdout.write(prompt)
@@ -653,12 +658,14 @@
 print helptext
 print '=' * 70
 continue
- if default and not line: return(default)
+ if default and not line:
+ return(default)
 if not line and required:
 print '*' * 70
 print 'This value cannot be empty.'
 print '==========================='
- if helptext: print helptext
+ if helptext:
+ print helptext
 print '*' * 70
 continue
 return(line)
@@ -669,7 +676,8 @@
 for key in troveList:
 subDict = dict
 for subkey in key.split(' :: '):
- if not subkey in subDict: subDict[subkey] = {}
+ if not subkey in subDict:
+ subDict[subkey] = {}
 subDict = subDict[subkey]
 return(dict)
 troveDict = buildTroveDict(troveList)
@@ -687,7 +695,8 @@
 
 
 def lookupOption(self, key):
- if not self.config.has_option('DEFAULT', key): return(None)
+ if not self.config.has_option('DEFAULT', key):
+ return(None)
 return(self.config.get('DEFAULT', key))
 
 
@@ -706,7 +715,8 @@
 self.config.set('DEFAULT', compareKey,
 self.setupData[compareKey])
 
- if not valuesDifferent: return
+ if not valuesDifferent:
+ return
 
 self.config.write(open(os.path.expanduser('~/.pygiver'), 'w'))
 
@@ -718,7 +728,7 @@
 def inspectFile(self, path):
 fp = open(path, 'r')
 try:
- for line in [ fp.readline() for x in range(10) ]:
+ for line in [fp.readline() for x in range(10)]:
 m = re.match(r'^#!.*python((?P<major>\d)(\.\d+)?)?$', line)
 if m:
 if m.group('major') == '3':
@@ -761,16 +771,16 @@
 self.setupData.get('version'), helpText['version'])
 self.setupData['description'] = ask('Package description',
 self.setupData.get('description'), helpText['description'],
- lengthy = True)
+ lengthy=True)
 self.setupData['author'] = ask('Author name',
 self.setupData.get('author'), helpText['author'])
 self.setupData['author_email'] = ask('Author e-mail address',
 self.setupData.get('author_email'), helpText['author_email'])
 self.setupData['url'] = ask('Project URL',
- self.setupData.get('url'), helpText['url'], required = False)
+ self.setupData.get('url'), helpText['url'], required=False)
 
 if (askYn('Do you want to set Trove classifiers?',
- helptext = helpText['do_classifier']) == 'y'):
+ helptext=helpText['do_classifier']) == 'y'):
 self.setTroveClassifier()
 
 
@@ -781,8 +791,10 @@
 
 
 def setTroveOther(self, classifierDict):
- if askYn('Do you want to set other trove identifiers', 'n',
- helpText['trove_generic']) != 'y': return
+ if askYn('Do you want to set other trove identifiers',
+ 'n',
+ helpText['trove_generic']) != 'y':
+ return
 
 self.walkTrove(classifierDict, [troveDict], '')
 
@@ -799,7 +811,7 @@
 continue
 
 if askYn('Do you want to set items under\n "%s" (%d sub-items)'
- % ( key, len(trove[key]) ), 'n',
+ % (key, len(trove[key])), 'n',
 helpText['trove_generic']) == 'y':
 self.walkTrove(classifierDict, trovePath + [trove[key]],
 desc + ' :: ' + key)
@@ -808,15 +820,18 @@
 def setTroveLicense(self, classifierDict):
 while True:
 license = ask('What license do you use',
- helptext = helpText['trove_license'], required = False)
- if not license: return
+ helptext=helpText['trove_license'],
+ required=False)
+ if not license:
+ return
 
 licenseWords = license.lower().split(' ')
 
 foundList = []
 for index in range(len(troveList)):
 troveItem = troveList[index]
- if not troveItem.startswith('License :: '): continue
+ if not troveItem.startswith('License :: '):
+ continue
 troveItem = troveItem[11:].lower()
 
 allMatch = True
@@ -824,17 +839,20 @@
 if not word in troveItem:
 allMatch = False
 break
- if allMatch: foundList.append(index)
+ if allMatch:
+ foundList.append(index)
 
 question = 'Matching licenses:\n\n'
 for i in xrange(1, len(foundList) + 1):
- question += ' %s) %s\n' % ( i, troveList[foundList[i - 1]] )
+ question += ' %s) %s\n' % (i, troveList[foundList[i - 1]])
 question += ('\nType the number of the license you wish to use or '
 '? to try again:')
- troveLicense = ask(question, required = False)
+ troveLicense = ask(question, required=False)
 
- if troveLicense == '?': continue
- if troveLicense == '': return
+ if troveLicense == '?':
+ continue
+ if troveLicense == '':
+ return
 foundIndex = foundList[int(troveLicense) - 1]
 classifierDict[troveList[foundIndex]] = 1
 try:
@@ -856,7 +874,7 @@
 6 - Mature
 7 - Inactive
 
-Status''', required = False)
+Status''', required=False)
 if devStatus:
 try:
 key = {
@@ -884,7 +902,8 @@
 return modified_pkgs
 
 def writeSetup(self):
- if os.path.exists('setup.py'): shutil.move('setup.py', 'setup.py.old')
+ if os.path.exists('setup.py'):
+ shutil.move('setup.py', 'setup.py.old')
 
 fp = open('setup.py', 'w')
 try:
diff --git a/src/distutils2/tests/pypi_server.py b/src/distutils2/tests/pypi_server.py
--- a/src/distutils2/tests/pypi_server.py
+++ b/src/distutils2/tests/pypi_server.py
@@ -1,13 +1,37 @@
-"""Mocked PyPI Server implementation, to use in tests.
+"""Mock PyPI Server implementation, to use in tests.
 
 This module also provides a simple test case to extend if you need to use
-the PyPIServer all along your test case. Be sure to read the documentation 
+the PyPIServer all along your test case. Be sure to read the documentation
 before any use.
+
+XXX TODO:
+
+The mock server can handle simple HTTP request (to simulate a simple index) or
+XMLRPC requests, over HTTP. Both does not have the same intergface to deal
+with, and I think it's a pain.
+
+A good idea could be to re-think a bit the way dstributions are handled in the
+mock server. As it should return malformed HTML pages, we need to keep the
+static behavior.
+
+I think of something like that:
+
+ >>> server = PyPIMockServer()
+ >>> server.startHTTP()
+ >>> server.startXMLRPC()
+
+Then, the server must have only one port to rely on, eg.
+
+ >>> server.fulladress()
+ "http://ip:port/"
+
+It could be simple to have one HTTP server, relaying the requests to the two
+implementations (static HTTP and XMLRPC over HTTP).
 """
 
-import os
 import Queue
 import SocketServer
+import os.path
 import select
 import socket
 import threading
@@ -29,7 +53,7 @@
 return use_pypi_server(*server_args, **server_kwargs)
 
 def use_pypi_server(*server_args, **server_kwargs):
- """Decorator to make use of the PyPIServer for test methods, 
+ """Decorator to make use of the PyPIServer for test methods,
 just when needed, and not for the entire duration of the testcase.
 """
 def wrapper(func):
@@ -51,8 +75,8 @@
 self.pypi.start()
 
 def tearDown(self):
+ super(PyPIServerTestCase, self).tearDown()
 self.pypi.stop()
- super(PyPIServerTestCase, self).tearDown()
 
 class PyPIServer(threading.Thread):
 """PyPI Mocked server.
@@ -65,8 +89,8 @@
 static_filesystem_paths=["default"],
 static_uri_paths=["simple"], serve_xmlrpc=False) :
 """Initialize the server.
- 
- Default behavior is to start the HTTP server. You can either start the 
+
+ Default behavior is to start the HTTP server. You can either start the
 xmlrpc server by setting xmlrpc to True. Caution: Only one server will
 be started.
 
@@ -80,6 +104,7 @@
 self._run = True
 self._serve_xmlrpc = serve_xmlrpc
 
+ #TODO allow to serve XMLRPC and HTTP static files at the same time.
 if not self._serve_xmlrpc:
 self.server = HTTPServer(('', 0), PyPIRequestHandler)
 self.server.RequestHandlerClass.pypi_server = self
@@ -89,7 +114,7 @@
 self.default_response_status = 200
 self.default_response_headers = [('Content-type', 'text/plain')]
 self.default_response_data = "hello"
- 
+
 # initialize static paths / filesystems
 self.static_uri_paths = static_uri_paths
 if test_static_path is not None:
@@ -97,7 +122,7 @@
 self.static_filesystem_paths = [PYPI_DEFAULT_STATIC_PATH + "/" + path
 for path in static_filesystem_paths]
 else:
- # xmlrpc server
+ # XMLRPC server
 self.server = PyPIXMLRPCServer(('', 0))
 self.xmlrpc = XMLRPCMockIndex()
 # register the xmlrpc methods
@@ -176,7 +201,7 @@
 # serve the content from local disc if we request an URL beginning
 # by a pattern defined in `static_paths`
 url_parts = self.path.split("/")
- if (len(url_parts) > 1 and 
+ if (len(url_parts) > 1 and
 url_parts[1] in self.pypi_server.static_uri_paths):
 data = None
 # always take the last first.
@@ -214,7 +239,7 @@
 try:
 status = int(status)
 except ValueError:
- # we probably got something like YYY Codename. 
+ # we probably got something like YYY Codename.
 # Just get the first 3 digits
 status = int(status[:3])
 
@@ -233,7 +258,7 @@
 self.server_port = port
 
 class MockDist(object):
- """Fake distribution, used in the Mock PyPI Server""" 
+ """Fake distribution, used in the Mock PyPI Server"""
 def __init__(self, name, version="1.0", hidden=False, url="http://url/",
 type="sdist", filename="", size=10000,
 digest="123456", downloads=7, has_sig=False,
@@ -252,7 +277,7 @@
 self.name = name
 self.version = version
 self.hidden = hidden
- 
+
 # URL infos
 self.url = url
 self.digest = digest
@@ -261,7 +286,7 @@
 self.python_version = python_version
 self.comment = comment
 self.type = type
- 
+
 # metadata
 self.author = author
 self.author_email = author_email
@@ -280,7 +305,7 @@
 self.cheesecake_documentation_id = documentation_id
 self.cheesecake_code_kwalitee_id = code_kwalitee_id
 self.cheesecake_installability_id = installability_id
- 
+
 self.obsoletes = obsoletes
 self.obsoletes_dist = obsoletes_dist
 self.provides = provides
@@ -289,7 +314,7 @@
 self.requires_dist = requires_dist
 self.requires_external = requires_external
 self.requires_python = requires_python
- 
+
 def url_infos(self):
 return {
 'url': self.url,
@@ -331,11 +356,12 @@
 'summary': self.summary,
 'home_page': self.homepage,
 'stable_version': self.stable_version,
- 'provides_dist': self.provides_dist,
+ 'provides_dist': self.provides_dist or "%s (%s)" % (self.name,
+ self.version),
 'requires': self.requires,
 'cheesecake_installability_id': self.cheesecake_installability_id,
 }
- 
+
 def search_result(self):
 return {
 '_pypi_ordering': 0,
@@ -346,7 +372,7 @@
 
 class XMLRPCMockIndex(object):
 """Mock XMLRPC server"""
- 
+
 def __init__(self, dists=[]):
 self._dists = dists
 
@@ -385,7 +411,7 @@
 # return only un-hidden
 return [d.version for d in self._dists if d.name == package_name
 and not d.hidden]
- 
+
 def release_urls(self, package_name, version):
 return [d.url_infos() for d in self._dists
 if d.name == package_name and d.version == version]
diff --git a/src/distutils2/tests/support.py b/src/distutils2/tests/support.py
--- a/src/distutils2/tests/support.py
+++ b/src/distutils2/tests/support.py
@@ -1,7 +1,7 @@
 """Support code for distutils2 test cases.
 
 Always import unittest from this module, it will be the right version
-(standard library unittest for 2.7 and higher, third-party unittest2
+(standard library unittest for 3.2 and higher, third-party unittest2
 release for older versions).
 
 Three helper classes are provided: LoggingSilencer, TempdirManager and
@@ -17,10 +17,10 @@
 tearDown):
 
 def setUp(self):
- super(self.__class__, self).setUp()
+ super(SomeTestCase, self).setUp()
 ... # other setup code
 
-Read each class' docstring to see their purpose and usage.
+Read each class' docstring to see its purpose and usage.
 
 Also provided is a DummyCommand class, useful to mock commands in the
 tests of another command that needs them (see docstring).
diff --git a/src/distutils2/tests/test_dist.py b/src/distutils2/tests/test_dist.py
--- a/src/distutils2/tests/test_dist.py
+++ b/src/distutils2/tests/test_dist.py
@@ -153,6 +153,27 @@
 my_file2 = os.path.join(tmp_dir, 'f2')
 dist.metadata.write_file(open(my_file, 'w'))
 
+ def test_bad_attr(self):
+ cls = Distribution
+
+ # catching warnings
+ warns = []
+ def _warn(msg):
+ warns.append(msg)
+
+ old_warn = warnings.warn
+ warnings.warn = _warn
+ try:
+ dist = cls(attrs={'author': 'xxx',
+ 'name': 'xxx',
+ 'version': 'xxx',
+ 'url': 'xxxx',
+ 'badoptname': 'xxx'})
+ finally:
+ warnings.warn = old_warn
+
+ self.assertTrue(len(warns)==1 and "Unknown distribution" in warns[0])
+
 def test_empty_options(self):
 # an empty options dictionary should not stay in the
 # list of attributes
@@ -176,6 +197,21 @@
 
 self.assertEqual(len(warns), 0)
 
+ def test_non_empty_options(self):
+ # TODO: how to actually use options is not documented except
+ # for a few cryptic comments in dist.py. If this is to stay
+ # in the public API, it deserves some better documentation.
+
+ # Here is an example of how it's used out there: 
+ # http://svn.pythonmac.org/py2app/py2app/trunk/doc/index.html#specifying-customizations
+ cls = Distribution
+ dist = cls(attrs={'author': 'xxx',
+ 'name': 'xxx',
+ 'version': 'xxx',
+ 'url': 'xxxx',
+ 'options': dict(sdist=dict(owner="root"))})
+ self.assertTrue("owner" in dist.get_option_dict("sdist"))
+
 def test_finalize_options(self):
 
 attrs = {'keywords': 'one,two',
diff --git a/src/distutils2/tests/test_install.py b/src/distutils2/tests/test_install.py
--- a/src/distutils2/tests/test_install.py
+++ b/src/distutils2/tests/test_install.py
@@ -202,6 +202,9 @@
 finally:
 f.close()
 
+ # XXX test that fancy_getopt is okay with options named
+ # record and no-record but unrelated
+
 def _test_debug_mode(self):
 # this covers the code called when DEBUG is set
 old_logs_len = len(self.logs)
diff --git a/src/distutils2/tests/test_install_with_deps.py b/src/distutils2/tests/test_install_with_deps.py
new file mode 100644
--- /dev/null
+++ b/src/distutils2/tests/test_install_with_deps.py
@@ -0,0 +1,152 @@
+"""Tests for the distutils2.index.xmlrpc module."""
+
+from distutils2.tests.pypi_server import use_xmlrpc_server
+from distutils2.tests import run_unittest
+from distutils2.tests.support import unittest
+from distutils2.index.xmlrpc import Client
+from distutils2.install_with_deps import (get_install_info, 
+ InstallationException)
+from distutils2.metadata import DistributionMetadata
+
+class FakeDist(object):
+ """A fake distribution object, for tests"""
+ def __init__(self, name, version, deps):
+ self.name = name
+ self.version = version
+ self.metadata = DistributionMetadata()
+ self.metadata['Requires-Dist'] = deps
+ self.metadata['Provides-Dist'] = ['%s (%s)' % (name, version)]
+
+ def __repr__(self):
+ return '<FakeDist %s>' % self.name
+
+def get_fake_dists(dists):
+ objects = []
+ for (name, version, deps) in dists:
+ objects.append(FakeDist(name, version, deps))
+ return objects
+
+class TestInstallWithDeps(unittest.TestCase):
+ def _get_client(self, server, *args, **kwargs):
+ return Client(server.full_address, *args, **kwargs)
+
+ @use_xmlrpc_server()
+ def test_existing_deps(self, server):
+ # Test that the installer get the dependencies from the metadatas
+ # and ask the index for this dependencies.
+ # In this test case, we have choxie that is dependent from towel-stuff
+ # 0.1, which is in-turn dependent on bacon <= 0.2:
+ # choxie -> towel-stuff -> bacon.
+ # Each release metadata is not provided in metadata 1.2.
+ client = self._get_client(server)
+ archive_path = '%s/distribution.tar.gz' % server.full_address
+ server.xmlrpc.set_distributions([
+ {'name':'choxie',
+ 'version': '2.0.0.9',
+ 'requires_dist': ['towel-stuff (0.1)',],
+ 'url': archive_path},
+ {'name':'towel-stuff',
+ 'version': '0.1',
+ 'requires_dist': ['bacon (<= 0.2)',],
+ 'url': archive_path},
+ {'name':'bacon',
+ 'version': '0.1',
+ 'requires_dist': [],
+ 'url': archive_path},
+ ])
+ installed = get_fake_dists([('bacon', '0.1', []),])
+ output = get_install_info("choxie", index=client, 
+ already_installed=installed)
+
+ # we dont have installed bacon as it's already installed on the system.
+ self.assertEqual(0, len(output['remove']))
+ self.assertEqual(2, len(output['install']))
+ readable_output = [(o.name, '%s' % o.version) 
+ for o in output['install']]
+ self.assertIn(('towel-stuff', '0.1'), readable_output)
+ self.assertIn(('choxie', '2.0.0.9'), readable_output)
+ 
+ @use_xmlrpc_server()
+ def test_upgrade_existing_deps(self, server):
+ # Tests that the existing distributions can be upgraded if needed.
+ client = self._get_client(server)
+ archive_path = '%s/distribution.tar.gz' % server.full_address
+ server.xmlrpc.set_distributions([
+ {'name':'choxie',
+ 'version': '2.0.0.9',
+ 'requires_dist': ['towel-stuff (0.1)',],
+ 'url': archive_path},
+ {'name':'towel-stuff',
+ 'version': '0.1',
+ 'requires_dist': ['bacon (>= 0.2)',],
+ 'url': archive_path},
+ {'name':'bacon',
+ 'version': '0.2',
+ 'requires_dist': [],
+ 'url': archive_path},
+ ])
+ 
+ output = get_install_info("choxie", index=client, already_installed= 
+ get_fake_dists([('bacon', '0.1', []),]))
+ installed = [(o.name, '%s' % o.version) for o in output['install']]
+ 
+ # we need bacon 0.2, but 0.1 is installed.
+ # So we expect to remove 0.1 and to install 0.2 instead.
+ remove = [(o.name, '%s' % o.version) for o in output['remove']]
+ self.assertIn(('choxie', '2.0.0.9'), installed)
+ self.assertIn(('towel-stuff', '0.1'), installed)
+ self.assertIn(('bacon', '0.2'), installed)
+ self.assertIn(('bacon', '0.1'), remove)
+ self.assertEqual(0, len(output['conflict']))
+
+ @use_xmlrpc_server()
+ def test_conflicts(self, server):
+ # Tests that conflicts are detected 
+ client = self._get_client(server)
+ archive_path = '%s/distribution.tar.gz' % server.full_address
+ server.xmlrpc.set_distributions([
+ {'name':'choxie',
+ 'version': '2.0.0.9',
+ 'requires_dist': ['towel-stuff (0.1)',],
+ 'url': archive_path},
+ {'name':'towel-stuff',
+ 'version': '0.1',
+ 'requires_dist': ['bacon (>= 0.2)',],
+ 'url': archive_path},
+ {'name':'bacon',
+ 'version': '0.2',
+ 'requires_dist': [],
+ 'url': archive_path},
+ ])
+ already_installed = [('bacon', '0.1', []), 
+ ('chicken', '1.1', ['bacon (0.1)'])] 
+ output = get_install_info("choxie", index=client, already_installed= 
+ get_fake_dists(already_installed))
+ 
+ # we need bacon 0.2, but 0.1 is installed.
+ # So we expect to remove 0.1 and to install 0.2 instead.
+ installed = [(o.name, '%s' % o.version) for o in output['install']]
+ remove = [(o.name, '%s' % o.version) for o in output['remove']]
+ conflict = [(o.name, '%s' % o.version) for o in output['conflict']]
+ self.assertIn(('choxie', '2.0.0.9'), installed)
+ self.assertIn(('towel-stuff', '0.1'), installed)
+ self.assertIn(('bacon', '0.2'), installed)
+ self.assertIn(('bacon', '0.1'), remove)
+ self.assertIn(('chicken', '1.1'), conflict)
+
+ @use_xmlrpc_server()
+ def test_installation_unexisting_project(self, server):
+ # Test that the isntalled raises an exception if the project does not
+ # exists.
+ client = self._get_client(server)
+ self.assertRaises(InstallationException, get_install_info, 
+ 'unexistant project', index=client)
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestInstallWithDeps))
+ return suite
+
+if __name__ == '__main__':
+ run_unittest(test_suite())
diff --git a/src/distutils2/version.py b/src/distutils2/version.py
--- a/src/distutils2/version.py
+++ b/src/distutils2/version.py
@@ -349,6 +349,7 @@
 }
 
 def __init__(self, predicate):
+ self._string = predicate
 predicate = predicate.strip()
 match = _PREDICATE.match(predicate)
 if match is None:
@@ -374,6 +375,9 @@
 return False
 return True
 
+ def __repr__(self):
+ return self._string
+
 
 class _Versions(VersionPredicate):
 def __init__(self, predicate):
@@ -418,3 +422,12 @@
 return False
 else:
 return True
+
+
+def get_version_predicate(requirements):
+ """Return a VersionPredicate object, from a string or an already
+ existing object.
+ """
+ if isinstance(requirements, str):
+ requirements = VersionPredicate(requirements)
+ return requirements
diff --git a/src/runtests-cov.py b/src/runtests-cov.py
--- a/src/runtests-cov.py
+++ b/src/runtests-cov.py
@@ -5,9 +5,19 @@
 """
 
 import sys
-from os.path import dirname, islink, realpath
+from os.path import dirname, islink, realpath, join, abspath
 from optparse import OptionParser
 
+COVERAGE_FILE = join(dirname(abspath(__file__)), '.coverage')
+
+def get_coverage():
+ """ Return a usable coverage object. """
+ # deferred import because coverage is optional
+ import coverage
+ cov = getattr(coverage, "the_coverage", None)
+ if not cov:
+ cov = coverage.coverage(COVERAGE_FILE)
+ return cov
 
 def ignore_prefixes(module):
 """ Return a list of prefixes to ignore in the coverage report if
@@ -44,10 +54,17 @@
 
 
 def coverage_report(opts):
- import coverage
 from distutils2.tests.support import unittest
- cov = coverage.coverage()
- cov.load()
+ cov = get_coverage()
+ if hasattr(cov, "load"):
+ # running coverage 3.x
+ cov.load()
+ morfs = None
+ else:
+ # running coverage 2.x
+ cov.cache = COVERAGE_FILE
+ cov.restore()
+ morfs = [m for m in cov.cexecuted.keys() if "distutils2" in m]
 
 prefixes = ["runtests", "distutils2/tests", "distutils2/_backport"]
 prefixes += ignore_prefixes(unittest)
@@ -66,7 +83,7 @@
 # that module is also completely optional
 pass
 
- cov.report(omit_prefixes=prefixes, show_missing=opts.show_missing)
+ cov.report(morfs, omit_prefixes=prefixes, show_missing=opts.show_missing)
 
 
 def test_main():
@@ -74,11 +91,8 @@
 verbose = not opts.quiet
 ret = 0
 
- if opts.coverage or opts.report:
- import coverage
-
 if opts.coverage:
- cov = coverage.coverage()
+ cov = get_coverage()
 cov.erase()
 cov.start()
 if not opts.report:
diff --git a/src/tests.sh b/src/tests.sh
--- a/src/tests.sh
+++ b/src/tests.sh
@@ -4,36 +4,37 @@
 python2.4 setup.py build_ext -f -q 2> /dev/null > /dev/null
 python2.4 -Wd runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
- echo "Failed"
+ echo Failed
 rm -f distutils2/_backport/_hashlib.so
 exit 1
 else
- echo "Success"
+ echo Success
 fi
 
 echo -n "Running tests for Python 2.5... "
 python2.5 -Wd runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
- echo "Failed"
+ echo Failed
 exit 1
 else
- echo "Success"
+ echo Success
 fi
 
 echo -n "Running tests for Python 2.6... "
 python2.6 -Wd runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
- echo "Failed"
+ echo Failed
 exit 1
 else
- echo "Success"
+ echo Success
 fi
 
 echo -n "Running tests for Python 2.7... "
 python2.7 -Wd -bb -3 runtests.py -q 2> /dev/null
 if [ $? -ne 0 ];then
- echo "Failed"
+ echo Failed
 exit 1
 else
- echo "Success"
+ echo Success
 fi
+echo Good job, commit now!
--
Repository URL: http://hg.python.org/distutils2


More information about the Python-checkins mailing list

AltStyle によって変換されたページ (->オリジナル) /