summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author?ric Araujo <merwok@netwok.org>2010-08-08 02:33:52 +0200
committer?ric Araujo <merwok@netwok.org>2010-08-08 02:33:52 +0200
commited993217835aac361f665cbc6a46414466f83b4b (patch)
tree4024a7755759a507e14f8aa478b19dc351645240 /src
parent05da924b72e68e8e16ee3d3c5917118a41619c6d (diff)
parent3f478eb52c69810dbfd0f87cbc4366d0056eb2e4 (diff)
downloaddisutils2-ed993217835aac361f665cbc6a46414466f83b4b.tar.gz
Merge from Jeremy
Diffstat (limited to 'src')
-rw-r--r--src/DEVNOTES.txt17
-rwxr-xr-xsrc/check.sh2
-rw-r--r--src/distutils2/__init__.py2
-rw-r--r--src/distutils2/_backport/_hashopenssl.c (renamed from src/Modules/_hashopenssl.c)0
-rw-r--r--src/distutils2/_backport/hashlib.py10
-rw-r--r--src/distutils2/_backport/md5.c (renamed from src/Modules/md5.c)0
-rw-r--r--src/distutils2/_backport/md5.h (renamed from src/Modules/md5.h)0
-rw-r--r--src/distutils2/_backport/md5module.c (renamed from src/Modules/md5module.c)0
-rw-r--r--src/distutils2/_backport/pkgutil.py170
-rw-r--r--src/distutils2/_backport/sha256module.c (renamed from src/Modules/sha256module.c)0
-rw-r--r--src/distutils2/_backport/sha512module.c (renamed from src/Modules/sha512module.c)0
-rw-r--r--src/distutils2/_backport/shamodule.c (renamed from src/Modules/shamodule.c)0
-rw-r--r--src/distutils2/_backport/tarfile.py7
-rw-r--r--src/distutils2/_backport/tests/__init__.py5
-rw-r--r--src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA1
-rw-r--r--src/distutils2/_backport/tests/fake_dists/truffles-5.0.egg-info3
-rw-r--r--src/distutils2/_backport/tests/test_pkgutil.py58
-rw-r--r--src/distutils2/command/__init__.py13
-rw-r--r--src/distutils2/command/bdist_dumb.py4
-rw-r--r--src/distutils2/command/bdist_msi.py5
-rw-r--r--src/distutils2/command/bdist_wininst.py9
-rw-r--r--src/distutils2/command/build_clib.py4
-rw-r--r--src/distutils2/command/build_ext.py13
-rw-r--r--src/distutils2/command/build_py.py4
-rw-r--r--src/distutils2/command/build_scripts.py3
-rw-r--r--src/distutils2/command/check.py12
-rw-r--r--src/distutils2/command/clean.py10
-rw-r--r--src/distutils2/command/cmd.py46
-rw-r--r--src/distutils2/command/install.py181
-rw-r--r--src/distutils2/command/install_data.py4
-rw-r--r--src/distutils2/command/install_distinfo.py172
-rw-r--r--src/distutils2/command/install_egg_info.py83
-rw-r--r--src/distutils2/command/install_headers.py3
-rw-r--r--src/distutils2/command/install_lib.py6
-rw-r--r--src/distutils2/command/install_scripts.py4
-rw-r--r--src/distutils2/command/register.py49
-rw-r--r--src/distutils2/command/sdist.py8
-rw-r--r--src/distutils2/command/upload.py51
-rw-r--r--src/distutils2/command/upload_docs.py32
-rw-r--r--src/distutils2/compiler/ccompiler.py3
-rw-r--r--src/distutils2/config.py155
-rw-r--r--src/distutils2/core.py7
-rw-r--r--src/distutils2/depgraph.py7
-rw-r--r--src/distutils2/dist.py122
-rw-r--r--src/distutils2/errors.py24
-rw-r--r--src/distutils2/extension.py8
-rw-r--r--src/distutils2/fancy_getopt.py6
-rw-r--r--src/distutils2/index/__init__.py11
-rw-r--r--src/distutils2/index/base.py46
-rw-r--r--src/distutils2/index/dist.py544
-rw-r--r--src/distutils2/index/errors.py45
-rw-r--r--src/distutils2/index/mirrors.py52
-rw-r--r--src/distutils2/index/simple.py434
-rw-r--r--src/distutils2/index/wrapper.py93
-rw-r--r--src/distutils2/index/xmlrpc.py179
-rw-r--r--src/distutils2/install_with_deps.py171
-rw-r--r--src/distutils2/manifest.py2
-rw-r--r--src/distutils2/metadata.py157
-rwxr-xr-xsrc/distutils2/mkpkg.py87
-rw-r--r--src/distutils2/pypi/__init__.py8
-rw-r--r--src/distutils2/pypi/dist.py315
-rw-r--r--src/distutils2/pypi/errors.py33
-rw-r--r--src/distutils2/pypi/simple.py393
-rw-r--r--src/distutils2/spawn.py192
-rw-r--r--src/distutils2/tests/__init__.py2
-rw-r--r--src/distutils2/tests/conversions/02_after.py2
-rw-r--r--src/distutils2/tests/conversions/02_before.py2
-rw-r--r--src/distutils2/tests/pypi_server.py289
-rw-r--r--src/distutils2/tests/pypiserver/project_list/simple/index.html5
-rw-r--r--src/distutils2/tests/support.py133
-rw-r--r--src/distutils2/tests/test_Mixin2to3.py5
-rw-r--r--src/distutils2/tests/test_bdist.py3
-rw-r--r--src/distutils2/tests/test_bdist_msi.py4
-rw-r--r--src/distutils2/tests/test_build_clib.py2
-rw-r--r--src/distutils2/tests/test_build_ext.py7
-rw-r--r--src/distutils2/tests/test_build_py.py2
-rw-r--r--src/distutils2/tests/test_check.py2
-rw-r--r--src/distutils2/tests/test_cmd.py2
-rw-r--r--src/distutils2/tests/test_config.py117
-rw-r--r--src/distutils2/tests/test_converter.py2
-rw-r--r--src/distutils2/tests/test_core.py6
-rw-r--r--src/distutils2/tests/test_dist.py81
-rw-r--r--src/distutils2/tests/test_index_dist.py248
-rw-r--r--src/distutils2/tests/test_index_simple.py (renamed from src/distutils2/tests/test_pypi_simple.py)201
-rw-r--r--src/distutils2/tests/test_index_xmlrpc.py92
-rw-r--r--src/distutils2/tests/test_install.py16
-rw-r--r--src/distutils2/tests/test_install_distinfo.py202
-rw-r--r--src/distutils2/tests/test_install_lib.py13
-rw-r--r--src/distutils2/tests/test_install_with_deps.py152
-rw-r--r--src/distutils2/tests/test_metadata.py81
-rw-r--r--src/distutils2/tests/test_msvc9compiler.py6
-rw-r--r--src/distutils2/tests/test_pypi_dist.py247
-rw-r--r--src/distutils2/tests/test_pypi_versions.py58
-rw-r--r--src/distutils2/tests/test_register.py17
-rw-r--r--src/distutils2/tests/test_sdist.py12
-rw-r--r--src/distutils2/tests/test_spawn.py60
-rw-r--r--src/distutils2/tests/test_upload.py63
-rw-r--r--src/distutils2/tests/test_upload_docs.py35
-rw-r--r--src/distutils2/tests/test_util.py149
-rw-r--r--src/distutils2/tests/test_version.py7
-rw-r--r--src/distutils2/util.py461
-rw-r--r--src/distutils2/version.py18
-rwxr-xr-xsrc/runtests-cov.py45
-rw-r--r--src/runtests.py6
-rw-r--r--src/setup.cfg3
-rw-r--r--src/setup.py28
-rwxr-xr-xsrc/tests.sh26
107 files changed, 4610 insertions, 2387 deletions
diff --git a/src/DEVNOTES.txt b/src/DEVNOTES.txt
index 4e506f8..e59656a 100644
--- a/src/DEVNOTES.txt
+++ b/src/DEVNOTES.txt
@@ -1,10 +1,21 @@
Notes for developers
====================
-- Distutils2 runs from 2.4 to 3.2 (3.x not implemented yet), so
- make sure you don't use a syntax that doesn't work under
+- Distutils2 runs on Python from 2.4 to 3.2 (3.x not implemented yet),
+ so make sure you don't use a syntax that doesn't work under
one of these Python versions.
- Always run tests.sh before you push a change. This implies
- that you have all Python versions installed from 2.4 to 2.6.
+ that you have all Python versions installed from 2.4 to 2.7.
+- With Python 2.4, if you want to run tests with runtests.py, or run
+ just one test directly, be sure to run python2.4 setup.py build_ext
+ first, else tests won't find _hashlib or _md5. When using tests.sh,
+ build_ext is automatically done.
+
+- Renaming to do:
+
+ - DistributionMetadata > Metadata or ReleaseMetadata
+ - pkgutil > pkgutil.__init__ + new pkgutil.database (or better name)
+ - RationalizedVersion > Version
+ - suggest_rationalized_version > suggest
diff --git a/src/check.sh b/src/check.sh
new file mode 100755
index 0000000..bc77bf9
--- /dev/null
+++ b/src/check.sh
@@ -0,0 +1,2 @@
+pep8 distutils2
+pyflakes distutils2
diff --git a/src/distutils2/__init__.py b/src/distutils2/__init__.py
index 0514da7..4d330b3 100644
--- a/src/distutils2/__init__.py
+++ b/src/distutils2/__init__.py
@@ -20,7 +20,7 @@ __revision__ = "$Id: __init__.py 78020 2010-02-06 16:37:32Z benjamin.peterson $"
__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/Modules/_hashopenssl.c b/src/distutils2/_backport/_hashopenssl.c
index fdcae5d..fdcae5d 100644
--- a/src/Modules/_hashopenssl.c
+++ b/src/distutils2/_backport/_hashopenssl.c
diff --git a/src/distutils2/_backport/hashlib.py b/src/distutils2/_backport/hashlib.py
index 48f2cfd..dd2b02a 100644
--- a/src/distutils2/_backport/hashlib.py
+++ b/src/distutils2/_backport/hashlib.py
@@ -65,20 +65,20 @@ __all__ = __always_supported + ('new', 'algorithms')
def __get_builtin_constructor(name):
if name in ('SHA1', 'sha1'):
- import _sha
+ from distutils2._backport import _sha
return _sha.new
elif name in ('MD5', 'md5'):
- import _md5
+ from distutils2._backport import _md5
return _md5.new
elif name in ('SHA256', 'sha256', 'SHA224', 'sha224'):
- import _sha256
+ from distutils2._backport import _sha256
bs = name[3:]
if bs == '256':
return _sha256.sha256
elif bs == '224':
return _sha256.sha224
elif name in ('SHA512', 'sha512', 'SHA384', 'sha384'):
- import _sha512
+ from distutils2._backport import _sha512
bs = name[3:]
if bs == '512':
return _sha512.sha512
@@ -122,7 +122,7 @@ def __hash_new(name, string=''):
try:
- import _hashlib
+ from distutils2._backport import _hashlib
new = __hash_new
__get_hash = __get_openssl_constructor
except ImportError:
diff --git a/src/Modules/md5.c b/src/distutils2/_backport/md5.c
index c35d96c..c35d96c 100644
--- a/src/Modules/md5.c
+++ b/src/distutils2/_backport/md5.c
diff --git a/src/Modules/md5.h b/src/distutils2/_backport/md5.h
index 1718401..1718401 100644
--- a/src/Modules/md5.h
+++ b/src/distutils2/_backport/md5.h
diff --git a/src/Modules/md5module.c b/src/distutils2/_backport/md5module.c
index 5e4f116..5e4f116 100644
--- a/src/Modules/md5module.c
+++ b/src/distutils2/_backport/md5module.c
diff --git a/src/distutils2/_backport/pkgutil.py b/src/distutils2/_backport/pkgutil.py
index b033082..ccf45bf 100644
--- a/src/distutils2/_backport/pkgutil.py
+++ b/src/distutils2/_backport/pkgutil.py
@@ -20,6 +20,7 @@ except ImportError:
import re
import warnings
+
__all__ = [
'get_importer', 'iter_importers', 'get_loader', 'find_loader',
'walk_packages', 'iter_modules', 'get_data',
@@ -27,6 +28,7 @@ __all__ = [
'Distribution', 'EggInfoDistribution', 'distinfo_dirname',
'get_distributions', 'get_distribution', 'get_file_users',
'provides_distribution', 'obsoletes_distribution',
+ 'enable_cache', 'disable_cache', 'clear_cache'
]
@@ -187,8 +189,8 @@ class ImpImporter(object):
searches the current ``sys.path``, plus any modules that are frozen
or built-in.
- Note that :class:`ImpImporter` does not currently support being used by placement
- on ``sys.meta_path``.
+ Note that :class:`ImpImporter` does not currently support being used by
+ placement on ``sys.meta_path``.
"""
def __init__(self, path=None):
@@ -577,7 +579,8 @@ def get_data(package, resource):
argument should be the name of a package, in standard module format
(``foo.bar``). The resource argument should be in the form of a relative
filename, using ``'/'`` as the path separator. The parent directory name
- ``'..'`` is not allowed, and nor is a rooted name (starting with a ``'/'``).
+ ``'..'`` is not allowed, and nor is a rooted name (starting with a
+ ``'/'``).
The function returns a binary string, which is the contents of the
specified resource.
@@ -613,6 +616,97 @@ def get_data(package, resource):
DIST_FILES = ('INSTALLER', 'METADATA', 'RECORD', 'REQUESTED',)
+# Cache
+_cache_name = {} # maps names to Distribution instances
+_cache_name_egg = {} # maps names to EggInfoDistribution instances
+_cache_path = {} # maps paths to Distribution instances
+_cache_path_egg = {} # maps paths to EggInfoDistribution instances
+_cache_generated = False # indicates if .dist-info distributions are cached
+_cache_generated_egg = False # indicates if .dist-info and .egg are cached
+_cache_enabled = True
+
+
+def enable_cache():
+ """
+ Enables the internal cache.
+
+ Note that this function will not clear the cache in any case, for that
+ functionality see :func:`clear_cache`.
+ """
+ global _cache_enabled
+
+ _cache_enabled = True
+
+def disable_cache():
+ """
+ Disables the internal cache.
+
+ Note that this function will not clear the cache in any case, for that
+ functionality see :func:`clear_cache`.
+ """
+ global _cache_enabled
+
+ _cache_enabled = False
+
+def clear_cache():
+ """ Clears the internal cache. """
+ global _cache_name, _cache_name_egg, cache_path, _cache_path_egg, \
+ _cache_generated, _cache_generated_egg
+
+ _cache_name = {}
+ _cache_name_egg = {}
+ _cache_path = {}
+ _cache_path_egg = {}
+ _cache_generated = False
+ _cache_generated_egg = False
+
+
+def _yield_distributions(include_dist, include_egg):
+ """
+ Yield .dist-info and .egg(-info) distributions, based on the arguments
+
+ :parameter include_dist: yield .dist-info distributions
+ :parameter include_egg: yield .egg(-info) distributions
+ """
+ for path in sys.path:
+ realpath = os.path.realpath(path)
+ if not os.path.isdir(realpath):
+ continue
+ for dir in os.listdir(realpath):
+ dist_path = os.path.join(realpath, dir)
+ if include_dist and dir.endswith('.dist-info'):
+ yield Distribution(dist_path)
+ elif include_egg and (dir.endswith('.egg-info') or
+ dir.endswith('.egg')):
+ yield EggInfoDistribution(dist_path)
+
+
+def _generate_cache(use_egg_info=False):
+ global _cache_generated, _cache_generated_egg
+
+ if _cache_generated_egg or (_cache_generated and not use_egg_info):
+ return
+ else:
+ gen_dist = not _cache_generated
+ gen_egg = use_egg_info
+
+ for dist in _yield_distributions(gen_dist, gen_egg):
+ if isinstance(dist, Distribution):
+ _cache_path[dist.path] = dist
+ if not dist.name in _cache_name:
+ _cache_name[dist.name] = []
+ _cache_name[dist.name].append(dist)
+ else:
+ _cache_path_egg[dist.path] = dist
+ if not dist.name in _cache_name_egg:
+ _cache_name_egg[dist.name] = []
+ _cache_name_egg[dist.name].append(dist)
+
+ if gen_dist:
+ _cache_generated = True
+ if gen_egg:
+ _cache_generated_egg = True
+
class Distribution(object):
"""Created with the *path* of the ``.dist-info`` directory provided to the
@@ -627,15 +721,23 @@ class Distribution(object):
"""A :class:`distutils2.metadata.DistributionMetadata` instance loaded with
the distribution's ``METADATA`` file."""
requested = False
- """A boolean that indicates whether the ``REQUESTED`` metadata file is present
- (in other words, whether the package was installed by user request)."""
+ """A boolean that indicates whether the ``REQUESTED`` metadata file is
+ present (in other words, whether the package was installed by user
+ request or it was installed as a dependency)."""
def __init__(self, path):
+ if _cache_enabled and path in _cache_path:
+ self.metadata = _cache_path[path].metadata
+ else:
+ metadata_path = os.path.join(path, 'METADATA')
+ self.metadata = DistributionMetadata(path=metadata_path)
+
self.path = path
- metadata_path = os.path.join(path, 'METADATA')
- self.metadata = DistributionMetadata(path=metadata_path)
self.name = self.metadata['name']
+ if _cache_enabled and not path in _cache_path:
+ _cache_path[path] = self
+
def _get_records(self, local=False):
RECORD = os.path.join(self.path, 'RECORD')
record_reader = csv_reader(open(RECORD, 'rb'), delimiter=',')
@@ -756,6 +858,11 @@ class EggInfoDistribution(object):
def __init__(self, path):
self.path = path
+ if _cache_enabled and path in _cache_path_egg:
+ self.metadata = _cache_path_egg[path].metadata
+ self.name = self.metadata['Name']
+ return
+
# reused from Distribute's pkg_resources
def yield_lines(strs):
"""Yield non-empty/non-comment lines of a ``basestring`` or sequence"""
@@ -787,7 +894,7 @@ class EggInfoDistribution(object):
requires = zipf.get_data('EGG-INFO/requires.txt')
except IOError:
requires = None
- self.name = self.metadata['name']
+ self.name = self.metadata['Name']
elif path.endswith('.egg-info'):
if os.path.isdir(path):
path = os.path.join(path, 'PKG-INFO')
@@ -840,6 +947,9 @@ class EggInfoDistribution(object):
else:
self.metadata['Requires'] += reqs
+ if _cache_enabled:
+ _cache_path_egg[self.path] = self
+
def get_installed_files(self, local=False):
return []
@@ -898,17 +1008,17 @@ def get_distributions(use_egg_info=False):
:rtype: iterator of :class:`Distribution` and :class:`EggInfoDistribution`
instances"""
- for path in sys.path:
- realpath = os.path.realpath(path)
- if not os.path.isdir(realpath):
- continue
- for dir in os.listdir(realpath):
- if dir.endswith('.dist-info'):
- dist = Distribution(os.path.join(realpath, dir))
- yield dist
- elif use_egg_info and (dir.endswith('.egg-info') or
- dir.endswith('.egg')):
- dist = EggInfoDistribution(os.path.join(realpath, dir))
+ if not _cache_enabled:
+ for dist in _yield_distributions(True, use_egg_info):
+ yield dist
+ else:
+ _generate_cache(use_egg_info)
+
+ for dist in _cache_path.itervalues():
+ yield dist
+
+ if use_egg_info:
+ for dist in _cache_path_egg.itervalues():
yield dist
@@ -928,17 +1038,19 @@ def get_distribution(name, use_egg_info=False):
value is expected. If the directory is not found, ``None`` is returned.
:rtype: :class:`Distribution` or :class:`EggInfoDistribution` or None"""
- found = None
- for dist in get_distributions():
- if dist.name == name:
- found = dist
- break
- if use_egg_info:
- for dist in get_distributions(True):
+ if not _cache_enabled:
+ for dist in _yield_distributions(True, use_egg_info):
if dist.name == name:
- found = dist
- break
- return found
+ return dist
+ else:
+ _generate_cache(use_egg_info)
+
+ if name in _cache_name:
+ return _cache_name[name][0]
+ elif use_egg_info and name in _cache_name_egg:
+ return _cache_name_egg[name][0]
+ else:
+ return None
def obsoletes_distribution(name, version=None, use_egg_info=False):
diff --git a/src/Modules/sha256module.c b/src/distutils2/_backport/sha256module.c
index 96cbd10..96cbd10 100644
--- a/src/Modules/sha256module.c
+++ b/src/distutils2/_backport/sha256module.c
diff --git a/src/Modules/sha512module.c b/src/distutils2/_backport/sha512module.c
index dbaec17..dbaec17 100644
--- a/src/Modules/sha512module.c
+++ b/src/distutils2/_backport/sha512module.c
diff --git a/src/Modules/shamodule.c b/src/distutils2/_backport/shamodule.c
index 171dfa1..171dfa1 100644
--- a/src/Modules/shamodule.c
+++ b/src/distutils2/_backport/shamodule.c
diff --git a/src/distutils2/_backport/tarfile.py b/src/distutils2/_backport/tarfile.py
index 0d2c49b..3c95868 100644
--- a/src/distutils2/_backport/tarfile.py
+++ b/src/distutils2/_backport/tarfile.py
@@ -56,13 +56,6 @@ import operator
if not hasattr(os, 'SEEK_SET'):
os.SEEK_SET = 0
-if sys.platform == 'mac':
- # This module needs work for MacOS9, especially in the area of pathname
- # handling. In many places it is assumed a simple substitution of / by the
- # local os.path.sep is good enough to convert pathnames, but this does not
- # work with the mac rooted:path:name versus :nonrooted:path:name syntax
- raise ImportError, "tarfile does not work for platform==mac"
-
try:
import grp, pwd
except ImportError:
diff --git a/src/distutils2/_backport/tests/__init__.py b/src/distutils2/_backport/tests/__init__.py
index e816dd0..78c8142 100644
--- a/src/distutils2/_backport/tests/__init__.py
+++ b/src/distutils2/_backport/tests/__init__.py
@@ -4,7 +4,7 @@ import sys
from distutils2.tests.support import unittest
-here = os.path.dirname(__file__)
+here = os.path.dirname(__file__) or os.curdir
def test_suite():
suite = unittest.TestSuite()
@@ -16,4 +16,5 @@ def test_suite():
suite.addTest(module.test_suite())
return suite
-
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA b/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
index 1978be7..0b99f52 100644
--- a/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
+++ b/src/distutils2/_backport/tests/fake_dists/grammar-1.0a4.dist-info/METADATA
@@ -2,3 +2,4 @@ Metadata-Version: 1.2
Name: grammar
Version: 1.0a4
Requires-Dist: truffles (>=1.2)
+Author: Sherlock Holmes
diff --git a/src/distutils2/_backport/tests/fake_dists/truffles-5.0.egg-info b/src/distutils2/_backport/tests/fake_dists/truffles-5.0.egg-info
new file mode 100644
index 0000000..45f0cf8
--- /dev/null
+++ b/src/distutils2/_backport/tests/fake_dists/truffles-5.0.egg-info
@@ -0,0 +1,3 @@
+Metadata-Version: 1.2
+Name: truffles
+Version: 5.0
diff --git a/src/distutils2/_backport/tests/test_pkgutil.py b/src/distutils2/_backport/tests/test_pkgutil.py
index fafb9a8..c052b4d 100644
--- a/src/distutils2/_backport/tests/test_pkgutil.py
+++ b/src/distutils2/_backport/tests/test_pkgutil.py
@@ -10,7 +10,7 @@ import zipfile
try:
from hashlib import md5
except ImportError:
- from md5 import md5
+ from distutils2._backport.hashlib import md5
from test.test_support import run_unittest, TESTFN
from distutils2.tests.support import unittest
@@ -25,12 +25,14 @@ except ImportError:
except ImportError:
from unittest2.compatibility import relpath
+# Adapted from Python 2.7's trunk
+
# TODO Add a test for getting a distribution that is provided by another
# distribution.
# TODO Add a test for absolute pathed RECORD items (e.g. /etc/myapp/config.ini)
-# Adapted from Python 2.7's trunk
+
class TestPkgUtilData(unittest.TestCase):
def setUp(self):
@@ -108,10 +110,14 @@ class TestPkgUtilData(unittest.TestCase):
del sys.modules[pkg]
+
# Adapted from Python 2.7's trunk
+
+
class TestPkgUtilPEP302(unittest.TestCase):
class MyTestLoader(object):
+
def load_module(self, fullname):
# Create an empty module
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
@@ -120,13 +126,14 @@ class TestPkgUtilPEP302(unittest.TestCase):
# Make it a package
mod.__path__ = []
# Count how many times the module is reloaded
- mod.__dict__['loads'] = mod.__dict__.get('loads',0) + 1
+ mod.__dict__['loads'] = mod.__dict__.get('loads', 0) + 1
return mod
def get_data(self, path):
return "Hello, world!"
class MyTestImporter(object):
+
def find_module(self, fullname, path=None):
return TestPkgUtilPEP302.MyTestLoader()
@@ -319,7 +326,7 @@ class TestPkgUtilPEP376(unittest.TestCase):
current_path = os.path.abspath(os.path.dirname(__file__))
self.sys_path = sys.path[:]
self.fake_dists_path = os.path.join(current_path, 'fake_dists')
- sys.path[0:0] = [self.fake_dists_path]
+ sys.path.insert(0, self.fake_dists_path)
def tearDown(self):
sys.path[:] = self.sys_path
@@ -366,8 +373,12 @@ class TestPkgUtilPEP376(unittest.TestCase):
if not isinstance(dist, Distribution):
self.fail("item received was not a Distribution instance: "
"%s" % type(dist))
- if dist.name in dict(fake_dists).keys():
+ if dist.name in dict(fake_dists).keys() and \
+ dist.path.startswith(self.fake_dists_path):
found_dists.append((dist.name, dist.metadata['version'],))
+ else:
+ # check that it doesn't find anything more than this
+ self.assertFalse(dist.path.startswith(self.fake_dists_path))
# otherwise we don't care what other distributions are found
# Finally, test that we found all that we were looking for
@@ -375,7 +386,8 @@ class TestPkgUtilPEP376(unittest.TestCase):
# Now, test if the egg-info distributions are found correctly as well
fake_dists += [('bacon', '0.1'), ('cheese', '2.0.2'),
- ('banana', '0.4'), ('strawberry', '0.6')]
+ ('banana', '0.4'), ('strawberry', '0.6'),
+ ('truffles', '5.0')]
found_dists = []
dists = [dist for dist in get_distributions(use_egg_info=True)]
@@ -384,8 +396,11 @@ class TestPkgUtilPEP376(unittest.TestCase):
isinstance(dist, EggInfoDistribution)):
self.fail("item received was not a Distribution or "
"EggInfoDistribution instance: %s" % type(dist))
- if dist.name in dict(fake_dists).keys():
+ if dist.name in dict(fake_dists).keys() and \
+ dist.path.startswith(self.fake_dists_path):
found_dists.append((dist.name, dist.metadata['version']))
+ else:
+ self.assertFalse(dist.path.startswith(self.fake_dists_path))
self.assertListEqual(sorted(fake_dists), sorted(found_dists))
@@ -485,7 +500,7 @@ class TestPkgUtilPEP376(unittest.TestCase):
l = [dist.name for dist in provides_distribution('truffles', '>1.5',
use_egg_info=True)]
- checkLists(l, ['bacon'])
+ checkLists(l, ['bacon', 'truffles'])
l = [dist.name for dist in provides_distribution('truffles', '>=1.0')]
checkLists(l, ['choxie', 'towel-stuff'])
@@ -549,6 +564,33 @@ class TestPkgUtilPEP376(unittest.TestCase):
l = [dist.name for dist in obsoletes_distribution('truffles', '0.2')]
checkLists(l, ['towel-stuff'])
+ def test_yield_distribution(self):
+ # tests the internal function _yield_distributions
+ from distutils2._backport.pkgutil import _yield_distributions
+ checkLists = lambda x, y: self.assertListEqual(sorted(x), sorted(y))
+
+ eggs = [('bacon', '0.1'), ('banana', '0.4'), ('strawberry', '0.6'),
+ ('truffles', '5.0'), ('cheese', '2.0.2')]
+ dists = [('choxie', '2.0.0.9'), ('grammar', '1.0a4'),
+ ('towel-stuff', '0.1')]
+
+ checkLists([], _yield_distributions(False, False))
+
+ found = [(dist.name, dist.metadata['Version'])
+ for dist in _yield_distributions(False, True)
+ if dist.path.startswith(self.fake_dists_path)]
+ checkLists(eggs, found)
+
+ found = [(dist.name, dist.metadata['Version'])
+ for dist in _yield_distributions(True, False)
+ if dist.path.startswith(self.fake_dists_path)]
+ checkLists(dists, found)
+
+ found = [(dist.name, dist.metadata['Version'])
+ for dist in _yield_distributions(True, True)
+ if dist.path.startswith(self.fake_dists_path)]
+ checkLists(dists + eggs, found)
+
def test_suite():
suite = unittest.TestSuite()
diff --git a/src/distutils2/command/__init__.py b/src/distutils2/command/__init__.py
index 9b3f401..8d77852 100644
--- a/src/distutils2/command/__init__.py
+++ b/src/distutils2/command/__init__.py
@@ -5,7 +5,8 @@ commands."""
__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 @@ __all__ = ['build',
'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/bdist_dumb.py b/src/distutils2/command/bdist_dumb.py
index 604bcb7..4113a1e 100644
--- a/src/distutils2/command/bdist_dumb.py
+++ b/src/distutils2/command/bdist_dumb.py
@@ -77,9 +77,7 @@ class bdist_dumb (Command):
("don't know how to create dumb built distributions " +
"on platform %s") % os.name
- self.set_undefined_options('bdist',
- ('dist_dir', 'dist_dir'),
- ('plat_name', 'plat_name'))
+ self.set_undefined_options('bdist', 'dist_dir', 'plat_name')
def run(self):
if not self.skip_build:
diff --git a/src/distutils2/command/bdist_msi.py b/src/distutils2/command/bdist_msi.py
index bbbcbcc..b759a06 100644
--- a/src/distutils2/command/bdist_msi.py
+++ b/src/distutils2/command/bdist_msi.py
@@ -153,10 +153,7 @@ class bdist_msi (Command):
else:
self.versions = list(self.all_versions)
- self.set_undefined_options('bdist',
- ('dist_dir', 'dist_dir'),
- ('plat_name', 'plat_name'),
- )
+ self.set_undefined_options('bdist', 'dist_dir', 'plat_name')
if self.pre_install_script:
raise DistutilsOptionError, "the pre-install-script feature is not yet implemented"
diff --git a/src/distutils2/command/bdist_wininst.py b/src/distutils2/command/bdist_wininst.py
index 7b55de1..264f4fb 100644
--- a/src/distutils2/command/bdist_wininst.py
+++ b/src/distutils2/command/bdist_wininst.py
@@ -100,10 +100,7 @@ class bdist_wininst (Command):
" option must be specified" % (short_version,)
self.target_version = short_version
- self.set_undefined_options('bdist',
- ('dist_dir', 'dist_dir'),
- ('plat_name', 'plat_name'),
- )
+ self.set_undefined_options('bdist', 'dist_dir', 'plat_name')
if self.install_script:
for script in self.distribution.scripts:
@@ -177,8 +174,8 @@ class bdist_wininst (Command):
# And make an archive relative to the root of the
# pseudo-installation tree.
- from tempfile import mktemp
- archive_basename = mktemp()
+ from tempfile import NamedTemporaryFile
+ archive_basename = NamedTemporaryFile().name
fullname = self.distribution.get_fullname()
arcname = self.make_archive(archive_basename, "zip",
root_dir=self.bdist_dir)
diff --git a/src/distutils2/command/build_clib.py b/src/distutils2/command/build_clib.py
index 40c4b6e..0a25aa8 100644
--- a/src/distutils2/command/build_clib.py
+++ b/src/distutils2/command/build_clib.py
@@ -76,9 +76,7 @@ class build_clib(Command):
self.set_undefined_options('build',
('build_temp', 'build_clib'),
('build_temp', 'build_temp'),
- ('compiler', 'compiler'),
- ('debug', 'debug'),
- ('force', 'force'))
+ 'compiler', 'debug', 'force')
self.libraries = self.distribution.libraries
if self.libraries:
diff --git a/src/distutils2/command/build_ext.py b/src/distutils2/command/build_ext.py
index 7e160ab..29530eb 100644
--- a/src/distutils2/command/build_ext.py
+++ b/src/distutils2/command/build_ext.py
@@ -177,13 +177,8 @@ class build_ext(Command):
def finalize_options(self):
self.set_undefined_options('build',
- ('build_lib', 'build_lib'),
- ('build_temp', 'build_temp'),
- ('compiler', 'compiler'),
- ('debug', 'debug'),
- ('force', 'force'),
- ('plat_name', 'plat_name'),
- )
+ 'build_lib', 'build_temp', 'compiler',
+ 'debug', 'force', 'plat_name')
if self.package is None:
self.package = self.distribution.ext_package
@@ -292,7 +287,7 @@ class build_ext(Command):
"config"))
else:
# building python standard extensions
- self.library_dirs.append('.')
+ self.library_dirs.append(os.curdir)
# for extensions under Linux or Solaris with a shared Python library,
# Python's library directory must be appended to library_dirs
@@ -305,7 +300,7 @@ class build_ext(Command):
self.library_dirs.append(sysconfig.get_config_var('LIBDIR'))
else:
# building python standard extensions
- self.library_dirs.append('.')
+ self.library_dirs.append(os.curdir)
# The argument parsing will result in self.define being a string, but
# it has to be a list of 2-tuples. All the preprocessor symbols
diff --git a/src/distutils2/command/build_py.py b/src/distutils2/command/build_py.py
index 340c249..505ffdd 100644
--- a/src/distutils2/command/build_py.py
+++ b/src/distutils2/command/build_py.py
@@ -95,9 +95,7 @@ class build_py(Command, Mixin2to3):
self._doctests_2to3 = []
def finalize_options(self):
- self.set_undefined_options('build',
- ('build_lib', 'build_lib'),
- ('force', 'force'))
+ self.set_undefined_options('build', 'build_lib', 'force')
# Get the distribution options that are aliases for build_py
# options -- list of packages and list of modules.
diff --git a/src/distutils2/command/build_scripts.py b/src/distutils2/command/build_scripts.py
index ad1c234..b62ade7 100644
--- a/src/distutils2/command/build_scripts.py
+++ b/src/distutils2/command/build_scripts.py
@@ -40,8 +40,7 @@ class build_scripts (Command):
def finalize_options (self):
self.set_undefined_options('build',
('build_scripts', 'build_dir'),
- ('force', 'force'),
- ('executable', 'executable'))
+ 'force', 'executable')
self.scripts = self.distribution.scripts
def get_source_files(self):
diff --git a/src/distutils2/command/check.py b/src/distutils2/command/check.py
index 844672d..b3175e7 100644
--- a/src/distutils2/command/check.py
+++ b/src/distutils2/command/check.py
@@ -8,13 +8,13 @@ from distutils2.core import Command
from distutils2.errors import DistutilsSetupError
class check(Command):
- """This command checks the meta-data of the package.
+ """This command checks the metadata of the package.
"""
description = ("perform some checks on the package")
- user_options = [('metadata', 'm', 'Verify meta-data'),
+ user_options = [('metadata', 'm', 'Verify metadata'),
('restructuredtext', 'r',
- ('Checks if long string meta-data syntax '
- 'are reStructuredText-compliant')),
+ ('Checks if long string metadata syntax '
+ 'is reStructuredText-compliant')),
('strict', 's',
'Will exit with an error if a check fails')]
@@ -53,7 +53,7 @@ class check(Command):
raise DistutilsSetupError(msg)
def check_metadata(self):
- """Ensures that all required elements of meta-data are supplied.
+ """Ensures that all required elements of metadata are supplied.
name, version, URL, (author and author_email) or
(maintainer and maintainer_email)).
@@ -62,7 +62,7 @@ class check(Command):
"""
missing, __ = self.distribution.metadata.check()
if missing != []:
- self.warn("missing required meta-data: %s" % ', '.join(missing))
+ self.warn("missing required metadata: %s" % ', '.join(missing))
def check_restructuredtext(self):
"""Checks if the long string fields are reST-compliant."""
diff --git a/src/distutils2/command/clean.py b/src/distutils2/command/clean.py
index baeaf05..8125dcd 100644
--- a/src/distutils2/command/clean.py
+++ b/src/distutils2/command/clean.py
@@ -40,13 +40,9 @@ class clean(Command):
self.all = None
def finalize_options(self):
- self.set_undefined_options('build',
- ('build_base', 'build_base'),
- ('build_lib', 'build_lib'),
- ('build_scripts', 'build_scripts'),
- ('build_temp', 'build_temp'))
- self.set_undefined_options('bdist',
- ('bdist_base', 'bdist_base'))
+ self.set_undefined_options('build', 'build_base', 'build_lib',
+ 'build_scripts', 'build_temp')
+ self.set_undefined_options('bdist', 'bdist_base')
def run(self):
# remove the build/temp.<plat> directory (unless it's already
diff --git a/src/distutils2/command/cmd.py b/src/distutils2/command/cmd.py
index ea001b3..73ac9e1 100644
--- a/src/distutils2/command/cmd.py
+++ b/src/distutils2/command/cmd.py
@@ -51,6 +51,12 @@ class Command(object):
# defined. The canonical example is the "install" command.
sub_commands = []
+ # Pre and post command hooks are run just before or just after the command
+ # itself. They are simple functions that receive the command instance. They
+ # should be specified as dotted strings.
+ pre_hook = None
+ post_hook = None
+
# -- Creation/initialization methods -------------------------------
@@ -297,26 +303,30 @@ class Command(object):
else:
return self.__class__.__name__
- def set_undefined_options(self, src_cmd, *option_pairs):
- """Set the values of any "undefined" options from corresponding
- option values in some other command object. "Undefined" here means
- "is None", which is the convention used to indicate that an option
- has not been changed between 'initialize_options()' and
- 'finalize_options()'. Usually called from 'finalize_options()' for
- options that depend on some other command rather than another
- option of the same command. 'src_cmd' is the other command from
- which option values will be taken (a command object will be created
- for it if necessary); the remaining arguments are
- '(src_option,dst_option)' tuples which mean "take the value of
- 'src_option' in the 'src_cmd' command object, and copy it to
- 'dst_option' in the current command object".
+ def set_undefined_options(self, src_cmd, *options):
+ """Set values of undefined options from another command.
+
+ Undefined options are options set to None, which is the convention
+ used to indicate that an option has not been changed between
+ 'initialize_options()' and 'finalize_options()'. This method is
+ usually called from 'finalize_options()' for options that depend on
+ some other command rather than another option of the same command,
+ typically subcommands.
+
+ The 'src_cmd' argument is the other command from which option values
+ will be taken (a command object will be created for it if necessary);
+ the remaining positional arguments are strings that give the name of
+ the option to set. If the name is different on the source and target
+ command, you can pass a tuple with '(name_on_source, name_on_dest)' so
+ that 'self.name_on_dest' will be set from 'src_cmd.name_on_source'.
"""
-
- # Option_pairs: list of (src_option, dst_option) tuples
-
src_cmd_obj = self.distribution.get_command_obj(src_cmd)
src_cmd_obj.ensure_finalized()
- for (src_option, dst_option) in option_pairs:
+ for obj in options:
+ if isinstance(obj, tuple):
+ src_option, dst_option = obj
+ else:
+ src_option, dst_option = obj, obj
if getattr(self, dst_option) is None:
setattr(self, dst_option,
getattr(src_cmd_obj, src_option))
@@ -411,7 +421,7 @@ class Command(object):
def spawn(self, cmd, search_path=1, level=1):
"""Spawn an external command respecting dry-run flag."""
- from distutils2.spawn import spawn
+ from distutils2.util import spawn
spawn(cmd, search_path, dry_run= self.dry_run)
def make_archive(self, base_name, format, root_dir=None, base_dir=None,
diff --git a/src/distutils2/command/install.py b/src/distutils2/command/install.py
index 588786f..1a6bd0e 100644
--- a/src/distutils2/command/install.py
+++ b/src/distutils2/command/install.py
@@ -18,6 +18,7 @@ from distutils2.util import write_file
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 @@ class install(Command):
('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 @@ class install(Command):
('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 @@ class install(Command):
('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,26 +78,45 @@ class install(Command):
#('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 (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']
+ boolean_options = ['compile', 'force', 'skip-build', 'no-distinfo',
+ 'requested', 'no-record']
+
+ if sys.version >= '2.6':
+ user_options.append(
+ ('user', None,
+ "install in user site-package [%s]" %
+ get_path('purelib', '%s_user' % os.name)))
- 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'}
+ 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
@@ -160,6 +180,11 @@ class install(Command):
self.record = None
+ # .dist-info related options
+ self.no_distinfo = None
+ self.installer = None
+ self.requested = None
+ self.no_record = None
# -- Option finalizing methods -------------------------------------
# (This is rather more involved than for most commands,
@@ -168,7 +193,6 @@ class install(Command):
# 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
@@ -185,18 +209,19 @@ class install(Command):
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":
@@ -231,18 +256,19 @@ class install(Command):
'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
@@ -270,12 +296,11 @@ class install(Command):
# 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',
@@ -287,7 +312,7 @@ class install(Command):
# 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
@@ -299,32 +324,16 @@ class install(Command):
self.dump_dirs("after prepending root")
# Find out the build directories, ie. where to install from.
- self.set_undefined_options('build',
- ('build_base', 'build_base'),
- ('build_lib', 'build_lib'))
+ self.set_undefined_options('build', 'build_base', 'build_lib')
# Punt on doc directories for now -- after all, we're punting on
# documentation completely!
- 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))
+ if self.no_distinfo is None:
+ self.no_distinfo = False
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
@@ -332,15 +341,15 @@ class install(Command):
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:
@@ -349,8 +358,8 @@ class install(Command):
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)
@@ -364,11 +373,11 @@ class install(Command):
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:
@@ -382,11 +391,27 @@ class install(Command):
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():
@@ -409,15 +434,14 @@ class install(Command):
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`."""
@@ -439,9 +463,9 @@ class install(Command):
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)
@@ -527,7 +551,6 @@ class install(Command):
else:
self.warn("path file '%s' not created" % filename)
-
# -- Reporting methods ---------------------------------------------
def get_outputs(self):
@@ -582,9 +605,11 @@ class install(Command):
# '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_egg_info', lambda self:True),
+ ('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
index a9980a1..1256a0d 100644
--- a/src/distutils2/command/install_data.py
+++ b/src/distutils2/command/install_data.py
@@ -37,9 +37,7 @@ class install_data(Command):
def finalize_options(self):
self.set_undefined_options('install',
('install_data', 'install_dir'),
- ('root', 'root'),
- ('force', 'force'),
- )
+ 'root', 'force')
def run(self):
self.mkpath(self.install_dir)
diff --git a/src/distutils2/command/install_distinfo.py b/src/distutils2/command/install_distinfo.py
new file mode 100644
index 0000000..0e30f09
--- /dev/null
+++ b/src/distutils2/command/install_distinfo.py
@@ -0,0 +1,172 @@
+"""
+distutils.command.install_distinfo
+==================================
+
+:Author: Josip Djolonga
+
+This module implements the ``install_distinfo`` command that creates the
+``.dist-info`` directory for the distribution, as specified in :pep:`376`.
+Usually, you do not have to call this command directly, it gets called
+automatically by the ``install`` command.
+"""
+
+# This file was created from the code for the former command install_egg_info
+
+import os
+import csv
+import re
+from distutils2.command.cmd import Command
+from distutils2 import log
+from distutils2._backport.shutil import rmtree
+try:
+ import hashlib
+except ImportError:
+ from distutils2._backport import hashlib
+
+
+class install_distinfo(Command):
+ """Install a .dist-info directory for the package"""
+
+ description = 'Install a .dist-info directory for the package'
+
+ 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"),
+ ]
+
+ boolean_options = ['requested', 'no-record']
+
+ negative_opt = {'no-requested': 'requested'}
+
+ def initialize_options(self):
+ self.distinfo_dir = None
+ self.installer = None
+ self.requested = None
+ self.no_record = None
+
+ def finalize_options(self):
+ self.set_undefined_options('install',
+ '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
+ if self.no_record is None:
+ self.no_record = False
+
+ metadata = self.distribution.metadata
+
+ basename = "%s-%s.dist-info" % (
+ to_filename(safe_name(metadata['Name'])),
+ to_filename(safe_version(metadata['Version'])),
+ )
+
+ self.distinfo_dir = os.path.join(self.distinfo_dir, basename)
+ self.outputs = []
+
+ def run(self):
+ if not self.dry_run:
+ target = self.distinfo_dir
+
+ if os.path.isdir(target) and not os.path.islink(target):
+ rmtree(target)
+ elif os.path.exists(target):
+ self.execute(os.unlink, (self.distinfo_dir,),
+ "Removing " + target)
+
+ self.execute(os.makedirs, (target,), "Creating " + target)
+
+ metadata_path = os.path.join(self.distinfo_dir, 'METADATA')
+ log.info('Creating %s' % (metadata_path,))
+ self.distribution.metadata.write(metadata_path)
+ self.outputs.append(metadata_path)
+
+ installer_path = os.path.join(self.distinfo_dir, 'INSTALLER')
+ log.info('Creating %s' % (installer_path,))
+ f = open(installer_path, 'w')
+ try:
+ f.write(self.installer)
+ finally:
+ f.close()
+ self.outputs.append(installer_path)
+
+ if self.requested:
+ requested_path = os.path.join(self.distinfo_dir, 'REQUESTED')
+ log.info('Creating %s' % (requested_path,))
+ f = open(requested_path, 'w')
+ f.close()
+ self.outputs.append(requested_path)
+
+ if not self.no_record:
+ record_path = os.path.join(self.distinfo_dir, 'RECORD')
+ log.info('Creating %s' % (record_path,))
+ f = open(record_path, 'wb')
+ try:
+ writer = csv.writer(f, delimiter=',',
+ lineterminator=os.linesep,
+ quotechar='"')
+
+ install = self.get_finalized_command('install')
+
+ for fpath in install.get_outputs():
+ if fpath.endswith('.pyc') or fpath.endswith('.pyo'):
+ # do not put size and md5 hash, as in PEP-376
+ writer.writerow((fpath, '', ''))
+ else:
+ size = os.path.getsize(fpath)
+ fd = open(fpath, 'r')
+ hash = hashlib.md5()
+ hash.update(fd.read())
+ md5sum = hash.hexdigest()
+ writer.writerow((fpath, md5sum, size))
+
+ # add the RECORD file itself
+ writer.writerow((record_path, '', ''))
+ self.outputs.append(record_path)
+ finally:
+ f.close()
+
+ def get_outputs(self):
+ return self.outputs
+
+
+# The following functions are taken from setuptools' pkg_resources module.
+
+def safe_name(name):
+ """Convert an arbitrary string to a standard distribution name
+
+ Any runs of non-alphanumeric/. characters are replaced with a single '-'.
+ """
+ return re.sub('[^A-Za-z0-9.]+', '-', name)
+
+
+def safe_version(version):
+ """Convert an arbitrary string to a standard version string
+
+ Spaces become dots, and all other non-alphanumeric characters become
+ dashes, with runs of multiple dashes condensed to a single dash.
+ """
+ version = version.replace(' ', '.')
+ return re.sub('[^A-Za-z0-9.]+', '-', version)
+
+
+def to_filename(name):
+ """Convert a project or version name to its filename-escaped form
+
+ Any '-' characters are currently replaced with '_'.
+ """
+ return name.replace('-', '_')
diff --git a/src/distutils2/command/install_egg_info.py b/src/distutils2/command/install_egg_info.py
deleted file mode 100644
index c8b568a..0000000
--- a/src/distutils2/command/install_egg_info.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""distutils.command.install_egg_info
-
-Implements the Distutils 'install_egg_info' command, for installing
-a package's PKG-INFO metadata."""
-
-
-from distutils2.command.cmd import Command
-from distutils2 import log
-from distutils2._backport.shutil import rmtree
-import os, sys, re
-
-class install_egg_info(Command):
- """Install an .egg-info file for the package"""
-
- description = "Install package's PKG-INFO metadata as an .egg-info file"
- user_options = [
- ('install-dir=', 'd', "directory to install to"),
- ]
-
- def initialize_options(self):
- self.install_dir = None
-
- def finalize_options(self):
- metadata = self.distribution.metadata
- self.set_undefined_options('install_lib',('install_dir','install_dir'))
- basename = "%s-%s-py%s.egg-info" % (
- to_filename(safe_name(metadata['Name'])),
- to_filename(safe_version(metadata['Version'])),
- sys.version[:3]
- )
- self.target = os.path.join(self.install_dir, basename)
- self.outputs = [self.target]
-
- def run(self):
- target = self.target
- if os.path.isdir(target) and not os.path.islink(target):
- if self.dry_run:
- pass # XXX
- else:
- rmtree(target)
- elif os.path.exists(target):
- self.execute(os.unlink,(self.target,),"Removing "+target)
- elif not os.path.isdir(self.install_dir):
- self.execute(os.makedirs, (self.install_dir,),
- "Creating "+self.install_dir)
- log.info("Writing %s", target)
- if not self.dry_run:
- f = open(target, 'w')
- self.distribution.metadata.write_file(f)
- f.close()
-
- def get_outputs(self):
- 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.
-
-def safe_name(name):
- """Convert an arbitrary string to a standard distribution name
-
- Any runs of non-alphanumeric/. characters are replaced with a single '-'.
- """
- return re.sub('[^A-Za-z0-9.]+', '-', name)
-
-
-def safe_version(version):
- """Convert an arbitrary string to a standard version string
-
- Spaces become dots, and all other non-alphanumeric characters become
- dashes, with runs of multiple dashes condensed to a single dash.
- """
- version = version.replace(' ','.')
- return re.sub('[^A-Za-z0-9.]+', '-', version)
-
-
-def to_filename(name):
- """Convert a project or version name to its filename-escaped form
-
- Any '-' characters are currently replaced with '_'.
- """
- return name.replace('-','_')
diff --git a/src/distutils2/command/install_headers.py b/src/distutils2/command/install_headers.py
index 762656d..dd63128 100644
--- a/src/distutils2/command/install_headers.py
+++ b/src/distutils2/command/install_headers.py
@@ -29,8 +29,7 @@ class install_headers(Command):
def finalize_options(self):
self.set_undefined_options('install',
('install_headers', 'install_dir'),
- ('force', 'force'))
-
+ 'force')
def run(self):
headers = self.distribution.headers
diff --git a/src/distutils2/command/install_lib.py b/src/distutils2/command/install_lib.py
index 7a9023b..c5e78f7 100644
--- a/src/distutils2/command/install_lib.py
+++ b/src/distutils2/command/install_lib.py
@@ -68,11 +68,7 @@ class install_lib(Command):
self.set_undefined_options('install',
('build_lib', 'build_dir'),
('install_lib', 'install_dir'),
- ('force', 'force'),
- ('compile', 'compile'),
- ('optimize', 'optimize'),
- ('skip_build', 'skip_build'),
- )
+ 'force', 'compile', 'optimize', 'skip_build')
if self.compile is None:
self.compile = 1
diff --git a/src/distutils2/command/install_scripts.py b/src/distutils2/command/install_scripts.py
index bdf7362..fa7587e 100644
--- a/src/distutils2/command/install_scripts.py
+++ b/src/distutils2/command/install_scripts.py
@@ -36,9 +36,7 @@ class install_scripts (Command):
self.set_undefined_options('build', ('build_scripts', 'build_dir'))
self.set_undefined_options('install',
('install_scripts', 'install_dir'),
- ('force', 'force'),
- ('skip_build', 'skip_build'),
- )
+ 'force', 'skip_build')
def run (self):
if not self.skip_build:
diff --git a/src/distutils2/command/register.py b/src/distutils2/command/register.py
index 29b87e4..3f27272 100644
--- a/src/distutils2/command/register.py
+++ b/src/distutils2/command/register.py
@@ -13,28 +13,41 @@ import urlparse
import StringIO
from warnings import warn
-from distutils2.core import PyPIRCCommand
+from distutils2.command.cmd import Command
from distutils2 import log
-
-class register(PyPIRCCommand):
-
- description = ("register the distribution with the Python package index")
- user_options = PyPIRCCommand.user_options + [
+from distutils2.util import (metadata_to_dict, read_pypirc, generate_pypirc,
+ DEFAULT_REPOSITORY, DEFAULT_REALM,
+ get_pypirc_path)
+
+class register(Command):
+
+ description = "register the distribution with the Python package index"
+ user_options = [
+ ('repository=', 'r',
+ "repository URL [default: %s]" % DEFAULT_REPOSITORY),
+ ('show-response', None,
+ "display full response text from server"),
('list-classifiers', None,
- 'list the valid Trove classifiers'),
+ "list valid Trove classifiers"),
('strict', None ,
- 'Will stop the registering if the meta-data are not fully compliant')
+ "stop the registration if the metadata is not fully compliant")
]
- boolean_options = PyPIRCCommand.boolean_options + [
- 'verify', 'list-classifiers', 'strict']
+
+ boolean_options = ['show-response', 'verify', 'list-classifiers',
+ 'strict']
def initialize_options(self):
- PyPIRCCommand.initialize_options(self)
+ self.repository = None
+ self.realm = None
+ self.show_response = 0
self.list_classifiers = 0
self.strict = 0
def finalize_options(self):
- PyPIRCCommand.finalize_options(self)
+ if self.repository is None:
+ self.repository = DEFAULT_REPOSITORY
+ if self.realm is None:
+ self.realm = DEFAULT_REALM
# setting options for the `check` subcommand
check_options = {'strict': ('register', self.strict),
'restructuredtext': ('register', 1)}
@@ -67,7 +80,7 @@ class register(PyPIRCCommand):
def _set_config(self):
''' Reads the configuration file and set attributes.
'''
- config = self._read_pypirc()
+ config = read_pypirc(self.repository, self.realm)
if config != {}:
self.username = config['username']
self.password = config['password']
@@ -75,10 +88,10 @@ class register(PyPIRCCommand):
self.realm = config['realm']
self.has_config = True
else:
- if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):
+ if self.repository not in ('pypi', DEFAULT_REPOSITORY):
raise ValueError('%s not found in .pypirc' % self.repository)
if self.repository == 'pypi':
- self.repository = self.DEFAULT_REPOSITORY
+ self.repository = DEFAULT_REPOSITORY
self.has_config = False
def classifiers(self):
@@ -177,14 +190,14 @@ Your selection [default 1]: ''', log.INFO)
self.announce(('I can store your PyPI login so future '
'submissions will be faster.'), log.INFO)
self.announce('(the login will be stored in %s)' % \
- self._get_rc_file(), log.INFO)
+ get_pypirc_path(), log.INFO)
choice = 'X'
while choice.lower() not in 'yn':
choice = raw_input('Save your login (y/N)?')
if not choice:
choice = 'n'
if choice.lower() == 'y':
- self._store_pypirc(username, password)
+ generate_pypirc(username, password)
elif choice == '2':
data = {':action': 'user'}
@@ -221,7 +234,7 @@ Your selection [default 1]: ''', log.INFO)
def build_post_data(self, action):
# figure the data to send - the metadata plus some additional
# information used by the package server
- data = self._metadata_to_pypy_dict()
+ data = metadata_to_dict(self.distribution.metadata)
data[':action'] = action
return data
diff --git a/src/distutils2/command/sdist.py b/src/distutils2/command/sdist.py
index 44fca10..464c7fe 100644
--- a/src/distutils2/command/sdist.py
+++ b/src/distutils2/command/sdist.py
@@ -70,8 +70,8 @@ class sdist(Command):
('dist-dir=', 'd',
"directory to put the source distribution archive(s) in "
"[default: dist]"),
- ('medata-check', None,
- "Ensure that all required elements of meta-data "
+ ('check-metadata', None,
+ "Ensure that all required elements of metadata "
"are supplied. Warn if any missing. [default]"),
('owner=', 'u',
"Owner name used when creating a tar file [default: current user]"),
@@ -80,7 +80,7 @@ class sdist(Command):
]
boolean_options = ['use-defaults', 'prune',
- 'manifest-only', 'keep-temp', 'metadata-check']
+ 'manifest-only', 'keep-temp', 'check-metadata']
help_options = [
('help-formats', None,
@@ -317,7 +317,7 @@ class sdist(Command):
'self.keep_temp' is true). The list of archive files created is
stored so it can be retrieved later by 'get_archive_files()'.
"""
- # Don't warn about missing meta-data here -- should be (and is!)
+ # Don't warn about missing metadata here -- should be (and is!)
# done elsewhere.
base_dir = self.distribution.get_fullname()
base_name = os.path.join(self.dist_dir, base_dir)
diff --git a/src/distutils2/command/upload.py b/src/distutils2/command/upload.py
index 14fe119..18ebb77 100644
--- a/src/distutils2/command/upload.py
+++ b/src/distutils2/command/upload.py
@@ -11,40 +11,57 @@ import StringIO as StringIO
try:
from hashlib import md5
except ImportError:
- from md5 import md5
+ from distutils2._backport.hashlib import md5
from distutils2.errors import DistutilsOptionError
-from distutils2.core import PyPIRCCommand
-from distutils2.spawn import spawn
+from distutils2.util import spawn
from distutils2 import log
+from distutils2.command.cmd import Command
+from distutils2 import log
+from distutils2.util import (metadata_to_dict, read_pypirc,
+ DEFAULT_REPOSITORY, DEFAULT_REALM)
+
-class upload(PyPIRCCommand):
+class upload(Command):
- description = "upload binary package to PyPI"
+ description = "upload distribution to PyPI"
- user_options = PyPIRCCommand.user_options + [
+ user_options = [
+ ('repository=', 'r',
+ "repository URL [default: %s]" % DEFAULT_REPOSITORY),
+ ('show-response', None,
+ "display full response text from server"),
('sign', 's',
- 'sign files to upload using gpg'),
- ('identity=', 'i', 'GPG identity used to sign files'),
+ "sign files to upload using gpg"),
+ ('identity=', 'i',
+ "GPG identity used to sign files"),
+ ('upload-docs', None,
+ "upload documentation too"),
]
- boolean_options = PyPIRCCommand.boolean_options + ['sign']
+ boolean_options = ['show-response', 'sign']
def initialize_options(self):
- PyPIRCCommand.initialize_options(self)
+ self.repository = None
+ self.realm = None
+ self.show_response = 0
self.username = ''
self.password = ''
self.show_response = 0
self.sign = False
self.identity = None
+ self.upload_docs = False
def finalize_options(self):
- PyPIRCCommand.finalize_options(self)
+ if self.repository is None:
+ self.repository = DEFAULT_REPOSITORY
+ if self.realm is None:
+ self.realm = DEFAULT_REALM
if self.identity and not self.sign:
raise DistutilsOptionError(
"Must use --sign for --identity to have meaning"
)
- config = self._read_pypirc()
+ config = read_pypirc(self.repository, self.realm)
if config != {}:
self.username = config['username']
self.password = config['password']
@@ -61,6 +78,12 @@ class upload(PyPIRCCommand):
raise DistutilsOptionError("No dist file created in earlier command")
for command, pyversion, filename in self.distribution.dist_files:
self.upload_file(command, pyversion, filename)
+ if self.upload_docs:
+ upload_docs = self.get_finalized_command("upload_docs")
+ upload_docs.repository = self.repository
+ upload_docs.username = self.username
+ upload_docs.password = self.password
+ upload_docs.run()
# XXX to be refactored with register.post_to_server
def upload_file(self, command, pyversion, filename):
@@ -81,11 +104,11 @@ class upload(PyPIRCCommand):
spawn(gpg_args,
dry_run=self.dry_run)
- # Fill in the data - send all the meta-data in case we need to
+ # Fill in the data - send all the metadata in case we need to
# register a new release
content = open(filename,'rb').read()
- data = self._metadata_to_pypy_dict()
+ data = metadata_to_dict(self.distribution.metadata)
# extra upload infos
data[':action'] = 'file_upload'
diff --git a/src/distutils2/command/upload_docs.py b/src/distutils2/command/upload_docs.py
index 256d4c2..7838d85 100644
--- a/src/distutils2/command/upload_docs.py
+++ b/src/distutils2/command/upload_docs.py
@@ -1,9 +1,11 @@
import base64, httplib, os.path, socket, urlparse, zipfile
from cStringIO import StringIO
+
from distutils2 import log
from distutils2.command.upload import upload
-from distutils2.core import PyPIRCCommand
+from distutils2.command.cmd import Command
from distutils2.errors import DistutilsFileError
+from distutils2.util import read_pypirc, DEFAULT_REPOSITORY, DEFAULT_REALM
def zip_dir(directory):
"""Compresses recursively contents of directory into a StringIO object"""
@@ -47,26 +49,36 @@ def encode_multipart(fields, files, boundary=None):
content_type = 'multipart/form-data; boundary=%s' % boundary
return content_type, body
-class upload_docs(PyPIRCCommand):
+class upload_docs(Command):
user_options = [
- ('repository=', 'r', "url of repository [default: %s]" % upload.DEFAULT_REPOSITORY),
- ('show-response', None, 'display full response text from server'),
- ('upload-dir=', None, 'directory to upload'),
+ ('repository=', 'r',
+ "repository URL [default: %s]" % DEFAULT_REPOSITORY),
+ ('show-response', None,
+ "display full response text from server"),
+ ('upload-dir=', None,
+ "directory to upload"),
]
def initialize_options(self):
- PyPIRCCommand.initialize_options(self)
- self.upload_dir = "build/docs"
+ self.repository = None
+ self.realm = None
+ self.show_response = 0
+ self.upload_dir = None
+ self.username = ''
+ self.password = ''
def finalize_options(self):
- PyPIRCCommand.finalize_options(self)
- if self.upload_dir == None:
+ if self.repository is None:
+ self.repository = DEFAULT_REPOSITORY
+ if self.realm is None:
+ self.realm = DEFAULT_REALM
+ if self.upload_dir is None:
build = self.get_finalized_command('build')
self.upload_dir = os.path.join(build.build_base, "docs")
self.announce('Using upload directory %s' % self.upload_dir)
self.verify_upload_dir(self.upload_dir)
- config = self._read_pypirc()
+ config = read_pypirc(self.repository, self.realm)
if config != {}:
self.username = config['username']
self.password = config['password']
diff --git a/src/distutils2/compiler/ccompiler.py b/src/distutils2/compiler/ccompiler.py
index adbf529..c9bb129 100644
--- a/src/distutils2/compiler/ccompiler.py
+++ b/src/distutils2/compiler/ccompiler.py
@@ -11,8 +11,7 @@ import re
from distutils2.errors import (CompileError, LinkError, UnknownFileError,
DistutilsPlatformError, DistutilsModuleError)
-from distutils2.spawn import spawn
-from distutils2.util import split_quoted, execute, newer_group
+from distutils2.util import split_quoted, execute, newer_group, spawn
from distutils2 import log
from shutil import move
diff --git a/src/distutils2/config.py b/src/distutils2/config.py
deleted file mode 100644
index 30ad479..0000000
--- a/src/distutils2/config.py
+++ /dev/null
@@ -1,155 +0,0 @@
-"""distutils.pypirc
-
-Provides the PyPIRCCommand class, the base class for the command classes
-that uses .pypirc in the distutils.command package.
-"""
-import os
-from ConfigParser import RawConfigParser
-
-from distutils2.command.cmd import Command
-
-DEFAULT_PYPIRC = """\
-[distutils]
-index-servers =
- pypi
-
-[pypi]
-username:%s
-password:%s
-"""
-
-class PyPIRCCommand(Command):
- """Base command that knows how to handle the .pypirc file
- """
- DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi'
- DEFAULT_REALM = 'pypi'
- repository = None
- realm = None
-
- user_options = [
- ('repository=', 'r',
- "url of repository [default: %s]" % \
- DEFAULT_REPOSITORY),
- ('show-response', None,
- 'display full response text from server')]
-
- boolean_options = ['show-response']
-
- def _get_rc_file(self):
- """Returns rc file path."""
- return os.path.join(os.path.expanduser('~'), '.pypirc')
-
- def _store_pypirc(self, username, password):
- """Creates a default .pypirc file."""
- rc = self._get_rc_file()
- f = open(rc, 'w')
- try:
- f.write(DEFAULT_PYPIRC % (username, password))
- finally:
- f.close()
- try:
- os.chmod(rc, 0600)
- except OSError:
- # should do something better here
- pass
-
- def _read_pypirc(self):
- """Reads the .pypirc file."""
- rc = self._get_rc_file()
- if os.path.exists(rc):
- self.announce('Using PyPI login from %s' % rc)
- repository = self.repository or self.DEFAULT_REPOSITORY
- config = RawConfigParser()
- config.read(rc)
- sections = config.sections()
- if 'distutils' in sections:
- # let's get the list of servers
- index_servers = config.get('distutils', 'index-servers')
- _servers = [server.strip() for server in
- index_servers.split('\n')
- if server.strip() != '']
- if _servers == []:
- # nothing set, let's try to get the default pypi
- if 'pypi' in sections:
- _servers = ['pypi']
- else:
- # the file is not properly defined, returning
- # an empty dict
- return {}
- for server in _servers:
- current = {'server': server}
- current['username'] = config.get(server, 'username')
-
- # optional params
- for key, default in (('repository',
- self.DEFAULT_REPOSITORY),
- ('realm', self.DEFAULT_REALM),
- ('password', None)):
- if config.has_option(server, key):
- current[key] = config.get(server, key)
- else:
- current[key] = default
- if (current['server'] == repository or
- current['repository'] == repository):
- return current
- elif 'server-login' in sections:
- # old format
- server = 'server-login'
- if config.has_option(server, 'repository'):
- repository = config.get(server, 'repository')
- else:
- repository = self.DEFAULT_REPOSITORY
- return {'username': config.get(server, 'username'),
- 'password': config.get(server, 'password'),
- 'repository': repository,
- 'server': server,
- 'realm': self.DEFAULT_REALM}
-
- return {}
-
- def _metadata_to_pypy_dict(self):
- meta = self.distribution.metadata
- data = {
- 'metadata_version' : meta.version,
- 'name': meta['Name'],
- 'version': meta['Version'],
- 'summary': meta['Summary'],
- 'home_page': meta['Home-page'],
- 'author': meta['Author'],
- 'author_email': meta['Author-email'],
- 'license': meta['License'],
- 'description': meta['Description'],
- 'keywords': meta['Keywords'],
- 'platform': meta['Platform'],
- 'classifier': meta['Classifier'],
- 'download_url': meta['Download-URL'],
- }
-
- if meta.version == '1.2':
- data['requires_dist'] = meta['Requires-Dist']
- data['requires_python'] = meta['Requires-Python']
- data['requires_external'] = meta['Requires-External']
- data['provides_dist'] = meta['Provides-Dist']
- data['obsoletes_dist'] = meta['Obsoletes-Dist']
- data['project_url'] = [','.join(url) for url in
- meta['Project-URL']]
-
- elif meta.version == '1.1':
- data['provides'] = meta['Provides']
- data['requires'] = meta['Requires']
- data['obsoletes'] = meta['Obsoletes']
-
- return data
-
- def initialize_options(self):
- """Initialize options."""
- self.repository = None
- self.realm = None
- self.show_response = 0
-
- def finalize_options(self):
- """Finalizes options."""
- if self.repository is None:
- self.repository = self.DEFAULT_REPOSITORY
- if self.realm is None:
- self.realm = self.DEFAULT_REALM
diff --git a/src/distutils2/core.py b/src/distutils2/core.py
index a81780b..0820adb 100644
--- a/src/distutils2/core.py
+++ b/src/distutils2/core.py
@@ -19,7 +19,6 @@ from distutils2.util import grok_environment_error
# Mainly import these so setup scripts can "from distutils2.core import" them.
from distutils2.dist import Distribution
from distutils2.command.cmd import Command
-from distutils2.config import PyPIRCCommand
from distutils2.extension import Extension
from distutils2.util import find_packages
@@ -34,6 +33,7 @@ usage: %(script)s [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: %(script)s cmd --help
"""
+
def gen_usage(script_name):
script = os.path.basename(script_name)
return USAGE % {'script': script}
@@ -60,6 +60,7 @@ extension_keywords = ('name', 'sources', 'include_dirs',
'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
@@ -156,7 +157,7 @@ def setup(**attrs):
def run_setup(script_name, script_args=None, stop_after="run"):
"""Run a setup script in a somewhat controlled environment, and
return the Distribution instance that drives things. This is useful
- if you need to find out the distribution meta-data (passed as
+ if you need to find out the distribution metadata (passed as
keyword args from 'script' to 'setup()', or the contents of the
config files or command-line.
@@ -206,8 +207,6 @@ def run_setup(script_name, script_args=None, stop_after="run"):
# Hmm, should we do something if exiting with a non-zero code
# (ie. error)?
pass
- except:
- raise
if _setup_distribution is None:
raise RuntimeError, \
diff --git a/src/distutils2/depgraph.py b/src/distutils2/depgraph.py
index 75bc4d3..2d66a39 100644
--- 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 @@ def generate_graph(dists):
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
index b393a0e..90b3af1 100644
--- a/src/distutils2/dist.py
+++ b/src/distutils2/dist.py
@@ -6,19 +6,17 @@ being built/installed/distributed.
__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
from distutils2.errors import (DistutilsOptionError, DistutilsArgError,
DistutilsModuleError, DistutilsClassError)
from distutils2.fancy_getopt import FancyGetopt, translate_longopt
-from distutils2.util import check_environ, strtobool
+from distutils2.util import check_environ, strtobool, resolve_dotted_name
from distutils2 import log
from distutils2.metadata import DistributionMetadata
@@ -26,7 +24,7 @@ from distutils2.metadata import DistributionMetadata
# 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):
@@ -43,7 +41,6 @@ class Distribution(object):
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
@@ -116,7 +113,7 @@ Common commands: (see '--help-commands' for more)
('use-2to3', None,
"use 2to3 to make source python 3.x compatible"),
('convert-2to3-doctests', None,
- "use 2to3 to convert doctests in seperate text files"),
+ "use 2to3 to convert doctests in seperate text files"),
]
display_option_names = map(lambda x: translate_longopt(x[0]),
display_options)
@@ -124,10 +121,8 @@ Common commands: (see '--help-commands' for more)
# negative options are options that exclude other options
negative_opt = {'quiet': 'verbose'}
-
# -- Creation/initialization methods -------------------------------
-
- def __init__ (self, attrs=None):
+ def __init__(self, attrs=None):
"""Construct a new Distribution instance: initialize all the
attributes of a Distribution, and then use 'attrs' (a dictionary
mapping attribute names to values) to assign some of those
@@ -145,17 +140,12 @@ Common commands: (see '--help-commands' for more)
for attr in self.display_option_names:
setattr(self, attr, 0)
- # Store the distribution meta-data (name, version, author, and so
+ # Store the distribution metadata (name, version, author, and so
# forth) in a separate object -- we're getting to have enough
# information here (and enough command-line options) that it's
- # worth it. Also delegate 'get_XXX()' methods to the 'metadata'
- # object in a sneaky and underhanded (but efficient!) way.
+ # worth it.
self.metadata = DistributionMetadata()
- #for basename in self.metadata._METHOD_BASENAMES:
- # method_name = "get_" + basename
- # setattr(self, method_name, getattr(self.metadata, method_name))
-
# 'cmdclass' maps command names to class objects, so we
# can 1) quickly figure out which class to instantiate when
# we need to create a new command object, and 2) have a way
@@ -257,10 +247,7 @@ Common commands: (see '--help-commands' for more)
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
@@ -286,10 +273,10 @@ Common commands: (see '--help-commands' for more)
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()
@@ -385,9 +372,21 @@ Common commands: (see '--help-commands' for more)
for opt in options:
if opt != '__name__':
- val = parser.get(section,opt)
+ val = parser.get(section, opt)
opt = opt.replace('-', '_')
- opt_dict[opt] = (filename, val)
+
+ # ... although practicality beats purity :(
+ if opt.startswith("pre_hook.") or opt.startswith("post_hook."):
+ hook_type, alias = opt.split(".")
+ # Hooks are somewhat exceptional, as they are
+ # gathered from many config files (not overriden as
+ # other options).
+ # The option_dict expects {"command": ("filename", # "value")}
+ # so for hooks, we store only the last config file processed
+ hook_dict = opt_dict.setdefault(hook_type, (filename, {}))[1]
+ hook_dict[alias] = val
+ else:
+ opt_dict[opt] = (filename, val)
# Make the RawConfigParser forget everything (so we retain
# the original filenames that options come from)
@@ -402,12 +401,12 @@ Common commands: (see '--help-commands' for more)
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 ----------------------------------
@@ -473,7 +472,7 @@ Common commands: (see '--help-commands' for more)
# 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
@@ -504,7 +503,7 @@ Common commands: (see '--help-commands' for more)
# 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
@@ -513,22 +512,21 @@ Common commands: (see '--help-commands' for more)
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.
@@ -545,7 +543,6 @@ Common commands: (see '--help-commands' for more)
else:
help_options = []
-
# All commands support the global options too, just by adding
# in 'global_options'.
parser.set_option_table(self.global_options +
@@ -559,10 +556,10 @@ Common commands: (see '--help-commands' for more)
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:
@@ -588,7 +585,7 @@ Common commands: (see '--help-commands' for more)
objects.
"""
if getattr(self, 'convert_2to3_doctests', None):
- self.convert_2to3_doctests = [os.path.join(p)
+ self.convert_2to3_doctests = [os.path.join(p)
for p in self.convert_2to3_doctests]
else:
self.convert_2to3_doctests = []
@@ -812,7 +809,7 @@ Common commands: (see '--help-commands' for more)
class_name = command
try:
- __import__ (module_name)
+ __import__(module_name)
module = sys.modules[module_name]
except ImportError:
continue
@@ -820,9 +817,9 @@ Common commands: (see '--help-commands' for more)
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
@@ -891,11 +888,11 @@ Common commands: (see '--help-commands' for more)
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
@@ -963,15 +960,23 @@ Common commands: (see '--help-commands' for more)
if self.have_run.get(command):
return
- log.info("running %s", command)
cmd_obj = self.get_command_obj(command)
cmd_obj.ensure_finalized()
+ self.run_command_hooks(cmd_obj, 'pre_hook')
+ log.info("running %s", command)
cmd_obj.run()
+ 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:
+ return
+ for hook in hooks.values():
+ hook_func = resolve_dotted_name(hook)
+ hook_func(cmd_obj)
# -- Distribution query methods ------------------------------------
-
def has_pure_modules(self):
return len(self.packages or self.py_modules or []) > 0
@@ -998,13 +1003,8 @@ Common commands: (see '--help-commands' for more)
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
index d5750f4..ef3e300 100644
--- a/src/distutils2/errors.py
+++ b/src/distutils2/errors.py
@@ -10,31 +10,38 @@ symbols whose names start with "Distutils" and end with "Error"."""
__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 @@ class DistutilsOptionError(DistutilsError):
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 @@ class HugeMajorVersionNumError(IrrationalVersionError):
This guard can be disabled by setting that option False.
"""
pass
-
diff --git a/src/distutils2/extension.py b/src/distutils2/extension.py
index 05ccd20..f38bf60 100644
--- a/src/distutils2/extension.py
+++ b/src/distutils2/extension.py
@@ -17,6 +17,7 @@ import warnings
# 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 @@ class Extension(object):
# 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 @@ class Extension(object):
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 @@ class Extension(object):
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
index e0ac736..19cb97b 100644
--- a/src/distutils2/fancy_getopt.py
+++ b/src/distutils2/fancy_getopt.py
@@ -30,6 +30,7 @@ neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat))
# (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 @@ class FancyGetopt(object):
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 @@ class FancyGetopt(object):
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/__init__.py b/src/distutils2/index/__init__.py
new file mode 100644
index 0000000..312662f
--- /dev/null
+++ b/src/distutils2/index/__init__.py
@@ -0,0 +1,11 @@
+"""Package containing ways to interact with Index APIs.
+
+"""
+
+__all__ = ['simple',
+ 'xmlrpc',
+ 'dist',
+ 'errors',
+ 'mirrors',]
+
+from dist import ReleaseInfo, ReleasesList, DistInfo
diff --git a/src/distutils2/index/base.py b/src/distutils2/index/base.py
new file mode 100644
index 0000000..04582a3
--- /dev/null
+++ b/src/distutils2/index/base.py
@@ -0,0 +1,46 @@
+from distutils2.index.dist import ReleasesList
+
+
+class BaseClient(object):
+ """Base class containing common methods for the index crawlers/clients"""
+
+ def __init__(self, prefer_final, prefer_source):
+ self._prefer_final = prefer_final
+ self._prefer_source = prefer_source
+ self._index = self
+
+ def _get_prefer_final(self, prefer_final=None):
+ """Return the prefer_final internal parameter or the specified one if
+ provided"""
+ if prefer_final:
+ return prefer_final
+ else:
+ return self._prefer_final
+
+ def _get_prefer_source(self, prefer_source=None):
+ """Return the prefer_source internal parameter or the specified one if
+ provided"""
+ if prefer_source:
+ return prefer_source
+ else:
+ return self._prefer_source
+
+ def _get_project(self, project_name):
+ """Return an project instance, create it if necessary"""
+ return self._projects.setdefault(project_name.lower(),
+ ReleasesList(project_name, index=self._index))
+
+ def download_distribution(self, requirements, temp_path=None,
+ prefer_source=None, prefer_final=None):
+ """Download a distribution from the last release according to the
+ requirements.
+
+ If temp_path is provided, download to this path, otherwise, create a
+ temporary location for the download and return it.
+ """
+ prefer_final = self._get_prefer_final(prefer_final)
+ prefer_source = self._get_prefer_source(prefer_source)
+ release = self.get_release(requirements, prefer_final)
+ if release:
+ dist = release.get_distribution(prefer_source=prefer_source)
+ return dist.download(temp_path)
diff --git a/src/distutils2/index/dist.py b/src/distutils2/index/dist.py
new file mode 100644
index 0000000..f5d0fd9
--- /dev/null
+++ b/src/distutils2/index/dist.py
@@ -0,0 +1,544 @@
+"""distutils2.index.dist
+
+Provides useful classes to represent the release and distributions retrieved
+from indexes.
+
+A project can have several releases (=versions) and each release can have
+several distributions (sdist, bdist).
+
+The release contains the metadata related informations (see PEP 384), and the
+distributions contains download related informations.
+
+"""
+import mimetypes
+import re
+import tarfile
+import tempfile
+import urllib
+import urlparse
+import zipfile
+
+try:
+ import hashlib
+except ImportError:
+ from distutils2._backport import hashlib
+
+from distutils2.errors import IrrationalVersionError
+from distutils2.index.errors import (HashDoesNotMatch, UnsupportedHashName,
+ CantParseArchiveName)
+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
+
+__all__ = ['ReleaseInfo', 'DistInfo', 'ReleasesList', 'get_infos_from_url']
+
+EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz .egg".split()
+MD5_HASH = re.compile(r'^.*#md5=([a-f0-9]+)$')
+DIST_TYPES = ['bdist', 'sdist']
+
+
+class IndexReference(object):
+ """Mixin used to store the index reference"""
+ def set_index(self, index=None):
+ self._index = index
+
+
+class ReleaseInfo(IndexReference):
+ """Represent a release of a project (a project with a specific version).
+ The release contain the _metadata informations related to this specific
+ version, and is also a container for distribution related informations.
+
+ See the DistInfo class for more information about distributions.
+ """
+
+ def __init__(self, name, version, metadata=None, hidden=False,
+ index=None, **kwargs):
+ """
+ :param name: the name of the distribution
+ :param version: the version of the distribution
+ :param metadata: the metadata fields of the release.
+ :type metadata: dict
+ :param kwargs: optional arguments for a new distribution.
+ """
+ self.set_index(index)
+ self.name = name
+ self._version = None
+ self.version = version
+ if metadata:
+ self.metadata = DistributionMetadata(mapping=metadata)
+ else:
+ self.metadata = None
+ self.dists = {}
+ self.hidden = hidden
+
+ if 'dist_type' in kwargs:
+ dist_type = kwargs.pop('dist_type')
+ self.add_distribution(dist_type, **kwargs)
+
+ def set_version(self, version):
+ try:
+ self._version = NormalizedVersion(version)
+ except IrrationalVersionError:
+ suggestion = suggest_normalized_version(version)
+ if suggestion:
+ self.version = suggestion
+ else:
+ raise IrrationalVersionError(version)
+
+ def get_version(self):
+ return self._version
+
+ version = property(get_version, set_version)
+
+ def fetch_metadata(self):
+ """If the metadata is not set, use the indexes to get it"""
+ if not self.metadata:
+ self._index.get_metadata(self.name, '%s' % self.version)
+ return self.metadata
+
+ @property
+ def is_final(self):
+ """proxy to version.is_final"""
+ return self.version.is_final
+
+ 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
+
+ def add_distribution(self, dist_type='sdist', python_version=None, **params):
+ """Add distribution informations to this release.
+ If distribution information is already set for this distribution type,
+ add the given url paths to the distribution. This can be useful while
+ some of them fails to download.
+
+ :param dist_type: the distribution type (eg. "sdist", "bdist", etc.)
+ :param params: the fields to be passed to the distribution object
+ (see the :class:DistInfo constructor).
+ """
+ if dist_type not in DIST_TYPES:
+ raise ValueError(dist_type)
+ if dist_type in self.dists:
+ self.dists[dist_type].add_url(**params)
+ else:
+ self.dists[dist_type] = DistInfo(self, dist_type,
+ index=self._index, **params)
+ if python_version:
+ self.dists[dist_type].python_version = python_version
+
+ def get_distribution(self, dist_type=None, prefer_source=True):
+ """Return a distribution.
+
+ If dist_type is set, find first for this distribution type, and just
+ act as an alias of __get_item__.
+
+ If prefer_source is True, search first for source distribution, and if
+ not return one existing distribution.
+ """
+ if len(self.dists) == 0:
+ raise LookupError()
+ if dist_type:
+ return self[dist_type]
+ if prefer_source:
+ if "sdist" in self.dists:
+ dist = self["sdist"]
+ else:
+ dist = self.dists.values()[0]
+ return dist
+
+ def download(self, temp_path=None, prefer_source=True):
+ """Download the distribution, using the requirements.
+
+ If more than one distribution match the requirements, use the last
+ version.
+ Download the distribution, and put it in the temp_path. If no temp_path
+ is given, creates and return one.
+
+ Returns the complete absolute path to the downloaded archive.
+ """
+ return self.get_distribution(prefer_source=prefer_source)\
+ .download(path=temp_path)
+
+ def set_metadata(self, metadata):
+ if not self.metadata:
+ self.metadata = DistributionMetadata()
+ self.metadata.update(metadata)
+
+ def __getitem__(self, item):
+ """distributions are available using release["sdist"]"""
+ return self.dists[item]
+
+ def _check_is_comparable(self, other):
+ if not isinstance(other, ReleaseInfo):
+ raise TypeError("cannot compare %s and %s"
+ % (type(self).__name__, type(other).__name__))
+ elif self.name != other.name:
+ raise TypeError("cannot compare %s and %s"
+ % (self.name, other.name))
+
+ def __repr__(self):
+ return "<%s %s>" % (self.name, self.version)
+
+ def __eq__(self, other):
+ self._check_is_comparable(other)
+ return self.version == other.version
+
+ def __lt__(self, other):
+ self._check_is_comparable(other)
+ return self.version < other.version
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __gt__(self, other):
+ return not (self.__lt__(other) or self.__eq__(other))
+
+ def __le__(self, other):
+ return self.__eq__(other) or self.__lt__(other)
+
+ def __ge__(self, other):
+ return self.__eq__(other) or self.__gt__(other)
+
+ # See http://docs.python.org/reference/datamodel#object.__hash__
+ __hash__ = object.__hash__
+
+
+class DistInfo(IndexReference):
+ """Represents a distribution retrieved from an index (sdist, bdist, ...)
+ """
+
+ def __init__(self, release, dist_type=None, url=None, hashname=None,
+ hashval=None, is_external=True, python_version=None,
+ index=None):
+ """Create a new instance of DistInfo.
+
+ :param release: a DistInfo class is relative to a release.
+ :param dist_type: the type of the dist (eg. source, bin-*, etc.)
+ :param url: URL where we found this distribution
+ :param hashname: the name of the hash we want to use. Refer to the
+ hashlib.new documentation for more information.
+ :param hashval: the hash value.
+ :param is_external: we need to know if the provided url comes from
+ an index browsing, or from an external resource.
+
+ """
+ self.set_index(index)
+ self.release = release
+ self.dist_type = dist_type
+ self.python_version = python_version
+ self._unpacked_dir = None
+ # set the downloaded path to None by default. The goal here
+ # is to not download distributions multiple times
+ self.downloaded_location = None
+ # We store urls in dict, because we need to have a bit more infos
+ # than the simple URL. It will be used later to find the good url to
+ # use.
+ # We have two _url* attributes: _url and urls. urls contains a list
+ # of dict for the different urls, and _url contains the choosen url, in
+ # order to dont make the selection process multiple times.
+ self.urls = []
+ self._url = None
+ self.add_url(url, hashname, hashval, is_external)
+
+ def add_url(self, url, hashname=None, hashval=None, is_external=True):
+ """Add a new url to the list of urls"""
+ if hashname is not None:
+ try:
+ hashlib.new(hashname)
+ except ValueError:
+ raise UnsupportedHashName(hashname)
+ if not url in [u['url'] for u in self.urls]:
+ self.urls.append({
+ 'url': url,
+ 'hashname': hashname,
+ 'hashval': hashval,
+ 'is_external': is_external,
+ })
+ # reset the url selection process
+ self._url = None
+
+ @property
+ def url(self):
+ """Pick up the right url for the list of urls in self.urls"""
+ # We return internal urls over externals.
+ # If there is more than one internal or external, return the first
+ # one.
+ if self._url is None:
+ if len(self.urls) > 1:
+ internals_urls = [u for u in self.urls \
+ if u['is_external'] == False]
+ if len(internals_urls) >= 1:
+ self._url = internals_urls[0]
+ if self._url is None:
+ self._url = self.urls[0]
+ return self._url
+
+ @property
+ def is_source(self):
+ """return if the distribution is a source one or not"""
+ return self.dist_type == 'sdist'
+
+ def download(self, path=None):
+ """Download the distribution to a path, and return it.
+
+ If the path is given in path, use this, otherwise, generates a new one
+ Return the download location.
+ """
+ if path is None:
+ path = tempfile.mkdtemp()
+
+ # if we do not have downloaded it yet, do it.
+ if self.downloaded_location is None:
+ url = self.url['url']
+ archive_name = urlparse.urlparse(url)[2].split('/')[-1]
+ filename, headers = urllib.urlretrieve(url,
+ path + "/" + archive_name)
+ self.downloaded_location = filename
+ self._check_md5(filename)
+ return self.downloaded_location
+
+ def unpack(self, path=None):
+ """Unpack the distribution to the given path.
+
+ If not destination is given, creates a temporary location.
+
+ Returns the location of the extracted files (root).
+ """
+ if not self._unpacked_dir:
+ if path is None:
+ path = tempfile.mkdtemp()
+
+ filename = self.download()
+ content_type = mimetypes.guess_type(filename)[0]
+
+ if (content_type == 'application/zip'
+ or filename.endswith('.zip')
+ or filename.endswith('.pybundle')
+ or zipfile.is_zipfile(filename)):
+ unzip_file(filename, path, flatten=not filename.endswith('.pybundle'))
+ elif (content_type == 'application/x-gzip'
+ or tarfile.is_tarfile(filename)
+ or splitext(filename)[1].lower() in ('.tar', '.tar.gz', '.tar.bz2', '.tgz', '.tbz')):
+ untar_file(filename, path)
+ self._unpacked_dir = path
+ return self._unpacked_dir
+
+ def _check_md5(self, filename):
+ """Check that the md5 checksum of the given file matches the one in
+ url param"""
+ hashname = self.url['hashname']
+ expected_hashval = self.url['hashval']
+ if not None in (expected_hashval, hashname):
+ f = open(filename)
+ hashval = hashlib.new(hashname)
+ hashval.update(f.read())
+ if hashval.hexdigest() != expected_hashval:
+ raise HashDoesNotMatch("got %s instead of %s"
+ % (hashval.hexdigest(), expected_hashval))
+
+ def __repr__(self):
+ return "<%s %s %s>" % (
+ self.release.name, self.release.version, self.dist_type or "")
+
+
+class ReleasesList(IndexReference):
+ """A container of Release.
+
+ Provides useful methods and facilities to sort and filter releases.
+ """
+ def __init__(self, name, releases=None, contains_hidden=False, index=None):
+ self.set_index(index)
+ self.releases = []
+ self.name = name
+ self.contains_hidden = contains_hidden
+ if releases:
+ self.add_releases(releases)
+
+ def fetch_releases(self):
+ self._index.get_releases(self.name)
+ return self.releases
+
+ def filter(self, predicate):
+ """Filter and return a subset of releases matching the given predicate.
+ """
+ return ReleasesList(self.name, [release for release in self.releases
+ if predicate.match(release.version)],
+ index=self._index)
+
+ 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]
+
+ def add_releases(self, releases):
+ """Add releases in the release list.
+
+ :param: releases is a list of ReleaseInfo objects.
+ """
+ for r in releases:
+ self.add_release(release=r)
+
+ def add_release(self, version=None, dist_type='sdist', release=None,
+ **dist_args):
+ """Add a release to the list.
+
+ The release can be passed in the `release` parameter, and in this case,
+ it will be crawled to extract the useful informations if necessary, or
+ the release informations can be directly passed in the `version` and
+ `dist_type` arguments.
+
+ Other keywords arguments can be provided, and will be forwarded to the
+ distribution creation (eg. the arguments of the DistInfo constructor).
+ """
+ if release:
+ if release.name.lower() != self.name.lower():
+ raise ValueError("%s is not the same project than %s" %
+ (release.name, self.name))
+ version = '%s' % release.version
+
+ if not version in self.get_versions():
+ # append only if not already exists
+ 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
+ and r.name == self.name]
+ if not matches:
+ release = ReleaseInfo(self.name, version, index=self._index)
+ self.releases.append(release)
+ else:
+ release = matches[0]
+
+ release.add_distribution(dist_type=dist_type, **dist_args)
+
+ def sort_releases(self, prefer_final=False, reverse=True, *args, **kwargs):
+ """Sort the results with the given properties.
+
+ The `prefer_final` argument can be used to specify if final
+ distributions (eg. not dev, bet or alpha) would be prefered or not.
+
+ Results can be inverted by using `reverse`.
+
+ Any other parameter provided will be forwarded to the sorted call. You
+ cannot redefine the key argument of "sorted" here, as it is used
+ internally to sort the releases.
+ """
+
+ sort_by = []
+ if prefer_final:
+ sort_by.append("is_final")
+ sort_by.append("version")
+
+ self.releases.sort(
+ key=lambda i: [getattr(i, arg) for arg in sort_by],
+ reverse=reverse, *args, **kwargs)
+
+ def get_release(self, version):
+ """Return a release from it's version.
+ """
+ matches = [r for r in self.releases if "%s" % r.version == version]
+ if len(matches) != 1:
+ raise KeyError(version)
+ return matches[0]
+
+ def get_versions(self):
+ """Return a list of releases versions contained"""
+ return ["%s" % r.version for r in self.releases]
+
+ def __getitem__(self, key):
+ return self.releases[key]
+
+ def __len__(self):
+ return len(self.releases)
+
+ def __repr__(self):
+ string = 'Project "%s"' % self.name
+ if self.get_versions():
+ string += ' versions: %s' % ', '.join(self.get_versions())
+ return '<%s>' % string
+
+
+def get_infos_from_url(url, probable_dist_name=None, is_external=True):
+ """Get useful informations from an URL.
+
+ Return a dict of (name, version, url, hashtype, hash, is_external)
+
+ :param url: complete url of the distribution
+ :param probable_dist_name: A probable name of the project.
+ :param is_external: Tell if the url commes from an index or from
+ an external URL.
+ """
+ # if the url contains a md5 hash, get it.
+ md5_hash = None
+ match = MD5_HASH.match(url)
+ if match is not None:
+ md5_hash = match.group(1)
+ # remove the hash
+ url = url.replace("#md5=%s" % md5_hash, "")
+
+ # parse the archive name to find dist name and version
+ archive_name = urlparse.urlparse(url)[2].split('/')[-1]
+ extension_matched = False
+ # remove the extension from the name
+ for ext in EXTENSIONS:
+ if archive_name.endswith(ext):
+ archive_name = archive_name[:-len(ext)]
+ extension_matched = True
+
+ name, version = split_archive_name(archive_name)
+ if extension_matched is True:
+ return {'name': name,
+ 'version': version,
+ 'url': url,
+ 'hashname': "md5",
+ 'hashval': md5_hash,
+ 'is_external': is_external,
+ 'dist_type': 'sdist'}
+
+
+def split_archive_name(archive_name, probable_name=None):
+ """Split an archive name into two parts: name and version.
+
+ Return the tuple (name, version)
+ """
+ # Try to determine wich part is the name and wich is the version using the
+ # "-" separator. Take the larger part to be the version number then reduce
+ # if this not works.
+ def eager_split(str, maxsplit=2):
+ # split using the "-" separator
+ splits = str.rsplit("-", maxsplit)
+ name = splits[0]
+ version = "-".join(splits[1:])
+ if version.startswith("-"):
+ version = version[1:]
+ if suggest_normalized_version(version) is None and maxsplit >= 0:
+ # we dont get a good version number: recurse !
+ return eager_split(str, maxsplit - 1)
+ else:
+ return (name, version)
+ if probable_name is not None:
+ probable_name = probable_name.lower()
+ name = None
+ if probable_name is not None and probable_name in archive_name:
+ # we get the name from probable_name, if given.
+ name = probable_name
+ version = archive_name.lstrip(name)
+ else:
+ name, version = eager_split(archive_name)
+
+ version = suggest_normalized_version(version)
+ if version is not None and name != "":
+ return (name.lower(), version)
+ else:
+ raise CantParseArchiveName(archive_name)
diff --git a/src/distutils2/index/errors.py b/src/distutils2/index/errors.py
new file mode 100644
index 0000000..730ccce
--- /dev/null
+++ b/src/distutils2/index/errors.py
@@ -0,0 +1,45 @@
+"""distutils2.pypi.errors
+
+All errors and exceptions raised by PyPiIndex classes.
+"""
+from distutils2.errors import DistutilsError
+
+
+class IndexesError(DistutilsError):
+ """The base class for errors of the index python package."""
+
+
+class ProjectNotFound(IndexesError):
+ """Project has not been found"""
+
+
+class DistributionNotFound(IndexesError):
+ """The release has not been found"""
+
+
+class ReleaseNotFound(IndexesError):
+ """The release has not been found"""
+
+
+class CantParseArchiveName(IndexesError):
+ """An archive name can't be parsed to find distribution name and version"""
+
+
+class DownloadError(IndexesError):
+ """An error has occurs while downloading"""
+
+
+class HashDoesNotMatch(DownloadError):
+ """Compared hashes does not match"""
+
+
+class UnsupportedHashName(IndexesError):
+ """A unsupported hashname has been used"""
+
+
+class UnableToDownload(IndexesError):
+ """All mirrors have been tried, without success"""
+
+
+class InvalidSearchField(IndexesError):
+ """An invalid search field has been used"""
diff --git a/src/distutils2/index/mirrors.py b/src/distutils2/index/mirrors.py
new file mode 100644
index 0000000..49d5dd1
--- /dev/null
+++ b/src/distutils2/index/mirrors.py
@@ -0,0 +1,52 @@
+"""Utilities related to the mirror infrastructure defined in PEP 381.
+See http://www.python.org/dev/peps/pep-0381/
+"""
+
+from string import ascii_lowercase
+import socket
+
+DEFAULT_MIRROR_URL = "last.pypi.python.org"
+
+def get_mirrors(hostname=None):
+ """Return the list of mirrors from the last record found on the DNS
+ entry::
+
+ >>> from distutils2.index.mirrors import get_mirrors
+ >>> get_mirrors()
+ ['a.pypi.python.org', 'b.pypi.python.org', 'c.pypi.python.org',
+ 'd.pypi.python.org']
+
+ """
+ if hostname is None:
+ hostname = DEFAULT_MIRROR_URL
+
+ # return the last mirror registered on PyPI.
+ try:
+ hostname = socket.gethostbyname_ex(hostname)[0]
+ except socket.gaierror:
+ return []
+ end_letter = hostname.split(".", 1)
+
+ # determine the list from the last one.
+ return ["%s.%s" % (s, end_letter[1]) for s in string_range(end_letter[0])]
+
+def string_range(last):
+ """Compute the range of string between "a" and last.
+
+ This works for simple "a to z" lists, but also for "a to zz" lists.
+ """
+ for k in range(len(last)):
+ for x in product(ascii_lowercase, repeat=k+1):
+ result = ''.join(x)
+ yield result
+ if result == last:
+ return
+
+def product(*args, **kwds):
+ pools = map(tuple, args) * kwds.get('repeat', 1)
+ result = [[]]
+ for pool in pools:
+ result = [x+[y] for x in result for y in pool]
+ for prod in result:
+ yield tuple(prod)
+
diff --git a/src/distutils2/index/simple.py b/src/distutils2/index/simple.py
new file mode 100644
index 0000000..cdb19c6
--- /dev/null
+++ b/src/distutils2/index/simple.py
@@ -0,0 +1,434 @@
+"""index.simple
+
+Contains the class "SimpleIndexCrawler", a simple spider to find and retrieve
+distributions on the Python Package Index, using it's "simple" API,
+avalaible at http://pypi.python.org/simple/
+"""
+from fnmatch import translate
+import httplib
+import re
+import socket
+import sys
+import urllib2
+import urlparse
+import logging
+import os
+
+from distutils2.index.base import BaseClient
+from distutils2.index.dist import (ReleasesList, EXTENSIONS,
+ get_infos_from_url, MD5_HASH)
+from distutils2.index.errors import (IndexesError, DownloadError,
+ UnableToDownload, CantParseArchiveName,
+ 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']
+
+# -- Constants -----------------------------------------------
+DEFAULT_SIMPLE_INDEX_URL = "http://a.pypi.python.org/simple/"
+DEFAULT_HOSTS = ("*",)
+SOCKET_TIMEOUT = 15
+USER_AGENT = "Python-urllib/%s distutils2/%s" % (
+ sys.version[:3], __distutils2_version__)
+
+# -- Regexps -------------------------------------------------
+EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.]+)$')
+HREF = re.compile("""href\\s*=\\s*['"]?([^'"> ]+)""", re.I)
+URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match
+
+# This pattern matches a character entity reference (a decimal numeric
+# references, a hexadecimal numeric reference, or a named reference).
+ENTITY_SUB = re.compile(r'&(#(\d+|x[\da-fA-F]+)|[\w.:-]+);?').sub
+REL = re.compile("""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I)
+
+
+def socket_timeout(timeout=SOCKET_TIMEOUT):
+ """Decorator to add a socket timeout when requesting pages on PyPI.
+ """
+ def _socket_timeout(func):
+ def _socket_timeout(self, *args, **kwargs):
+ old_timeout = socket.getdefaulttimeout()
+ if hasattr(self, "_timeout"):
+ timeout = self._timeout
+ socket.setdefaulttimeout(timeout)
+ try:
+ return func(self, *args, **kwargs)
+ finally:
+ socket.setdefaulttimeout(old_timeout)
+ return _socket_timeout
+ return _socket_timeout
+
+
+def with_mirror_support():
+ """Decorator that makes the mirroring support easier"""
+ def wrapper(func):
+ def wrapped(self, *args, **kwargs):
+ try:
+ return func(self, *args, **kwargs)
+ except DownloadError:
+ # if an error occurs, try with the next index_url
+ if self._mirrors_tries >= self._mirrors_max_tries:
+ try:
+ self._switch_to_next_mirror()
+ except KeyError:
+ raise UnableToDownload("Tried all mirrors")
+ else:
+ self._mirrors_tries += 1
+ self._projects.clear()
+ return wrapped(self, *args, **kwargs)
+ return wrapped
+ return wrapper
+
+
+class Crawler(BaseClient):
+ """Provides useful tools to request the Python Package Index simple API.
+
+ You can specify both mirrors and mirrors_url, but mirrors_url will only be
+ used if mirrors is set to None.
+
+ :param index_url: the url of the simple index to search on.
+ :param prefer_final: if the version is not mentioned, and the last
+ version is not a "final" one (alpha, beta, etc.),
+ pick up the last final version.
+ :param prefer_source: if the distribution type is not mentioned, pick up
+ the source one if available.
+ :param follow_externals: tell if following external links is needed or
+ not. Default is False.
+ :param hosts: a list of hosts allowed to be processed while using
+ follow_externals=True. Default behavior is to follow all
+ hosts.
+ :param follow_externals: tell if following external links is needed or
+ not. Default is False.
+ :param mirrors_url: the url to look on for DNS records giving mirror
+ adresses.
+ :param mirrors: a list of mirrors (see PEP 381).
+ :param timeout: time in seconds to consider a url has timeouted.
+ :param mirrors_max_tries": number of times to try requesting informations
+ on mirrors before switching.
+ """
+
+ def __init__(self, index_url=DEFAULT_SIMPLE_INDEX_URL, prefer_final=False,
+ prefer_source=True, hosts=DEFAULT_HOSTS,
+ follow_externals=False, mirrors_url=None, mirrors=None,
+ timeout=SOCKET_TIMEOUT, mirrors_max_tries=0):
+ super(Crawler, self).__init__(prefer_final, prefer_source)
+ self.follow_externals = follow_externals
+
+ # mirroring attributes.
+ if not index_url.endswith("/"):
+ index_url += "/"
+ # if no mirrors are defined, use the method described in PEP 381.
+ if mirrors is None:
+ mirrors = get_mirrors(mirrors_url)
+ self._mirrors = set(mirrors)
+ self._mirrors_used = set()
+ self.index_url = index_url
+ self._mirrors_max_tries = mirrors_max_tries
+ self._mirrors_tries = 0
+ self._timeout = timeout
+
+ # create a regexp to match all given hosts
+ self._allowed_hosts = re.compile('|'.join(map(translate, hosts))).match
+
+ # we keep an index of pages we have processed, in order to avoid
+ # scanning them multple time (eg. if there is multiple pages pointing
+ # on one)
+ self._processed_urls = []
+ self._projects = {}
+
+ @with_mirror_support()
+ def search_projects(self, name=None, **kwargs):
+ """Search the index for projects containing the given name.
+
+ Return a list of names.
+ """
+ index = self._open_url(self.index_url)
+ projectname = re.compile("""<a[^>]*>(.?[^<]*%s.?[^<]*)</a>""" % name,
+ flags=re.I)
+ matching_projects = []
+ for match in projectname.finditer(index.read()):
+ project_name = match.group(1)
+ matching_projects.append(self._get_project(project_name))
+ return matching_projects
+
+ def get_releases(self, requirements, prefer_final=None,
+ force_update=False):
+ """Search for releases and return a ReleaseList object containing
+ the results.
+ """
+ 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)
+ self._process_index_page(predicate.name)
+
+ if not self._projects.has_key(predicate.name.lower()):
+ raise ProjectNotFound()
+
+ releases = self._projects.get(predicate.name.lower())
+ releases.sort_releases(prefer_final=prefer_final)
+ return releases
+
+ def get_release(self, requirements, prefer_final=None):
+ """Return only one release that fulfill the given requirements"""
+ predicate = get_version_predicate(requirements)
+ release = self.get_releases(predicate, prefer_final)\
+ .get_last(predicate)
+ if not release:
+ raise ReleaseNotFound("No release matches the given criterias")
+ return release
+
+ def get_distributions(self, project_name, version):
+ """Return the distributions found on the index for the specific given
+ release"""
+ # as the default behavior of get_release is to return a release
+ # containing the distributions, just alias it.
+ return self.get_release("%s (%s)" % (project_name, version))
+
+ def get_metadata(self, project_name, version):
+ """Return the metadatas from the simple index.
+
+ Currently, download one archive, extract it and use the PKG-INFO file.
+ """
+ release = self.get_distributions(project_name, version)
+ if not release._metadata:
+ location = release.get_distribution().unpack()
+ pkg_info = os.path.join(location, 'PKG-INFO')
+ release._metadata = DistributionMetadata(pkg_info)
+ return release
+
+ def _switch_to_next_mirror(self):
+ """Switch to the next mirror (eg. point self.index_url to the next
+ mirror url.
+
+ Raise a KeyError if all mirrors have been tried.
+ """
+ self._mirrors_used.add(self.index_url)
+ index_url = self._mirrors.pop()
+ if not ("http://" or "https://" or "file://") in index_url:
+ index_url = "http://%s" % index_url
+
+ if not index_url.endswith("/simple"):
+ index_url = "%s/simple/" % index_url
+
+ self.index_url = index_url
+
+ def _is_browsable(self, url):
+ """Tell if the given URL can be browsed or not.
+
+ It uses the follow_externals and the hosts list to tell if the given
+ url is browsable or not.
+ """
+ # if _index_url is contained in the given URL, we are browsing the
+ # index, and it's always "browsable".
+ # local files are always considered browable resources
+ if self.index_url in url or urlparse.urlparse(url)[0] == "file":
+ return True
+ elif self.follow_externals:
+ if self._allowed_hosts(urlparse.urlparse(url)[1]): # 1 is netloc
+ return True
+ else:
+ return False
+ return False
+
+ def _is_distribution(self, link):
+ """Tell if the given URL matches to a distribution name or not.
+ """
+ #XXX find a better way to check that links are distributions
+ # Using a regexp ?
+ for ext in EXTENSIONS:
+ if ext in link:
+ return True
+ return False
+
+ def _register_release(self, release=None, release_info={}):
+ """Register a new release.
+
+ Both a release or a dict of release_info can be provided, the prefered
+ way (eg. the quicker) is the dict one.
+
+ Return the list of existing releases for the given project.
+ """
+ # Check if the project already has a list of releases (refering to
+ # the project name). If not, create a new release list.
+ # Then, add the release to the list.
+ if release:
+ name = release.name
+ else:
+ name = release_info['name']
+ if not name.lower() in self._projects:
+ self._projects[name.lower()] = ReleasesList(name,
+ index=self._index)
+
+ if release:
+ self._projects[name.lower()].add_release(release=release)
+ else:
+ name = release_info.pop('name')
+ version = release_info.pop('version')
+ dist_type = release_info.pop('dist_type')
+ self._projects[name.lower()].add_release(version, dist_type,
+ **release_info)
+ return self._projects[name.lower()]
+
+ def _process_url(self, url, project_name=None, follow_links=True):
+ """Process an url and search for distributions packages.
+
+ For each URL found, if it's a download, creates a PyPIdistribution
+ object. If it's a homepage and we can follow links, process it too.
+
+ :param url: the url to process
+ :param project_name: the project name we are searching for.
+ :param follow_links: Do not want to follow links more than from one
+ level. This parameter tells if we want to follow
+ the links we find (eg. run recursively this
+ method on it)
+ """
+ f = self._open_url(url)
+ base_url = f.url
+ if url not in self._processed_urls:
+ self._processed_urls.append(url)
+ link_matcher = self._get_link_matcher(url)
+ for link, is_download in link_matcher(f.read(), base_url):
+ if link not in self._processed_urls:
+ if self._is_distribution(link) or is_download:
+ self._processed_urls.append(link)
+ # it's a distribution, so create a dist object
+ try:
+ infos = get_infos_from_url(link, project_name,
+ is_external=not self.index_url in url)
+ except CantParseArchiveName, e:
+ logging.warning("version has not been parsed: %s"
+ % e)
+ else:
+ self._register_release(release_info=infos)
+ else:
+ if self._is_browsable(link) and follow_links:
+ self._process_url(link, project_name,
+ follow_links=False)
+
+ def _get_link_matcher(self, url):
+ """Returns the right link matcher function of the given url
+ """
+ if self.index_url in url:
+ return self._simple_link_matcher
+ else:
+ return self._default_link_matcher
+
+ def _get_full_url(self, url, base_url):
+ return urlparse.urljoin(base_url, self._htmldecode(url))
+
+ def _simple_link_matcher(self, content, base_url):
+ """Yield all links with a rel="download" or rel="homepage".
+
+ This matches the simple index requirements for matching links.
+ If follow_externals is set to False, dont yeld the external
+ urls.
+ """
+ for match in HREF.finditer(content):
+ url = self._get_full_url(match.group(1), base_url)
+ if MD5_HASH.match(url):
+ yield (url, True)
+
+ for match in REL.finditer(content):
+ # search for rel links.
+ tag, rel = match.groups()
+ rels = map(str.strip, rel.lower().split(','))
+ if 'homepage' in rels or 'download' in rels:
+ for match in HREF.finditer(tag):
+ url = self._get_full_url(match.group(1), base_url)
+ if 'download' in rels or self._is_browsable(url):
+ # yield a list of (url, is_download)
+ yield (url, 'download' in rels)
+
+ def _default_link_matcher(self, content, base_url):
+ """Yield all links found on the page.
+ """
+ for match in HREF.finditer(content):
+ url = self._get_full_url(match.group(1), base_url)
+ if self._is_browsable(url):
+ yield (url, False)
+
+ @with_mirror_support()
+ def _process_index_page(self, name):
+ """Find and process a PyPI page for the given project name.
+
+ :param name: the name of the project to find the page
+ """
+ # Browse and index the content of the given PyPI page.
+ url = self.index_url + name + "/"
+ self._process_url(url, name)
+
+ @socket_timeout()
+ def _open_url(self, url):
+ """Open a urllib2 request, handling HTTP authentication, and local
+ files support.
+
+ """
+ scheme, netloc, path, params, query, frag = urlparse.urlparse(url)
+
+ # authentication stuff
+ if scheme in ('http', 'https'):
+ auth, host = urllib2.splituser(netloc)
+ else:
+ auth = None
+
+ # add index.html automatically for filesystem paths
+ if scheme == 'file':
+ if url.endswith('/'):
+ url += "index.html"
+
+ # add authorization headers if auth is provided
+ if auth:
+ auth = "Basic " + \
+ urllib2.unquote(auth).encode('base64').strip()
+ new_url = urlparse.urlunparse((
+ scheme, host, path, params, query, frag))
+ request = urllib2.Request(new_url)
+ request.add_header("Authorization", auth)
+ else:
+ request = urllib2.Request(url)
+ request.add_header('User-Agent', USER_AGENT)
+ try:
+ fp = urllib2.urlopen(request)
+ except (ValueError, httplib.InvalidURL), v:
+ msg = ' '.join([str(arg) for arg in v.args])
+ raise IndexesError('%s %s' % (url, msg))
+ except urllib2.HTTPError, v:
+ return v
+ except urllib2.URLError, v:
+ raise DownloadError("Download error for %s: %s" % (url, v.reason))
+ except httplib.BadStatusLine, v:
+ raise DownloadError('%s returned a bad status line. '
+ 'The server might be down, %s' % (url, v.line))
+ except httplib.HTTPException, v:
+ raise DownloadError("Download error for %s: %s" % (url, v))
+ except socket.timeout:
+ raise DownloadError("The server timeouted")
+
+ if auth:
+ # Put authentication info back into request URL if same host,
+ # so that links found on the page will work
+ s2, h2, path2, param2, query2, frag2 = \
+ urlparse.urlparse(fp.url)
+ if s2 == scheme and h2 == host:
+ fp.url = urlparse.urlunparse(
+ (s2, netloc, path2, param2, query2, frag2))
+ return fp
+
+ def _decode_entity(self, match):
+ what = match.group(1)
+ if what.startswith('#x'):
+ what = int(what[2:], 16)
+ elif what.startswith('#'):
+ what = int(what[1:])
+ else:
+ from htmlentitydefs import name2codepoint
+ what = name2codepoint.get(what, match.group(0))
+ return unichr(what)
+
+ def _htmldecode(self, text):
+ """Decode HTML entities in the given text."""
+ return ENTITY_SUB(self._decode_entity, text)
diff --git a/src/distutils2/index/wrapper.py b/src/distutils2/index/wrapper.py
new file mode 100644
index 0000000..e3c4551
--- /dev/null
+++ b/src/distutils2/index/wrapper.py
@@ -0,0 +1,93 @@
+import xmlrpc
+import simple
+
+_WRAPPER_MAPPINGS = {'get_release': 'simple',
+ 'get_releases': 'simple',
+ 'search_projects': 'simple',
+ 'get_metadata': 'xmlrpc',
+ 'get_distributions': 'simple'}
+
+_WRAPPER_INDEXES = {'xmlrpc': xmlrpc.Client,
+ 'simple': simple.Crawler}
+
+def switch_index_if_fails(func, wrapper):
+ """Decorator that switch of index (for instance from xmlrpc to simple)
+ if the first mirror return an empty list or raises an exception.
+ """
+ def decorator(*args, **kwargs):
+ retry = True
+ exception = None
+ methods = [func]
+ for f in wrapper._indexes.values():
+ if f != func.im_self and hasattr(f, func.__name__):
+ methods.append(getattr(f, func.__name__))
+ for method in methods:
+ try:
+ response = method(*args, **kwargs)
+ retry = False
+ except Exception, e:
+ exception = e
+ if not retry:
+ break
+ if retry and exception:
+ raise exception
+ else:
+ return response
+ return decorator
+
+
+class ClientWrapper(object):
+ """Wrapper around simple and xmlrpc clients,
+
+ Choose the best implementation to use depending the needs, using the given
+ mappings.
+ If one of the indexes returns an error, tries to use others indexes.
+
+ :param index: tell wich index to rely on by default.
+ :param index_classes: a dict of name:class to use as indexes.
+ :param indexes: a dict of name:index already instantiated
+ :param mappings: the mappings to use for this wrapper
+ """
+
+ def __init__(self, default_index='simple', index_classes=_WRAPPER_INDEXES,
+ indexes={}, mappings=_WRAPPER_MAPPINGS):
+ self._projects = {}
+ self._mappings = mappings
+ self._indexes = indexes
+ self._default_index = default_index
+
+ # instantiate the classes and set their _project attribute to the one
+ # of the wrapper.
+ for name, cls in index_classes.items():
+ obj = self._indexes.setdefault(name, cls())
+ obj._projects = self._projects
+ obj._index = self
+
+ def __getattr__(self, method_name):
+ """When asking for methods of the wrapper, return the implementation of
+ the wrapped classes, depending the mapping.
+
+ Decorate the methods to switch of implementation if an error occurs
+ """
+ real_method = None
+ if method_name in _WRAPPER_MAPPINGS:
+ obj = self._indexes[_WRAPPER_MAPPINGS[method_name]]
+ real_method = getattr(obj, method_name)
+ else:
+ # the method is not defined in the mappings, so we try first to get
+ # it via the default index, and rely on others if needed.
+ try:
+ real_method = getattr(self._indexes[self._default_index],
+ method_name)
+ except AttributeError:
+ other_indexes = [i for i in self._indexes
+ if i != self._default_index]
+ for index in other_indexes:
+ real_method = getattr(self._indexes[index], method_name, None)
+ if real_method:
+ break
+ if real_method:
+ return switch_index_if_fails(real_method, self)
+ else:
+ raise AttributeError("No index have attribute '%s'" % method_name)
+
diff --git a/src/distutils2/index/xmlrpc.py b/src/distutils2/index/xmlrpc.py
new file mode 100644
index 0000000..b9d66e0
--- /dev/null
+++ b/src/distutils2/index/xmlrpc.py
@@ -0,0 +1,179 @@
+import logging
+import xmlrpclib
+
+from distutils2.errors import IrrationalVersionError
+from distutils2.index.base import BaseClient
+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']
+
+DEFAULT_XMLRPC_INDEX_URL = 'http://python.org/pypi'
+
+_SEARCH_FIELDS = ['name', 'version', 'author', 'author_email', 'maintainer',
+ 'maintainer_email', 'home_page', 'license', 'summary',
+ 'description', 'keywords', 'platform', 'download_url']
+
+
+class Client(BaseClient):
+ """Client to query indexes using XML-RPC method calls.
+
+ If no server_url is specified, use the default PyPI XML-RPC URL,
+ defined in the DEFAULT_XMLRPC_INDEX_URL constant::
+
+ >>> client = XMLRPCClient()
+ >>> client.server_url == DEFAULT_XMLRPC_INDEX_URL
+ True
+
+ >>> client = XMLRPCClient("http://someurl/")
+ >>> client.server_url
+ 'http://someurl/'
+ """
+
+ def __init__(self, server_url=DEFAULT_XMLRPC_INDEX_URL, prefer_final=False,
+ prefer_source=True):
+ super(Client, self).__init__(prefer_final, prefer_source)
+ self.server_url = server_url
+ self._projects = {}
+
+ def get_release(self, requirements, prefer_final=False):
+ """Return a release with all complete metadata and distribution
+ related informations.
+ """
+ prefer_final = self._get_prefer_final(prefer_final)
+ 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)
+ self.get_distributions(release.name, "%s" % release.version)
+ return release
+
+ def get_releases(self, requirements, prefer_final=None, show_hidden=True,
+ force_update=False):
+ """Return the list of existing releases for a specific project.
+
+ Cache the results from one call to another.
+
+ If show_hidden is True, return the hidden releases too.
+ If force_update is True, reprocess the index to update the
+ informations (eg. make a new XML-RPC call).
+ ::
+
+ >>> client = XMLRPCClient()
+ >>> client.get_releases('Foo')
+ ['1.1', '1.2', '1.3']
+
+ If no such project exists, raise a ProjectNotFound exception::
+
+ >>> client.get_project_versions('UnexistingProject')
+ ProjectNotFound: UnexistingProject
+
+ """
+ def get_versions(project_name, show_hidden):
+ return self.proxy.package_releases(project_name, show_hidden)
+
+ 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):
+ project = self._projects[project_name.lower()]
+ if not project.contains_hidden and show_hidden:
+ # if hidden releases are requested, and have an existing
+ # list of releases that does not contains hidden ones
+ all_versions = get_versions(project_name, show_hidden)
+ existing_versions = project.get_versions()
+ hidden_versions = list(set(all_versions) -
+ set(existing_versions))
+ for version in hidden_versions:
+ project.add_release(release=ReleaseInfo(project_name,
+ version, index=self._index))
+ else:
+ versions = get_versions(project_name, show_hidden)
+ if not versions:
+ raise ProjectNotFound(project_name)
+ project = self._get_project(project_name)
+ project.add_releases([ReleaseInfo(project_name, version,
+ 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
+
+
+ def get_distributions(self, project_name, version):
+ """Grab informations about distributions from XML-RPC.
+
+ Return a ReleaseInfo object, with distribution-related informations
+ filled in.
+ """
+ url_infos = self.proxy.release_urls(project_name, version)
+ project = self._get_project(project_name)
+ if version not in project.get_versions():
+ project.add_release(release=ReleaseInfo(project_name, version,
+ index=self._index))
+ release = project.get_release(version)
+ for info in url_infos:
+ packagetype = info['packagetype']
+ dist_infos = {'url': info['url'],
+ 'hashval': info['md5_digest'],
+ 'hashname': 'md5',
+ 'is_external': False,
+ 'python_version': info['python_version']}
+ release.add_distribution(packagetype, **dist_infos)
+ return release
+
+ def get_metadata(self, project_name, version):
+ """Retreive project metadatas.
+
+ Return a ReleaseInfo object, with metadata informations filled in.
+ """
+ metadata = self.proxy.release_data(project_name, version)
+ project = self._get_project(project_name)
+ if version not in project.get_versions():
+ project.add_release(release=ReleaseInfo(project_name, version,
+ index=self._index))
+ release = project.get_release(version)
+ release.set_metadata(metadata)
+ return release
+
+ def search_projects(self, name=None, operator="or", **kwargs):
+ """Find using the keys provided in kwargs.
+
+ You can set operator to "and" or "or".
+ """
+ for key in kwargs:
+ if key not in _SEARCH_FIELDS:
+ raise InvalidSearchField(key)
+ if name:
+ kwargs["name"] = name
+ projects = self.proxy.search(kwargs, operator)
+ for p in projects:
+ project = self._get_project(p['name'])
+ try:
+ project.add_release(release=ReleaseInfo(p['name'],
+ p['version'], metadata={'summary': p['summary']},
+ index=self._index))
+ except IrrationalVersionError, e:
+ logging.warn("Irrational version error found: %s" % e)
+
+ return [self._projects[p['name'].lower()] for p in projects]
+
+ @property
+ def proxy(self):
+ """Property used to return the XMLRPC server proxy.
+
+ If no server proxy is defined yet, creates a new one::
+
+ >>> client = XmlRpcClient()
+ >>> client.proxy()
+ <ServerProxy for python.org/pypi>
+
+ """
+ if not hasattr(self, '_server_proxy'):
+ self._server_proxy = xmlrpclib.ServerProxy(self.server_url)
+
+ return self._server_proxy
diff --git a/src/distutils2/install_with_deps.py b/src/distutils2/install_with_deps.py
new file mode 100644
index 0000000..6951ff9
--- /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
index 87634c9..0d838ae 100644
--- a/src/distutils2/manifest.py
+++ b/src/distutils2/manifest.py
@@ -22,7 +22,7 @@ __all__ = ['Manifest']
# 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
index 55f1020..ed80fac 100644
--- a/src/distutils2/metadata.py
+++ b/src/distutils2/metadata.py
@@ -1,13 +1,12 @@
-"""
-Implementation of the Metadata for Python packages
+"""Implementation of the Metadata for Python packages PEPs.
-Supports all Metadata formats (1.0, 1.1, 1.2).
+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
@@ -53,12 +52,12 @@ PKG_INFO_ENCODING = 'utf-8'
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',
@@ -66,7 +65,7 @@ _314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
_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',
@@ -78,12 +77,11 @@ _345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python',
'Obsoletes-Dist', 'Requires-External', 'Maintainer',
'Maintainer-email', 'Project-URL')
-_ALL_FIELDS = []
+_ALL_FIELDS = set()
+_ALL_FIELDS.update(_241_FIELDS)
+_ALL_FIELDS.update(_314_FIELDS)
+_ALL_FIELDS.update(_345_FIELDS)
-for field in _241_FIELDS + _314_FIELDS + _345_FIELDS:
- if field in _ALL_FIELDS:
- continue
- _ALL_FIELDS.append(field)
def _version2fieldlist(version):
if version == '1.0':
@@ -94,8 +92,9 @@ def _version2fieldlist(version):
return _345_FIELDS
raise MetadataUnrecognizedVersionError(version)
+
def _best_version(fields):
- """Will detect the best version depending on the fields used."""
+ """Detect the best version depending on the fields used."""
def _has_marker(keys, markers):
for marker in markers:
if marker in keys:
@@ -182,19 +181,33 @@ _UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
class DistributionMetadata(object):
- """Distribution meta-data class (1.0 or 1.2).
+ """The metadata of a release.
+
+ Supports versions 1.0, 1.1 and 1.2 (auto-detected). You can
+ instantiate the class with one of these arguments (or none):
+ - *path*, the path to a METADATA file
+ - *fileobj* give a file-like object with METADATA as content
+ - *mapping* is a dict-like object
"""
+ # TODO document that execution_context and platform_dependent are used
+ # to filter on query, not when setting a key
+ # also document the mapping API and UNKNOWN default key
+
def __init__(self, path=None, platform_dependent=False,
- execution_context=None, fileobj=None):
+ execution_context=None, fileobj=None, mapping=None):
self._fields = {}
self.version = None
self.docutils_support = _HAS_DOCUTILS
self.platform_dependent = platform_dependent
+ self.execution_context = execution_context
+ if [path, fileobj, mapping].count(None) < 2:
+ raise TypeError('path, fileobj and mapping are exclusive')
if path is not None:
self.read(path)
elif fileobj is not None:
self.read_file(fileobj)
- self.execution_context = execution_context
+ elif mapping is not None:
+ self.update(mapping)
def _set_best_version(self):
self.version = _best_version(self._fields)
@@ -203,10 +216,6 @@ class DistributionMetadata(object):
def _write_field(self, file, name, value):
file.write('%s: %s\n' % (name, value))
- def _write_list(self, file, name, values):
- for value in values:
- self._write_field(file, name, value)
-
def _encode_field(self, value):
if isinstance(value, unicode):
return value.encode(PKG_INFO_ENCODING)
@@ -226,17 +235,15 @@ class DistributionMetadata(object):
if name in _ALL_FIELDS:
return name
name = name.replace('-', '_').lower()
- if name in _ATTR2FIELD:
- return _ATTR2FIELD[name]
- return name
+ return _ATTR2FIELD.get(name, name)
def _default_value(self, name):
- if name in _LISTFIELDS + _ELEMENTSFIELD:
+ if name in _LISTFIELDS or name in _ELEMENTSFIELD:
return []
return 'UNKNOWN'
def _check_rst_data(self, data):
- """Returns warnings when the provided data doesn't compile."""
+ """Return warnings when the provided data has syntax errors."""
source_path = StringIO()
parser = Parser()
settings = frontend.OptionParser().get_default_values()
@@ -271,7 +278,7 @@ class DistributionMetadata(object):
return _LINE_PREFIX.sub('\n', value)
#
- # Public APIs
+ # Public API
#
def get_fullname(self):
return '%s-%s' % (self['Name'], self['Version'])
@@ -284,7 +291,7 @@ class DistributionMetadata(object):
self.read_file(open(filepath))
def read_file(self, fileob):
- """Reads the metadata values from a file object."""
+ """Read the metadata values from a file object."""
msg = message_from_file(fileob)
self.version = msg['metadata-version']
@@ -302,8 +309,7 @@ class DistributionMetadata(object):
self.set(field, value)
def write(self, filepath):
- """Write the metadata fields into path.
- """
+ """Write the metadata fields to filepath."""
pkg_info = open(filepath, 'w')
try:
self.write_file(pkg_info)
@@ -311,8 +317,7 @@ class DistributionMetadata(object):
pkg_info.close()
def write_file(self, fileobject):
- """Write the PKG-INFO format data to a file object.
- """
+ """Write the PKG-INFO format data to a file object."""
self._set_best_version()
for field in _version2fieldlist(self.version):
values = self.get(field)
@@ -330,18 +335,50 @@ class DistributionMetadata(object):
for value in values:
self._write_field(fileobject, field, value)
+ def update(self, other=None, **kwargs):
+ """Set metadata values from the given mapping
+
+ Convert the keys to Metadata fields. Given keys that don't match a
+ metadata argument will not be used.
+
+ If overwrite is set to False, just add metadata values that are
+ actually not defined.
+
+ If there is existing values in conflict with the dictionary ones, the
+ new values prevails.
+
+ Empty values (e.g. None and []) are not setted this way.
+ """
+ def _set(key, value):
+ if value not in ([], None, '') and key in _ATTR2FIELD:
+ self.set(self._convert_name(key), value)
+
+ if other is None:
+ pass
+ elif hasattr(other, 'iteritems'): # iteritems saves memory and lookups
+ for k, v in other.iteritems():
+ _set(k, v)
+ elif hasattr(other, 'keys'):
+ for k in other.keys():
+ _set(k, v)
+ else:
+ for k, v in other:
+ _set(k, v)
+ if kwargs:
+ self.update(kwargs)
+
def set(self, name, value):
- """Controls then sets a metadata field"""
+ """Control then set a metadata field."""
name = self._convert_name(name)
- if (name in _ELEMENTSFIELD + ('Platform',) and
+ if ((name in _ELEMENTSFIELD or name == 'Platform') and
not isinstance(value, (list, tuple))):
if isinstance(value, str):
value = value.split(',')
else:
value = []
elif (name in _LISTFIELDS and
- not isinstance(value, (list, tuple))):
+ not isinstance(value, (list, tuple))):
if isinstance(value, str):
value = [value]
else:
@@ -351,13 +388,16 @@ class DistributionMetadata(object):
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)
@@ -368,7 +408,7 @@ class DistributionMetadata(object):
self._set_best_version()
def get(self, name):
- """Gets a metadata field."""
+ """Get a metadata field."""
name = self._convert_name(name)
if name not in self._fields:
return self._default_value(name)
@@ -403,24 +443,21 @@ class DistributionMetadata(object):
return value
def check(self):
- """Checks if the metadata are compliant."""
+ """Check if the metadata is compliant."""
# XXX should check the versions (if the file was loaded)
- missing = []
+ missing, warnings = [], []
for attr in ('Name', 'Version', 'Home-page'):
value = self[attr]
if value == 'UNKNOWN':
missing.append(attr)
if _HAS_DOCUTILS:
- warnings = self._check_rst_data(self['Description'])
- else:
- warnings = []
+ 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':
return missing, warnings
-
def is_valid_predicates(value):
for v in value:
if not is_valid_predicate(v.split(';')[0]):
@@ -428,16 +465,15 @@ class DistributionMetadata(object):
return True
for fields, controller in ((_PREDICATE_FIELDS, is_valid_predicates),
- (_VERSIONS_FIELDS, is_valid_versions),
- (_VERSION_FIELDS, is_valid_version)):
+ (_VERSIONS_FIELDS, is_valid_versions),
+ (_VERSION_FIELDS, is_valid_version)):
for field in fields:
value = self[field]
if value == 'UNKNOWN':
continue
if not controller(value):
- warnings.append('Wrong value for "%s": %s' \
- % (field, value))
+ warnings.append('Wrong value for %r: %s' % (field, value))
return missing, warnings
@@ -454,7 +490,6 @@ class DistributionMetadata(object):
#
# micro-language for PEP 345 environment markers
#
-_STR_LIMIT = "'\""
# allowed operators
_OPERATORS = {'==': lambda x, y: x == y,
@@ -466,17 +501,18 @@ _OPERATORS = {'==': lambda x, y: x == y,
'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)
# restricted set of variables
_VARS = {'sys.platform': sys.platform,
- 'python_version': '%s.%s' % (sys.version_info[0],
- sys.version_info[1]),
- 'python_full_version': sys.version.split()[0],
+ 'python_version': sys.version[:3],
+ 'python_full_version': sys.version.split(' ', 1)[0],
'os.name': os.name,
- 'platform.version': platform.version,
- 'platform.machine': platform.machine}
+ 'platform.version': platform.version(),
+ 'platform.machine': platform.machine()}
+
class _Operation(object):
@@ -499,7 +535,7 @@ class _Operation(object):
def _is_string(self, value):
if value is None or len(value) < 2:
return False
- for delimiter in _STR_LIMIT:
+ for delimiter in '"\'':
if value[0] == value[-1] == delimiter:
return True
return False
@@ -510,7 +546,7 @@ class _Operation(object):
def _convert(self, value):
if value in _VARS:
return self._get_var(value)
- return value.strip(_STR_LIMIT)
+ return value.strip('"\'')
def _check_name(self, value):
if value not in _VARS:
@@ -538,6 +574,7 @@ class _Operation(object):
right = self._convert(self.right)
return _operate(self.op, left, right)
+
class _OR(object):
def __init__(self, left, right=None):
self.left = left
@@ -547,7 +584,7 @@ class _OR(object):
return self.right is not None
def __repr__(self):
- return 'OR(%s, %s)' % (repr(self.left), repr(self.right))
+ return 'OR(%r, %r)' % (self.left, self.right)
def __call__(self):
return self.left() or self.right()
@@ -562,11 +599,12 @@ class _AND(object):
return self.right is not None
def __repr__(self):
- return 'AND(%s, %s)' % (repr(self.left), repr(self.right))
+ return 'AND(%r, %r)' % (self.left, self.right)
def __call__(self):
return self.left() and self.right()
+
class _CHAIN(object):
def __init__(self, execution_context=None):
@@ -628,8 +666,9 @@ class _CHAIN(object):
return False
return True
+
def _interpret(marker, execution_context=None):
- """Interprets a marker and return a result given the environment."""
+ """Interpret a marker and return a result depending on environment."""
marker = marker.strip()
operations = _CHAIN(execution_context)
tokenize(StringIO(marker).readline, operations.eat)
diff --git a/src/distutils2/mkpkg.py b/src/distutils2/mkpkg.py
index 3fe82e3..0512592 100755
--- 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 @@ def askYn(question, default = None, helptext = None):
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 @@ def ask(question, default = None, helptext = None, required = True,
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 @@ def buildTroveDict(troveList):
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 @@ class SetupClass(object):
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 @@ class SetupClass(object):
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 @@ class SetupClass(object):
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':
@@ -737,14 +747,14 @@ class SetupClass(object):
self.setupData['name'] = m.group(1)
self.setupData['version'] = m.group(2)
- for root, dirs, files in os.walk('.'):
+ for root, dirs, files in os.walk(os.curdir):
for file in files:
- if root == '.' and file == 'setup.py': continue
+ if root == os.curdir and file == 'setup.py': continue
fileName = os.path.join(root, file)
self.inspectFile(fileName)
if file == '__init__.py':
- trySrc = os.path.join('.', 'src')
+ trySrc = os.path.join(os.curdir, 'src')
tmpRoot = root
if tmpRoot.startswith(trySrc):
tmpRoot = tmpRoot[len(trySrc):]
@@ -761,16 +771,16 @@ class SetupClass(object):
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 @@ class SetupClass(object):
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 @@ class SetupClass(object):
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 @@ class SetupClass(object):
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 @@ class SetupClass(object):
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 @@ class SetupClass(object):
6 - Mature
7 - Inactive
-Status''', required = False)
+Status''', required=False)
if devStatus:
try:
key = {
@@ -884,7 +902,8 @@ Status''', required = False)
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/pypi/__init__.py b/src/distutils2/pypi/__init__.py
deleted file mode 100644
index 88efa14..0000000
--- a/src/distutils2/pypi/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""distutils2.pypi
-
-Package containing ways to interact with the PyPI APIs.
-"""
-
-__all__ = ['simple',
- 'dist',
-]
diff --git a/src/distutils2/pypi/dist.py b/src/distutils2/pypi/dist.py
deleted file mode 100644
index 418186e..0000000
--- a/src/distutils2/pypi/dist.py
+++ /dev/null
@@ -1,315 +0,0 @@
-"""distutils2.pypi.dist
-
-Provides the PyPIDistribution class thats represents a distribution retrieved
-on PyPI.
-"""
-import re
-import urlparse
-import urllib
-import tempfile
-from operator import attrgetter
-
-try:
- import hashlib
-except ImportError:
- from distutils2._backport import hashlib
-
-from distutils2.version import suggest_normalized_version, NormalizedVersion
-from distutils2.pypi.errors import HashDoesNotMatch, UnsupportedHashName
-
-EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz .egg".split()
-MD5_HASH = re.compile(r'^.*#md5=([a-f0-9]+)$')
-
-
-class PyPIDistribution(object):
- """Represents a distribution retrieved from PyPI.
-
- This is a simple container for various attributes as name, version,
- downloaded_location, url etc.
-
- The PyPIDistribution class is used by the pypi.*Index class to return
- information about distributions.
- """
-
- @classmethod
- def from_url(cls, url, probable_dist_name=None, is_external=True):
- """Build a Distribution from a url archive (egg or zip or tgz).
-
- :param url: complete url of the distribution
- :param probable_dist_name: A probable name of the distribution.
- :param is_external: Tell if the url commes from an index or from
- an external URL.
- """
- # if the url contains a md5 hash, get it.
- md5_hash = None
- match = MD5_HASH.match(url)
- if match is not None:
- md5_hash = match.group(1)
- # remove the hash
- url = url.replace("#md5=%s" % md5_hash, "")
-
- # parse the archive name to find dist name and version
- archive_name = urlparse.urlparse(url)[2].split('/')[-1]
- extension_matched = False
- # remove the extension from the name
- for ext in EXTENSIONS:
- if archive_name.endswith(ext):
- archive_name = archive_name[:-len(ext)]
- extension_matched = True
-
- name, version = split_archive_name(archive_name)
- if extension_matched is True:
- return PyPIDistribution(name, version, url=url, url_hashname="md5",
- url_hashval=md5_hash,
- url_is_external=is_external)
-
- def __init__(self, name, version, type=None, url=None, url_hashname=None,
- url_hashval=None, url_is_external=True):
- """Create a new instance of PyPIDistribution.
-
- :param name: the name of the distribution
- :param version: the version of the distribution
- :param type: the type of the dist (eg. source, bin-*, etc.)
- :param url: URL where we found this distribution
- :param url_hashname: the name of the hash we want to use. Refer to the
- hashlib.new documentation for more information.
- :param url_hashval: the hash value.
- :param url_is_external: we need to know if the provided url comes from an
- index browsing, or from an external resource.
-
- """
- self.name = name
- self.version = NormalizedVersion(version)
- self.type = type
- # set the downloaded path to None by default. The goal here
- # is to not download distributions multiple times
- self.downloaded_location = None
- # We store urls in dict, because we need to have a bit more informations
- # than the simple URL. It will be used later to find the good url to
- # use.
- # We have two _url* attributes: _url and _urls. _urls contains a list of
- # dict for the different urls, and _url contains the choosen url, in
- # order to dont make the selection process multiple times.
- self._urls = []
- self._url = None
- self.add_url(url, url_hashname, url_hashval, url_is_external)
-
- def add_url(self, url, hashname=None, hashval=None, is_external=True):
- """Add a new url to the list of urls"""
- if hashname is not None:
- try:
- hashlib.new(hashname)
- except ValueError:
- raise UnsupportedHashName(hashname)
-
- self._urls.append({
- 'url': url,
- 'hashname': hashname,
- 'hashval': hashval,
- 'is_external': is_external,
- })
- # reset the url selection process
- self._url = None
-
- @property
- def url(self):
- """Pick up the right url for the list of urls in self.urls"""
- # We return internal urls over externals.
- # If there is more than one internal or external, return the first
- # one.
- if self._url is None:
- if len(self._urls) > 1:
- internals_urls = [u for u in self._urls \
- if u['is_external'] == False]
- if len(internals_urls) >= 1:
- self._url = internals_urls[0]
- if self._url is None:
- self._url = self._urls[0]
- return self._url
-
- @property
- def is_source(self):
- """return if the distribution is a source one or not"""
- return self.type == 'source'
-
- @property
- def is_final(self):
- """proxy to version.is_final"""
- return self.version.is_final
-
- def download(self, path=None):
- """Download the distribution to a path, and return it.
-
- If the path is given in path, use this, otherwise, generates a new one
- """
- if path is None:
- path = tempfile.mkdtemp()
-
- # if we do not have downloaded it yet, do it.
- if self.downloaded_location is None:
- url = self.url['url']
- archive_name = urlparse.urlparse(url)[2].split('/')[-1]
- filename, headers = urllib.urlretrieve(url,
- path + "/" + archive_name)
- self.downloaded_location = filename
- self._check_md5(filename)
- return self.downloaded_location
-
- def _check_md5(self, filename):
- """Check that the md5 checksum of the given file matches the one in
- url param"""
- hashname = self.url['hashname']
- expected_hashval = self.url['hashval']
- if not None in (expected_hashval, hashname):
- f = open(filename)
- hashval = hashlib.new(hashname)
- hashval.update(f.read())
- if hashval.hexdigest() != expected_hashval:
- raise HashDoesNotMatch("got %s instead of %s"
- % (hashval.hexdigest(), expected_hashval))
-
- def __repr__(self):
- return "%s %s %s %s" \
- % (self.__class__.__name__, self.name, self.version,
- self.type or "")
-
- def _check_is_comparable(self, other):
- if not isinstance(other, PyPIDistribution):
- raise TypeError("cannot compare %s and %s"
- % (type(self).__name__, type(other).__name__))
- elif self.name != other.name:
- raise TypeError("cannot compare %s and %s"
- % (self.name, other.name))
-
- def __eq__(self, other):
- self._check_is_comparable(other)
- return self.version == other.version
-
- def __lt__(self, other):
- self._check_is_comparable(other)
- return self.version < other.version
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- def __gt__(self, other):
- return not (self.__lt__(other) or self.__eq__(other))
-
- def __le__(self, other):
- return self.__eq__(other) or self.__lt__(other)
-
- def __ge__(self, other):
- return self.__eq__(other) or self.__gt__(other)
-
- # See http://docs.python.org/reference/datamodel#object.__hash__
- __hash__ = object.__hash__
-
-
-class PyPIDistributions(list):
- """A container of PyPIDistribution objects.
-
- Contains methods and facilities to sort and filter distributions.
- """
- def __init__(self, list=[]):
- # To disable the ability to pass lists on instanciation
- super(PyPIDistributions, self).__init__()
- for item in list:
- self.append(item)
-
- def filter(self, predicate):
- """Filter the distributions and return a subset of distributions that
- match the given predicate
- """
- return PyPIDistributions(
- [dist for dist in self if dist.name == predicate.name and
- predicate.match(dist.version)])
-
- def get_last(self, predicate, prefer_source=None, prefer_final=None):
- """Return the most up to date version, that satisfy the given
- predicate
- """
- distributions = self.filter(predicate)
- distributions.sort_distributions(prefer_source, prefer_final, reverse=True)
- return distributions[0]
-
- def get_same_name_and_version(self):
- """Return lists of PyPIDistribution objects that refer to the same
- name and version number. This do not consider the type (source, binary,
- etc.)"""
- processed = []
- duplicates = []
- for dist in self:
- if (dist.name, dist.version) not in processed:
- processed.append((dist.name, dist.version))
- found_duplicates = [d for d in self if d.name == dist.name and
- d.version == dist.version]
- if len(found_duplicates) > 1:
- duplicates.append(found_duplicates)
- return duplicates
-
- def append(self, o):
- """Append a new distribution to the list.
-
- If a distribution with the same name and version exists, just grab the
- URL informations and add a new new url for the existing one.
- """
- similar_dists = [d for d in self if d.name == o.name and
- d.version == o.version and d.type == o.type]
- if len(similar_dists) > 0:
- dist = similar_dists[0]
- dist.add_url(**o.url)
- else:
- super(PyPIDistributions, self).append(o)
-
- def sort_distributions(self, prefer_source=True, prefer_final=False,
- reverse=True, *args, **kwargs):
- """order the results with the given properties"""
-
- sort_by = []
- if prefer_final:
- sort_by.append("is_final")
- sort_by.append("version")
-
- if prefer_source:
- sort_by.append("is_source")
-
- super(PyPIDistributions, self).sort(
- key=lambda i: [getattr(i, arg) for arg in sort_by],
- reverse=reverse, *args, **kwargs)
-
-
-def split_archive_name(archive_name, probable_name=None):
- """Split an archive name into two parts: name and version.
-
- Return the tuple (name, version)
- """
- # Try to determine wich part is the name and wich is the version using the
- # "-" separator. Take the larger part to be the version number then reduce
- # if this not works.
- def eager_split(str, maxsplit=2):
- # split using the "-" separator
- splits = str.rsplit("-", maxsplit)
- name = splits[0]
- version = "-".join(splits[1:])
- if version.startswith("-"):
- version = version[1:]
- if suggest_normalized_version(version) is None and maxsplit >= 0:
- # we dont get a good version number: recurse !
- return eager_split(str, maxsplit - 1)
- else:
- return (name, version)
- if probable_name is not None:
- probable_name = probable_name.lower()
- name = None
- if probable_name is not None and probable_name in archive_name:
- # we get the name from probable_name, if given.
- name = probable_name
- version = archive_name.lstrip(name)
- else:
- name, version = eager_split(archive_name)
-
- version = suggest_normalized_version(version)
- if version != "" and name != "":
- return (name.lower(), version)
- else:
- raise CantParseArchiveName(archive_name)
diff --git a/src/distutils2/pypi/errors.py b/src/distutils2/pypi/errors.py
deleted file mode 100644
index c9b232d..0000000
--- a/src/distutils2/pypi/errors.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""distutils2.pypi.errors
-
-All errors and exceptions raised by PyPiIndex classes.
-"""
-from distutils2.errors import DistutilsError
-
-
-class PyPIError(DistutilsError):
- """The base class for errors of the pypi python package."""
-
-
-class DistributionNotFound(PyPIError):
- """No distribution match the given requirements."""
-
-
-class CantParseArchiveName(PyPIError):
- """An archive name can't be parsed to find distribution name and version"""
-
-
-class DownloadError(PyPIError):
- """An error has occurs while downloading"""
-
-
-class HashDoesNotMatch(DownloadError):
- """Compared hashes does not match"""
-
-
-class UnsupportedHashName(PyPIError):
- """A unsupported hashname has been used"""
-
-
-class UnableToDownload(PyPIError):
- """All mirrors have been tried, without success"""
diff --git a/src/distutils2/pypi/simple.py b/src/distutils2/pypi/simple.py
deleted file mode 100644
index 1a6a35d..0000000
--- a/src/distutils2/pypi/simple.py
+++ /dev/null
@@ -1,393 +0,0 @@
-"""pypi.simple
-
-Contains the class "SimpleIndex", a simple spider to find and retrieve
-distributions on the Python Package Index, using it's "simple" API,
-avalaible at http://pypi.python.org/simple/
-"""
-from fnmatch import translate
-import httplib
-import re
-import socket
-import sys
-import urllib2
-import urlparse
-
-from distutils2.version import VersionPredicate
-from distutils2.pypi.dist import (PyPIDistribution, PyPIDistributions,
- EXTENSIONS)
-from distutils2.pypi.errors import (PyPIError, DistributionNotFound,
- DownloadError, UnableToDownload)
-from distutils2 import __version__ as __distutils2_version__
-
-# -- Constants -----------------------------------------------
-PYPI_DEFAULT_INDEX_URL = "http://pypi.python.org/simple/"
-PYPI_DEFAULT_MIRROR_URL = "mirrors.pypi.python.org"
-DEFAULT_HOSTS = ("*",)
-SOCKET_TIMEOUT = 15
-USER_AGENT = "Python-urllib/%s distutils2/%s" % (
- sys.version[:3], __distutils2_version__)
-
-# -- Regexps -------------------------------------------------
-EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.]+)$')
-HREF = re.compile("""href\\s*=\\s*['"]?([^'"> ]+)""", re.I)
-PYPI_MD5 = re.compile(
- '<a href="([^"#]+)">([^<]+)</a>\n\s+\\(<a (?:title="MD5 hash"\n\s+)'
- 'href="[^?]+\?:action=show_md5&amp;digest=([0-9a-f]{32})">md5</a>\\)')
-URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match
-
-# This pattern matches a character entity reference (a decimal numeric
-# references, a hexadecimal numeric reference, or a named reference).
-ENTITY_SUB = re.compile(r'&(#(\d+|x[\da-fA-F]+)|[\w.:-]+);?').sub
-REL = re.compile("""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I)
-
-
-def socket_timeout(timeout=SOCKET_TIMEOUT):
- """Decorator to add a socket timeout when requesting pages on PyPI.
- """
- def _socket_timeout(func):
- def _socket_timeout(self, *args, **kwargs):
- old_timeout = socket.getdefaulttimeout()
- if hasattr(self, "_timeout"):
- timeout = self._timeout
- socket.setdefaulttimeout(timeout)
- try:
- return func(self, *args, **kwargs)
- finally:
- socket.setdefaulttimeout(old_timeout)
- return _socket_timeout
- return _socket_timeout
-
-
-class SimpleIndex(object):
- """Provides useful tools to request the Python Package Index simple API
-
- :param index_url: the url of the simple index to search on.
- :param follow_externals: tell if following external links is needed or
- not. Default is False.
- :param hosts: a list of hosts allowed to be processed while using
- follow_externals=True. Default behavior is to follow all
- hosts.
- :param follow_externals: tell if following external links is needed or
- not. Default is False.
- :param prefer_source: if there is binary and source distributions, the
- source prevails.
- :param prefer_final: if the version is not mentioned, and the last
- version is not a "final" one (alpha, beta, etc.),
- pick up the last final version.
- :param mirrors_url: the url to look on for DNS records giving mirror
- adresses.
- :param mirrors: a list of mirrors to check out if problems
- occurs while working with the one given in "url"
- :param timeout: time in seconds to consider a url has timeouted.
- """
-
- def __init__(self, index_url=PYPI_DEFAULT_INDEX_URL, hosts=DEFAULT_HOSTS,
- follow_externals=False, prefer_source=True,
- prefer_final=False, mirrors_url=PYPI_DEFAULT_MIRROR_URL,
- mirrors=None, timeout=SOCKET_TIMEOUT):
- self.follow_externals = follow_externals
-
- if not index_url.endswith("/"):
- index_url += "/"
- self._index_urls = [index_url]
- # if no mirrors are defined, use the method described in PEP 381.
- if mirrors is None:
- try:
- mirrors = socket.gethostbyname_ex(mirrors_url)[-1]
- except socket.gaierror:
- mirrors = []
- self._index_urls.extend(mirrors)
- self._current_index_url = 0
- self._timeout = timeout
- self._prefer_source = prefer_source
- self._prefer_final = prefer_final
-
- # create a regexp to match all given hosts
- self._allowed_hosts = re.compile('|'.join(map(translate, hosts))).match
-
- # we keep an index of pages we have processed, in order to avoid
- # scanning them multple time (eg. if there is multiple pages pointing
- # on one)
- self._processed_urls = []
- self._distributions = {}
-
- def find(self, requirements, prefer_source=None, prefer_final=None):
- """Browse the PyPI to find distributions that fullfil the given
- requirements.
-
- :param requirements: A project name and it's distribution, using
- version specifiers, as described in PEP345.
- :type requirements: You can pass either a version.VersionPredicate
- or a string.
- :param prefer_source: if there is binary and source distributions, the
- source prevails.
- :param prefer_final: if the version is not mentioned, and the last
- version is not a "final" one (alpha, beta, etc.),
- pick up the last final version.
- """
- requirements = self._get_version_predicate(requirements)
- if prefer_source is None:
- prefer_source = self._prefer_source
- if prefer_final is None:
- prefer_final = self._prefer_final
-
- # process the index for this project
- self._process_pypi_page(requirements.name)
-
- # filter with requirements and return the results
- if requirements.name in self._distributions:
- dists = self._distributions[requirements.name].filter(requirements)
- dists.sort_distributions(prefer_source=prefer_source,
- prefer_final=prefer_final)
- else:
- dists = []
-
- return dists
-
- def get(self, requirements, *args, **kwargs):
- """Browse the PyPI index to find distributions that fullfil the
- given requirements, and return the most recent one.
-
- You can specify prefer_final and prefer_source arguments here.
- If not, the default one will be used.
- """
- predicate = self._get_version_predicate(requirements)
- dists = self.find(predicate, *args, **kwargs)
-
- if len(dists) == 0:
- raise DistributionNotFound(requirements)
-
- return dists.get_last(predicate)
-
- def download(self, requirements, temp_path=None, *args, **kwargs):
- """Download the distribution, using the requirements.
-
- If more than one distribution match the requirements, use the last
- version.
- Download the distribution, and put it in the temp_path. If no temp_path
- is given, creates and return one.
-
- Returns the complete absolute path to the downloaded archive.
-
- :param requirements: The same as the find attribute of `find`.
-
- You can specify prefer_final and prefer_source arguments here.
- If not, the default one will be used.
- """
- return self.get(requirements, *args, **kwargs)\
- .download(path=temp_path)
-
- 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
-
- @property
- def index_url(self):
- return self._index_urls[self._current_index_url]
-
- def _switch_to_next_mirror(self):
- """Switch to the next mirror (eg. point self.index_url to the next
- url.
- """
- # Internally, iter over the _index_url iterable, if we have read all
- # of the available indexes, raise an exception.
- if self._current_index_url < len(self._index_urls):
- self._current_index_url = self._current_index_url + 1
- else:
- raise UnableToDownload("All mirrors fails")
-
- def _is_browsable(self, url):
- """Tell if the given URL can be browsed or not.
-
- It uses the follow_externals and the hosts list to tell if the given
- url is browsable or not.
- """
- # if _index_url is contained in the given URL, we are browsing the
- # index, and it's always "browsable".
- # local files are always considered browable resources
- if self.index_url in url or urlparse.urlparse(url)[0] == "file":
- return True
- elif self.follow_externals:
- if self._allowed_hosts(urlparse.urlparse(url)[1]): # 1 is netloc
- return True
- else:
- return False
- return False
-
- def _is_distribution(self, link):
- """Tell if the given URL matches to a distribution name or not.
- """
- #XXX find a better way to check that links are distributions
- # Using a regexp ?
- for ext in EXTENSIONS:
- if ext in link:
- return True
- return False
-
- def _register_dist(self, dist):
- """Register a distribution as a part of fetched distributions for
- SimpleIndex.
-
- Return the PyPIDistributions object for the specified project name
- """
- # Internally, check if a entry exists with the project name, if not,
- # create a new one, and if exists, add the dist to the pool.
- if not dist.name in self._distributions:
- self._distributions[dist.name] = PyPIDistributions()
- self._distributions[dist.name].append(dist)
- return self._distributions[dist.name]
-
- def _process_url(self, url, project_name=None, follow_links=True):
- """Process an url and search for distributions packages.
-
- For each URL found, if it's a download, creates a PyPIdistribution
- object. If it's a homepage and we can follow links, process it too.
-
- :param url: the url to process
- :param project_name: the project name we are searching for.
- :param follow_links: Do not want to follow links more than from one
- level. This parameter tells if we want to follow
- the links we find (eg. run recursively this
- method on it)
- """
- f = self._open_url(url)
- base_url = f.url
- if url not in self._processed_urls:
- self._processed_urls.append(url)
- link_matcher = self._get_link_matcher(url)
- for link, is_download in link_matcher(f.read(), base_url):
- if link not in self._processed_urls:
- if self._is_distribution(link) or is_download:
- self._processed_urls.append(link)
- # it's a distribution, so create a dist object
- dist = PyPIDistribution.from_url(link, project_name,
- is_external=not self.index_url in url)
- self._register_dist(dist)
- else:
- if self._is_browsable(link) and follow_links:
- self._process_url(link, project_name,
- follow_links=False)
-
- def _get_link_matcher(self, url):
- """Returns the right link matcher function of the given url
- """
- if self.index_url in url:
- return self._simple_link_matcher
- else:
- return self._default_link_matcher
-
- def _simple_link_matcher(self, content, base_url):
- """Yield all links with a rel="download" or rel="homepage".
-
- This matches the simple index requirements for matching links.
- If follow_externals is set to False, dont yeld the external
- urls.
- """
- for match in REL.finditer(content):
- tag, rel = match.groups()
- rels = map(str.strip, rel.lower().split(','))
- if 'homepage' in rels or 'download' in rels:
- for match in HREF.finditer(tag):
- url = urlparse.urljoin(base_url,
- self._htmldecode(match.group(1)))
- if 'download' in rels or self._is_browsable(url):
- # yield a list of (url, is_download)
- yield (urlparse.urljoin(base_url, url),
- 'download' in rels)
-
- def _default_link_matcher(self, content, base_url):
- """Yield all links found on the page.
- """
- for match in HREF.finditer(content):
- url = urlparse.urljoin(base_url, self._htmldecode(match.group(1)))
- if self._is_browsable(url):
- yield (url, False)
-
- def _process_pypi_page(self, name):
- """Find and process a PyPI page for the given project name.
-
- :param name: the name of the project to find the page
- """
- try:
- # Browse and index the content of the given PyPI page.
- url = self.index_url + name + "/"
- self._process_url(url, name)
- except DownloadError:
- # if an error occurs, try with the next index_url
- # (provided by the mirrors)
- self._switch_to_next_mirror()
- self._distributions.clear()
- self._process_pypi_page(name)
-
- @socket_timeout()
- def _open_url(self, url):
- """Open a urllib2 request, handling HTTP authentication, and local
- files support.
-
- """
- try:
- scheme, netloc, path, params, query, frag = urlparse.urlparse(url)
-
- if scheme in ('http', 'https'):
- auth, host = urllib2.splituser(netloc)
- else:
- auth = None
-
- # add index.html automatically for filesystem paths
- if scheme == 'file':
- if url.endswith('/'):
- url += "index.html"
-
- if auth:
- auth = "Basic " + \
- urllib2.unquote(auth).encode('base64').strip()
- new_url = urlparse.urlunparse((
- scheme, host, path, params, query, frag))
- request = urllib2.Request(new_url)
- request.add_header("Authorization", auth)
- else:
- request = urllib2.Request(url)
- request.add_header('User-Agent', USER_AGENT)
- fp = urllib2.urlopen(request)
-
- if auth:
- # Put authentication info back into request URL if same host,
- # so that links found on the page will work
- s2, h2, path2, param2, query2, frag2 = \
- urlparse.urlparse(fp.url)
- if s2 == scheme and h2 == host:
- fp.url = urlparse.urlunparse(
- (s2, netloc, path2, param2, query2, frag2))
-
- return fp
- except (ValueError, httplib.InvalidURL), v:
- msg = ' '.join([str(arg) for arg in v.args])
- raise PyPIError('%s %s' % (url, msg))
- except urllib2.HTTPError, v:
- return v
- except urllib2.URLError, v:
- raise DownloadError("Download error for %s: %s" % (url, v.reason))
- except httplib.BadStatusLine, v:
- raise DownloadError('%s returned a bad status line. '
- 'The server might be down, %s' % (url, v.line))
- except httplib.HTTPException, v:
- raise DownloadError("Download error for %s: %s" % (url, v))
-
- def _decode_entity(self, match):
- what = match.group(1)
- if what.startswith('#x'):
- what = int(what[2:], 16)
- elif what.startswith('#'):
- what = int(what[1:])
- else:
- from htmlentitydefs import name2codepoint
- what = name2codepoint.get(what, match.group(0))
- return unichr(what)
-
- def _htmldecode(self, text):
- """Decode HTML entities in the given text."""
- return ENTITY_SUB(self._decode_entity, text)
diff --git a/src/distutils2/spawn.py b/src/distutils2/spawn.py
deleted file mode 100644
index 08996fc..0000000
--- a/src/distutils2/spawn.py
+++ /dev/null
@@ -1,192 +0,0 @@
-"""distutils.spawn
-
-Provides the 'spawn()' function, a front-end to various platform-
-specific functions for launching another program in a sub-process.
-Also provides the 'find_executable()' to search the path for a given
-executable name.
-"""
-
-__revision__ = "$Id: spawn.py 73147 2009-06-02 15:58:43Z tarek.ziade $"
-
-import sys
-import os
-
-from distutils2.errors import DistutilsPlatformError, DistutilsExecError
-from distutils2 import log
-
-def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None):
- """Run another program, specified as a command list 'cmd', in a new process.
-
- 'cmd' is just the argument list for the new process, ie.
- cmd[0] is the program to run and cmd[1:] are the rest of its arguments.
- There is no way to run a program with a name different from that of its
- executable.
-
- If 'search_path' is true (the default), the system's executable
- search path will be used to find the program; otherwise, cmd[0]
- must be the exact path to the executable. If 'dry_run' is true,
- the command will not actually be run.
-
- If 'env' is given, it's a environment dictionary used for the execution
- environment.
-
- Raise DistutilsExecError if running the program fails in any way; just
- return on success.
- """
- if os.name == 'posix':
- _spawn_posix(cmd, search_path, dry_run=dry_run, env=env)
- elif os.name == 'nt':
- _spawn_nt(cmd, search_path, dry_run=dry_run, env=env)
- elif os.name == 'os2':
- _spawn_os2(cmd, search_path, dry_run=dry_run, env=env)
- else:
- raise DistutilsPlatformError, \
- "don't know how to spawn programs on platform '%s'" % os.name
-
-def _nt_quote_args(args):
- """Quote command-line arguments for DOS/Windows conventions.
-
- Just wraps every argument which contains blanks in double quotes, and
- returns a new argument list.
- """
- # XXX this doesn't seem very robust to me -- but if the Windows guys
- # say it'll work, I guess I'll have to accept it. (What if an arg
- # contains quotes? What other magic characters, other than spaces,
- # have to be escaped? Is there an escaping mechanism other than
- # quoting?)
- for i, arg in enumerate(args):
- if ' ' in arg:
- args[i] = '"%s"' % arg
- return args
-
-def _spawn_nt(cmd, search_path=1, verbose=0, dry_run=0, env=None):
- executable = cmd[0]
- cmd = _nt_quote_args(cmd)
- if search_path:
- # either we find one or it stays the same
- executable = find_executable(executable) or executable
- log.info(' '.join([executable] + cmd[1:]))
- if not dry_run:
- # spawn for NT requires a full path to the .exe
- try:
- if env is None:
- rc = os.spawnv(os.P_WAIT, executable, cmd)
- else:
- rc = os.spawnve(os.P_WAIT, executable, cmd, env)
-
- except OSError, exc:
- # this seems to happen when the command isn't found
- raise DistutilsExecError, \
- "command '%s' failed: %s" % (cmd[0], exc[-1])
- if rc != 0:
- # and this reflects the command running but failing
- raise DistutilsExecError, \
- "command '%s' failed with exit status %d" % (cmd[0], rc)
-
-def _spawn_os2(cmd, search_path=1, verbose=0, dry_run=0, env=None):
- executable = cmd[0]
- if search_path:
- # either we find one or it stays the same
- executable = find_executable(executable) or executable
- log.info(' '.join([executable] + cmd[1:]))
- if not dry_run:
- # spawnv for OS/2 EMX requires a full path to the .exe
- try:
- if env is None:
- rc = os.spawnv(os.P_WAIT, executable, cmd)
- else:
- rc = os.spawnve(os.P_WAIT, executable, cmd, env)
-
- except OSError, exc:
- # this seems to happen when the command isn't found
- raise DistutilsExecError, \
- "command '%s' failed: %s" % (cmd[0], exc[-1])
- if rc != 0:
- # and this reflects the command running but failing
- log.debug("command '%s' failed with exit status %d" % (cmd[0], rc))
- raise DistutilsExecError, \
- "command '%s' failed with exit status %d" % (cmd[0], rc)
-
-
-def _spawn_posix(cmd, search_path=1, verbose=0, dry_run=0, env=None):
- log.info(' '.join(cmd))
- if dry_run:
- return
-
- if env is None:
- exec_fn = search_path and os.execvp or os.execv
- else:
- exec_fn = search_path and os.execvpe or os.execve
-
- pid = os.fork()
-
- if pid == 0: # in the child
- try:
- if env is None:
- exec_fn(cmd[0], cmd)
- else:
- exec_fn(cmd[0], cmd, env)
- except OSError, e:
- sys.stderr.write("unable to execute %s: %s\n" %
- (cmd[0], e.strerror))
- os._exit(1)
-
- sys.stderr.write("unable to execute %s for unknown reasons" % cmd[0])
- os._exit(1)
- else: # in the parent
- # Loop until the child either exits or is terminated by a signal
- # (ie. keep waiting if it's merely stopped)
- while 1:
- try:
- pid, status = os.waitpid(pid, 0)
- except OSError, exc:
- import errno
- if exc.errno == errno.EINTR:
- continue
- raise DistutilsExecError, \
- "command '%s' failed: %s" % (cmd[0], exc[-1])
- if os.WIFSIGNALED(status):
- raise DistutilsExecError, \
- "command '%s' terminated by signal %d" % \
- (cmd[0], os.WTERMSIG(status))
-
- elif os.WIFEXITED(status):
- exit_status = os.WEXITSTATUS(status)
- if exit_status == 0:
- return # hey, it succeeded!
- else:
- raise DistutilsExecError, \
- "command '%s' failed with exit status %d" % \
- (cmd[0], exit_status)
-
- elif os.WIFSTOPPED(status):
- continue
-
- else:
- raise DistutilsExecError, \
- "unknown error executing '%s': termination status %d" % \
- (cmd[0], status)
-
-def find_executable(executable, path=None):
- """Tries to find 'executable' in the directories listed in 'path'.
-
- A string listing directories separated by 'os.pathsep'; defaults to
- os.environ['PATH']. Returns the complete filename or None if not found.
- """
- if path is None:
- path = os.environ['PATH']
- paths = path.split(os.pathsep)
- base, ext = os.path.splitext(executable)
-
- if (sys.platform == 'win32' or os.name == 'os2') and (ext != '.exe'):
- executable = executable + '.exe'
-
- if not os.path.isfile(executable):
- for p in paths:
- f = os.path.join(p, executable)
- if os.path.isfile(f):
- # the file exists, we have a shot at spawn working
- return f
- return None
- else:
- return executable
diff --git a/src/distutils2/tests/__init__.py b/src/distutils2/tests/__init__.py
index 8a8d187..de40aaa 100644
--- a/src/distutils2/tests/__init__.py
+++ b/src/distutils2/tests/__init__.py
@@ -23,7 +23,7 @@ from distutils2.tests.support import unittest
from test.test_support import TESTFN # use TESTFN from stdlib/test_support.
-here = os.path.dirname(__file__)
+here = os.path.dirname(__file__) or os.curdir
verbose = 1
diff --git a/src/distutils2/tests/conversions/02_after.py b/src/distutils2/tests/conversions/02_after.py
index 473d035..bfc2d8f 100644
--- a/src/distutils2/tests/conversions/02_after.py
+++ b/src/distutils2/tests/conversions/02_after.py
@@ -1,4 +1,4 @@
-# -*- encoding: utf8 -*-
+# -*- encoding: utf-8 -*-
import sys
import os
from distutils2.core import setup, Extension
diff --git a/src/distutils2/tests/conversions/02_before.py b/src/distutils2/tests/conversions/02_before.py
index 6fdb418..f7ccc12 100644
--- a/src/distutils2/tests/conversions/02_before.py
+++ b/src/distutils2/tests/conversions/02_before.py
@@ -1,4 +1,4 @@
-# -*- encoding: utf8 -*-
+# -*- encoding: utf-8 -*-
import sys
import os
from distutils.core import setup, Extension
diff --git a/src/distutils2/tests/pypi_server.py b/src/distutils2/tests/pypi_server.py
index 3927262..2db4577 100644
--- a/src/distutils2/tests/pypi_server.py
+++ b/src/distutils2/tests/pypi_server.py
@@ -1,25 +1,59 @@
-"""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 Queue
+import SocketServer
+import os.path
+import select
+import socket
import threading
-import time
-import urllib2
+
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
-import os.path
-import select
+from SimpleXMLRPCServer import SimpleXMLRPCServer
from distutils2.tests.support import unittest
PYPI_DEFAULT_STATIC_PATH = os.path.dirname(os.path.abspath(__file__)) + "/pypiserver"
+def use_xmlrpc_server(*server_args, **server_kwargs):
+ server_kwargs['serve_xmlrpc'] = True
+ return use_pypi_server(*server_args, **server_kwargs)
+
+def use_http_server(*server_args, **server_kwargs):
+ server_kwargs['serve_xmlrpc'] = False
+ 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):
@@ -52,38 +86,59 @@ class PyPIServer(threading.Thread):
"""
def __init__(self, test_static_path=None,
- static_filesystem_paths=["default"], static_uri_paths=["simple"]):
+ 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
+ xmlrpc server by setting xmlrpc to True. Caution: Only one server will
+ be started.
+
static_uri_paths and static_base_path are parameters used to provides
respectively the http_paths to serve statically, and where to find the
matching files on the filesystem.
"""
+ # we want to launch the server in a new dedicated thread, to not freeze
+ # tests.
threading.Thread.__init__(self)
self._run = True
- self.httpd = HTTPServer(('', 0), PyPIRequestHandler)
- self.httpd.RequestHandlerClass.log_request = lambda *_: None
- self.httpd.RequestHandlerClass.pypi_server = self
- self.address = (self.httpd.server_name, self.httpd.server_port)
- self.request_queue = Queue.Queue()
- self._requests = []
- 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:
- static_filesystem_paths.append(test_static_path)
- self.static_filesystem_paths = [PYPI_DEFAULT_STATIC_PATH + "/" + path
- for path in static_filesystem_paths]
+ 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
+
+ self.request_queue = Queue.Queue()
+ self._requests = []
+ 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:
+ static_filesystem_paths.append(test_static_path)
+ self.static_filesystem_paths = [PYPI_DEFAULT_STATIC_PATH + "/" + path
+ for path in static_filesystem_paths]
+ else:
+ # XMLRPC server
+ self.server = PyPIXMLRPCServer(('', 0))
+ self.xmlrpc = XMLRPCMockIndex()
+ # register the xmlrpc methods
+ self.server.register_introspection_functions()
+ self.server.register_instance(self.xmlrpc)
+
+ self.address = (self.server.server_name, self.server.server_port)
+ # to not have unwanted outputs.
+ self.server.RequestHandlerClass.log_request = lambda *_: None
def run(self):
# loop because we can't stop it otherwise, for python < 2.6
while self._run:
- r, w, e = select.select([self.httpd], [], [], 0.5)
+ r, w, e = select.select([self.server], [], [], 0.5)
if r:
- self.httpd.handle_request()
+ self.server.handle_request()
def stop(self):
"""self shutdown is not supported for python < 2.6"""
@@ -146,7 +201,7 @@ class PyPIRequestHandler(SimpleHTTPRequestHandler):
# 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.
@@ -184,7 +239,7 @@ class PyPIRequestHandler(SimpleHTTPRequestHandler):
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])
@@ -193,3 +248,181 @@ class PyPIRequestHandler(SimpleHTTPRequestHandler):
self.send_header(header, value)
self.end_headers()
self.wfile.write(data)
+
+class PyPIXMLRPCServer(SimpleXMLRPCServer):
+ def server_bind(self):
+ """Override server_bind to store the server name."""
+ SocketServer.TCPServer.server_bind(self)
+ host, port = self.socket.getsockname()[:2]
+ self.server_name = socket.getfqdn(host)
+ self.server_port = port
+
+class MockDist(object):
+ """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,
+ python_version="source", comment="comment",
+ author="John Doe", author_email="john@doe.name",
+ maintainer="Main Tayner", maintainer_email="maintainer_mail",
+ project_url="http://project_url/", homepage="http://homepage/",
+ keywords="", platform="UNKNOWN", classifiers=[], licence="",
+ description="Description", summary="Summary", stable_version="",
+ ordering="", documentation_id="", code_kwalitee_id="",
+ installability_id="", obsoletes=[], obsoletes_dist=[],
+ provides=[], provides_dist=[], requires=[], requires_dist=[],
+ requires_external=[], requires_python=""):
+
+ # basic fields
+ self.name = name
+ self.version = version
+ self.hidden = hidden
+
+ # URL infos
+ self.url = url
+ self.digest = digest
+ self.downloads = downloads
+ self.has_sig = has_sig
+ self.python_version = python_version
+ self.comment = comment
+ self.type = type
+
+ # metadata
+ self.author = author
+ self.author_email = author_email
+ self.maintainer = maintainer
+ self.maintainer_email = maintainer_email
+ self.project_url = project_url
+ self.homepage = homepage
+ self.keywords = keywords
+ self.platform = platform
+ self.classifiers = classifiers
+ self.licence = licence
+ self.description = description
+ self.summary = summary
+ self.stable_version = stable_version
+ self.ordering = ordering
+ 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
+ self.provides_dist = provides_dist
+ self.requires = requires
+ self.requires_dist = requires_dist
+ self.requires_external = requires_external
+ self.requires_python = requires_python
+
+ def url_infos(self):
+ return {
+ 'url': self.url,
+ 'packagetype': self.type,
+ 'filename': 'filename.tar.gz',
+ 'size': '6000',
+ 'md5_digest': self.digest,
+ 'downloads': self.downloads,
+ 'has_sig': self.has_sig,
+ 'python_version': self.python_version,
+ 'comment_text': self.comment,
+ }
+
+ def metadata(self):
+ return {
+ 'maintainer': self.maintainer,
+ 'project_url': [self.project_url],
+ 'maintainer_email': self.maintainer_email,
+ 'cheesecake_code_kwalitee_id': self.cheesecake_code_kwalitee_id,
+ 'keywords': self.keywords,
+ 'obsoletes_dist': self.obsoletes_dist,
+ 'requires_external': self.requires_external,
+ 'author': self.author,
+ 'author_email': self.author_email,
+ 'download_url': self.url,
+ 'platform': self.platform,
+ 'version': self.version,
+ 'obsoletes': self.obsoletes,
+ 'provides': self.provides,
+ 'cheesecake_documentation_id': self.cheesecake_documentation_id,
+ '_pypi_hidden': self.hidden,
+ 'description': self.description,
+ '_pypi_ordering': 19,
+ 'requires_dist': self.requires_dist,
+ 'requires_python': self.requires_python,
+ 'classifiers': [],
+ 'name': self.name,
+ 'licence': self.licence,
+ 'summary': self.summary,
+ 'home_page': self.homepage,
+ 'stable_version': self.stable_version,
+ '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,
+ 'version': self.version,
+ 'name': self.name,
+ 'summary': self.summary,
+ }
+
+class XMLRPCMockIndex(object):
+ """Mock XMLRPC server"""
+
+ def __init__(self, dists=[]):
+ self._dists = dists
+
+ def add_distributions(self, dists):
+ for dist in dists:
+ self._dists.append(MockDist(**dist))
+
+ def set_distributions(self, dists):
+ self._dists = []
+ self.add_distributions(dists)
+
+ def set_search_result(self, result):
+ """set a predefined search result"""
+ self._search_result = result
+
+ def _get_search_results(self):
+ results = []
+ for name in self._search_result:
+ found_dist = [d for d in self._dists if d.name == name]
+ if found_dist:
+ results.append(found_dist[0])
+ else:
+ dist = MockDist(name)
+ results.append(dist)
+ self._dists.append(dist)
+ return [r.search_result() for r in results]
+
+ def list_package(self):
+ return [d.name for d in self._dists]
+
+ def package_releases(self, package_name, show_hidden=False):
+ if show_hidden:
+ # return all
+ return [d.version for d in self._dists if d.name == package_name]
+ else:
+ # 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]
+
+ def release_data(self, package_name, version):
+ release = [d for d in self._dists
+ if d.name == package_name and d.version == version]
+ if release:
+ return release[0].metadata()
+ else:
+ return {}
+
+ def search(self, spec, operator="and"):
+ return self._get_search_results()
diff --git a/src/distutils2/tests/pypiserver/project_list/simple/index.html b/src/distutils2/tests/pypiserver/project_list/simple/index.html
new file mode 100644
index 0000000..b36d728
--- /dev/null
+++ b/src/distutils2/tests/pypiserver/project_list/simple/index.html
@@ -0,0 +1,5 @@
+<a class="test" href="yeah">FooBar-bar</a>
+<a class="test" href="yeah">Foobar-baz</a>
+<a class="test" href="yeah">Baz-FooBar</a>
+<a class="test" href="yeah">Baz</a>
+<a class="test" href="yeah">Foo</a>
diff --git a/src/distutils2/tests/support.py b/src/distutils2/tests/support.py
index 518cedf..fa83b30 100644
--- a/src/distutils2/tests/support.py
+++ b/src/distutils2/tests/support.py
@@ -1,8 +1,29 @@
"""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
+EnvironGuard. They are written to be used as mixins, e.g. ::
+
+ from distutils2.tests.support import unittest
+ from distutils2.tests.support import LoggingSilencer
+
+ class SomeTestCase(LoggingSilencer, unittest.TestCase):
+
+If you need to define a setUp method on your test class, you have to
+call the mixin class' setUp method or it won't work (same thing for
+tearDown):
+
+ def setUp(self):
+ super(SomeTestCase, self).setUp()
+ ... # other setup code
+
+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).
"""
import os
@@ -10,26 +31,36 @@ import sys
import shutil
import tempfile
from copy import deepcopy
-import warnings
from distutils2 import log
from distutils2.log import DEBUG, INFO, WARN, ERROR, FATAL
-if sys.version_info >= (2, 7):
- # improved unittest package from 2.7's standard library
+if sys.version_info >= (3, 2):
+ # improved unittest package from 3.2's standard library
import unittest
else:
# external release of same package for older versions
import unittest2 as unittest
+__all__ = ['LoggingSilencer', 'TempdirManager', 'EnvironGuard',
+ 'DummyCommand', 'unittest']
+
+
class LoggingSilencer(object):
+ """TestCase-compatible mixin to catch logging calls.
+
+ Every log message that goes through distutils2.log will get appended to
+ self.logs instead of being printed. You can check that your code logs
+ warnings and errors as documented by inspecting that list; helper methods
+ get_logs and clear_logs are also provided.
+ """
def setUp(self):
super(LoggingSilencer, self).setUp()
- self.threshold = log.set_threshold(log.FATAL)
+ self.threshold = log.set_threshold(FATAL)
# catching warnings
- # when log will be replaced by logging
- # we won't need such monkey-patch anymore
+ # when log is replaced by logging we won't need
+ # such monkey-patching anymore
self._old_log = log.Log._log
log.Log._log = self._log
self.logs = []
@@ -45,6 +76,10 @@ class LoggingSilencer(object):
self.logs.append((level, msg, args))
def get_logs(self, *levels):
+ """Return a list of caught messages with level in `levels`.
+
+ Example: self.get_logs(log.WARN, log.DEBUG) -> list
+ """
def _format(msg, args):
if len(args) == 0:
return msg
@@ -53,48 +88,41 @@ class LoggingSilencer(object):
in self.logs if level in levels]
def clear_logs(self):
- self.logs = []
+ """Empty the internal list of caught messages."""
+ del self.logs[:]
+
class TempdirManager(object):
- """Mix-in class that handles temporary directories for test cases.
+ """TestCase-compatible mixin to create temporary directories and files.
- This is intended to be used with unittest.TestCase.
+ Directories and files created in a test_* method will be removed after it
+ has run.
"""
def setUp(self):
super(TempdirManager, self).setUp()
- self.tempdirs = []
- self.tempfiles = []
+ self._basetempdir = tempfile.mkdtemp()
def tearDown(self):
super(TempdirManager, self).tearDown()
- while self.tempdirs:
- d = self.tempdirs.pop()
- shutil.rmtree(d, os.name in ('nt', 'cygwin'))
- for file_ in self.tempfiles:
- if os.path.exists(file_):
- os.remove(file_)
+ shutil.rmtree(self._basetempdir, os.name in ('nt', 'cygwin'))
def mktempfile(self):
- """Create a temporary file that will be cleaned up."""
- tempfile_ = tempfile.NamedTemporaryFile()
- self.tempfiles.append(tempfile_.name)
- return tempfile_
+ """Create a read-write temporary file and return it."""
+ fd, fn = tempfile.mkstemp(dir=self._basetempdir)
+ os.close(fd)
+ return open(fn, 'w+')
def mkdtemp(self):
- """Create a temporary directory that will be cleaned up.
-
- Returns the path of the directory.
- """
- d = tempfile.mkdtemp()
- self.tempdirs.append(d)
+ """Create a temporary directory and return its path."""
+ d = tempfile.mkdtemp(dir=self._basetempdir)
return d
def write_file(self, path, content='xxx'):
- """Writes a file in the given path.
-
+ """Write a file at the given path.
- path can be a string or a sequence.
+ path can be a string, a tuple or a list; if it's a tuple or list,
+ os.path.join will be used to produce a path.
"""
if isinstance(path, (list, tuple)):
path = os.path.join(*path)
@@ -105,41 +133,35 @@ class TempdirManager(object):
f.close()
def create_dist(self, pkg_name='foo', **kw):
- """Will generate a test environment.
+ """Create a stub distribution object and files.
- This function creates:
- - a Distribution instance using keywords
- - a temporary directory with a package structure
+ This function creates a Distribution instance (use keyword arguments
+ to customize it) and a temporary directory with a project structure
+ (currently an empty directory).
- It returns the package directory and the distribution
- instance.
+ It returns the path to the directory and the Distribution instance.
+ You can use TempdirManager.write_file to write any file in that
+ directory, e.g. setup scripts or Python modules.
"""
+ # Late import so that third parties can import support without
+ # loading a ton of distutils2 modules in memory.
from distutils2.dist import Distribution
tmp_dir = self.mkdtemp()
pkg_dir = os.path.join(tmp_dir, pkg_name)
os.mkdir(pkg_dir)
dist = Distribution(attrs=kw)
-
return pkg_dir, dist
-class DummyCommand:
- """Class to store options for retrieval via set_undefined_options()."""
-
- def __init__(self, **kwargs):
- for kw, val in kwargs.items():
- setattr(self, kw, val)
-
- def ensure_finalized(self):
- pass
class EnvironGuard(object):
+ """TestCase-compatible mixin to save and restore the environment."""
def setUp(self):
super(EnvironGuard, self).setUp()
self.old_environ = deepcopy(os.environ)
def tearDown(self):
- for key, value in self.old_environ.items():
+ for key, value in self.old_environ.iteritems():
if os.environ.get(key) != value:
os.environ[key] = value
@@ -148,3 +170,18 @@ class EnvironGuard(object):
del os.environ[key]
super(EnvironGuard, self).tearDown()
+
+
+class DummyCommand(object):
+ """Class to store options for retrieval via set_undefined_options().
+
+ Useful for mocking one dependency command in the tests for another
+ command, see e.g. the dummy build command in test_build_scripts.
+ """
+
+ def __init__(self, **kwargs):
+ for kw, val in kwargs.iteritems():
+ setattr(self, kw, val)
+
+ def ensure_finalized(self):
+ pass
diff --git a/src/distutils2/tests/test_Mixin2to3.py b/src/distutils2/tests/test_Mixin2to3.py
index a455fc2..4b397c7 100644
--- a/src/distutils2/tests/test_Mixin2to3.py
+++ b/src/distutils2/tests/test_Mixin2to3.py
@@ -1,6 +1,5 @@
"""Tests for distutils.command.build_py."""
import sys
-import tempfile
import distutils2
from distutils2.tests import support
@@ -10,7 +9,7 @@ from distutils2.command.build_py import Mixin2to3
class Mixin2to3TestCase(support.TempdirManager, unittest.TestCase):
- @unittest.skipUnless(sys.version > '2.6', 'Need >= 2.6')
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_convert_code_only(self):
# used to check if code gets converted properly.
code_content = "print 'test'\n"
@@ -27,7 +26,7 @@ class Mixin2to3TestCase(support.TempdirManager, unittest.TestCase):
self.assertEquals(new_code_content, converted_code_content)
- @unittest.skipUnless(sys.version > '2.6', 'Need >= 2.6')
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_doctests_only(self):
# used to check if doctests gets converted properly.
doctest_content = '"""\n>>> print test\ntest\n"""\nprint test\n\n'
diff --git a/src/distutils2/tests/test_bdist.py b/src/distutils2/tests/test_bdist.py
index 345c74c..2be01e1 100644
--- a/src/distutils2/tests/test_bdist.py
+++ b/src/distutils2/tests/test_bdist.py
@@ -8,8 +8,7 @@ from distutils2.core import Distribution
from distutils2.command.bdist import bdist
from distutils2.tests import support
from distutils2.tests.support import unittest
-from distutils2.spawn import find_executable
-from distutils2 import spawn
+from distutils2.util import find_executable
from distutils2.errors import DistutilsExecError
class BuildTestCase(support.TempdirManager,
diff --git a/src/distutils2/tests/test_bdist_msi.py b/src/distutils2/tests/test_bdist_msi.py
index ebde4db..4ad54ef 100644
--- a/src/distutils2/tests/test_bdist_msi.py
+++ b/src/distutils2/tests/test_bdist_msi.py
@@ -10,8 +10,8 @@ class BDistMSITestCase(support.TempdirManager,
support.LoggingSilencer,
unittest.TestCase):
- @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
- def test_minial(self):
+ @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
+ def test_minimal(self):
# minimal test XXX need more tests
from distutils2.command.bdist_msi import bdist_msi
pkg_pth, dist = self.create_dist()
diff --git a/src/distutils2/tests/test_build_clib.py b/src/distutils2/tests/test_build_clib.py
index 48af7e1..348f700 100644
--- a/src/distutils2/tests/test_build_clib.py
+++ b/src/distutils2/tests/test_build_clib.py
@@ -5,7 +5,7 @@ import sys
from distutils2.command.build_clib import build_clib
from distutils2.errors import DistutilsSetupError
from distutils2.tests import support
-from distutils2.spawn import find_executable
+from distutils2.util import find_executable
from distutils2.tests.support import unittest
class BuildCLibTestCase(support.TempdirManager,
diff --git a/src/distutils2/tests/test_build_ext.py b/src/distutils2/tests/test_build_ext.py
index f561923..1ae0bd8 100644
--- a/src/distutils2/tests/test_build_ext.py
+++ b/src/distutils2/tests/test_build_ext.py
@@ -45,7 +45,7 @@ class BuildExtTestCase(support.TempdirManager,
build_ext.USER_BASE = site.USER_BASE
# XXX only works with 2.6 > -- dunno why yet
- @unittest.skipUnless(sys.version_info >= (2, 6,), 'works for >= 2.6')
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_build_ext(self):
global ALREADY_TESTED
xx_c = os.path.join(self.tmp_dir, 'xxmodule.c')
@@ -126,11 +126,8 @@ class BuildExtTestCase(support.TempdirManager,
# make sure we get some library dirs under solaris
self.assertTrue(len(cmd.library_dirs) > 0)
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_user_site(self):
- # site.USER_SITE was introduced in 2.6
- if sys.version < '2.6':
- return
-
import site
dist = Distribution({'name': 'xx'})
cmd = build_ext(dist)
diff --git a/src/distutils2/tests/test_build_py.py b/src/distutils2/tests/test_build_py.py
index 4b46da9..aed24d3 100644
--- a/src/distutils2/tests/test_build_py.py
+++ b/src/distutils2/tests/test_build_py.py
@@ -95,7 +95,7 @@ class BuildPyTestCase(support.TempdirManager,
sys.stdout = old_stdout
@unittest.skipUnless(hasattr(sys, 'dont_write_bytecode'),
- 'dont_write_bytecode support')
+ 'sys.dont_write_bytecode not supported')
def test_dont_write_bytecode(self):
# makes sure byte_compile is not used
pkg_dir, dist = self.create_dist()
diff --git a/src/distutils2/tests/test_check.py b/src/distutils2/tests/test_check.py
index 7a672e1..02d171f 100644
--- a/src/distutils2/tests/test_check.py
+++ b/src/distutils2/tests/test_check.py
@@ -43,7 +43,7 @@ class CheckTestCase(support.LoggingSilencer,
# get an error if there are missing metadata
self.assertRaises(DistutilsSetupError, self._run, {}, **{'strict': 1})
- # and of course, no error when all metadata are present
+ # and of course, no error when all metadata fields are present
cmd = self._run(metadata, strict=1)
self.assertEqual(len(cmd._warnings), 0)
diff --git a/src/distutils2/tests/test_cmd.py b/src/distutils2/tests/test_cmd.py
index aa4a709..c768f79 100644
--- a/src/distutils2/tests/test_cmd.py
+++ b/src/distutils2/tests/test_cmd.py
@@ -98,7 +98,7 @@ class CommandTestCase(unittest.TestCase):
def test_ensure_dirname(self):
cmd = self.cmd
- cmd.option1 = os.path.dirname(__file__)
+ cmd.option1 = os.path.dirname(__file__) or os.curdir
cmd.ensure_dirname('option1')
cmd.option2 = 'xxx'
self.assertRaises(DistutilsOptionError, cmd.ensure_dirname, 'option2')
diff --git a/src/distutils2/tests/test_config.py b/src/distutils2/tests/test_config.py
deleted file mode 100644
index 09b6d02..0000000
--- a/src/distutils2/tests/test_config.py
+++ /dev/null
@@ -1,117 +0,0 @@
-"""Tests for distutils.pypirc.pypirc."""
-import sys
-import os
-import shutil
-
-from distutils2.core import PyPIRCCommand
-from distutils2.core import Distribution
-from distutils2.log import set_threshold
-from distutils2.log import WARN
-
-from distutils2.tests import support
-from distutils2.tests.support import unittest
-
-PYPIRC = """\
-[distutils]
-
-index-servers =
- server1
- server2
-
-[server1]
-username:me
-password:secret
-
-[server2]
-username:meagain
-password: secret
-realm:acme
-repository:http://another.pypi/
-"""
-
-PYPIRC_OLD = """\
-[server-login]
-username:tarek
-password:secret
-"""
-
-WANTED = """\
-[distutils]
-index-servers =
- pypi
-
-[pypi]
-username:tarek
-password:xxx
-"""
-
-
-class PyPIRCCommandTestCase(support.TempdirManager,
- support.LoggingSilencer,
- support.EnvironGuard,
- unittest.TestCase):
-
- def setUp(self):
- """Patches the environment."""
- super(PyPIRCCommandTestCase, self).setUp()
- self.tmp_dir = self.mkdtemp()
- os.environ['HOME'] = self.tmp_dir
- self.rc = os.path.join(self.tmp_dir, '.pypirc')
- self.dist = Distribution()
-
- class command(PyPIRCCommand):
- def __init__(self, dist):
- PyPIRCCommand.__init__(self, dist)
- def initialize_options(self):
- pass
- finalize_options = initialize_options
-
- self._cmd = command
- self.old_threshold = set_threshold(WARN)
-
- def tearDown(self):
- """Removes the patch."""
- set_threshold(self.old_threshold)
- super(PyPIRCCommandTestCase, self).tearDown()
-
- def test_server_registration(self):
- # This test makes sure PyPIRCCommand knows how to:
- # 1. handle several sections in .pypirc
- # 2. handle the old format
-
- # new format
- self.write_file(self.rc, PYPIRC)
- cmd = self._cmd(self.dist)
- config = cmd._read_pypirc()
-
- config = config.items()
- config.sort()
- expected = [('password', 'secret'), ('realm', 'pypi'),
- ('repository', 'http://pypi.python.org/pypi'),
- ('server', 'server1'), ('username', 'me')]
- self.assertEqual(config, expected)
-
- # old format
- self.write_file(self.rc, PYPIRC_OLD)
- config = cmd._read_pypirc()
- config = config.items()
- config.sort()
- expected = [('password', 'secret'), ('realm', 'pypi'),
- ('repository', 'http://pypi.python.org/pypi'),
- ('server', 'server-login'), ('username', 'tarek')]
- self.assertEqual(config, expected)
-
- def test_server_empty_registration(self):
- cmd = self._cmd(self.dist)
- rc = cmd._get_rc_file()
- self.assertTrue(not os.path.exists(rc))
- cmd._store_pypirc('tarek', 'xxx')
- self.assertTrue(os.path.exists(rc))
- content = open(rc).read()
- self.assertEqual(content, WANTED)
-
-def test_suite():
- return unittest.makeSuite(PyPIRCCommandTestCase)
-
-if __name__ == "__main__":
- unittest.main(defaultTest="test_suite")
diff --git a/src/distutils2/tests/test_converter.py b/src/distutils2/tests/test_converter.py
index f0717cc..c4cb387 100644
--- a/src/distutils2/tests/test_converter.py
+++ b/src/distutils2/tests/test_converter.py
@@ -17,7 +17,7 @@ def _read_file(path):
class ConverterTestCase(unittest.TestCase):
- @unittest.skipUnless(not sys.version < '2.6', 'Needs Python >=2.6')
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_conversions(self):
# for all XX_before in the conversions/ dir
# we run the refactoring tool
diff --git a/src/distutils2/tests/test_core.py b/src/distutils2/tests/test_core.py
index 9f4ad27..883187e 100644
--- a/src/distutils2/tests/test_core.py
+++ b/src/distutils2/tests/test_core.py
@@ -64,13 +64,13 @@ class CoreTestCase(support.EnvironGuard, unittest.TestCase):
f = self.write_setup(setup_using___file__)
for s in ['init', 'config', 'commandline', 'run']:
distutils2.core.run_setup(f, stop_after=s)
- self.assertRaises(ValueError, distutils2.core.run_setup,
+ self.assertRaises(ValueError, distutils2.core.run_setup,
f, stop_after='bob')
def test_run_setup_args(self):
f = self.write_setup(setup_using___file__)
- d = distutils2.core.run_setup(f, script_args=["--help"],
- stop_after="init")
+ d = distutils2.core.run_setup(f, script_args=["--help"],
+ stop_after="init")
self.assertEqual(['--help'], d.script_args)
def test_run_setup_uses_current_dir(self):
diff --git a/src/distutils2/tests/test_dist.py b/src/distutils2/tests/test_dist.py
index f528684..290ce01 100644
--- a/src/distutils2/tests/test_dist.py
+++ b/src/distutils2/tests/test_dist.py
@@ -1,4 +1,4 @@
-# -*- coding: utf8 -*-
+# -*- coding: utf-8 -*-
"""Tests for distutils2.dist."""
import os
@@ -153,6 +153,27 @@ class DistributionTestCase(support.TempdirManager,
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 @@ class DistributionTestCase(support.TempdirManager,
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',
@@ -240,6 +276,49 @@ class DistributionTestCase(support.TempdirManager,
# make sure --no-user-cfg disables the user cfg file
self.assertEqual(len(all_files)-1, len(files))
+ def test_special_hooks_parsing(self):
+ temp_home = self.mkdtemp()
+ config_files = [os.path.join(temp_home, "config1.cfg"),
+ os.path.join(temp_home, "config2.cfg")]
+
+ # Store two aliased hooks in config files
+ self.write_file((temp_home, "config1.cfg"), '[test_dist]\npre-hook.a = type')
+ self.write_file((temp_home, "config2.cfg"), '[test_dist]\npre-hook.b = type')
+
+ sys.argv.extend(["--command-packages",
+ "distutils2.tests",
+ "test_dist"])
+ cmd = self.create_distribution(config_files).get_command_obj("test_dist")
+ self.assertEqual(cmd.pre_hook, {"a": 'type', "b": 'type'})
+
+
+ def test_hooks_get_run(self):
+ temp_home = self.mkdtemp()
+ config_file = os.path.join(temp_home, "config1.cfg")
+
+ self.write_file((temp_home, "config1.cfg"), textwrap.dedent('''
+ [test_dist]
+ pre-hook.test = distutils2.tests.test_dist.DistributionTestCase.log_pre_call
+ post-hook.test = distutils2.tests.test_dist.DistributionTestCase.log_post_call'''))
+
+ sys.argv.extend(["--command-packages",
+ "distutils2.tests",
+ "test_dist"])
+ d = self.create_distribution([config_file])
+ cmd = d.get_command_obj("test_dist")
+
+ # prepare the call recorders
+ record = []
+ DistributionTestCase.log_pre_call = staticmethod(lambda _cmd: record.append(('pre', _cmd)))
+ DistributionTestCase.log_post_call = staticmethod(lambda _cmd: record.append(('post', _cmd)))
+ test_dist.run = lambda _cmd: record.append(('run', _cmd))
+ test_dist.finalize_options = lambda _cmd: record.append(('finalize_options', _cmd))
+
+ d.run_command('test_dist')
+ self.assertEqual(record, [('finalize_options', cmd),
+ ('pre', cmd),
+ ('run', cmd),
+ ('post', cmd)])
class MetadataTestCase(support.TempdirManager, support.EnvironGuard,
unittest.TestCase):
diff --git a/src/distutils2/tests/test_index_dist.py b/src/distutils2/tests/test_index_dist.py
new file mode 100644
index 0000000..9020635
--- /dev/null
+++ b/src/distutils2/tests/test_index_dist.py
@@ -0,0 +1,248 @@
+"""Tests for the distutils2.index.dist module."""
+
+import os
+
+from distutils2.tests.pypi_server import use_pypi_server
+from distutils2.tests import run_unittest
+from distutils2.tests.support import unittest, TempdirManager
+from distutils2.version import VersionPredicate
+from distutils2.index.errors import HashDoesNotMatch, UnsupportedHashName
+from distutils2.index.dist import (ReleaseInfo, ReleasesList, DistInfo,
+ split_archive_name, get_infos_from_url)
+
+
+def Dist(*args, **kwargs):
+ # DistInfo takes a release as a first parameter, avoid this in tests.
+ return DistInfo(None, *args, **kwargs)
+
+
+class TestReleaseInfo(unittest.TestCase):
+
+ def test_instantiation(self):
+ # Test the DistInfo class provides us the good attributes when
+ # given on construction
+ release = ReleaseInfo("FooBar", "1.1")
+ self.assertEqual("FooBar", release.name)
+ self.assertEqual("1.1", "%s" % release.version)
+
+ def test_add_dist(self):
+ # empty distribution type should assume "sdist"
+ release = ReleaseInfo("FooBar", "1.1")
+ release.add_distribution(url="http://example.org/")
+ # should not fail
+ release['sdist']
+
+ def test_get_unknown_distribution(self):
+ # should raise a KeyError
+ pass
+
+ def test_get_infos_from_url(self):
+ # Test that the the URLs are parsed the right way
+ url_list = {
+ 'FooBar-1.1.0.tar.gz': {
+ 'name': 'foobar', # lowercase the name
+ 'version': '1.1.0',
+ },
+ 'Foo-Bar-1.1.0.zip': {
+ 'name': 'foo-bar', # keep the dash
+ 'version': '1.1.0',
+ },
+ 'foobar-1.1b2.tar.gz#md5=123123123123123': {
+ 'name': 'foobar',
+ 'version': '1.1b2',
+ 'url': 'http://example.org/foobar-1.1b2.tar.gz', # no hash
+ 'hashval': '123123123123123',
+ 'hashname': 'md5',
+ },
+ 'foobar-1.1-rc2.tar.gz': { # use suggested name
+ 'name': 'foobar',
+ 'version': '1.1c2',
+ 'url': 'http://example.org/foobar-1.1-rc2.tar.gz',
+ }
+ }
+
+ for url, attributes in url_list.items():
+ # for each url
+ infos = get_infos_from_url("http://example.org/" + url)
+ for attribute, expected in attributes.items():
+ got = infos.get(attribute)
+ if attribute == "version":
+ self.assertEqual("%s" % got, expected)
+ else:
+ self.assertEqual(got, expected)
+
+ def test_split_archive_name(self):
+ # Test we can split the archive names
+ names = {
+ 'foo-bar-baz-1.0-rc2': ('foo-bar-baz', '1.0c2'),
+ 'foo-bar-baz-1.0': ('foo-bar-baz', '1.0'),
+ 'foobarbaz-1.0': ('foobarbaz', '1.0'),
+ }
+ for name, results in names.items():
+ self.assertEqual(results, split_archive_name(name))
+
+
+class TestDistInfo(TempdirManager, unittest.TestCase):
+
+ def test_get_url(self):
+ # Test that the url property works well
+
+ d = Dist(url="test_url")
+ self.assertDictEqual(d.url, {
+ "url": "test_url",
+ "is_external": True,
+ "hashname": None,
+ "hashval": None,
+ })
+
+ # add a new url
+ d.add_url(url="internal_url", is_external=False)
+ self.assertEqual(d._url, None)
+ self.assertDictEqual(d.url, {
+ "url": "internal_url",
+ "is_external": False,
+ "hashname": None,
+ "hashval": None,
+ })
+ self.assertEqual(2, len(d.urls))
+
+ def test_comparison(self):
+ # Test that we can compare DistInfoributionInfoList
+ foo1 = ReleaseInfo("foo", "1.0")
+ foo2 = ReleaseInfo("foo", "2.0")
+ bar = ReleaseInfo("bar", "2.0")
+ # assert we use the version to compare
+ self.assertTrue(foo1 < foo2)
+ self.assertFalse(foo1 > foo2)
+ self.assertFalse(foo1 == foo2)
+
+ # assert we can't compare dists with different names
+ self.assertRaises(TypeError, foo1.__eq__, bar)
+
+ @use_pypi_server("downloads_with_md5")
+ def test_download(self, server):
+ # Download is possible, and the md5 is checked if given
+
+ url = "%s/simple/foobar/foobar-0.1.tar.gz" % server.full_address
+ # check md5 if given
+ dist = Dist(url=url, hashname="md5",
+ hashval="d41d8cd98f00b204e9800998ecf8427e")
+ dist.download(self.mkdtemp())
+
+ # a wrong md5 fails
+ dist2 = Dist(url=url, hashname="md5", hashval="wrongmd5")
+
+ self.assertRaises(HashDoesNotMatch, dist2.download, self.mkdtemp())
+
+ # we can omit the md5 hash
+ dist3 = Dist(url=url)
+ dist3.download(self.mkdtemp())
+
+ # and specify a temporary location
+ # for an already downloaded dist
+ path1 = self.mkdtemp()
+ dist3.download(path=path1)
+ # and for a new one
+ path2_base = self.mkdtemp()
+ dist4 = Dist(url=url)
+ path2 = dist4.download(path=path2_base)
+ self.assertTrue(path2_base in path2)
+
+ def test_hashname(self):
+ # Invalid hashnames raises an exception on assignation
+ Dist(hashname="md5", hashval="value")
+
+ self.assertRaises(UnsupportedHashName, Dist,
+ hashname="invalid_hashname",
+ hashval="value")
+
+
+class TestReleasesList(unittest.TestCase):
+
+ def test_filter(self):
+ # Test we filter the distributions the right way, using version
+ # predicate match method
+ releases = ReleasesList('FooBar', (
+ ReleaseInfo("FooBar", "1.1"),
+ ReleaseInfo("FooBar", "1.1.1"),
+ ReleaseInfo("FooBar", "1.2"),
+ ReleaseInfo("FooBar", "1.2.1"),
+ ))
+ filtered = releases.filter(VersionPredicate("FooBar (<1.2)"))
+ self.assertNotIn(releases[2], filtered)
+ self.assertNotIn(releases[3], filtered)
+ self.assertIn(releases[0], filtered)
+ self.assertIn(releases[1], filtered)
+
+ def test_append(self):
+ # When adding a new item to the list, the behavior is to test if
+ # a release with the same name and version number already exists,
+ # and if so, to add a new distribution for it. If the distribution type
+ # is already defined too, add url informations to the existing DistInfo
+ # object.
+
+ releases = ReleasesList("FooBar", [
+ ReleaseInfo("FooBar", "1.1", url="external_url",
+ dist_type="sdist"),
+ ])
+ self.assertEqual(1, len(releases))
+ releases.add_release(release=ReleaseInfo("FooBar", "1.1",
+ url="internal_url",
+ is_external=False,
+ dist_type="sdist"))
+ self.assertEqual(1, len(releases))
+ self.assertEqual(2, len(releases[0]['sdist'].urls))
+
+ releases.add_release(release=ReleaseInfo("FooBar", "1.1.1",
+ dist_type="sdist"))
+ self.assertEqual(2, len(releases))
+
+ # when adding a distribution whith a different type, a new distribution
+ # has to be added.
+ releases.add_release(release=ReleaseInfo("FooBar", "1.1.1",
+ dist_type="bdist"))
+ self.assertEqual(2, len(releases))
+ self.assertEqual(2, len(releases[1].dists))
+
+ def test_prefer_final(self):
+ # Can order the distributions using prefer_final
+
+ fb10 = ReleaseInfo("FooBar", "1.0") # final distribution
+ fb11a = ReleaseInfo("FooBar", "1.1a1") # alpha
+ fb12a = ReleaseInfo("FooBar", "1.2a1") # alpha
+ fb12b = ReleaseInfo("FooBar", "1.2b1") # beta
+ dists = ReleasesList("FooBar", [fb10, fb11a, fb12a, fb12b])
+
+ dists.sort_releases(prefer_final=True)
+ self.assertEqual(fb10, dists[0])
+
+ dists.sort_releases(prefer_final=False)
+ self.assertEqual(fb12b, dists[0])
+
+# def test_prefer_source(self):
+# # Ordering support prefer_source
+# fb_source = Dist("FooBar", "1.0", type="source")
+# fb_binary = Dist("FooBar", "1.0", type="binary")
+# fb2_binary = Dist("FooBar", "2.0", type="binary")
+# dists = ReleasesList([fb_binary, fb_source])
+#
+# dists.sort_distributions(prefer_source=True)
+# self.assertEqual(fb_source, dists[0])
+#
+# dists.sort_distributions(prefer_source=False)
+# self.assertEqual(fb_binary, dists[0])
+#
+# dists.append(fb2_binary)
+# dists.sort_distributions(prefer_source=True)
+# self.assertEqual(fb2_binary, dists[0])
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestDistInfo))
+ suite.addTest(unittest.makeSuite(TestReleaseInfo))
+ suite.addTest(unittest.makeSuite(TestReleasesList))
+ return suite
+
+if __name__ == '__main__':
+ run_unittest(test_suite())
diff --git a/src/distutils2/tests/test_pypi_simple.py b/src/distutils2/tests/test_index_simple.py
index 85d8b77..d21bda5 100644
--- a/src/distutils2/tests/test_pypi_simple.py
+++ b/src/distutils2/tests/test_index_simple.py
@@ -3,36 +3,34 @@
"""
import sys
import os
-import shutil
-import tempfile
import urllib2
-from distutils2.pypi import simple
-from distutils2.tests import support, run_unittest
+from distutils2.index.simple import Crawler
+from distutils2.tests import support
from distutils2.tests.support import unittest
from distutils2.tests.pypi_server import (use_pypi_server, PyPIServer,
PYPI_DEFAULT_STATIC_PATH)
-class PyPISimpleTestCase(support.TempdirManager,
- unittest.TestCase):
+class SimpleCrawlerTestCase(support.TempdirManager, unittest.TestCase):
- def _get_simple_index(self, server, base_url="/simple/", hosts=None,
+ def _get_simple_crawler(self, server, base_url="/simple/", hosts=None,
*args, **kwargs):
- """Build and return a SimpleSimpleIndex instance, with the test server
+ """Build and return a SimpleIndex instance, with the test server
urls
"""
if hosts is None:
hosts = (server.full_address.strip("http://"),)
kwargs['hosts'] = hosts
- return simple.SimpleIndex(server.full_address + base_url, *args,
+ return Crawler(server.full_address + base_url, *args,
**kwargs)
- def test_bad_urls(self):
- index = simple.SimpleIndex()
+ @use_pypi_server()
+ def test_bad_urls(self, server):
+ crawler = Crawler()
url = 'http://127.0.0.1:0/nonesuch/test_simple'
try:
- v = index._open_url(url)
+ v = crawler._open_url(url)
except Exception, v:
self.assertTrue(url in str(v))
else:
@@ -41,10 +39,10 @@ class PyPISimpleTestCase(support.TempdirManager,
# issue 16
# easy_install inquant.contentmirror.plone breaks because of a typo
# in its home URL
- index = simple.SimpleIndex(hosts=('www.example.com',))
+ crawler = Crawler(hosts=('example.org',))
url = 'url:%20https://svn.plone.org/svn/collective/inquant.contentmirror.plone/trunk'
try:
- v = index._open_url(url)
+ v = crawler._open_url(url)
except Exception, v:
self.assertTrue(url in str(v))
else:
@@ -56,10 +54,10 @@ class PyPISimpleTestCase(support.TempdirManager,
old_urlopen = urllib2.urlopen
urllib2.urlopen = _urlopen
- url = 'http://example.com'
+ url = 'http://example.org'
try:
try:
- v = index._open_url(url)
+ v = crawler._open_url(url)
except Exception, v:
self.assertTrue('line' in str(v))
else:
@@ -70,91 +68,91 @@ class PyPISimpleTestCase(support.TempdirManager,
# issue 20
url = 'http://http://svn.pythonpaste.org/Paste/wphp/trunk'
try:
- index._open_url(url)
+ crawler._open_url(url)
except Exception, v:
self.assertTrue('nonnumeric port' in str(v))
# issue #160
if sys.version_info[0] == 2 and sys.version_info[1] == 7:
# this should not fail
- url = 'http://example.com'
+ url = server.full_address
page = ('<a href="http://www.famfamfam.com]('
'http://www.famfamfam.com/">')
- index._process_url(url, page)
+ crawler._process_url(url, page)
@use_pypi_server("test_found_links")
def test_found_links(self, server):
- # Browse the index, asking for a specified distribution version
+ # Browse the index, asking for a specified release version
# The PyPI index contains links for version 1.0, 1.1, 2.0 and 2.0.1
- index = self._get_simple_index(server)
- last_distribution = index.get("foobar")
+ crawler = self._get_simple_crawler(server)
+ last_release = crawler.get_release("foobar")
# we have scanned the index page
self.assertIn(server.full_address + "/simple/foobar/",
- index._processed_urls)
+ crawler._processed_urls)
- # we have found 4 distributions in this page
- self.assertEqual(len(index._distributions["foobar"]), 4)
+ # we have found 4 releases in this page
+ self.assertEqual(len(crawler._projects["foobar"]), 4)
# and returned the most recent one
- self.assertEqual("%s" % last_distribution.version, '2.0.1')
+ self.assertEqual("%s" % last_release.version, '2.0.1')
def test_is_browsable(self):
- index = simple.SimpleIndex(follow_externals=False)
- self.assertTrue(index._is_browsable(index.index_url + "test"))
+ crawler = Crawler(follow_externals=False)
+ self.assertTrue(crawler._is_browsable(crawler.index_url + "test"))
# Now, when following externals, we can have a list of hosts to trust.
# and don't follow other external links than the one described here.
- index = simple.SimpleIndex(hosts=["pypi.python.org", "test.org"],
+ crawler = Crawler(hosts=["pypi.python.org", "example.org"],
follow_externals=True)
good_urls = (
"http://pypi.python.org/foo/bar",
"http://pypi.python.org/simple/foobar",
- "http://test.org",
- "http://test.org/",
- "http://test.org/simple/",
+ "http://example.org",
+ "http://example.org/",
+ "http://example.org/simple/",
)
bad_urls = (
"http://python.org",
- "http://test.tld",
+ "http://example.tld",
)
for url in good_urls:
- self.assertTrue(index._is_browsable(url))
+ self.assertTrue(crawler._is_browsable(url))
for url in bad_urls:
- self.assertFalse(index._is_browsable(url))
+ self.assertFalse(crawler._is_browsable(url))
# allow all hosts
- index = simple.SimpleIndex(follow_externals=True, hosts=("*",))
- self.assertTrue(index._is_browsable("http://an-external.link/path"))
- self.assertTrue(index._is_browsable("pypi.test.tld/a/path"))
+ crawler = Crawler(follow_externals=True, hosts=("*",))
+ self.assertTrue(crawler._is_browsable("http://an-external.link/path"))
+ self.assertTrue(crawler._is_browsable("pypi.example.org/a/path"))
# specify a list of hosts we want to allow
- index = simple.SimpleIndex(follow_externals=True,
- hosts=("*.test.tld",))
- self.assertFalse(index._is_browsable("http://an-external.link/path"))
- self.assertTrue(index._is_browsable("http://pypi.test.tld/a/path"))
+ crawler = Crawler(follow_externals=True,
+ hosts=("*.example.org",))
+ self.assertFalse(crawler._is_browsable("http://an-external.link/path"))
+ self.assertTrue(crawler._is_browsable("http://pypi.example.org/a/path"))
@use_pypi_server("with_externals")
- def test_restrict_hosts(self, server):
+ def test_follow_externals(self, server):
# Include external pages
# Try to request the package index, wich contains links to "externals"
# resources. They have to be scanned too.
- index = self._get_simple_index(server, follow_externals=True)
- index.get("foobar")
+ crawler = self._get_simple_crawler(server, follow_externals=True)
+ crawler.get_release("foobar")
self.assertIn(server.full_address + "/external/external.html",
- index._processed_urls)
+ crawler._processed_urls)
@use_pypi_server("with_real_externals")
def test_restrict_hosts(self, server):
# Only use a list of allowed hosts is possible
# Test that telling the simple pyPI client to not retrieve external
# works
- index = self._get_simple_index(server, follow_externals=False)
- index.get("foobar")
+ crawler = self._get_simple_crawler(server, follow_externals=False)
+ crawler.get_release("foobar")
self.assertNotIn(server.full_address + "/external/external.html",
- index._processed_urls)
+ crawler._processed_urls)
@use_pypi_server(static_filesystem_paths=["with_externals"],
static_uri_paths=["simple", "external"])
@@ -168,23 +166,26 @@ class PyPISimpleTestCase(support.TempdirManager,
# - someone manually coindexes this link (with the md5 in the url) onto
# an external page accessible from the package page.
# - someone reuploads the package (with a different md5)
- # - while easy_installing, an MD5 error occurs because the external link
- # is used
+ # - while easy_installing, an MD5 error occurs because the external
+ # link is used
# -> The index should use the link from pypi, not the external one.
# start an index server
index_url = server.full_address + '/simple/'
# scan a test index
- index = simple.SimpleIndex(index_url, follow_externals=True)
- dists = index.find("foobar")
+ crawler = Crawler(index_url, follow_externals=True)
+ releases = crawler.get_releases("foobar")
server.stop()
# we have only one link, because links are compared without md5
- self.assertEqual(len(dists), 1)
+ self.assertEqual(1, len(releases))
+ self.assertEqual(1, len(releases[0].dists))
# the link should be from the index
- self.assertEqual('12345678901234567', dists[0].url['hashval'])
- self.assertEqual('md5', dists[0].url['hashname'])
+ self.assertEqual(2, len(releases[0].dists['sdist'].urls))
+ self.assertEqual('12345678901234567',
+ releases[0].dists['sdist'].url['hashval'])
+ self.assertEqual('md5', releases[0].dists['sdist'].url['hashname'])
@use_pypi_server(static_filesystem_paths=["with_norel_links"],
static_uri_paths=["simple", "external"])
@@ -194,22 +195,22 @@ class PyPISimpleTestCase(support.TempdirManager,
# to not be processed by the package index, while processing "pages".
# process the pages
- index = self._get_simple_index(server, follow_externals=True)
- index.find("foobar")
+ crawler = self._get_simple_crawler(server, follow_externals=True)
+ crawler.get_releases("foobar")
# now it should have processed only pages with links rel="download"
# and rel="homepage"
self.assertIn("%s/simple/foobar/" % server.full_address,
- index._processed_urls) # it's the simple index page
+ crawler._processed_urls) # it's the simple index page
self.assertIn("%s/external/homepage.html" % server.full_address,
- index._processed_urls) # the external homepage is rel="homepage"
+ crawler._processed_urls) # the external homepage is rel="homepage"
self.assertNotIn("%s/external/nonrel.html" % server.full_address,
- index._processed_urls) # this link contains no rel=*
+ crawler._processed_urls) # this link contains no rel=*
self.assertNotIn("%s/unrelated-0.2.tar.gz" % server.full_address,
- index._processed_urls) # linked from simple index (no rel)
+ crawler._processed_urls) # linked from simple index (no rel)
self.assertIn("%s/foobar-0.1.tar.gz" % server.full_address,
- index._processed_urls) # linked from simple index (rel)
+ crawler._processed_urls) # linked from simple index (rel)
self.assertIn("%s/foobar-2.0.tar.gz" % server.full_address,
- index._processed_urls) # linked from external homepage (rel)
+ crawler._processed_urls) # linked from external homepage (rel)
def test_uses_mirrors(self):
# When the main repository seems down, try using the given mirrors"""
@@ -219,18 +220,18 @@ class PyPISimpleTestCase(support.TempdirManager,
try:
# create the index using both servers
- index = simple.SimpleIndex(server.full_address + "/simple/",
+ crawler = Crawler(server.full_address + "/simple/",
hosts=('*',), timeout=1, # set the timeout to 1s for the tests
- mirrors=[mirror.full_address + "/simple/",])
+ mirrors=[mirror.full_address])
# this should not raise a timeout
- self.assertEqual(4, len(index.find("foo")))
+ self.assertEqual(4, len(crawler.get_releases("foo")))
finally:
mirror.stop()
def test_simple_link_matcher(self):
# Test that the simple link matcher yields the right links"""
- index = simple.SimpleIndex(follow_externals=False)
+ crawler = Crawler(follow_externals=False)
# Here, we define:
# 1. one link that must be followed, cause it's a download one
@@ -238,27 +239,33 @@ class PyPISimpleTestCase(support.TempdirManager,
# returns false for it.
# 3. one link that must be followed cause it's a homepage that is
# browsable
- self.assertTrue(index._is_browsable("%stest" % index.index_url))
- self.assertFalse(index._is_browsable("http://dl-link2"))
+ # 4. one link that must be followed, because it contain a md5 hash
+ self.assertTrue(crawler._is_browsable("%stest" % crawler.index_url))
+ self.assertFalse(crawler._is_browsable("http://dl-link2"))
content = """
<a href="http://dl-link1" rel="download">download_link1</a>
<a href="http://dl-link2" rel="homepage">homepage_link1</a>
- <a href="%stest" rel="homepage">homepage_link2</a>
- """ % index.index_url
+ <a href="%(index_url)stest" rel="homepage">homepage_link2</a>
+ <a href="%(index_url)stest/foobar-1.tar.gz#md5=abcdef>download_link2</a>
+ """ % {'index_url': crawler.index_url }
# Test that the simple link matcher yield the good links.
- generator = index._simple_link_matcher(content, index.index_url)
+ generator = crawler._simple_link_matcher(content, crawler.index_url)
+ self.assertEqual(('%stest/foobar-1.tar.gz#md5=abcdef' % crawler.index_url,
+ True), generator.next())
self.assertEqual(('http://dl-link1', True), generator.next())
- self.assertEqual(('%stest' % index.index_url, False),
+ self.assertEqual(('%stest' % crawler.index_url, False),
generator.next())
self.assertRaises(StopIteration, generator.next)
- # Follow the external links is possible
- index.follow_externals = True
- generator = index._simple_link_matcher(content, index.index_url)
+ # Follow the external links is possible (eg. homepages)
+ crawler.follow_externals = True
+ generator = crawler._simple_link_matcher(content, crawler.index_url)
+ self.assertEqual(('%stest/foobar-1.tar.gz#md5=abcdef' % crawler.index_url,
+ True), generator.next())
self.assertEqual(('http://dl-link1', True), generator.next())
self.assertEqual(('http://dl-link2', False), generator.next())
- self.assertEqual(('%stest' % index.index_url, False),
+ self.assertEqual(('%stest' % crawler.index_url, False),
generator.next())
self.assertRaises(StopIteration, generator.next)
@@ -266,12 +273,44 @@ class PyPISimpleTestCase(support.TempdirManager,
# Test that we can browse local files"""
index_path = os.sep.join(["file://" + PYPI_DEFAULT_STATIC_PATH,
"test_found_links", "simple"])
- index = simple.SimpleIndex(index_path)
- dists = index.find("foobar")
+ crawler = Crawler(index_path)
+ dists = crawler.get_releases("foobar")
self.assertEqual(4, len(dists))
+ def test_get_link_matcher(self):
+ crawler = Crawler("http://example.org")
+ self.assertEqual('_simple_link_matcher', crawler._get_link_matcher(
+ "http://example.org/some/file").__name__)
+ self.assertEqual('_default_link_matcher', crawler._get_link_matcher(
+ "http://other-url").__name__)
+
+ def test_default_link_matcher(self):
+ crawler = Crawler("http://example.org", mirrors=[])
+ crawler.follow_externals = True
+ crawler._is_browsable = lambda *args:True
+ base_url = "http://example.org/some/file/"
+ content = """
+<a href="../homepage" rel="homepage">link</a>
+<a href="../download" rel="download">link2</a>
+<a href="../simpleurl">link2</a>
+ """
+ found_links = dict(crawler._default_link_matcher(content,
+ base_url)).keys()
+ self.assertIn('http://example.org/some/homepage', found_links)
+ self.assertIn('http://example.org/some/simpleurl', found_links)
+ self.assertIn('http://example.org/some/download', found_links)
+
+ @use_pypi_server("project_list")
+ def test_search_projects(self, server):
+ # we can search the index for some projects, on their names
+ # the case used no matters here
+ crawler = self._get_simple_crawler(server)
+ projects = [p.name for p in crawler.search_projects("Foobar")]
+ self.assertListEqual(['FooBar-bar', 'Foobar-baz', 'Baz-FooBar'],
+ projects)
+
def test_suite():
- return unittest.makeSuite(PyPISimpleTestCase)
+ return unittest.makeSuite(SimpleCrawlerTestCase)
if __name__ == '__main__':
unittest.main(defaultTest="test_suite")
diff --git a/src/distutils2/tests/test_index_xmlrpc.py b/src/distutils2/tests/test_index_xmlrpc.py
new file mode 100644
index 0000000..50ea739
--- /dev/null
+++ b/src/distutils2/tests/test_index_xmlrpc.py
@@ -0,0 +1,92 @@
+"""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, InvalidSearchField, ProjectNotFound
+
+
+class TestXMLRPCClient(unittest.TestCase):
+ def _get_client(self, server, *args, **kwargs):
+ return Client(server.full_address, *args, **kwargs)
+
+ @use_xmlrpc_server()
+ def test_search_projects(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_search_result(['FooBar', 'Foo', 'FooFoo'])
+ results = [r.name for r in client.search_projects(name='Foo')]
+ self.assertEqual(3, len(results))
+ self.assertIn('FooBar', results)
+ self.assertIn('Foo', results)
+ self.assertIn('FooFoo', results)
+
+ def test_search_projects_bad_fields(self):
+ client = Client()
+ self.assertRaises(InvalidSearchField, client.search_projects,
+ invalid="test")
+
+ @use_xmlrpc_server()
+ def test_get_releases(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_distributions([
+ {'name': 'FooBar', 'version': '1.1'},
+ {'name': 'FooBar', 'version': '1.2', 'url': 'http://some/url/'},
+ {'name': 'FooBar', 'version': '1.3', 'url': 'http://other/url/'},
+ ])
+
+ # use a lambda here to avoid an useless mock call
+ server.xmlrpc.list_releases = lambda *a, **k: ['1.1', '1.2', '1.3']
+
+ releases = client.get_releases('FooBar (<=1.2)')
+ # dont call release_data and release_url; just return name and version.
+ self.assertEqual(2, len(releases))
+ versions = releases.get_versions()
+ self.assertIn('1.1', versions)
+ self.assertIn('1.2', versions)
+ self.assertNotIn('1.3', versions)
+
+ self.assertRaises(ProjectNotFound, client.get_releases,'Foo')
+
+ @use_xmlrpc_server()
+ def test_get_distributions(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_distributions([
+ {'name':'FooBar', 'version': '1.1', 'url':
+ 'http://example.org/foobar-1.1-sdist.tar.gz',
+ 'digest': '1234567', 'type': 'sdist', 'python_version':'source'},
+ {'name':'FooBar', 'version': '1.1', 'url':
+ 'http://example.org/foobar-1.1-bdist.tar.gz',
+ 'digest': '8912345', 'type': 'bdist'},
+ ])
+
+ releases = client.get_releases('FooBar', '1.1')
+ client.get_distributions('FooBar', '1.1')
+ release = releases.get_release('1.1')
+ self.assertTrue('http://example.org/foobar-1.1-sdist.tar.gz',
+ release['sdist'].url['url'])
+ self.assertTrue('http://example.org/foobar-1.1-bdist.tar.gz',
+ release['bdist'].url['url'])
+ self.assertEqual(release['sdist'].python_version, 'source')
+
+ @use_xmlrpc_server()
+ def test_get_metadata(self, server):
+ client = self._get_client(server)
+ server.xmlrpc.set_distributions([
+ {'name':'FooBar',
+ 'version': '1.1',
+ 'keywords': '',
+ 'obsoletes_dist': ['FooFoo'],
+ 'requires_external': ['Foo'],
+ }])
+ release = client.get_metadata('FooBar', '1.1')
+ self.assertEqual(['Foo'], release.metadata['requires_external'])
+ self.assertEqual(['FooFoo'], release.metadata['obsoletes_dist'])
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestXMLRPCClient))
+ return suite
+
+if __name__ == '__main__':
+ run_unittest(test_suite())
diff --git a/src/distutils2/tests/test_install.py b/src/distutils2/tests/test_install.py
index 5cf6cd9..f3d9c59 100644
--- a/src/distutils2/tests/test_install.py
+++ b/src/distutils2/tests/test_install.py
@@ -75,11 +75,9 @@ class InstallTestCase(support.TempdirManager,
check_path(cmd.install_scripts, os.path.join(destination, "bin"))
check_path(cmd.install_data, destination)
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_user_site(self):
- # site.USER_SITE was introduced in 2.6
- if sys.version < '2.6':
- return
-
+ # test install with --user
# preparing the environment for the test
self.old_user_base = get_config_var('userbase')
self.old_user_site = get_path('purelib', '%s_user' % os.name)
@@ -195,14 +193,18 @@ class InstallTestCase(support.TempdirManager,
cmd.ensure_finalized()
cmd.run()
- # let's check the RECORD file was created with one
- # line (the egg info file)
+ # let's check the RECORD file was created with four
+ # lines, one for each .dist-info entry: METADATA,
+ # INSTALLER, REQUSTED, RECORD
f = open(cmd.record)
try:
- self.assertEqual(len(f.readlines()), 1)
+ self.assertEqual(len(f.readlines()), 4)
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_distinfo.py b/src/distutils2/tests/test_install_distinfo.py
new file mode 100644
index 0000000..fbfa722
--- /dev/null
+++ b/src/distutils2/tests/test_install_distinfo.py
@@ -0,0 +1,202 @@
+"""Tests for ``distutils2.command.install_distinfo``. """
+
+import os
+import sys
+import csv
+
+from distutils2.command.install_distinfo import install_distinfo
+from distutils2.core import Command
+from distutils2.metadata import DistributionMetadata
+from distutils2.tests import support
+from distutils2.tests.support import unittest
+
+try:
+ import hashlib
+except ImportError:
+ from distutils2._backport import hashlib
+
+
+class DummyInstallCmd(Command):
+
+ def __init__(self, dist=None):
+ self.outputs = []
+ self.distribution = dist
+
+ def __getattr__(self, name):
+ return None
+
+ def ensure_finalized(self):
+ pass
+
+ def get_outputs(self):
+ return self.outputs + \
+ self.get_finalized_command('install_distinfo').get_outputs()
+
+
+class InstallDistinfoTestCase(support.TempdirManager,
+ support.LoggingSilencer,
+ support.EnvironGuard,
+ unittest.TestCase):
+
+ checkLists = lambda self, x, y: self.assertListEqual(sorted(x), sorted(y))
+
+ def test_empty_install(self):
+ pkg_dir, dist = self.create_dist(name='foo',
+ version='1.0')
+ install_dir = self.mkdtemp()
+
+ install = DummyInstallCmd(dist)
+ dist.command_obj['install'] = install
+
+ cmd = install_distinfo(dist)
+ dist.command_obj['install_distinfo'] = cmd
+
+ cmd.initialize_options()
+ cmd.distinfo_dir = install_dir
+ cmd.ensure_finalized()
+ cmd.run()
+
+ self.checkLists(os.listdir(install_dir), ['foo-1.0.dist-info'])
+
+ dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+ self.checkLists(os.listdir(dist_info),
+ ['METADATA', 'RECORD', 'REQUESTED', 'INSTALLER'])
+ self.assertEqual(open(os.path.join(dist_info, 'INSTALLER')).read(),
+ 'distutils')
+ self.assertEqual(open(os.path.join(dist_info, 'REQUESTED')).read(),
+ '')
+ meta_path = os.path.join(dist_info, 'METADATA')
+ self.assertTrue(DistributionMetadata(path=meta_path).check())
+
+ def test_installer(self):
+ pkg_dir, dist = self.create_dist(name='foo',
+ version='1.0')
+ install_dir = self.mkdtemp()
+
+ install = DummyInstallCmd(dist)
+ dist.command_obj['install'] = install
+
+ cmd = install_distinfo(dist)
+ dist.command_obj['install_distinfo'] = cmd
+
+ cmd.initialize_options()
+ cmd.distinfo_dir = install_dir
+ cmd.installer = 'bacon-python'
+ cmd.ensure_finalized()
+ cmd.run()
+
+ dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+ self.assertEqual(open(os.path.join(dist_info, 'INSTALLER')).read(),
+ 'bacon-python')
+
+ def test_requested(self):
+ pkg_dir, dist = self.create_dist(name='foo',
+ version='1.0')
+ install_dir = self.mkdtemp()
+
+ install = DummyInstallCmd(dist)
+ dist.command_obj['install'] = install
+
+ cmd = install_distinfo(dist)
+ dist.command_obj['install_distinfo'] = cmd
+
+ cmd.initialize_options()
+ cmd.distinfo_dir = install_dir
+ cmd.requested = False
+ cmd.ensure_finalized()
+ cmd.run()
+
+ dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+ self.checkLists(os.listdir(dist_info),
+ ['METADATA', 'RECORD', 'INSTALLER'])
+
+ def test_no_record(self):
+ pkg_dir, dist = self.create_dist(name='foo',
+ version='1.0')
+ install_dir = self.mkdtemp()
+
+ install = DummyInstallCmd(dist)
+ dist.command_obj['install'] = install
+
+ cmd = install_distinfo(dist)
+ dist.command_obj['install_distinfo'] = cmd
+
+ cmd.initialize_options()
+ cmd.distinfo_dir = install_dir
+ cmd.no_record = True
+ cmd.ensure_finalized()
+ cmd.run()
+
+ dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+ self.checkLists(os.listdir(dist_info),
+ ['METADATA', 'REQUESTED', 'INSTALLER'])
+
+ def test_record(self):
+ pkg_dir, dist = self.create_dist(name='foo',
+ version='1.0')
+ install_dir = self.mkdtemp()
+
+ install = DummyInstallCmd(dist)
+ dist.command_obj['install'] = install
+
+ fake_dists = os.path.join(os.path.dirname(__file__), '..',
+ '_backport', 'tests', 'fake_dists')
+ fake_dists = os.path.realpath(fake_dists)
+
+ # for testing, we simply add all files from _backport's fake_dists
+ dirs = []
+ for dir in os.listdir(fake_dists):
+ full_path = os.path.join(fake_dists, dir)
+ if (not dir.endswith('.egg') or dir.endswith('.egg-info') or
+ dir.endswith('.dist-info')) and os.path.isdir(full_path):
+ dirs.append(full_path)
+
+ for dir in dirs:
+ for (path, subdirs, files) in os.walk(dir):
+ install.outputs += [os.path.join(path, f) for f in files]
+ install.outputs += [os.path.join('path', f + 'c')
+ for f in files if f.endswith('.py')]
+
+
+ cmd = install_distinfo(dist)
+ dist.command_obj['install_distinfo'] = cmd
+
+ cmd.initialize_options()
+ cmd.distinfo_dir = install_dir
+ cmd.ensure_finalized()
+ cmd.run()
+
+ dist_info = os.path.join(install_dir, 'foo-1.0.dist-info')
+
+ expected = []
+ for f in install.get_outputs():
+ if f.endswith('.pyc') or \
+ f == os.path.join(install_dir, 'foo-1.0.dist-info', 'RECORD'):
+ expected.append([f, '', ''])
+ else:
+ size = os.path.getsize(f)
+ md5 = hashlib.md5()
+ md5.update(open(f).read())
+ hash = md5.hexdigest()
+ expected.append([f, hash, str(size)])
+
+ parsed = []
+ f = open(os.path.join(dist_info, 'RECORD'), 'rb')
+ try:
+ reader = csv.reader(f, delimiter=',',
+ lineterminator=os.linesep,
+ quotechar='"')
+ parsed = list(reader)
+ finally:
+ f.close()
+
+ self.maxDiff = None
+ self.checkLists(parsed, expected)
+
+
+def test_suite():
+ return unittest.makeSuite(InstallDistinfoTestCase)
+
+
+if __name__ == "__main__":
+ unittest.main(defaultTest="test_suite")
diff --git a/src/distutils2/tests/test_install_lib.py b/src/distutils2/tests/test_install_lib.py
index 680eaf1..a96c889 100644
--- a/src/distutils2/tests/test_install_lib.py
+++ b/src/distutils2/tests/test_install_lib.py
@@ -57,9 +57,8 @@ class InstallLibTestCase(support.TempdirManager,
# setting up a dist environment
cmd.compile = cmd.optimize = 1
cmd.install_dir = pkg_dir
- f = os.path.join(pkg_dir, 'foo.py')
- self.write_file(f, '# python file')
- cmd.distribution.py_modules = [pkg_dir]
+ f = os.path.join(pkg_dir, '__init__.py')
+ self.write_file(f, '# python package')
cmd.distribution.ext_modules = [Extension('foo', ['xxx'])]
cmd.distribution.packages = [pkg_dir]
cmd.distribution.script_name = 'setup.py'
@@ -74,9 +73,8 @@ class InstallLibTestCase(support.TempdirManager,
# setting up a dist environment
cmd.compile = cmd.optimize = 1
cmd.install_dir = pkg_dir
- f = os.path.join(pkg_dir, 'foo.py')
- self.write_file(f, '# python file')
- cmd.distribution.py_modules = [pkg_dir]
+ f = os.path.join(pkg_dir, '__init__.py')
+ self.write_file(f, '# python package')
cmd.distribution.ext_modules = [Extension('foo', ['xxx'])]
cmd.distribution.packages = [pkg_dir]
cmd.distribution.script_name = 'setup.py'
@@ -84,7 +82,8 @@ class InstallLibTestCase(support.TempdirManager,
# get_input should return 2 elements
self.assertEqual(len(cmd.get_inputs()), 2)
- @unittest.skipUnless(bytecode_support, 'sys.dont_write_bytecode not supported')
+ @unittest.skipUnless(bytecode_support,
+ 'sys.dont_write_bytecode not supported')
def test_dont_write_bytecode(self):
# makes sure byte_compile is not used
pkg_dir, dist = self.create_dist()
diff --git a/src/distutils2/tests/test_install_with_deps.py b/src/distutils2/tests/test_install_with_deps.py
new file mode 100644
index 0000000..1b808b2
--- /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/tests/test_metadata.py b/src/distutils2/tests/test_metadata.py
index ee763c0..e7e8d68 100644
--- a/src/distutils2/tests/test_metadata.py
+++ b/src/distutils2/tests/test_metadata.py
@@ -1,6 +1,7 @@
"""Tests for distutils.command.bdist."""
import os
import sys
+import platform
from StringIO import StringIO
from distutils2.metadata import (DistributionMetadata, _interpret,
@@ -12,25 +13,59 @@ from distutils2.errors import (MetadataConflictError,
class DistributionMetadataTestCase(LoggingSilencer, unittest.TestCase):
+ def test_instantiation(self):
+ PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO')
+ fp = open(PKG_INFO)
+ try:
+ contents = fp.read()
+ finally:
+ fp.close()
+ fp = StringIO(contents)
+
+ m = DistributionMetadata()
+ self.assertRaises(MetadataUnrecognizedVersionError, m.items)
+
+ m = DistributionMetadata(PKG_INFO)
+ self.assertEqual(len(m.items()), 22)
+
+ m = DistributionMetadata(fileobj=fp)
+ self.assertEqual(len(m.items()), 22)
+
+ m = DistributionMetadata(mapping=dict(name='Test', version='1.0'))
+ self.assertEqual(len(m.items()), 11)
+
+ d = dict(m.items())
+ self.assertRaises(TypeError, DistributionMetadata,
+ PKG_INFO, fileobj=fp)
+ self.assertRaises(TypeError, DistributionMetadata,
+ PKG_INFO, mapping=d)
+ self.assertRaises(TypeError, DistributionMetadata,
+ fileobj=fp, mapping=d)
+ self.assertRaises(TypeError, DistributionMetadata,
+ PKG_INFO, mapping=m, fileobj=fp)
def test_interpret(self):
- platform = sys.platform
+ sys_platform = sys.platform
version = sys.version.split()[0]
os_name = os.name
+ platform_version = platform.version()
+ platform_machine = platform.machine()
- self.assertTrue(_interpret("sys.platform == '%s'" % platform))
+ self.assertTrue(_interpret("sys.platform == '%s'" % sys_platform))
self.assertTrue(_interpret(
- "sys.platform == '%s' or python_version == '2.4'" % platform))
+ "sys.platform == '%s' or python_version == '2.4'" % sys_platform))
self.assertTrue(_interpret(
"sys.platform == '%s' and python_full_version == '%s'" %
- (platform, version)))
- self.assertTrue(_interpret("'%s' == sys.platform" % platform))
-
+ (sys_platform, version)))
+ self.assertTrue(_interpret("'%s' == sys.platform" % sys_platform))
self.assertTrue(_interpret('os.name == "%s"' % os_name))
+ self.assertTrue(_interpret(
+ 'platform.version == "%s" and platform.machine == "%s"' %
+ (platform_version, platform_machine)))
# stuff that need to raise a syntax error
ops = ('os.name == os.name', 'os.name == 2', "'2' == '2'",
- 'okpjonon', '', 'os.name ==')
+ 'okpjonon', '', 'os.name ==', 'python_version == 2.4')
for op in ops:
self.assertRaises(SyntaxError, _interpret, op)
@@ -62,17 +97,13 @@ class DistributionMetadataTestCase(LoggingSilencer, unittest.TestCase):
PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO')
metadata = DistributionMetadata(PKG_INFO)
- res = StringIO()
- metadata.write_file(res)
- res.seek(0)
- res = res.read()
- f = open(PKG_INFO)
- try:
- # XXX this is not used
- wanted = f.read()
- finally:
- f.close()
- self.assertTrue('Keywords: keyring,password,crypt' in res)
+ out = StringIO()
+ metadata.write_file(out)
+ out.seek(0)
+ res = DistributionMetadata()
+ res.read_file(out)
+ for k in metadata.keys():
+ self.assertTrue(metadata[k] == res[k])
def test_metadata_markers(self):
# see if we can be platform-aware
@@ -82,6 +113,10 @@ class DistributionMetadataTestCase(LoggingSilencer, unittest.TestCase):
metadata = DistributionMetadata(platform_dependent=True)
metadata.read_file(StringIO(content))
self.assertEqual(metadata['Requires-Dist'], ['bar'])
+ metadata['Name'] = "baz; sys.platform == 'blah'"
+ # FIXME is None or 'UNKNOWN' correct here?
+ # where is that documented?
+ self.assertEquals(metadata['Name'], None)
# test with context
context = {'sys.platform': 'okook'}
@@ -109,15 +144,15 @@ class DistributionMetadataTestCase(LoggingSilencer, unittest.TestCase):
metadata.read_file(out)
self.assertEqual(wanted, metadata['Description'])
- def test_mapper_apis(self):
+ def test_mapping_api(self):
PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO')
content = open(PKG_INFO).read()
content = content % sys.platform
- metadata = DistributionMetadata()
- metadata.read_file(StringIO(content))
+ metadata = DistributionMetadata(fileobj=StringIO(content))
self.assertIn('Version', metadata.keys())
self.assertIn('0.5', metadata.values())
self.assertIn(('Version', '0.5'), metadata.items())
+ #TODO test update
def test_versions(self):
metadata = DistributionMetadata()
@@ -211,6 +246,10 @@ class DistributionMetadataTestCase(LoggingSilencer, unittest.TestCase):
metadata = DistributionMetadata()
metadata['Version'] = 'rr'
metadata['Requires-dist'] = ['Foo (a)']
+ if metadata.docutils_support:
+ missing, warnings = metadata.check()
+ self.assertEqual(len(warnings), 2)
+ metadata.docutils_support = False
missing, warnings = metadata.check()
self.assertEqual(missing, ['Name', 'Home-page'])
self.assertEqual(len(warnings), 2)
diff --git a/src/distutils2/tests/test_msvc9compiler.py b/src/distutils2/tests/test_msvc9compiler.py
index 452c3f5..eea83fa 100644
--- a/src/distutils2/tests/test_msvc9compiler.py
+++ b/src/distutils2/tests/test_msvc9compiler.py
@@ -64,7 +64,7 @@ _CLEANED_MANIFEST = """\
class msvc9compilerTestCase(support.TempdirManager,
unittest.TestCase):
- @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
+ @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
def test_no_compiler(self):
# makes sure query_vcvarsall throws
# a DistutilsPlatformError if the compiler
@@ -86,7 +86,7 @@ class msvc9compilerTestCase(support.TempdirManager,
finally:
msvc9compiler.find_vcvarsall = old_find_vcvarsall
- @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
+ @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
def test_reg_class(self):
from distutils2.msvccompiler import get_build_version
if get_build_version() < 8.0:
@@ -110,7 +110,7 @@ class msvc9compilerTestCase(support.TempdirManager,
keys = Reg.read_keys(HKCU, r'Control Panel')
self.assertTrue('Desktop' in keys)
- @unittest.skipUnless(sys.platform=="win32", "These tests are only for win32")
+ @unittest.skipUnless(sys.platform == "win32", "runs only on win32")
def test_remove_visual_c_ref(self):
from distutils2.msvc9compiler import MSVCCompiler
tempdir = self.mkdtemp()
diff --git a/src/distutils2/tests/test_pypi_dist.py b/src/distutils2/tests/test_pypi_dist.py
deleted file mode 100644
index 0267760..0000000
--- a/src/distutils2/tests/test_pypi_dist.py
+++ /dev/null
@@ -1,247 +0,0 @@
-"""Tests for the distutils2.pypi.dist module."""
-
-import os
-import shutil
-import tempfile
-
-from distutils2.tests.pypi_server import use_pypi_server
-from distutils2.tests import run_unittest
-from distutils2.tests.support import unittest, TempdirManager
-from distutils2.version import VersionPredicate
-from distutils2.pypi.errors import HashDoesNotMatch, UnsupportedHashName
-from distutils2.pypi.dist import (PyPIDistribution as Dist,
- PyPIDistributions as Dists,
- split_archive_name)
-
-
-class TestPyPIDistribution(TempdirManager,
- unittest.TestCase):
- """Tests the pypi.dist.PyPIDistribution class"""
-
- def test_instanciation(self):
- # Test the Distribution class provides us the good attributes when
- # given on construction
- dist = Dist("FooBar", "1.1")
- self.assertEqual("FooBar", dist.name)
- self.assertEqual("1.1", "%s" % dist.version)
-
- def test_create_from_url(self):
- # Test that the Distribution object can be built from a single URL
- url_list = {
- 'FooBar-1.1.0.tar.gz': {
- 'name': 'foobar', # lowercase the name
- 'version': '1.1',
- },
- 'Foo-Bar-1.1.0.zip': {
- 'name': 'foo-bar', # keep the dash
- 'version': '1.1',
- },
- 'foobar-1.1b2.tar.gz#md5=123123123123123': {
- 'name': 'foobar',
- 'version': '1.1b2',
- 'url': {
- 'url': 'http://test.tld/foobar-1.1b2.tar.gz', # no hash
- 'hashval': '123123123123123',
- 'hashname': 'md5',
- }
- },
- 'foobar-1.1-rc2.tar.gz': { # use suggested name
- 'name': 'foobar',
- 'version': '1.1c2',
- 'url': {
- 'url': 'http://test.tld/foobar-1.1-rc2.tar.gz',
- }
- }
- }
-
- for url, attributes in url_list.items():
- dist = Dist.from_url("http://test.tld/" + url)
- for attribute, value in attributes.items():
- if isinstance(value, dict):
- mylist = getattr(dist, attribute)
- for val in value.keys():
- self.assertEqual(value[val], mylist[val])
- else:
- if attribute == "version":
- self.assertEqual("%s" % getattr(dist, "version"), value)
- else:
- self.assertEqual(getattr(dist, attribute), value)
-
- def test_get_url(self):
- # Test that the url property works well
-
- d = Dist("foobar", "1.1", url="test_url")
- self.assertDictEqual(d.url, {
- "url": "test_url",
- "is_external": True,
- "hashname": None,
- "hashval": None,
- })
-
- # add a new url
- d.add_url(url="internal_url", is_external=False)
- self.assertEqual(d._url, None)
- self.assertDictEqual(d.url, {
- "url": "internal_url",
- "is_external": False,
- "hashname": None,
- "hashval": None,
- })
- self.assertEqual(2, len(d._urls))
-
- def test_comparaison(self):
- # Test that we can compare PyPIDistributions
- foo1 = Dist("foo", "1.0")
- foo2 = Dist("foo", "2.0")
- bar = Dist("bar", "2.0")
- # assert we use the version to compare
- self.assertTrue(foo1 < foo2)
- self.assertFalse(foo1 > foo2)
- self.assertFalse(foo1 == foo2)
-
- # assert we can't compare dists with different names
- self.assertRaises(TypeError, foo1.__eq__, bar)
-
- def test_split_archive_name(self):
- # Test we can split the archive names
- names = {
- 'foo-bar-baz-1.0-rc2': ('foo-bar-baz', '1.0c2'),
- 'foo-bar-baz-1.0': ('foo-bar-baz', '1.0'),
- 'foobarbaz-1.0': ('foobarbaz', '1.0'),
- }
- for name, results in names.items():
- self.assertEqual(results, split_archive_name(name))
-
- @use_pypi_server("downloads_with_md5")
- def test_download(self, server):
- # Download is possible, and the md5 is checked if given
-
- add_to_tmpdirs = lambda x: self.tempdirs.append(os.path.dirname(x))
-
- url = "%s/simple/foobar/foobar-0.1.tar.gz" % server.full_address
- # check md5 if given
- dist = Dist("FooBar", "0.1", url=url,
- url_hashname="md5", url_hashval="d41d8cd98f00b204e9800998ecf8427e")
- add_to_tmpdirs(dist.download())
-
- # a wrong md5 fails
- dist2 = Dist("FooBar", "0.1", url=url,
- url_hashname="md5", url_hashval="wrongmd5")
-
- self.assertRaises(HashDoesNotMatch, dist2.download)
- add_to_tmpdirs(dist2.downloaded_location)
-
- # we can omit the md5 hash
- dist3 = Dist("FooBar", "0.1", url=url)
- add_to_tmpdirs(dist3.download())
-
- # and specify a temporary location
- # for an already downloaded dist
- path1 = self.mkdtemp()
- dist3.download(path=path1)
- # and for a new one
- path2_base = self.mkdtemp()
- dist4 = Dist("FooBar", "0.1", url=url)
- path2 = dist4.download(path=path2_base)
- self.assertTrue(path2_base in path2)
-
- def test_hashname(self):
- # Invalid hashnames raises an exception on assignation
- Dist("FooBar", "0.1", url_hashname="md5", url_hashval="value")
-
- self.assertRaises(UnsupportedHashName, Dist, "FooBar", "0.1",
- url_hashname="invalid_hashname", url_hashval="value")
-
-
-class TestPyPIDistributions(unittest.TestCase):
-
- def test_filter(self):
- # Test we filter the distributions the right way, using version
- # predicate match method
- dists = Dists((
- Dist("FooBar", "1.1"),
- Dist("FooBar", "1.1.1"),
- Dist("FooBar", "1.2"),
- Dist("FooBar", "1.2.1"),
- ))
- filtered = dists.filter(VersionPredicate("FooBar (<1.2)"))
- self.assertNotIn(dists[2], filtered)
- self.assertNotIn(dists[3], filtered)
- self.assertIn(dists[0], filtered)
- self.assertIn(dists[1], filtered)
-
- def test_append(self):
- # When adding a new item to the list, the behavior is to test if
- # a distribution with the same name and version number already exists,
- # and if so, to add url informations to the existing PyPIDistribution
- # object.
- # If no object matches, just add "normally" the object to the list.
-
- dists = Dists([
- Dist("FooBar", "1.1", url="external_url", type="source"),
- ])
- self.assertEqual(1, len(dists))
- dists.append(Dist("FooBar", "1.1", url="internal_url",
- url_is_external=False, type="source"))
- self.assertEqual(1, len(dists))
- self.assertEqual(2, len(dists[0]._urls))
-
- dists.append(Dist("Foobar", "1.1.1", type="source"))
- self.assertEqual(2, len(dists))
-
- # when adding a distribution whith a different type, a new distribution
- # has to be added.
- dists.append(Dist("Foobar", "1.1.1", type="binary"))
- self.assertEqual(3, len(dists))
-
- def test_prefer_final(self):
- # Can order the distributions using prefer_final
-
- fb10 = Dist("FooBar", "1.0") # final distribution
- fb11a = Dist("FooBar", "1.1a1") # alpha
- fb12a = Dist("FooBar", "1.2a1") # alpha
- fb12b = Dist("FooBar", "1.2b1") # beta
- dists = Dists([fb10, fb11a, fb12a, fb12b])
-
- dists.sort_distributions(prefer_final=True)
- self.assertEqual(fb10, dists[0])
-
- dists.sort_distributions(prefer_final=False)
- self.assertEqual(fb12b, dists[0])
-
- def test_prefer_source(self):
- # Ordering support prefer_source
- fb_source = Dist("FooBar", "1.0", type="source")
- fb_binary = Dist("FooBar", "1.0", type="binary")
- fb2_binary = Dist("FooBar", "2.0", type="binary")
- dists = Dists([fb_binary, fb_source])
-
- dists.sort_distributions(prefer_source=True)
- self.assertEqual(fb_source, dists[0])
-
- dists.sort_distributions(prefer_source=False)
- self.assertEqual(fb_binary, dists[0])
-
- dists.append(fb2_binary)
- dists.sort_distributions(prefer_source=True)
- self.assertEqual(fb2_binary, dists[0])
-
- def test_get_same_name_and_version(self):
- # PyPIDistributions can return a list of "duplicates"
- fb_source = Dist("FooBar", "1.0", type="source")
- fb_binary = Dist("FooBar", "1.0", type="binary")
- fb2_binary = Dist("FooBar", "2.0", type="binary")
- dists = Dists([fb_binary, fb_source, fb2_binary])
- duplicates = dists.get_same_name_and_version()
- self.assertTrue(1, len(duplicates))
- self.assertIn(fb_source, duplicates[0])
-
-
-def test_suite():
- suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(TestPyPIDistribution))
- suite.addTest(unittest.makeSuite(TestPyPIDistributions))
- return suite
-
-if __name__ == '__main__':
- run_unittest(test_suite())
diff --git a/src/distutils2/tests/test_pypi_versions.py b/src/distutils2/tests/test_pypi_versions.py
index 7781982..bbb9395 100644
--- a/src/distutils2/tests/test_pypi_versions.py
+++ b/src/distutils2/tests/test_pypi_versions.py
@@ -1,43 +1,39 @@
-#
-## test_pypi_versions.py
-##
-## A very simple test to see what percentage of the current pypi packages
-## have versions that can be converted automatically by distutils' new
-## suggest_normalized_version() into PEP-386 compatible versions.
-##
-## Requires : Python 2.5+
-##
-## Written by: ssteinerX@gmail.com
-#
+"""PEP 386 compatibility test with current distributions on PyPI.
+
+A very simple test to see what percentage of the current PyPI packages
+have versions that can be converted automatically by distutils2's new
+suggest_normalized_version into PEP 386-compatible versions.
+"""
+
+# XXX This file does not actually run tests, move it to a script
+
+# Written by ssteinerX@gmail.com
+
+import os
+import xmlrpclib
try:
import cPickle as pickle
-except:
+except ImportError:
import pickle
-import xmlrpclib
-import os.path
-
from distutils2.version import suggest_normalized_version
from distutils2.tests import run_unittest
from distutils2.tests.support import unittest
def test_pypi():
- #
- ## To re-run from scratch, just delete these two .pkl files
- #
+ # FIXME need a better way to do that
+ # To re-run from scratch, just delete these two .pkl files
INDEX_PICKLE_FILE = 'pypi-index.pkl'
VERSION_PICKLE_FILE = 'pypi-version.pkl'
package_info = version_info = []
- #
- ## if there's a saved version of the package list
- ## restore it
- ## else:
- ## pull the list down from pypi
- ## save a pickled version of it
- #
+ # if there's a saved version of the package list
+ # restore it
+ # else:
+ # pull the list down from pypi
+ # save a pickled version of it
if os.path.exists(INDEX_PICKLE_FILE):
print "Loading saved pypi data..."
f = open(INDEX_PICKLE_FILE, 'rb')
@@ -57,13 +53,11 @@ def test_pypi():
finally:
f.close()
- #
- ## If there's a saved list of the versions from the packages
- ## restore it
- ## else
- ## extract versions from the package list
- ## save a pickled version of it
- #
+ # If there's a saved list of the versions from the packages
+ # restore it
+ # else
+ # extract versions from the package list
+ # save a pickled version of it
versions = []
if os.path.exists(VERSION_PICKLE_FILE):
print "Loading saved version info..."
diff --git a/src/distutils2/tests/test_register.py b/src/distutils2/tests/test_register.py
index be782d1..5081f49 100644
--- a/src/distutils2/tests/test_register.py
+++ b/src/distutils2/tests/test_register.py
@@ -1,5 +1,5 @@
+# -*- encoding: utf-8 -*-
"""Tests for distutils.command.register."""
-# -*- encoding: utf8 -*-
import sys
import os
import getpass
@@ -19,7 +19,7 @@ from distutils2.errors import DistutilsSetupError
from distutils2.tests import support
from distutils2.tests.support import unittest
-from distutils2.tests.test_config import PYPIRC, PyPIRCCommandTestCase
+
PYPIRC_NOPASSWORD = """\
[distutils]
@@ -68,10 +68,15 @@ class FakeOpener(object):
def read(self):
return 'xxx'
-class RegisterTestCase(PyPIRCCommandTestCase):
+class RegisterTestCase(support.TempdirManager, support.EnvironGuard,
+ unittest.TestCase):
def setUp(self):
super(RegisterTestCase, self).setUp()
+ self.tmp_dir = self.mkdtemp()
+ self.rc = os.path.join(self.tmp_dir, '.pypirc')
+ os.environ['HOME'] = self.tmp_dir
+
# patching the password prompt
self._old_getpass = getpass.getpass
def _getpass(prompt):
@@ -148,15 +153,15 @@ class RegisterTestCase(PyPIRCCommandTestCase):
self.write_file(self.rc, PYPIRC_NOPASSWORD)
cmd = self._get_cmd()
- cmd._set_config()
cmd.finalize_options()
+ cmd._set_config()
cmd.send_metadata()
# dist.password should be set
# therefore used afterwards by other commands
self.assertEqual(cmd.distribution.password, 'password')
- def test_registering(self):
+ def test_registration(self):
# this test runs choice 2
cmd = self._get_cmd()
inputs = RawInputs('2', 'tarek', 'tarek@ziade.org')
@@ -205,7 +210,7 @@ class RegisterTestCase(PyPIRCCommandTestCase):
cmd.strict = 1
self.assertRaises(DistutilsSetupError, cmd.run)
- # metadata are OK but long_description is broken
+ # metadata is OK but long_description is broken
metadata = {'home_page': 'xxx', 'author': 'xxx',
'author_email': u'éxéxé',
'name': 'xxx', 'version': 'xxx',
diff --git a/src/distutils2/tests/test_sdist.py b/src/distutils2/tests/test_sdist.py
index ec4c87a..c974175 100644
--- a/src/distutils2/tests/test_sdist.py
+++ b/src/distutils2/tests/test_sdist.py
@@ -28,9 +28,8 @@ from distutils2.command.sdist import sdist
from distutils2.command.sdist import show_formats
from distutils2.core import Distribution
from distutils2.tests.support import unittest
-from distutils2.tests.test_config import PyPIRCCommandTestCase
from distutils2.errors import DistutilsExecError, DistutilsOptionError
-from distutils2.spawn import find_executable
+from distutils2.util import find_executable
from distutils2.tests import support
from distutils2.log import WARN
try:
@@ -58,12 +57,15 @@ somecode%(sep)sdoc.dat
somecode%(sep)sdoc.txt
"""
-class SDistTestCase(PyPIRCCommandTestCase):
+class SDistTestCase(support.TempdirManager, support.LoggingSilencer,
+ support.EnvironGuard, unittest.TestCase):
def setUp(self):
# PyPIRCCommandTestCase creates a temp dir already
# and put it in self.tmp_dir
super(SDistTestCase, self).setUp()
+ self.tmp_dir = self.mkdtemp()
+ os.environ['HOME'] = self.tmp_dir
# setting up an environment
self.old_path = os.getcwd()
os.mkdir(join(self.tmp_dir, 'somecode'))
@@ -239,7 +241,7 @@ class SDistTestCase(PyPIRCCommandTestCase):
@unittest.skipUnless(zlib, "requires zlib")
def test_metadata_check_option(self):
- # testing the `medata-check` option
+ # testing the `check-metadata` option
dist, cmd = self.get_cmd(metadata={})
# this should raise some warnings !
@@ -293,7 +295,7 @@ class SDistTestCase(PyPIRCCommandTestCase):
self.assertRaises(DistutilsOptionError, cmd.finalize_options)
@unittest.skipUnless(zlib, "requires zlib")
- @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support")
+ @unittest.skipUnless(UID_GID_SUPPORT, "requires grp and pwd support")
def test_make_distribution_owner_group(self):
# check if tar and gzip are installed
diff --git a/src/distutils2/tests/test_spawn.py b/src/distutils2/tests/test_spawn.py
deleted file mode 100644
index 2c7f1ad..0000000
--- a/src/distutils2/tests/test_spawn.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""Tests for distutils.spawn."""
-import os
-import time
-from distutils2.tests import captured_stdout
-
-from distutils2.spawn import _nt_quote_args
-from distutils2.spawn import spawn, find_executable
-from distutils2.errors import DistutilsExecError
-from distutils2.tests import support
-from distutils2.tests.support import unittest
-
-class SpawnTestCase(support.TempdirManager,
- support.LoggingSilencer,
- unittest.TestCase):
-
- def test_nt_quote_args(self):
-
- for (args, wanted) in ((['with space', 'nospace'],
- ['"with space"', 'nospace']),
- (['nochange', 'nospace'],
- ['nochange', 'nospace'])):
- res = _nt_quote_args(args)
- self.assertEqual(res, wanted)
-
-
- @unittest.skipUnless(os.name in ('nt', 'posix'),
- 'Runs only under posix or nt')
- def test_spawn(self):
- tmpdir = self.mkdtemp()
-
- # creating something executable
- # through the shell that returns 1
- if os.name == 'posix':
- exe = os.path.join(tmpdir, 'foo.sh')
- self.write_file(exe, '#!/bin/sh\nexit 1')
- os.chmod(exe, 0777)
- else:
- exe = os.path.join(tmpdir, 'foo.bat')
- self.write_file(exe, 'exit 1')
-
- os.chmod(exe, 0777)
- self.assertRaises(DistutilsExecError, spawn, [exe])
-
- # now something that works
- if os.name == 'posix':
- exe = os.path.join(tmpdir, 'foo.sh')
- self.write_file(exe, '#!/bin/sh\nexit 0')
- os.chmod(exe, 0777)
- else:
- exe = os.path.join(tmpdir, 'foo.bat')
- self.write_file(exe, 'exit 0')
-
- os.chmod(exe, 0777)
- spawn([exe]) # should work without any error
-
-def test_suite():
- return unittest.makeSuite(SpawnTestCase)
-
-if __name__ == "__main__":
- unittest.main(defaultTest="test_suite")
diff --git a/src/distutils2/tests/test_upload.py b/src/distutils2/tests/test_upload.py
index 5ca88a1..3959872 100644
--- a/src/distutils2/tests/test_upload.py
+++ b/src/distutils2/tests/test_upload.py
@@ -1,5 +1,5 @@
+# -*- encoding: utf-8 -*-
"""Tests for distutils.command.upload."""
-# -*- encoding: utf8 -*-
import os
import sys
@@ -9,7 +9,6 @@ from distutils2.core import Distribution
from distutils2.tests import support
from distutils2.tests.pypi_server import PyPIServer, PyPIServerTestCase
from distutils2.tests.support import unittest
-from distutils2.tests.test_config import PYPIRC, PyPIRCCommandTestCase
PYPIRC_NOPASSWORD = """\
@@ -22,7 +21,33 @@ index-servers =
username:me
"""
-class UploadTestCase(PyPIServerTestCase, PyPIRCCommandTestCase):
+PYPIRC = """\
+[distutils]
+
+index-servers =
+ server1
+ server2
+
+[server1]
+username:me
+password:secret
+
+[server2]
+username:meagain
+password: secret
+realm:acme
+repository:http://another.pypi/
+"""
+
+
+class UploadTestCase(support.TempdirManager, support.EnvironGuard,
+ PyPIServerTestCase):
+
+ def setUp(self):
+ super(UploadTestCase, self).setUp()
+ self.tmp_dir = self.mkdtemp()
+ self.rc = os.path.join(self.tmp_dir, '.pypirc')
+ os.environ['HOME'] = self.tmp_dir
def test_finalize_options(self):
# new format
@@ -76,6 +101,38 @@ class UploadTestCase(PyPIServerTestCase, PyPIRCCommandTestCase):
self.assertEqual(handler.command, 'POST')
self.assertNotIn('\n', headers['authorization'])
+ def test_upload_docs(self):
+ path = os.path.join(self.tmp_dir, 'xxx')
+ self.write_file(path)
+ command, pyversion, filename = 'xxx', '2.6', path
+ dist_files = [(command, pyversion, filename)]
+ docs_path = os.path.join(self.tmp_dir, "build", "docs")
+ os.makedirs(docs_path)
+ self.write_file(os.path.join(docs_path, "index.html"), "yellow")
+ self.write_file(self.rc, PYPIRC)
+
+ # lets run it
+ pkg_dir, dist = self.create_dist(dist_files=dist_files, author=u'dédé')
+
+ cmd = upload(dist)
+ cmd.get_finalized_command("build").run()
+ cmd.upload_docs = True
+ cmd.ensure_finalized()
+ cmd.repository = self.pypi.full_address
+ try:
+ prev_dir = os.getcwd()
+ os.chdir(self.tmp_dir)
+ cmd.run()
+ finally:
+ os.chdir(prev_dir)
+
+ handler, request_data = self.pypi.requests[-1]
+ action, name, content =\
+ request_data.split("----------------GHSKFJDLGDS7543FJKLFHRE75642756743254")[1:4]
+
+ self.assertIn('name=":action"', action)
+ self.assertIn("doc_upload", action)
+
def test_suite():
return unittest.makeSuite(UploadTestCase)
diff --git a/src/distutils2/tests/test_upload_docs.py b/src/distutils2/tests/test_upload_docs.py
index 2147968..c90f6b4 100644
--- a/src/distutils2/tests/test_upload_docs.py
+++ b/src/distutils2/tests/test_upload_docs.py
@@ -1,18 +1,23 @@
-"""Tests for distutils.command.upload_docs."""
# -*- encoding: utf8 -*-
-import httplib, os, os.path, shutil, sys, tempfile, zipfile
-from cStringIO import StringIO
+"""Tests for distutils.command.upload_docs."""
+import os
+import sys
+import httplib
+import shutil
+import zipfile
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
from distutils2.command import upload_docs as upload_docs_mod
from distutils2.command.upload_docs import (upload_docs, zip_dir,
- encode_multipart)
+ encode_multipart)
from distutils2.core import Distribution
-
from distutils2.errors import DistutilsFileError, DistutilsOptionError
from distutils2.tests import support
from distutils2.tests.pypi_server import PyPIServer, PyPIServerTestCase
-from distutils2.tests.test_config import PyPIRCCommandTestCase
from distutils2.tests.support import unittest
@@ -47,16 +52,20 @@ username = real_slim_shady
password = long_island
"""
-class UploadDocsTestCase(PyPIServerTestCase, PyPIRCCommandTestCase):
+class UploadDocsTestCase(support.TempdirManager, support.EnvironGuard,
+ PyPIServerTestCase):
def setUp(self):
super(UploadDocsTestCase, self).setUp()
+ self.tmp_dir = self.mkdtemp()
+ self.rc = os.path.join(self.tmp_dir, '.pypirc')
+ os.environ['HOME'] = self.tmp_dir
self.dist = Distribution()
self.dist.metadata['Name'] = "distr-name"
self.cmd = upload_docs(self.dist)
def test_default_uploaddir(self):
- sandbox = tempfile.mkdtemp()
+ sandbox = self.mkdtemp()
previous = os.getcwd()
os.chdir(sandbox)
try:
@@ -69,7 +78,7 @@ class UploadDocsTestCase(PyPIServerTestCase, PyPIRCCommandTestCase):
def prepare_sample_dir(self, sample_dir=None):
if sample_dir is None:
- sample_dir = tempfile.mkdtemp()
+ sample_dir = self.mkdtemp()
os.mkdir(os.path.join(sample_dir, "docs"))
self.write_file(os.path.join(sample_dir, "docs", "index.html"), "Ce mortel ennui")
self.write_file(os.path.join(sample_dir, "index.html"), "Oh la la")
@@ -178,9 +187,13 @@ class UploadDocsTestCase(PyPIServerTestCase, PyPIRCCommandTestCase):
def test_show_response(self):
orig_stdout = sys.stdout
write_args = []
+
class MockStdIn(object):
def write(self, arg):
write_args.append(arg)
+ def flush(self):
+ pass
+
sys.stdout = MockStdIn()
try:
self.prepare_command()
@@ -188,8 +201,10 @@ class UploadDocsTestCase(PyPIServerTestCase, PyPIRCCommandTestCase):
self.cmd.run()
finally:
sys.stdout = orig_stdout
+
self.assertTrue(write_args[0], "should report the response")
- self.assertIn(self.pypi.default_response_data + "\n", write_args[0])
+ self.assertIn(self.pypi.default_response_data + "\n",
+ '\n'.join(write_args))
def test_suite():
return unittest.makeSuite(UploadDocsTestCase)
diff --git a/src/distutils2/tests/test_util.py b/src/distutils2/tests/test_util.py
index c770c59..26f4ee1 100644
--- a/src/distutils2/tests/test_util.py
+++ b/src/distutils2/tests/test_util.py
@@ -4,22 +4,60 @@ import sys
from copy import copy
from StringIO import StringIO
import subprocess
-import tempfile
import time
+from distutils2.tests import captured_stdout
+from distutils2.tests.support import unittest
from distutils2.errors import (DistutilsPlatformError,
DistutilsByteCompileError,
- DistutilsFileError)
-
+ DistutilsFileError,
+ DistutilsExecError)
from distutils2.util import (convert_path, change_root,
- check_environ, split_quoted, strtobool,
- rfc822_escape, get_compiler_versions,
- _find_exe_version, _MAC_OS_X_LD_VERSION,
- byte_compile, find_packages)
+ check_environ, split_quoted, strtobool,
+ rfc822_escape, get_compiler_versions,
+ _find_exe_version, _MAC_OS_X_LD_VERSION,
+ byte_compile, find_packages, spawn, find_executable,
+ _nt_quote_args, get_pypirc_path, generate_pypirc,
+ read_pypirc, resolve_dotted_name)
+
from distutils2 import util
from distutils2.tests import support
from distutils2.tests.support import unittest
+
+PYPIRC = """\
+[distutils]
+index-servers =
+ pypi
+ server1
+
+[pypi]
+username:me
+password:xxxx
+
+[server1]
+repository:http://example.com
+username:tarek
+password:secret
+"""
+
+PYPIRC_OLD = """\
+[server-login]
+username:tarek
+password:secret
+"""
+
+WANTED = """\
+[distutils]
+index-servers =
+ pypi
+
+[pypi]
+username:tarek
+password:xxx
+"""
+
+
class FakePopen(object):
test_class = None
def __init__(self, cmd, shell, stdout, stderr):
@@ -40,6 +78,9 @@ class UtilTestCase(support.EnvironGuard,
def setUp(self):
super(UtilTestCase, self).setUp()
+ self.tmp_dir = self.mkdtemp()
+ self.rc = os.path.join(self.tmp_dir, '.pypirc')
+ os.environ['HOME'] = self.tmp_dir
# saving the environment
self.name = os.name
self.platform = sys.platform
@@ -246,7 +287,7 @@ class UtilTestCase(support.EnvironGuard,
self.assertEqual(res[2], None)
@unittest.skipUnless(hasattr(sys, 'dont_write_bytecode'),
- 'no dont_write_bytecode support')
+ 'sys.dont_write_bytecode not supported')
def test_dont_write_bytecode(self):
# makes sure byte_compile raise a DistutilsError
# if sys.dont_write_bytecode is True
@@ -259,9 +300,9 @@ class UtilTestCase(support.EnvironGuard,
def test_newer(self):
self.assertRaises(DistutilsFileError, util.newer, 'xxx', 'xxx')
- self.newer_f1 = tempfile.NamedTemporaryFile()
+ self.newer_f1 = self.mktempfile()
time.sleep(1)
- self.newer_f2 = tempfile.NamedTemporaryFile()
+ self.newer_f2 = self.mktempfile()
self.assertTrue(util.newer(self.newer_f2.name, self.newer_f1.name))
def test_find_packages(self):
@@ -301,7 +342,17 @@ class UtilTestCase(support.EnvironGuard,
res = find_packages([root], ['pkg1.pkg2'])
self.assertEqual(set(res), set(['pkg1', 'pkg5', 'pkg1.pkg3', 'pkg1.pkg3.pkg6']))
- @unittest.skipUnless(sys.version > '2.6', 'Need Python 2.6 or more')
+ def test_resolve_dotted_name(self):
+ self.assertEqual(UtilTestCase, resolve_dotted_name("distutils2.tests.test_util.UtilTestCase"))
+ self.assertEqual(UtilTestCase.test_resolve_dotted_name,
+ resolve_dotted_name("distutils2.tests.test_util.UtilTestCase.test_resolve_dotted_name"))
+
+ self.assertRaises(ImportError, resolve_dotted_name,
+ "distutils2.tests.test_util.UtilTestCaseNot")
+ self.assertRaises(ImportError, resolve_dotted_name,
+ "distutils2.tests.test_util.UtilTestCase.nonexistent_attribute")
+
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_run_2to3_on_code(self):
content = "print 'test'"
converted_content = "print('test')"
@@ -316,7 +367,7 @@ class UtilTestCase(support.EnvironGuard,
file_handle.close()
self.assertEquals(new_content, converted_content)
- @unittest.skipUnless(sys.version > '2.6', 'Need Python 2.6 or more')
+ @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_run_2to3_on_doctests(self):
# to check if text files containing doctests only get converted.
content = ">>> print 'test'\ntest\n"
@@ -332,6 +383,80 @@ class UtilTestCase(support.EnvironGuard,
file_handle.close()
self.assertEquals(new_content, converted_content)
+ def test_nt_quote_args(self):
+
+ for (args, wanted) in ((['with space', 'nospace'],
+ ['"with space"', 'nospace']),
+ (['nochange', 'nospace'],
+ ['nochange', 'nospace'])):
+ res = _nt_quote_args(args)
+ self.assertEqual(res, wanted)
+
+
+ @unittest.skipUnless(os.name in ('nt', 'posix'),
+ 'runs only under posix or nt')
+ def test_spawn(self):
+ tmpdir = self.mkdtemp()
+
+ # creating something executable
+ # through the shell that returns 1
+ if os.name == 'posix':
+ exe = os.path.join(tmpdir, 'foo.sh')
+ self.write_file(exe, '#!/bin/sh\nexit 1')
+ os.chmod(exe, 0777)
+ else:
+ exe = os.path.join(tmpdir, 'foo.bat')
+ self.write_file(exe, 'exit 1')
+
+ os.chmod(exe, 0777)
+ self.assertRaises(DistutilsExecError, spawn, [exe])
+
+ # now something that works
+ if os.name == 'posix':
+ exe = os.path.join(tmpdir, 'foo.sh')
+ self.write_file(exe, '#!/bin/sh\nexit 0')
+ os.chmod(exe, 0777)
+ else:
+ exe = os.path.join(tmpdir, 'foo.bat')
+ self.write_file(exe, 'exit 0')
+
+ os.chmod(exe, 0777)
+ spawn([exe]) # should work without any error
+
+ def test_server_registration(self):
+ # This test makes sure we know how to:
+ # 1. handle several sections in .pypirc
+ # 2. handle the old format
+
+ # new format
+ self.write_file(self.rc, PYPIRC)
+ config = read_pypirc()
+
+ config = config.items()
+ config.sort()
+ expected = [('password', 'xxxx'), ('realm', 'pypi'),
+ ('repository', 'http://pypi.python.org/pypi'),
+ ('server', 'pypi'), ('username', 'me')]
+ self.assertEqual(config, expected)
+
+ # old format
+ self.write_file(self.rc, PYPIRC_OLD)
+ config = read_pypirc()
+ config = config.items()
+ config.sort()
+ expected = [('password', 'secret'), ('realm', 'pypi'),
+ ('repository', 'http://pypi.python.org/pypi'),
+ ('server', 'server-login'), ('username', 'tarek')]
+ self.assertEqual(config, expected)
+
+ def test_server_empty_registration(self):
+ rc = get_pypirc_path()
+ self.assertTrue(not os.path.exists(rc))
+ generate_pypirc('tarek', 'xxx')
+ self.assertTrue(os.path.exists(rc))
+ content = open(rc).read()
+ self.assertEqual(content, WANTED)
+
def test_suite():
return unittest.makeSuite(UtilTestCase)
diff --git a/src/distutils2/tests/test_version.py b/src/distutils2/tests/test_version.py
index 343b397..1e392f2 100644
--- a/src/distutils2/tests/test_version.py
+++ b/src/distutils2/tests/test_version.py
@@ -188,6 +188,13 @@ class VersionTestCase(unittest.TestCase):
# XXX need to silent the micro version in this case
#assert not VersionPredicate('Ho (<3.0,!=2.6)').match('2.6.3')
+ def test_predicate_name(self):
+ # Test that names are parsed the right way
+
+ self.assertEqual('Hey', VersionPredicate('Hey (<1.1)').name)
+ self.assertEqual('Foo-Bar', VersionPredicate('Foo-Bar (1.1)').name)
+ self.assertEqual('Foo Bar', VersionPredicate('Foo Bar (1.1)').name)
+
def test_is_final(self):
# VersionPredicate knows is a distribution is a final one or not.
final_versions = ('1.0', '1.0.post456')
diff --git a/src/distutils2/util.py b/src/distutils2/util.py
index b9aa3f7..178cb95 100644
--- a/src/distutils2/util.py
+++ b/src/distutils2/util.py
@@ -5,16 +5,20 @@ Miscellaneous utility functions.
__revision__ = "$Id: util.py 77761 2010-01-26 22:46:15Z tarek.ziade $"
-import sys
import os
-import string
+import posixpath
import re
+import string
+import sys
+import shutil
+import tarfile
+import zipfile
from copy import copy
from fnmatch import fnmatchcase
+from ConfigParser import RawConfigParser
from distutils2.errors import (DistutilsPlatformError, DistutilsFileError,
- DistutilsByteCompileError)
-from distutils2.spawn import spawn, find_executable
+ DistutilsByteCompileError, DistutilsExecError)
from distutils2 import log
from distutils2._backport import sysconfig as _sysconfig
@@ -84,8 +88,8 @@ def convert_path(pathname):
raise ValueError("path '%s' cannot end with '/'" % pathname)
paths = pathname.split('/')
- while '.' in paths:
- paths.remove('.')
+ while os.curdir in paths:
+ paths.remove(os.curdir)
if not paths:
return os.curdir
return os.path.join(*paths)
@@ -117,15 +121,6 @@ def change_root(new_root, pathname):
path = path[1:]
return os.path.join(new_root, path)
- elif os.name == 'mac':
- if not os.path.isabs(pathname):
- return os.path.join(new_root, pathname)
- else:
- # Chop off volume name from start of path
- elements = pathname.split(":", 1)
- pathname = ":" + elements[1]
- return os.path.join(new_root, pathname)
-
else:
raise DistutilsPlatformError("nothing known about "
"platform '%s'" % os.name)
@@ -595,7 +590,7 @@ def _package_name(root_path, path):
return path[len(root_path) + 1:].replace(os.sep, '.')
-def find_packages(paths=('.',), exclude=()):
+def find_packages(paths=(os.curdir,), exclude=()):
"""Return a list all Python packages found recursively within
directories 'paths'
@@ -642,6 +637,24 @@ def find_packages(paths=('.',), exclude=()):
return packages
+def resolve_dotted_name(dotted_name):
+ module_name, rest = dotted_name.split('.')[0], dotted_name.split('.')[1:]
+ while len(rest) > 0:
+ try:
+ ret = __import__(module_name)
+ break
+ except ImportError:
+ if rest == []:
+ raise
+ module_name += ('.' + rest[0])
+ rest = rest[1:]
+ while len(rest) > 0:
+ try:
+ ret = getattr(ret, rest.pop(0))
+ except AttributeError:
+ raise ImportError
+ return ret
+
# utility functions for 2to3 support
def run_2to3(files, doctests_only=False, fixer_names=None, options=None,
@@ -688,3 +701,419 @@ class Mixin2to3:
""" Issues a call to util.run_2to3. """
return run_2to3(files, doctests_only, self.fixer_names,
self.options, self.explicit)
+
+
+def splitext(path):
+ """Like os.path.splitext, but take off .tar too"""
+ base, ext = posixpath.splitext(path)
+ if base.lower().endswith('.tar'):
+ ext = base[-4:] + ext
+ base = base[:-4]
+ return base, ext
+
+
+def unzip_file(filename, location, flatten=True):
+ """Unzip the file (zip file located at filename) to the destination
+ location"""
+ if not os.path.exists(location):
+ os.makedirs(location)
+ zipfp = open(filename, 'rb')
+ try:
+ zip = zipfile.ZipFile(zipfp)
+ leading = has_leading_dir(zip.namelist()) and flatten
+ for name in zip.namelist():
+ data = zip.read(name)
+ fn = name
+ if leading:
+ fn = split_leading_dir(name)[1]
+ fn = os.path.join(location, fn)
+ dir = os.path.dirname(fn)
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+ if fn.endswith('/') or fn.endswith('\\'):
+ # A directory
+ if not os.path.exists(fn):
+ os.makedirs(fn)
+ else:
+ fp = open(fn, 'wb')
+ try:
+ fp.write(data)
+ finally:
+ fp.close()
+ finally:
+ zipfp.close()
+
+
+def untar_file(filename, location):
+ """Untar the file (tar file located at filename) to the destination
+ location
+ """
+ if not os.path.exists(location):
+ os.makedirs(location)
+ if filename.lower().endswith('.gz') or filename.lower().endswith('.tgz'):
+ mode = 'r:gz'
+ elif (filename.lower().endswith('.bz2')
+ or filename.lower().endswith('.tbz')):
+ mode = 'r:bz2'
+ elif filename.lower().endswith('.tar'):
+ mode = 'r'
+ else:
+ mode = 'r:*'
+ tar = tarfile.open(filename, mode)
+ try:
+ leading = has_leading_dir([member.name for member in tar.getmembers()])
+ for member in tar.getmembers():
+ fn = member.name
+ if leading:
+ fn = split_leading_dir(fn)[1]
+ path = os.path.join(location, fn)
+ if member.isdir():
+ if not os.path.exists(path):
+ os.makedirs(path)
+ else:
+ try:
+ fp = tar.extractfile(member)
+ except (KeyError, AttributeError), e:
+ # Some corrupt tar files seem to produce this
+ # (specifically bad symlinks)
+ continue
+ if not os.path.exists(os.path.dirname(path)):
+ os.makedirs(os.path.dirname(path))
+ destfp = open(path, 'wb')
+ try:
+ shutil.copyfileobj(fp, destfp)
+ finally:
+ destfp.close()
+ fp.close()
+ finally:
+ tar.close()
+
+
+def has_leading_dir(paths):
+ """Returns true if all the paths have the same leading path name
+ (i.e., everything is in one subdirectory in an archive)"""
+ common_prefix = None
+ for path in paths:
+ prefix, rest = split_leading_dir(path)
+ if not prefix:
+ return False
+ elif common_prefix is None:
+ common_prefix = prefix
+ elif prefix != common_prefix:
+ return False
+ return True
+
+
+def split_leading_dir(path):
+ path = str(path)
+ path = path.lstrip('/').lstrip('\\')
+ if '/' in path and (('\\' in path and path.find('/') < path.find('\\'))
+ or '\\' not in path):
+ return path.split('/', 1)
+ elif '\\' in path:
+ return path.split('\\', 1)
+ else:
+ return path, ''
+
+
+def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None):
+ """Run another program specified as a command list 'cmd' in a new process.
+
+ 'cmd' is just the argument list for the new process, ie.
+ cmd[0] is the program to run and cmd[1:] are the rest of its arguments.
+ There is no way to run a program with a name different from that of its
+ executable.
+
+ If 'search_path' is true (the default), the system's executable
+ search path will be used to find the program; otherwise, cmd[0]
+ must be the exact path to the executable. If 'dry_run' is true,
+ the command will not actually be run.
+
+ If 'env' is given, it's a environment dictionary used for the execution
+ environment.
+
+ Raise DistutilsExecError if running the program fails in any way; just
+ return on success.
+ """
+ if os.name == 'posix':
+ _spawn_posix(cmd, search_path, dry_run=dry_run, env=env)
+ elif os.name == 'nt':
+ _spawn_nt(cmd, search_path, dry_run=dry_run, env=env)
+ elif os.name == 'os2':
+ _spawn_os2(cmd, search_path, dry_run=dry_run, env=env)
+ else:
+ raise DistutilsPlatformError(
+ "don't know how to spawn programs on platform '%s'" % os.name)
+
+
+def _nt_quote_args(args):
+ """Quote command-line arguments for DOS/Windows conventions.
+
+ Just wraps every argument which contains blanks in double quotes, and
+ returns a new argument list.
+ """
+ # XXX this doesn't seem very robust to me -- but if the Windows guys
+ # say it'll work, I guess I'll have to accept it. (What if an arg
+ # contains quotes? What other magic characters, other than spaces,
+ # have to be escaped? Is there an escaping mechanism other than
+ # quoting?)
+ for i, arg in enumerate(args):
+ if ' ' in arg:
+ args[i] = '"%s"' % arg
+ return args
+
+
+def _spawn_nt(cmd, search_path=1, verbose=0, dry_run=0, env=None):
+ executable = cmd[0]
+ cmd = _nt_quote_args(cmd)
+ if search_path:
+ # either we find one or it stays the same
+ executable = find_executable(executable) or executable
+ log.info(' '.join([executable] + cmd[1:]))
+ if not dry_run:
+ # spawn for NT requires a full path to the .exe
+ try:
+ if env is None:
+ rc = os.spawnv(os.P_WAIT, executable, cmd)
+ else:
+ rc = os.spawnve(os.P_WAIT, executable, cmd, env)
+
+ except OSError, exc:
+ # this seems to happen when the command isn't found
+ raise DistutilsExecError(
+ "command '%s' failed: %s" % (cmd[0], exc[-1]))
+ if rc != 0:
+ # and this reflects the command running but failing
+ raise DistutilsExecError(
+ "command '%s' failed with exit status %d" % (cmd[0], rc))
+
+
+def _spawn_os2(cmd, search_path=1, verbose=0, dry_run=0, env=None):
+ executable = cmd[0]
+ if search_path:
+ # either we find one or it stays the same
+ executable = find_executable(executable) or executable
+ log.info(' '.join([executable] + cmd[1:]))
+ if not dry_run:
+ # spawnv for OS/2 EMX requires a full path to the .exe
+ try:
+ if env is None:
+ rc = os.spawnv(os.P_WAIT, executable, cmd)
+ else:
+ rc = os.spawnve(os.P_WAIT, executable, cmd, env)
+
+ except OSError, exc:
+ # this seems to happen when the command isn't found
+ raise DistutilsExecError(
+ "command '%s' failed: %s" % (cmd[0], exc[-1]))
+ if rc != 0:
+ # and this reflects the command running but failing
+ log.debug("command '%s' failed with exit status %d" % (cmd[0], rc))
+ raise DistutilsExecError(
+ "command '%s' failed with exit status %d" % (cmd[0], rc))
+
+
+def _spawn_posix(cmd, search_path=1, verbose=0, dry_run=0, env=None):
+ log.info(' '.join(cmd))
+ if dry_run:
+ return
+
+ if env is None:
+ exec_fn = search_path and os.execvp or os.execv
+ else:
+ exec_fn = search_path and os.execvpe or os.execve
+
+ pid = os.fork()
+
+ if pid == 0: # in the child
+ try:
+ if env is None:
+ exec_fn(cmd[0], cmd)
+ else:
+ exec_fn(cmd[0], cmd, env)
+ except OSError, e:
+ sys.stderr.write("unable to execute %s: %s\n" %
+ (cmd[0], e.strerror))
+ os._exit(1)
+
+ sys.stderr.write("unable to execute %s for unknown reasons" % cmd[0])
+ os._exit(1)
+ else: # in the parent
+ # Loop until the child either exits or is terminated by a signal
+ # (ie. keep waiting if it's merely stopped)
+ while 1:
+ try:
+ pid, status = os.waitpid(pid, 0)
+ except OSError, exc:
+ import errno
+ if exc.errno == errno.EINTR:
+ continue
+ raise DistutilsExecError(
+ "command '%s' failed: %s" % (cmd[0], exc[-1]))
+ if os.WIFSIGNALED(status):
+ raise DistutilsExecError(
+ "command '%s' terminated by signal %d" % \
+ (cmd[0], os.WTERMSIG(status)))
+
+ elif os.WIFEXITED(status):
+ exit_status = os.WEXITSTATUS(status)
+ if exit_status == 0:
+ return # hey, it succeeded!
+ else:
+ raise DistutilsExecError(
+ "command '%s' failed with exit status %d" % \
+ (cmd[0], exit_status))
+
+ elif os.WIFSTOPPED(status):
+ continue
+
+ else:
+ raise DistutilsExecError(
+ "unknown error executing '%s': termination status %d" % \
+ (cmd[0], status))
+
+
+def find_executable(executable, path=None):
+ """Tries to find 'executable' in the directories listed in 'path'.
+
+ A string listing directories separated by 'os.pathsep'; defaults to
+ os.environ['PATH']. Returns the complete filename or None if not found.
+ """
+ if path is None:
+ path = os.environ['PATH']
+ paths = path.split(os.pathsep)
+ base, ext = os.path.splitext(executable)
+
+ if (sys.platform == 'win32' or os.name == 'os2') and (ext != '.exe'):
+ executable = executable + '.exe'
+
+ if not os.path.isfile(executable):
+ for p in paths:
+ f = os.path.join(p, executable)
+ if os.path.isfile(f):
+ # the file exists, we have a shot at spawn working
+ return f
+ return None
+ else:
+ return executable
+
+
+DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi'
+DEFAULT_REALM = 'pypi'
+DEFAULT_PYPIRC = """\
+[distutils]
+index-servers =
+ pypi
+
+[pypi]
+username:%s
+password:%s
+"""
+
+def get_pypirc_path():
+ """Returns rc file path."""
+ return os.path.join(os.path.expanduser('~'), '.pypirc')
+
+
+def generate_pypirc(username, password):
+ """Creates a default .pypirc file."""
+ rc = get_pypirc_path()
+ f = open(rc, 'w')
+ try:
+ f.write(DEFAULT_PYPIRC % (username, password))
+ finally:
+ f.close()
+ try:
+ os.chmod(rc, 0600)
+ except OSError:
+ # should do something better here
+ pass
+
+
+def read_pypirc(repository=DEFAULT_REPOSITORY, realm=DEFAULT_REALM):
+ """Reads the .pypirc file."""
+ rc = get_pypirc_path()
+ if os.path.exists(rc):
+ config = RawConfigParser()
+ config.read(rc)
+ sections = config.sections()
+ if 'distutils' in sections:
+ # let's get the list of servers
+ index_servers = config.get('distutils', 'index-servers')
+ _servers = [server.strip() for server in
+ index_servers.split('\n')
+ if server.strip() != '']
+ if _servers == []:
+ # nothing set, let's try to get the default pypi
+ if 'pypi' in sections:
+ _servers = ['pypi']
+ else:
+ # the file is not properly defined, returning
+ # an empty dict
+ return {}
+ for server in _servers:
+ current = {'server': server}
+ current['username'] = config.get(server, 'username')
+
+ # optional params
+ for key, default in (('repository',
+ DEFAULT_REPOSITORY),
+ ('realm', DEFAULT_REALM),
+ ('password', None)):
+ if config.has_option(server, key):
+ current[key] = config.get(server, key)
+ else:
+ current[key] = default
+ if (current['server'] == repository or
+ current['repository'] == repository):
+ return current
+ elif 'server-login' in sections:
+ # old format
+ server = 'server-login'
+ if config.has_option(server, 'repository'):
+ repository = config.get(server, 'repository')
+ else:
+ repository = DEFAULT_REPOSITORY
+
+ return {'username': config.get(server, 'username'),
+ 'password': config.get(server, 'password'),
+ 'repository': repository,
+ 'server': server,
+ 'realm': DEFAULT_REALM}
+
+ return {}
+
+
+def metadata_to_dict(meta):
+ """XXX might want to move it to the Metadata class."""
+ data = {
+ 'metadata_version' : meta.version,
+ 'name': meta['Name'],
+ 'version': meta['Version'],
+ 'summary': meta['Summary'],
+ 'home_page': meta['Home-page'],
+ 'author': meta['Author'],
+ 'author_email': meta['Author-email'],
+ 'license': meta['License'],
+ 'description': meta['Description'],
+ 'keywords': meta['Keywords'],
+ 'platform': meta['Platform'],
+ 'classifier': meta['Classifier'],
+ 'download_url': meta['Download-URL'],
+ }
+
+ if meta.version == '1.2':
+ data['requires_dist'] = meta['Requires-Dist']
+ data['requires_python'] = meta['Requires-Python']
+ data['requires_external'] = meta['Requires-External']
+ data['provides_dist'] = meta['Provides-Dist']
+ data['obsoletes_dist'] = meta['Obsoletes-Dist']
+ data['project_url'] = [','.join(url) for url in
+ meta['Project-URL']]
+
+ elif meta.version == '1.1':
+ data['provides'] = meta['Provides']
+ data['requires'] = meta['Requires']
+ data['obsoletes'] = meta['Obsoletes']
+
+ return data
diff --git a/src/distutils2/version.py b/src/distutils2/version.py
index e61c60e..64c33f7 100644
--- a/src/distutils2/version.py
+++ b/src/distutils2/version.py
@@ -321,7 +321,7 @@ def suggest_normalized_version(s):
return None
-_PREDICATE = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)")
+_PREDICATE = re.compile(r"(?i)^\s*([a-z_][\sa-zA-Z_-]*(?:\.[a-z_]\w*)*)(.*)")
_VERSIONS = re.compile(r"^\s*\((.*)\)\s*$")
_PLAIN_VERSIONS = re.compile(r"^\s*(.*)\s*$")
_SPLIT_CMP = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$")
@@ -349,12 +349,14 @@ class VersionPredicate(object):
}
def __init__(self, predicate):
+ self._string = predicate
predicate = predicate.strip()
match = _PREDICATE.match(predicate)
if match is None:
raise ValueError('Bad predicate "%s"' % predicate)
- self.name, predicates = match.groups()
+ name, predicates = match.groups()
+ self.name = name.strip()
predicates = predicates.strip()
predicates = _VERSIONS.match(predicates)
if predicates is not None:
@@ -373,6 +375,9 @@ class VersionPredicate(object):
return False
return True
+ def __repr__(self):
+ return self._string
+
class _Versions(VersionPredicate):
def __init__(self, predicate):
@@ -417,3 +422,12 @@ def is_valid_version(predicate):
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
index 6c2ab41..d6678b4 100755
--- a/src/runtests-cov.py
+++ b/src/runtests-cov.py
@@ -5,9 +5,20 @@ The tests for distutils2 are defined in the distutils2.tests package.
"""
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
we want to completely skip `module`.
@@ -16,16 +27,17 @@ def ignore_prefixes(module):
# distributions, such a Ubuntu, really like to build link farm in
# /usr/lib in order to save a few bytes on the disk.
dirnames = [dirname(module.__file__)]
-
+
pymod = module.__file__.rstrip("c")
if islink(pymod):
dirnames.append(dirname(realpath(pymod)))
return dirnames
+
def parse_opts():
parser = OptionParser(usage="%prog [OPTIONS]",
description="run the distutils2 unittests")
-
+
parser.add_option("-q", "--quiet", help="do not print verbose messages",
action="store_true", default=False)
parser.add_option("-c", "--coverage", action="store_true", default=False,
@@ -36,15 +48,23 @@ def parse_opts():
default=False,
help=("Show line numbers of statements in each module "
"that weren't executed."))
-
+
opts, args = parser.parse_args()
return opts, args
+
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)
@@ -63,7 +83,7 @@ def coverage_report(opts):
# 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():
@@ -71,11 +91,8 @@ def test_main():
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:
@@ -89,6 +106,7 @@ def test_main():
return ret
+
def run_tests(verbose):
import distutils2.tests
from distutils2.tests import run_unittest, reap_children, TestFailed
@@ -108,12 +126,11 @@ def run_tests(verbose):
finally:
reap_children()
+
if __name__ == "__main__":
try:
from distutils2.tests.support import unittest
except ImportError:
sys.stderr.write('Error: You have to install unittest2')
sys.exit(1)
-
sys.exit(test_main())
-
diff --git a/src/runtests.py b/src/runtests.py
index b9e2477..415d37a 100644
--- a/src/runtests.py
+++ b/src/runtests.py
@@ -1,9 +1,12 @@
+#!/usr/bin/env python
"""Tests for distutils2.
The tests for distutils2 are defined in the distutils2.tests package.
"""
+
import sys
+
def test_main():
import distutils2.tests
from distutils2.tests import run_unittest, reap_children, TestFailed
@@ -23,12 +26,11 @@ def test_main():
finally:
reap_children()
+
if __name__ == "__main__":
try:
from distutils2.tests.support import unittest
except ImportError:
sys.stderr.write('Error: You have to install unittest2')
sys.exit(1)
-
sys.exit(test_main())
-
diff --git a/src/setup.cfg b/src/setup.cfg
new file mode 100644
index 0000000..169aaf6
--- /dev/null
+++ b/src/setup.cfg
@@ -0,0 +1,3 @@
+[build_ext]
+# needed so that tests work without mucking with sys.path
+inplace = 1
diff --git a/src/setup.py b/src/setup.py
index 5345b37..1233b21 100644
--- a/src/setup.py
+++ b/src/setup.py
@@ -168,29 +168,25 @@ def prepare_hashlib_extensions():
# The _hashlib module wraps optimized implementations
# of hash functions from the OpenSSL library.
- exts.append(Extension('_hashlib', ['_hashopenssl.c'],
+ exts.append(Extension('distutils2._backport._hashlib',
+ ['distutils2/_backport/_hashopenssl.c'],
include_dirs = [ssl_inc_dir],
library_dirs = [os.path.dirname(ssl_lib)],
libraries = oslibs[os.name]))
else:
- exts.append(Extension('_sha', ['shamodule.c']) )
- exts.append(Extension('_md5',
- sources=['md5module.c', 'md5.c'],
- depends=['md5.h']) )
+ exts.append(Extension('distutils2._backport._sha',
+ ['distutils2/_backport/shamodule.c']))
+ exts.append(Extension('distutils2._backport._md5',
+ sources=['distutils2/_backport/md5module.c',
+ 'distutils2/_backport/md5.c'],
+ depends=['distutils2/_backport/md5.h']) )
if (not ssl_lib or openssl_ver < 0x00908000):
# OpenSSL doesn't do these until 0.9.8 so we'll bring our own
- exts.append(Extension('_sha256', ['sha256module.c']))
- exts.append(Extension('_sha512', ['sha512module.c']))
-
- def prepend_modules(filename):
- return os.path.join('Modules', filename)
-
- # all the C code is in the Modules subdirectory, prepend the path
- for ext in exts:
- ext.sources = [prepend_modules(fn) for fn in ext.sources]
- if hasattr(ext, 'depends') and ext.depends is not None:
- ext.depends = [prepend_modules(fn) for fn in ext.depends]
+ exts.append(Extension('distutils2._backport._sha256',
+ ['distutils2/_backport/sha256module.c']))
+ exts.append(Extension('distutils2._backport._sha512',
+ ['distutils2/_backport/sha512module.c']))
return exts
diff --git a/src/tests.sh b/src/tests.sh
index aa339a3..8e6295d 100755
--- a/src/tests.sh
+++ b/src/tests.sh
@@ -1,40 +1,40 @@
#!/bin/sh
echo -n "Running tests for Python 2.4... "
-rm -rf _hashlib.so
-python2.4 setup.py build_ext -i -q 2> /dev/null > /dev/null
+rm -f distutils2/_backport/_hashlib.so
+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... "
-rm -rf _hashlib.so
-python2.5 setup.py build_ext -i -q 2> /dev/null > /dev/null
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 -bb -3 runtests.py -q 2> /dev/null
+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!