Commit 38aa9be2 authored by PJ Eby's avatar PJ Eby

Added the ``extras`` attribute to ``Distribution``, the ``find_plugins()``

method to ``WorkingSet``, and the ``__add__()`` and ``__iadd__()`` methods
to ``Environment``.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4042358
parent f4be0720
...@@ -243,10 +243,47 @@ And adding a callback more than once has no effect, either:: ...@@ -243,10 +243,47 @@ And adding a callback more than once has no effect, either::
>>> ws.subscribe(added) # no callbacks >>> ws.subscribe(added) # no callbacks
# and no double-callbacks on subsequent additions, either # and no double-callbacks on subsequent additions, either
>>> ws.add(Distribution(project_name="JustATest", version="0.99")) >>> just_a_test = Distribution(project_name="JustATest", version="0.99")
>>> ws.add(just_a_test)
Added JustATest 0.99 Added JustATest 0.99
Finding Plugins
---------------
``WorkingSet`` objects can be used to figure out what plugins in an
``Environment`` can be loaded without any resolution errors::
>>> from pkg_resources import Environment
>>> plugins = Environment([]) # normally, a list of plugin directories
>>> plugins.add(foo12)
>>> plugins.add(foo14)
>>> plugins.add(just_a_test)
In the simplest case, we just get the newest version of each distribution in
the plugin environment::
>>> ws = WorkingSet([])
>>> ws.find_plugins(plugins)
([JustATest 0.99, Foo 1.4 (f14)], {})
But if there's a problem with a version conflict or missing requirements, the
method falls back to older versions, and the error info dict will contain an
exception instance for each unloadable plugin::
>>> ws.add(foo12) # this will conflict with Foo 1.4
>>> ws.find_plugins(plugins)
([JustATest 0.99, Foo 1.2 (f12)], {Foo 1.4 (f14): <...VersionConflict...>})
But if you disallow fallbacks, the failed plugin will be skipped instead of
trying older versions::
>>> ws.find_plugins(plugins, fallback=False)
([JustATest 0.99], {Foo 1.4 (f14): <...VersionConflict...>})
Platform Compatibility Rules Platform Compatibility Rules
---------------------------- ----------------------------
......
...@@ -490,6 +490,88 @@ class WorkingSet(object): ...@@ -490,6 +490,88 @@ class WorkingSet(object):
return to_activate # return list of distros to activate return to_activate # return list of distros to activate
def find_plugins(self,
plugin_env, full_env=None, installer=None, fallback=True
):
"""Find all activatable distributions in `plugin_env`
Example usage::
distributions, errors = working_set.find_plugins(
Environment(plugin_dirlist)
)
map(working_set.add, distributions) # add plugins+libs to sys.path
print "Couldn't load", errors # display errors
The `plugin_env` should be an ``Environment`` instance that contains
only distributions that are in the project's "plugin directory" or
directories. The `full_env`, if supplied, should be an ``Environment``
contains all currently-available distributions. If `full_env` is not
supplied, one is created automatically from the ``WorkingSet`` this
method is called on, which will typically mean that every directory on
``sys.path`` will be scanned for distributions.
`installer` is a standard installer callback as used by the
``resolve()`` method. The `fallback` flag indicates whether we should
attempt to resolve older versions of a plugin if the newest version
cannot be resolved.
This method returns a 2-tuple: (`distributions`, `error_info`), where
`distributions` is a list of the distributions found in `plugin_env`
that were loadable, along with any other distributions that are needed
to resolve their dependencies. `error_info` is a dictionary mapping
unloadable plugin distributions to an exception instance describing the
error that occurred. Usually this will be a ``DistributionNotFound`` or
``VersionConflict`` instance.
"""
plugin_projects = list(plugin_env)
plugin_projects.sort() # scan project names in alphabetic order
error_info = {}
distributions = {}
if full_env is None:
env = Environment(self.entries)
env += plugin_env
else:
env = full_env + plugin_env
shadow_set = self.__class__([])
map(shadow_set.add, self) # put all our entries in shadow_set
for project_name in plugin_projects:
for dist in plugin_env[project_name]:
req = [dist.as_requirement()]
try:
resolvees = shadow_set.resolve(req, env, installer)
except ResolutionError,v:
error_info[dist] = v # save error info
if fallback:
continue # try the next older version of project
else:
break # give up on this project, keep going
else:
map(shadow_set.add, resolvees)
distributions.update(dict.fromkeys(resolvees))
# success, no need to try any more versions of this project
break
distributions = list(distributions)
distributions.sort()
return distributions, error_info
def require(self, *requirements): def require(self, *requirements):
"""Ensure that distributions matching `requirements` are activated """Ensure that distributions matching `requirements` are activated
...@@ -651,9 +733,50 @@ class Environment(object): ...@@ -651,9 +733,50 @@ class Environment(object):
for key in self._distmap.keys(): for key in self._distmap.keys():
if self[key]: yield key if self[key]: yield key
def __iadd__(self, other):
"""In-place addition of a distribution or environment"""
if isinstance(other,Distribution):
self.add(other)
elif isinstance(other,Environment):
for project in other:
for dist in other[project]:
self.add(dist)
else:
raise TypeError("Can't add %r to environment" % (other,))
return self
def __add__(self, other):
"""Add an environment or distribution to an environment"""
new = self.__class__([], platform=None, python=None)
for env in self, other:
new += env
return new
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
...@@ -1966,6 +2089,12 @@ class Distribution(object): ...@@ -1966,6 +2089,12 @@ class Distribution(object):
#@property
def extras(self):
return [dep for dep in self._dep_map if dep]
extras = property(extras)
def issue_warning(*args,**kw): def issue_warning(*args,**kw):
level = 1 level = 1
g = globals() g = globals()
...@@ -1994,12 +2123,6 @@ def issue_warning(*args,**kw): ...@@ -1994,12 +2123,6 @@ def issue_warning(*args,**kw):
......
...@@ -360,6 +360,78 @@ function are for. ...@@ -360,6 +360,78 @@ function are for.
``pkg_resources.working_set.subscribe()``. ``pkg_resources.working_set.subscribe()``.
Locating Plugins
----------------
Extensible applications will sometimes have a "plugin directory" or a set of
plugin directories, from which they want to load entry points or other
metadata. The ``find_plugins()`` method allows you to do this, by
``find_plugins(plugin_env, full_env=None, fallback=True)``
Scan `plugin_env` and identify which distributions could be added to this
working set without version conflicts or missing requirements.
Example usage::
distributions, errors = working_set.find_plugins(
Environment(plugin_dirlist)
)
map(working_set.add, distributions) # add plugins+libs to sys.path
print "Couldn't load", errors # display errors
The `plugin_env` should be an ``Environment`` instance that contains only
distributions that are in the project's "plugin directory" or directories.
The `full_env`, if supplied, should be an ``Environment`` instance that
contains all currently-available distributions.
If `full_env` is not supplied, one is created automatically from the
``WorkingSet`` this method is called on, which will typically mean that
every directory on ``sys.path`` will be scanned for distributions.
This method returns a 2-tuple: (`distributions`, `error_info`), where
`distributions` is a list of the distributions found in `plugin_env` that
were loadable, along with any other distributions that are needed to resolve
their dependencies. `error_info` is a dictionary mapping unloadable plugin
distributions to an exception instance describing the error that occurred.
Usually this will be a ``DistributionNotFound`` or ``VersionConflict``
instance.
Most applications will use this method mainly on the master ``working_set``
instance in ``pkg_resources``, and then immediately add the returned
distributions to the working set so that they are available on sys.path.
This will make it possible to find any entry points, and allow any other
metadata tracking and hooks to be activated.
The resolution algorithm used by ``find_plugins()`` is as follows. First,
the project names of the distributions present in `plugin_env` are sorted.
Then, each project's eggs are tried in descending version order (i.e.,
newest version first).
An attempt is made to resolve each egg's dependencies. If the attempt is
successful, the egg and its dependencies are added to the output list and to
a temporary copy of the working set. The resolution process continues with
the next project name, and no older eggs for that project are tried.
If the resolution attempt fails, however, the error is added to the error
dictionary. If the `fallback` flag is true, the next older version of the
plugin is tried, until a working version is found. If false, the resolution
process continues with the next plugin project name.
Some applications may have stricter fallback requirements than others. For
example, an application that has a database schema or persistent objects
may not be able to safely downgrade a version of a package. Others may want
to ensure that a new plugin configuration is either 100% good or else
revert to a known-good configuration. (That is, they may wish to revert to
a known configuration if the `error_info` return value is non-empty.)
Note that this algorithm gives precedence to satisfying the dependencies of
alphabetically prior project names in case of version conflicts. If two
projects named "AaronsPlugin" and "ZekesPlugin" both need different versions
of "TomsLibrary", then "AaronsPlugin" will win and "ZekesPlugin" will be
disabled due to version conflict.
``Environment`` Objects ``Environment`` Objects
======================= =======================
...@@ -409,8 +481,25 @@ distributions during dependency resolution. ...@@ -409,8 +481,25 @@ distributions during dependency resolution.
``can_add(dist)`` ``can_add(dist)``
Is distribution `dist` acceptable for this environment? If it's not Is distribution `dist` acceptable for this environment? If it's not
compatible with the platform and python version specified at creation of compatible with the ``platform`` and ``python`` version values specified
the environment, False is returned. when the environment was created, a false value is returned.
``__add__(dist_or_env)`` (``+`` operator)
Add a distribution or environment to an ``Environment`` instance, returning
a *new* environment object that contains all the distributions previously
contained by both. The new environment will have a ``platform`` and
``python`` of ``None``, meaning that it will not reject any distributions
from being added to it; it will simply accept whatever is added. If you
want the added items to be filtered for platform and Python version, or
you want to add them to the *same* environment instance, you should use
in-place addition (``+=``) instead.
``__iadd__(dist_or_env)`` (``+=`` operator)
Add a distribution or environment to an ``Environment`` instance
*in-place*, updating the existing instance and returning it. The
``platform`` and ``python`` filter attributes take effect, so distributions
in the source that do not have a suitable platform string or Python version
are silently ignored.
``best_match(req, working_set, installer=None)`` ``best_match(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`
...@@ -809,6 +898,11 @@ key ...@@ -809,6 +898,11 @@ key
``dist.key`` is short for ``dist.project_name.lower()``. It's used for ``dist.key`` is short for ``dist.project_name.lower()``. It's used for
case-insensitive comparison and indexing of distributions by project name. case-insensitive comparison and indexing of distributions by project name.
extras
A list of strings, giving the names of extra features defined by the
project's dependency list (the ``extras_require`` argument specified in
the project's setup script).
version version
A string denoting what release of the project this distribution contains. A string denoting what release of the project this distribution contains.
When a ``Distribution`` is constructed, the `version` argument is passed When a ``Distribution`` is constructed, the `version` argument is passed
...@@ -1535,6 +1629,10 @@ Release Notes/Change History ...@@ -1535,6 +1629,10 @@ Release Notes/Change History
---------------------------- ----------------------------
0.6a10 0.6a10
* Added the ``extras`` attribute to ``Distribution``, the ``find_plugins()``
method to ``WorkingSet``, and the ``__add__()`` and ``__iadd__()`` methods
to ``Environment``.
* ``safe_name()`` now allows dots in project names. * ``safe_name()`` now allows dots in project names.
* There is a new ``to_filename()`` function that escapes project names and * There is a new ``to_filename()`` function that escapes project names and
......
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