Commit 15233b3d authored by PJ Eby's avatar PJ Eby

Document the "Environment" class, and simplify its API.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4041194
parent 8672fe00
...@@ -537,8 +537,8 @@ class Environment(object): ...@@ -537,8 +537,8 @@ class Environment(object):
def __init__(self,search_path=None,platform=get_platform(),python=PY_MAJOR): def __init__(self,search_path=None,platform=get_platform(),python=PY_MAJOR):
"""Snapshot distributions available on a search path """Snapshot distributions available on a search path
Any distributions found on `search_path` are added to the distribution Any distributions found on `search_path` are added to the environment.
map. `search_path` should be a sequence of ``sys.path`` items. If not `search_path` should be a sequence of ``sys.path`` items. If not
supplied, ``sys.path`` is used. supplied, ``sys.path`` is used.
`platform` is an optional string specifying the name of the platform `platform` is an optional string specifying the name of the platform
...@@ -558,31 +558,24 @@ class Environment(object): ...@@ -558,31 +558,24 @@ class Environment(object):
self.scan(search_path) self.scan(search_path)
def can_add(self, dist): def can_add(self, dist):
"""Is distribution `dist` acceptable for this collection?""" """Is distribution `dist` acceptable for this environment?
The distribution must match the platform and python version
requirements specified when this environment was created, or False
is returned.
"""
return (self.python is None or dist.py_version is None return (self.python is None or dist.py_version is None
or dist.py_version==self.python) \ or dist.py_version==self.python) \
and compatible_platforms(dist.platform,self.platform) and compatible_platforms(dist.platform,self.platform)
def __iter__(self): def remove(self, dist):
"""Iterate over distribution keys""" """Remove `dist` from the environment"""
return iter(self._distmap.keys()) self._distmap[dist.key].remove(dist)
def __contains__(self,name):
"""Has a distribution named `name` ever been added to this map?"""
return name.lower() in 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): def scan(self, search_path=None):
"""Scan `search_path` for distributions usable on `platform` """Scan `search_path` for distributions usable in this environment
Any distributions found are added to the distribution map. Any distributions found are added to the environment.
`search_path` should be a sequence of ``sys.path`` items. If not `search_path` should be a sequence of ``sys.path`` items. If not
supplied, ``sys.path`` is used. Only distributions conforming to supplied, ``sys.path`` is used. Only distributions conforming to
the platform/python version defined at initialization are added. the platform/python version defined at initialization are added.
...@@ -594,66 +587,73 @@ class Environment(object): ...@@ -594,66 +587,73 @@ class Environment(object):
for dist in find_distributions(item): for dist in find_distributions(item):
self.add(dist) self.add(dist)
def __getitem__(self,key): def __getitem__(self,project_name):
"""Return a newest-to-oldest list of distributions for the given key """Return a newest-to-oldest list of distributions for `project_name`
The returned list may be modified in-place, e.g. for narrowing down
usable distributions.
""" """
try: try:
return self._cache[key] return self._cache[project_name]
except KeyError: except KeyError:
key = key.lower() project_name = project_name.lower()
if key not in self._distmap: if project_name not in self._distmap:
raise return []
if key not in self._cache: if project_name not in self._cache:
dists = self._cache[key] = self._distmap[key] dists = self._cache[project_name] = self._distmap[project_name]
_sort_dists(dists) _sort_dists(dists)
return self._cache[key] return self._cache[project_name]
def add(self,dist): def add(self,dist):
"""Add `dist` to the distribution map, only if it's suitable""" """Add `dist` if we ``can_add()`` it and it isn't already added"""
if self.can_add(dist): if self.can_add(dist):
self._distmap.setdefault(dist.key,[]).append(dist) dists = self._distmap.setdefault(dist.key,[])
if dist not in dists:
dists.append(dist)
if dist.key in self._cache: if dist.key in self._cache:
_sort_dists(self._cache[dist.key]) _sort_dists(self._cache[dist.key])
def remove(self,dist):
"""Remove `dist` from the distribution map"""
self._distmap[dist.key].remove(dist)
def best_match(self, req, working_set, installer=None): def best_match(self, req, working_set, installer=None):
"""Find distribution best matching `req` and usable on `working_set` """Find distribution best matching `req` and usable on `working_set`
If a distribution that's already active in `working_set` is unsuitable, This calls the ``find(req)`` method of the `working_set` to see if a
a VersionConflict is raised. If one or more suitable distributions are suitable distribution is already active. (This may raise
already active, the leftmost distribution (i.e., the one first in ``VersionConflict`` if an unsuitable version of the project is already
the search path) is returned. Otherwise, the available distribution active in the specified `working_set`.) If a suitable distribution
with the highest version number is returned. If nothing is available, isn't active, this method returns the newest distribution in the
returns ``obtain(req,installer)`` or ``None`` if no distribution can environment that meets the ``Requirement`` in `req`. If no suitable
be obtained. distribution is found, and `installer` is supplied, then the result of
calling the environment's ``obtain(req, installer)`` method will be
returned.
""" """
dist = working_set.find(req) dist = working_set.find(req)
if dist is not None: if dist is not None:
return dist return dist
for dist in self[req.key]:
for dist in self.get(req.key, ()):
if dist in req: if dist in req:
return dist return dist
return self.obtain(req, installer) # try and download/install return self.obtain(req, installer) # try and download/install
def obtain(self, requirement, installer=None): def obtain(self, requirement, installer=None):
"""Obtain a distro that matches requirement (e.g. via download)""" """Obtain a distribution matching `requirement` (e.g. via download)
Obtain a distro that matches requirement (e.g. via download). In the
base ``Environment`` class, this routine just returns
``installer(requirement)``, unless `installer` is None, in which case
None is returned instead. This method is a hook that allows subclasses
to attempt other ways of obtaining a distribution before falling back
to the `installer` argument."""
if installer is not None: if installer is not None:
return installer(requirement) return installer(requirement)
def __len__(self): return len(self._distmap) def __iter__(self):
"""Yield the unique project names of the available distributions"""
for key in self._distmap.keys():
if self[key]: yield key
AvailableDistributions = Environment # XXX backward compatibility AvailableDistributions = Environment # XXX backward compatibility
class ResourceManager: class ResourceManager:
"""Manage resource extraction and packages""" """Manage resource extraction and packages"""
extraction_path = None extraction_path = None
......
...@@ -58,12 +58,91 @@ declare_namespace, fixup_namespace_packages, register_namespace_handler ...@@ -58,12 +58,91 @@ declare_namespace, fixup_namespace_packages, register_namespace_handler
====================== ======================
Listeners Listeners
working_set
``Environment`` Objects ``Environment`` Objects
======================= =======================
XXX An "environment" is a collection of ``Distribution`` objects, usually ones
that are present and potentially importable on the current platform.
``Environment`` objects are used by ``pkg_resources`` to index available
distributions during dependency resolution.
``Environment(search_path=None, platform=get_platform(), python=PY_MAJOR)``
Create an environment snapshot by scanning `search_path` for distributions
compatible with `platform` and `python`. `search_path` should be a
sequence of strings such as might be used on ``sys.path``. If a
`search_path` isn't 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. `python` is an
optional string naming the desired version of Python (e.g. ``'2.4'``);
it defaults to the currently-running version.
You may explicitly set `platform` (and/or `python`) to ``None`` if you
wish to include *all* distributions, not just those compatible with the
running platform or Python version.
Note that `search_path` is scanned immediately for distributions, and the
resulting ``Environment`` is a snapshot of the found distributions. It
is not automatically updated if the system's state changes due to e.g.
installation or removal of distributions.
``__getitem__(project_name)``
Returns a list of distributions for the given project name, ordered
from newest to oldest version. (And highest to lowest format precedence
for distributions that contain the same version of the project.) If there
are no distributions for the project, returns an empty list.
``__iter__()``
Yield the unique project names of the distributions in this environment.
The yielded names are always in lower case.
``add(dist)``
Add `dist` to the environment if it matches the platform and python version
specified at creation time, and only if the distribution hasn't already
been added. (i.e., adding the same distribution more than once is a no-op.)
``remove(dist)``
Remove `dist` from the environment.
``can_add(dist)``
Is distribution `dist` acceptable for this environment? If it's not
compatible with the platform and python version specified at creation of
the environment, False is returned.
``best_match(req, working_set, installer=None)``
Find distribution best matching `req` and usable on `working_set`
This calls the ``find(req)`` method of the `working_set` to see if a
suitable distribution is already active. (This may raise
``VersionConflict`` if an unsuitable version of the project is already
active in the specified `working_set`.) If a suitable distribution isn't
active, this method returns the newest distribution in the environment
that meets the ``Requirement`` in `req`. If no suitable distribution is
found, and `installer` is supplied, then the result of calling
the environment's ``obtain(req, installer)`` method will be returned.
``obtain(requirement, installer=None)``
Obtain a distro that matches requirement (e.g. via download). In the
base ``Environment`` class, this routine just returns
``installer(requirement)``, unless `installer` is None, in which case
None is returned instead. This method is a hook that allows subclasses
to attempt other ways of obtaining a distribution before falling back
to the `installer` argument.
``scan(search_path=None)``
Scan `search_path` for distributions usable on `platform`
Any distributions found are added to the environment. `search_path` should
be a sequence of strings such as might be used on ``sys.path``. If not
supplied, ``sys.path`` is used. Only distributions conforming to
the platform/python version defined at initialization are added. This
method is a shortcut for using the ``find_distributions()`` function to
find the distributions from each item in `search_path`, and then calling
``add()`` to add each one to the environment.
``Requirement`` Objects ``Requirement`` Objects
...@@ -79,14 +158,17 @@ Requirements Parsing ...@@ -79,14 +158,17 @@ Requirements Parsing
-------------------- --------------------
``parse_requirements(s)`` ``parse_requirements(s)``
Yield ``Requirement`` objects for a string or list of lines. Each Yield ``Requirement`` objects for a string or iterable of lines. Each
requirement must start on a new line. See below for syntax. requirement must start on a new line. See below for syntax.
``Requirement.parse(s)`` ``Requirement.parse(s)``
Create a ``Requirement`` object from a string or list of lines. A Create a ``Requirement`` object from a string or iterable of lines. A
``ValueError`` is raised if the string or lines do not contain a valid ``ValueError`` is raised if the string or lines do not contain a valid
requirement specifier. The syntax of a requirement specifier can be requirement specifier, or if they contain more than one specifier. (To
defined in EBNF as follows:: parse multiple specifiers from a string or iterable of strings, use
``parse_requirements()`` instead.)
The syntax of a requirement specifier can be defined in EBNF as follows::
requirement ::= project_name versionspec? extras? requirement ::= project_name versionspec? extras?
versionspec ::= comparison version (',' comparison version)* versionspec ::= comparison version (',' comparison version)*
...@@ -324,6 +406,10 @@ addition, the following methods are provided: ...@@ -324,6 +406,10 @@ addition, the following methods are provided:
taking a ``Requirement`` instance and returning a matching importable taking a ``Requirement`` instance and returning a matching importable
``Distribution`` instance or None. ``Distribution`` instance or None.
``__str__()``
The string form of an ``EntryPoint`` is a string that could be passed to
``EntryPoint.parse()`` to yield an equivalent ``EntryPoint``.
``Distribution`` Objects ``Distribution`` Objects
======================== ========================
......
...@@ -1852,7 +1852,11 @@ Release Notes/Change History ...@@ -1852,7 +1852,11 @@ Release Notes/Change History
that tells it to only yield distributions whose location is the passed-in that tells it to only yield distributions whose location is the passed-in
path. (It defaults to False, so that the default behavior is unchanged.) path. (It defaults to False, so that the default behavior is unchanged.)
* ``AvailableDistributions`` is now called ``Environment`` * ``AvailableDistributions`` is now called ``Environment``, and the
``get()``, ``__len__()``, and ``__contains__()`` methods were removed,
because they weren't particularly useful. ``__getitem__()`` no longer
raises ``KeyError``; it just returns an empty list if there are no
distributions for the named project.
* The ``resolve()`` method of ``Environment`` is now a method of * The ``resolve()`` method of ``Environment`` is now a method of
``WorkingSet`` instead, and the ``best_match()`` method now uses a working ``WorkingSet`` instead, and the ``best_match()`` method now uses a working
......
...@@ -264,7 +264,7 @@ class PackageIndex(Environment): ...@@ -264,7 +264,7 @@ class PackageIndex(Environment):
def obtain(self, requirement, installer=None): def obtain(self, requirement, installer=None):
self.find_packages(requirement) self.find_packages(requirement)
for dist in self.get(requirement.key, ()): for dist in self[requirement.key]:
if dist in requirement: if dist in requirement:
return dist return dist
self.debug("%s does not match %s", requirement, dist) self.debug("%s does not match %s", requirement, dist)
...@@ -344,7 +344,7 @@ class PackageIndex(Environment): ...@@ -344,7 +344,7 @@ class PackageIndex(Environment):
self.info("Searching for %s", requirement) self.info("Searching for %s", requirement)
def find(req): def find(req):
for dist in self.get(req.key, ()): for dist in self[req.key]:
if dist in req and (dist.precedence<=SOURCE_DIST or not source): if dist in req and (dist.precedence<=SOURCE_DIST or not source):
self.info("Best match: %s", dist) self.info("Best match: %s", dist)
return self.download(dist.location, tmpdir) return self.download(dist.location, tmpdir)
......
...@@ -25,19 +25,19 @@ class DistroTests(TestCase): ...@@ -25,19 +25,19 @@ class DistroTests(TestCase):
# empty path should produce no distributions # empty path should produce no distributions
ad = Environment([], python=None) ad = Environment([], python=None)
self.assertEqual(list(ad), []) self.assertEqual(list(ad), [])
self.assertEqual(len(ad),0) self.assertEqual(ad['FooPkg'],[])
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.3_1.egg"))
ad.add(Distribution.from_filename("FooPkg-1.4-py2.4-win32.egg")) ad.add(Distribution.from_filename("FooPkg-1.4-py2.4-win32.egg"))
ad.add(Distribution.from_filename("FooPkg-1.2-py2.4.egg")) ad.add(Distribution.from_filename("FooPkg-1.2-py2.4.egg"))
# Name is in there now # Name is in there now
self.failUnless('FooPkg' in ad) self.failUnless(ad['FooPkg'])
# But only 1 package # But only 1 package
self.assertEqual(list(ad), ['foopkg']) self.assertEqual(list(ad), ['foopkg'])
self.assertEqual(len(ad),1)
# Distributions sort by version # Distributions sort by version
self.assertEqual( self.assertEqual(
...@@ -46,7 +46,7 @@ class DistroTests(TestCase): ...@@ -46,7 +46,7 @@ class DistroTests(TestCase):
# Removing a distribution leaves sequence alone # Removing a distribution leaves sequence alone
ad.remove(ad['FooPkg'][1]) ad.remove(ad['FooPkg'][1])
self.assertEqual( self.assertEqual(
[dist.version for dist in ad.get('FooPkg')], ['1.4','1.2'] [dist.version for dist in ad['FooPkg']], ['1.4','1.2']
) )
# And inserting adds them in order # And inserting adds them in order
ad.add(Distribution.from_filename("FooPkg-1.9.egg")) ad.add(Distribution.from_filename("FooPkg-1.9.egg"))
......
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