[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