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
......@@ -234,7 +234,7 @@ def get_distribution(dist):
def load_entry_point(dist, group, name):
"""Return `name` entry point of `group` for `dist` or raise ImportError"""
return get_distribution(dist).load_entry_point(group, name)
def get_entry_map(dist, group=None):
"""Return the entry point map for `group`, or the full entry map"""
return get_distribution(dist).get_entry_map(group)
......@@ -537,8 +537,8 @@ class Environment(object):
def __init__(self,search_path=None,platform=get_platform(),python=PY_MAJOR):
"""Snapshot distributions available on a search path
Any distributions found on `search_path` are added to the distribution
map. `search_path` should be a sequence of ``sys.path`` items. If not
Any distributions found on `search_path` are added to the environment.
`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
......@@ -558,31 +558,24 @@ class Environment(object):
self.scan(search_path)
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
or dist.py_version==self.python) \
and compatible_platforms(dist.platform,self.platform)
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 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 remove(self, dist):
"""Remove `dist` from the environment"""
self._distmap[dist.key].remove(dist)
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
supplied, ``sys.path`` is used. Only distributions conforming to
the platform/python version defined at initialization are added.
......@@ -594,66 +587,73 @@ class Environment(object):
for dist in find_distributions(item):
self.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.
def __getitem__(self,project_name):
"""Return a newest-to-oldest list of distributions for `project_name`
"""
try:
return self._cache[key]
return self._cache[project_name]
except KeyError:
key = key.lower()
if key not in self._distmap:
raise
project_name = project_name.lower()
if project_name not in self._distmap:
return []
if key not in self._cache:
dists = self._cache[key] = self._distmap[key]
if project_name not in self._cache:
dists = self._cache[project_name] = self._distmap[project_name]
_sort_dists(dists)
return self._cache[key]
return self._cache[project_name]
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):
self._distmap.setdefault(dist.key,[]).append(dist)
if dist.key in self._cache:
_sort_dists(self._cache[dist.key])
dists = self._distmap.setdefault(dist.key,[])
if dist not in dists:
dists.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)
def best_match(self, req, working_set, installer=None):
"""Find distribution best matching `req` and usable on `working_set`
If a distribution that's already active in `working_set` is unsuitable,
a VersionConflict is raised. If one or more suitable distributions are
already active, the leftmost distribution (i.e., the one first in
the search path) is returned. Otherwise, the available distribution
with the highest version number is returned. If nothing is available,
returns ``obtain(req,installer)`` or ``None`` if no distribution can
be obtained.
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.
"""
dist = working_set.find(req)
if dist is not None:
return dist
for dist in self.get(req.key, ()):
for dist in self[req.key]:
if dist in req:
return dist
return self.obtain(req, installer) # try and download/install
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:
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
class ResourceManager:
"""Manage resource extraction and packages"""
extraction_path = None
......@@ -744,7 +744,7 @@ class ResourceManager:
is based on the ``PYTHON_EGG_CACHE`` environment variable, with various
platform-specific fallbacks. See that routine's documentation for more
details.)
Resources are extracted to subdirectories of this path based upon
information given by the ``IResourceProvider``. You may set this to a
temporary directory, but then you must call ``cleanup_resources()`` to
......@@ -1369,7 +1369,7 @@ def find_on_path(importer, path_item, only=False):
)
else:
# scan for .egg and .egg-info in directory
for entry in os.listdir(path_item):
for entry in os.listdir(path_item):
lower = entry.lower()
if lower.endswith('.egg-info'):
fullpath = os.path.join(path_item, entry)
......@@ -1637,7 +1637,7 @@ class EntryPoint(object):
raise UnknownExtra("Can't require() without a distribution", self)
map(working_set.add,
working_set.resolve(self.dist.requires(self.extras),env,installer))
#@classmethod
def parse(cls, src, dist=None):
"""Parse a single entry point from string `src`
......
......@@ -58,12 +58,91 @@ declare_namespace, fixup_namespace_packages, register_namespace_handler
======================
Listeners
working_set
``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
......@@ -75,18 +154,21 @@ some purpose. These objects (or their string form) are used by various
distribution needs.
Requirements Parsing
Requirements Parsing
--------------------
``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.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
requirement specifier. The syntax of a requirement specifier can be
defined in EBNF as follows::
requirement specifier, or if they contain more than one specifier. (To
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?
versionspec ::= comparison version (',' comparison version)*
......@@ -100,7 +182,7 @@ Requirements Parsing
Tokens can be separated by whitespace, and a requirement can be continued
over multiple lines using a backslash (``\\``). Line-end comments (using
``#``) are also allowed.
Some examples of valid requirement specifiers::
FooProject >= 1.2
......@@ -258,7 +340,7 @@ Creating and Parsing
import baz
advertised_object = baz.foo.bar
The `extras` are an optional tuple of "extra feature" names that the
distribution needs in order to provide this entry point. When the
entry point is loaded, these extra features are looked up in the `dist`
......@@ -266,7 +348,7 @@ Creating and Parsing
on sys.path; see the ``load()`` method for more details. The `extras`
argument is only meaningful if `dist` is specified. `dist` must be
a ``Distribution`` instance.
``EntryPoint.parse(src, dist=None)`` (classmethod)
Parse a single entry point from string `src`
......@@ -324,6 +406,10 @@ addition, the following methods are provided:
taking a ``Requirement`` instance and returning a matching importable
``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
========================
......
......@@ -1852,7 +1852,11 @@ Release Notes/Change History
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.)
* ``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
``WorkingSet`` instead, and the ``best_match()`` method now uses a working
......
......@@ -264,7 +264,7 @@ class PackageIndex(Environment):
def obtain(self, requirement, installer=None):
self.find_packages(requirement)
for dist in self.get(requirement.key, ()):
for dist in self[requirement.key]:
if dist in requirement:
return dist
self.debug("%s does not match %s", requirement, dist)
......@@ -344,7 +344,7 @@ class PackageIndex(Environment):
self.info("Searching for %s", requirement)
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):
self.info("Best match: %s", dist)
return self.download(dist.location, tmpdir)
......
......@@ -25,19 +25,19 @@ class DistroTests(TestCase):
# empty path should produce no distributions
ad = Environment([], python=None)
self.assertEqual(list(ad), [])
self.assertEqual(len(ad),0)
self.assertEqual(ad.get('FooPkg'),None)
self.failIf('FooPkg' in ad)
self.assertEqual(ad['FooPkg'],[])
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)
self.failUnless(ad['FooPkg'])
# But only 1 package
self.assertEqual(list(ad), ['foopkg'])
self.assertEqual(len(ad),1)
# Distributions sort by version
self.assertEqual(
......@@ -46,7 +46,7 @@ class DistroTests(TestCase):
# 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']
[dist.version for dist in ad['FooPkg']], ['1.4','1.2']
)
# And inserting adds them in order
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