diff options
-rw-r--r-- | pkg_resources.py | 243 | ||||
-rw-r--r-- | setuptools/command/bdist_egg.py | 8 | ||||
-rw-r--r-- | setuptools/tests/test_resources.py | 53 |
3 files changed, 213 insertions, 91 deletions
diff --git a/pkg_resources.py b/pkg_resources.py index f034be61..a3f9ee7c 100644 --- a/pkg_resources.py +++ b/pkg_resources.py @@ -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): diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 7a30c1cf..e8309a71 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -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): diff --git a/setuptools/tests/test_resources.py b/setuptools/tests/test_resources.py index 5cab42da..b2b5c2ce 100644 --- a/setuptools/tests/test_resources.py +++ b/setuptools/tests/test_resources.py @@ -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) |