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. ...@@ -15,13 +15,30 @@ method.
""" """
__all__ = [ __all__ = [
'register_loader_type', 'get_provider', 'IResourceProvider', 'register_loader_type', 'get_provider', 'IResourceProvider',
'ResourceManager', 'iter_distributions', 'require', 'resource_string', 'ResourceManager', 'AvailableDistributions', 'require', 'resource_string',
'resource_stream', 'resource_filename', 'set_extraction_path', 'resource_stream', 'resource_filename', 'set_extraction_path',
'cleanup_resources', 'parse_requirements', 'parse_version', 'cleanup_resources', 'parse_requirements', 'parse_version',
'compatible_platforms', 'get_platform',
'Distribution', # 'glob_resources' 'Distribution', # 'glob_resources'
] ]
import sys, os, zipimport, time, re 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 = {} _provider_factories = {}
def register_loader_type(loader_type, provider_factory): def register_loader_type(loader_type, provider_factory):
...@@ -39,6 +56,30 @@ def get_provider(moduleName): ...@@ -39,6 +56,30 @@ def get_provider(moduleName):
loader = getattr(module, '__loader__', None) loader = getattr(module, '__loader__', None)
return _find_adapter(_provider_factories, loader)(module) 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: class IResourceProvider:
"""An object that provides access to package resources""" """An object that provides access to package resources"""
...@@ -80,46 +121,128 @@ class IResourceProvider: ...@@ -80,46 +121,128 @@ class IResourceProvider:
class ResourceManager: class AvailableDistributions(object):
"""Manage resource extraction and packages""" """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): `search_path` should be a sequence of ``sys.path`` items. If not
self.cached_files = [] supplied, ``sys.path`` is used.
def resource_exists(self, package_name, resource_name): The `platform` is an optional string specifying the name of the
"""Does the named resource exist in the named package?""" platform that platform-specific distributions must be compatible
return get_provider(package_name).has_resource(self, resource_name) 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): You may explicitly set `platform` to ``None`` if you wish to map *all*
"""Return a true filesystem path for specified resource""" distributions, not just those compatible with the running platform.
return get_provider(package_name).get_resource_filename(self,resource_name) """
def resource_stream(self, package_name, resource_name): self._distmap = {}
"""Return a readable file-like object for specified resource""" self._cache = {}
return get_provider(package_name).get_resource_stream(self,resource_name) self.scan(search_path,platform)
def resource_string(self, package_name, resource_name): def __iter__(self):
"""Return specified resource as a string""" """Iterate over distribution keys"""
return get_provider(package_name).get_resource_string(self,resource_name) 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=()): def get_cache_path(self, archive_name, names=()):
"""Return absolute location in cache for `archive_name` and `names` """Return absolute location in cache for `archive_name` and `names`
...@@ -203,47 +326,6 @@ class ResourceManager: ...@@ -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): def require(*requirements):
"""Ensure that distributions matching `requirements` are on ``sys.path`` """Ensure that distributions matching `requirements` are on ``sys.path``
...@@ -251,14 +333,8 @@ def require(*requirements): ...@@ -251,14 +333,8 @@ def require(*requirements):
thereof, specifying the distributions and versions required. thereof, specifying the distributions and versions required.
XXX THIS IS DRAFT CODE FOR DESIGN PURPOSES ONLY RIGHT NOW XXX THIS IS DRAFT CODE FOR DESIGN PURPOSES ONLY RIGHT NOW
""" """
all_distros = {} all_distros = AvailableDistributions()
installed = {} 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 = {} all_requirements = {}
def _require(requirements,source=None): def _require(requirements,source=None):
...@@ -285,6 +361,12 @@ def require(*requirements): ...@@ -285,6 +361,12 @@ def require(*requirements):
_require(requirements) _require(requirements)
class DefaultProvider: class DefaultProvider:
"""Provides access to package resources in the filesystem""" """Provides access to package resources in the filesystem"""
...@@ -577,14 +659,15 @@ class Distribution(object): ...@@ -577,14 +659,15 @@ class Distribution(object):
def __init__(self, def __init__(self,
path_str, metadata=None, name=None, version=None, path_str, metadata=None, name=None, version=None,
py_version=sys.version[:3] py_version=sys.version[:3], platform=None
): ):
if name: if name:
self.name = name self.name = name.replace('_','-')
if version: if version:
self.version = version self.version = version.replace('_','-')
self.py_version = py_version self.py_version = py_version
self.platform = platform
self.path = path_str self.path = path_str
self.normalized_path = os.path.normpath(os.path.normcase(path_str)) self.normalized_path = os.path.normpath(os.path.normcase(path_str))
...@@ -612,7 +695,6 @@ class Distribution(object): ...@@ -612,7 +695,6 @@ class Distribution(object):
#@classmethod #@classmethod
def from_filename(cls,filename,metadata=None): def from_filename(cls,filename,metadata=None):
name,version,py_version,platform = [None]*4 name,version,py_version,platform = [None]*4
...@@ -623,11 +705,9 @@ class Distribution(object): ...@@ -623,11 +705,9 @@ class Distribution(object):
name,version,py_version,platform = match.group( name,version,py_version,platform = match.group(
'name','ver','pyver','plat' 'name','ver','pyver','plat'
) )
name = name.replace('_','-')
if version and '_' in version:
version = version.replace('_','-')
return cls( 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) from_filename = classmethod(from_filename)
...@@ -654,6 +734,8 @@ class Distribution(object): ...@@ -654,6 +734,8 @@ class Distribution(object):
parsed_version = property(parsed_version) parsed_version = property(parsed_version)
def parse_requirements(strs): def parse_requirements(strs):
"""Yield ``Requirement`` objects for each specification in `strs` """Yield ``Requirement`` objects for each specification in `strs`
...@@ -695,7 +777,6 @@ def parse_requirements(strs): ...@@ -695,7 +777,6 @@ def parse_requirements(strs):
def _get_mro(cls): def _get_mro(cls):
"""Get an mro for a type or classic class""" """Get an mro for a type or classic class"""
if not isinstance(cls,type): if not isinstance(cls,type):
......
...@@ -10,7 +10,7 @@ from distutils.dir_util import create_tree, remove_tree, ensure_relative,mkpath ...@@ -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.sysconfig import get_python_version
from distutils.errors import * from distutils.errors import *
from distutils import log from distutils import log
from pkg_resources import parse_requirements from pkg_resources import parse_requirements, get_platform
class bdist_egg(Command): class bdist_egg(Command):
...@@ -75,9 +75,9 @@ class bdist_egg(Command): ...@@ -75,9 +75,9 @@ class bdist_egg(Command):
if self.bdist_dir is None: if self.bdist_dir is None:
bdist_base = self.get_finalized_command('bdist').bdist_base bdist_base = self.get_finalized_command('bdist').bdist_base
self.bdist_dir = os.path.join(bdist_base, 'egg') self.bdist_dir = os.path.join(bdist_base, 'egg')
self.set_undefined_options('bdist', if self.plat_name is None:
('dist_dir', 'dist_dir'), self.plat_name = get_platform()
('plat_name', 'plat_name')) self.set_undefined_options('bdist',('dist_dir', 'dist_dir'))
def write_stub(self, resource, pyfile): def write_stub(self, resource, pyfile):
......
...@@ -4,21 +4,53 @@ import pkg_resources, sys ...@@ -4,21 +4,53 @@ import pkg_resources, sys
class DistroTests(TestCase): class DistroTests(TestCase):
def testEmptyiter(self): def testCollection(self):
# empty path should produce no distributions # 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): def checkFooPkg(self,d):
self.assertEqual(d.name, "FooPkg") self.assertEqual(d.name, "FooPkg")
self.assertEqual(d.key, "foopkg") self.assertEqual(d.key, "foopkg")
self.assertEqual(d.version, "1.3-1") self.assertEqual(d.version, "1.3-1")
self.assertEqual(d.py_version, "2.4") self.assertEqual(d.py_version, "2.4")
self.assertEqual(d.platform, "win32")
self.assertEqual(d.parsed_version, parse_version("1.3-1")) self.assertEqual(d.parsed_version, parse_version("1.3-1"))
def testDistroBasics(self): def testDistroBasics(self):
d = Distribution( d = Distribution(
"/some/path", "/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.checkFooPkg(d)
self.failUnless(d.installed_on(["/some/path"])) self.failUnless(d.installed_on(["/some/path"]))
...@@ -26,6 +58,7 @@ class DistroTests(TestCase): ...@@ -26,6 +58,7 @@ class DistroTests(TestCase):
d = Distribution("/some/path") d = Distribution("/some/path")
self.assertEqual(d.py_version, sys.version[:3]) self.assertEqual(d.py_version, sys.version[:3])
self.assertEqual(d.platform, None)
def testDistroParse(self): def testDistroParse(self):
d = Distribution.from_filename("FooPkg-1.3_1-py2.4-win32.egg") d = Distribution.from_filename("FooPkg-1.3_1-py2.4-win32.egg")
...@@ -39,6 +72,14 @@ class DistroTests(TestCase): ...@@ -39,6 +72,14 @@ class DistroTests(TestCase):
class ParseTests(TestCase): class ParseTests(TestCase):
def testEmptyParse(self): def testEmptyParse(self):
......
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