Commit 58217442 authored by PJ Eby's avatar PJ Eby

Added "AvailableDistributions" class that finds and indexes usable

distributions; this replaces the previous "iter_distributions()" API.
Added basic platform support to Distribution and AvailableDistributions so
that platform-independent distros as well as local platform-compatible
distros are acceptable.  The actual platform scheme is currently delegated
to distutils.util.get_platform(), but needs to be replaced with a better
scheme of some kind, especially for OS X.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4041004
parent ac8607b2
......@@ -15,13 +15,30 @@ method.
"""
__all__ = [
'register_loader_type', 'get_provider', 'IResourceProvider',
'ResourceManager', 'iter_distributions', 'require', 'resource_string',
'ResourceManager', 'AvailableDistributions', 'require', 'resource_string',
'resource_stream', 'resource_filename', 'set_extraction_path',
'cleanup_resources', 'parse_requirements', 'parse_version',
'compatible_platforms', 'get_platform',
'Distribution', # 'glob_resources'
]
import sys, os, zipimport, time, re
def _sort_dists(dists):
tmp = [(dist.version,dist) for dist in dists]
tmp.sort()
tmp.reverse()
dists[:] = [d for v,d in tmp]
_provider_factories = {}
def register_loader_type(loader_type, provider_factory):
......@@ -39,6 +56,30 @@ def get_provider(moduleName):
loader = getattr(module, '__loader__', None)
return _find_adapter(_provider_factories, loader)(module)
def get_platform():
"""Return this platform's string for platform-specific distributions
XXX Currently this is the same as ``distutils.util.get_platform()``, but it
needs some hacks for Linux and Mac OS X.
"""
from distutils.util import get_platform
return get_platform()
def compatible_platforms(provided,required):
"""Can code for the `provided` platform run on the `required` platform?
Returns true if either platform is ``None``, or the platforms are equal.
XXX Needs compatibility checks for Linux and Mac OS X.
"""
if provided is None or required is None or provided==required:
return True # easy case
# XXX all the tricky cases go here
return False
class IResourceProvider:
"""An object that provides access to package resources"""
......@@ -80,46 +121,128 @@ class IResourceProvider:
class ResourceManager:
"""Manage resource extraction and packages"""
class AvailableDistributions(object):
"""Searchable snapshot of distributions on a search path"""
extraction_path = None
def __init__(self, search_path=None, platform=get_platform()):
"""Snapshot distributions available on a search path
def __init__(self):
self.cached_files = []
`search_path` should be a sequence of ``sys.path`` items. If not
supplied, ``sys.path`` is used.
def resource_exists(self, package_name, resource_name):
"""Does the named resource exist in the named package?"""
return get_provider(package_name).has_resource(self, resource_name)
The `platform` is an optional string specifying the name of the
platform that platform-specific distributions must be compatible
with. If not specified, it defaults to the current platform
(as defined by the result of ``get_platform()`` when ``pkg_resources``
was first imported.)
def resource_filename(self, package_name, resource_name):
"""Return a true filesystem path for specified resource"""
return get_provider(package_name).get_resource_filename(self,resource_name)
You may explicitly set `platform` to ``None`` if you wish to map *all*
distributions, not just those compatible with the running platform.
"""
def resource_stream(self, package_name, resource_name):
"""Return a readable file-like object for specified resource"""
return get_provider(package_name).get_resource_stream(self,resource_name)
self._distmap = {}
self._cache = {}
self.scan(search_path,platform)
def resource_string(self, package_name, resource_name):
"""Return specified resource as a string"""
return get_provider(package_name).get_resource_string(self,resource_name)
def __iter__(self):
"""Iterate over distribution keys"""
return iter(self._distmap.keys())
def __contains__(self,name):
"""Has a distribution named `name` ever been added to this map?"""
return name.lower() in self._distmap
def __len__(self):
return len(self._distmap)
def get(self,key,default=None):
"""Return ``self[key]`` if `key` in self, otherwise return `default`"""
if key in self:
return self[key]
else:
return default
def scan(self, search_path=None, platform=get_platform()):
"""Scan `search_path` for distributions usable on `platform`
Any distributions found are added to the distribution map.
`search_path` should be a sequence of ``sys.path`` items. If not
supplied, ``sys.path`` is used. `platform` is an optional string
specifying the name of the platform that platform-specific
distributions must be compatible with. If unspecified, it defaults to
the current platform.
You may explicitly set `platform` to ``None`` if you wish to map *all*
distributions, not just those compatible with the running platform.
"""
if search_path is None:
search_path = sys.path
add = self.add
for item in search_path:
source = get_dist_source(item)
for dist in source.iter_distributions(requirement):
if compatible_platforms(dist.platform, platform):
add(dist)
def __getitem__(self,key):
"""Return a newest-to-oldest list of distributions for the given key
The returned list may be modified in-place, e.g. for narrowing down
usable distributions.
"""
try:
return self._cache[key]
except KeyError:
key = key.lower()
if key not in self._distmap:
raise
if key not in self._cache:
dists = self._cache[key] = self._distmap[key]
_sort_dists(dists)
return self._cache[key]
def add(self,dist):
"""Add `dist` to the distribution map"""
self._distmap.setdefault(dist.key,[]).append(dist)
if dist.key in self._cache:
_sort_dists(self._cache[dist.key])
def remove(self,dist):
"""Remove `dist` from the distribution map"""
self._distmap[dist.key].remove(dist)
class ResourceManager:
"""Manage resource extraction and packages"""
extraction_path = None
def __init__(self):
self.cached_files = []
def resource_exists(self, package_name, resource_name):
"""Does the named resource exist in the named package?"""
return get_provider(package_name).has_resource(self, resource_name)
def resource_filename(self, package_name, resource_name):
"""Return a true filesystem path for specified resource"""
return get_provider(package_name).get_resource_filename(
self,resource_name
)
def resource_stream(self, package_name, resource_name):
"""Return a readable file-like object for specified resource"""
return get_provider(package_name).get_resource_stream(
self, resource_name
)
def resource_string(self, package_name, resource_name):
"""Return specified resource as a string"""
return get_provider(package_name).get_resource_string(
self, resource_name
)
def get_cache_path(self, archive_name, names=()):
"""Return absolute location in cache for `archive_name` and `names`
......@@ -203,47 +326,6 @@ class ResourceManager:
def iter_distributions(requirement=None, path=None):
"""Iterate over distributions in `path` matching `requirement`
The `path` is a sequence of ``sys.path`` items. If not supplied,
``sys.path`` is used.
The `requirement` is an optional string specifying the name of the
desired distribution.
"""
if path is None:
path = sys.path
if requirement is not None:
requirements = list(parse_requirements(requirement))
try:
requirement, = requirements
except ValueError:
raise ValueError("Must specify exactly one requirement")
for item in path:
source = get_dist_source(item)
for dist in source.iter_distributions(requirement):
yield dist
def require(*requirements):
"""Ensure that distributions matching `requirements` are on ``sys.path``
......@@ -251,14 +333,8 @@ def require(*requirements):
thereof, specifying the distributions and versions required.
XXX THIS IS DRAFT CODE FOR DESIGN PURPOSES ONLY RIGHT NOW
"""
all_distros = {}
all_distros = AvailableDistributions()
installed = {}
for dist in iter_distributions():
key = dist.name.lower()
all_distros.setdefault(key,[]).append(dist)
if dist.installed():
installed[key] = dist # XXX what if more than one on path?
all_requirements = {}
def _require(requirements,source=None):
......@@ -282,9 +358,15 @@ def require(*requirements):
pass
# find "best" distro for key and install it
# after _require()-ing its requirements
_require(requirements)
class DefaultProvider:
"""Provides access to package resources in the filesystem"""
......@@ -544,7 +626,7 @@ def parse_version(s):
dropped, but dashes are retained. Trailing zeros between alpha segments
or dashes are suppressed, so that e.g. 2.4.0 is considered the same as 2.4.
Alphanumeric parts are lower-cased.
The algorithm assumes that strings like '-' and any alpha string > "final"
represents a "patch level". So, "2.4-1" is assumed to be a branch or patch
of "2.4", and therefore "2.4.1" is considered newer than "2.4-1".
......@@ -556,7 +638,7 @@ def parse_version(s):
Finally, to handle miscellaneous cases, the strings "pre", "preview", and
"rc" are treated as if they were "c", i.e. as though they were release
candidates, and therefore are not as new as a version string that does not
contain them.
contain them.
"""
parts = []
for part in _parse_version_parts(s.lower()):
......@@ -574,17 +656,18 @@ def parse_version(s):
class Distribution(object):
"""Wrap an actual or potential sys.path entry w/metadata"""
def __init__(self,
path_str, metadata=None, name=None, version=None,
py_version=sys.version[:3]
py_version=sys.version[:3], platform=None
):
if name:
self.name = name
self.name = name.replace('_','-')
if version:
self.version = version
self.version = version.replace('_','-')
self.py_version = py_version
self.platform = platform
self.path = path_str
self.normalized_path = os.path.normpath(os.path.normcase(path_str))
......@@ -612,7 +695,6 @@ class Distribution(object):
#@classmethod
def from_filename(cls,filename,metadata=None):
name,version,py_version,platform = [None]*4
......@@ -623,11 +705,9 @@ class Distribution(object):
name,version,py_version,platform = match.group(
'name','ver','pyver','plat'
)
name = name.replace('_','-')
if version and '_' in version:
version = version.replace('_','-')
return cls(
filename,metadata,name=name,version=version,py_version=py_version
filename, metadata, name=name, version=version,
py_version=py_version, platform=platform
)
from_filename = classmethod(from_filename)
......@@ -653,7 +733,9 @@ class Distribution(object):
return pv
parsed_version = property(parsed_version)
def parse_requirements(strs):
"""Yield ``Requirement`` objects for each specification in `strs`
......@@ -695,7 +777,6 @@ def parse_requirements(strs):
def _get_mro(cls):
"""Get an mro for a type or classic class"""
if not isinstance(cls,type):
......
......@@ -10,7 +10,7 @@ from distutils.dir_util import create_tree, remove_tree, ensure_relative,mkpath
from distutils.sysconfig import get_python_version
from distutils.errors import *
from distutils import log
from pkg_resources import parse_requirements
from pkg_resources import parse_requirements, get_platform
class bdist_egg(Command):
......@@ -75,9 +75,9 @@ class bdist_egg(Command):
if self.bdist_dir is None:
bdist_base = self.get_finalized_command('bdist').bdist_base
self.bdist_dir = os.path.join(bdist_base, 'egg')
self.set_undefined_options('bdist',
('dist_dir', 'dist_dir'),
('plat_name', 'plat_name'))
if self.plat_name is None:
self.plat_name = get_platform()
self.set_undefined_options('bdist',('dist_dir', 'dist_dir'))
def write_stub(self, resource, pyfile):
......
......@@ -4,21 +4,53 @@ import pkg_resources, sys
class DistroTests(TestCase):
def testEmptyiter(self):
def testCollection(self):
# empty path should produce no distributions
self.assertEqual(list(iter_distributions(path=[])), [])
ad = AvailableDistributions([])
self.assertEqual(list(ad), [])
self.assertEqual(len(ad),0)
self.assertEqual(ad.get('FooPkg'),None)
self.failIf('FooPkg' in ad)
ad.add(Distribution.from_filename("FooPkg-1.3_1.egg"))
ad.add(Distribution.from_filename("FooPkg-1.4-py2.4-win32.egg"))
ad.add(Distribution.from_filename("FooPkg-1.2-py2.4.egg"))
# Name is in there now
self.failUnless('FooPkg' in ad)
# But only 1 package
self.assertEqual(list(ad), ['foopkg'])
self.assertEqual(len(ad),1)
# Distributions sort by version
self.assertEqual(
[dist.version for dist in ad['FooPkg']], ['1.4','1.3-1','1.2']
)
# Removing a distribution leaves sequence alone
ad.remove(ad['FooPkg'][1])
self.assertEqual(
[dist.version for dist in ad.get('FooPkg')], ['1.4','1.2']
)
# And inserting adds them in order
ad.add(Distribution.from_filename("FooPkg-1.9.egg"))
self.assertEqual(
[dist.version for dist in ad['FooPkg']], ['1.9','1.4','1.2']
)
def checkFooPkg(self,d):
self.assertEqual(d.name, "FooPkg")
self.assertEqual(d.key, "foopkg")
self.assertEqual(d.version, "1.3-1")
self.assertEqual(d.py_version, "2.4")
self.assertEqual(d.platform, "win32")
self.assertEqual(d.parsed_version, parse_version("1.3-1"))
def testDistroBasics(self):
d = Distribution(
"/some/path",
name="FooPkg",version="1.3-1",py_version="2.4"
name="FooPkg",version="1.3-1",py_version="2.4",platform="win32"
)
self.checkFooPkg(d)
self.failUnless(d.installed_on(["/some/path"]))
......@@ -26,11 +58,20 @@ class DistroTests(TestCase):
d = Distribution("/some/path")
self.assertEqual(d.py_version, sys.version[:3])
self.assertEqual(d.platform, None)
def testDistroParse(self):
d = Distribution.from_filename("FooPkg-1.3_1-py2.4-win32.egg")
self.checkFooPkg(d)
......@@ -107,7 +148,7 @@ class ParseTests(TestCase):
0.79.9999+0.80.0pre2-3 0.79.9999+0.80.0pre2-2
0.77.2-1 0.77.1-1 0.77.0-1
""".split()
for p,v1 in enumerate(torture):
for v2 in torture[p+1:]:
c(v2,v1)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment