summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPJ Eby <distutils-sig@python.org>2005-07-24 22:47:06 +0000
committerPJ Eby <distutils-sig@python.org>2005-07-24 22:47:06 +0000
commita4c8421afadea9ba3da8bf42adc71251c60b2c1e (patch)
tree2cc3fa1af58200d5199b30771053c527ef91bd93
parent377e21167f0625324c391dc47d83cf91cdea05e9 (diff)
downloadpython-setuptools-bitbucket-a4c8421afadea9ba3da8bf42adc71251c60b2c1e.tar.gz
Implement "entry points" for dynamic discovery of drivers and plugins.
Change setuptools to discover setup commands using an entry point group called "distutils.commands". Thanks to Ian Bicking for the suggestion that led to designing this super-cool feature.
-rw-r--r--pkg_resources.py287
-rwxr-xr-xsetup.py20
-rwxr-xr-xsetuptools.egg-info/entry_points.txt17
-rwxr-xr-xsetuptools.txt145
-rw-r--r--setuptools/command/__init__.py13
-rwxr-xr-xsetuptools/command/egg_info.py38
-rw-r--r--setuptools/dist.py69
-rw-r--r--setuptools/tests/test_resources.py84
8 files changed, 562 insertions, 111 deletions
diff --git a/pkg_resources.py b/pkg_resources.py
index 16604b8b..1171b761 100644
--- a/pkg_resources.py
+++ b/pkg_resources.py
@@ -12,21 +12,6 @@ The package resource API is designed to work with normal filesystem packages,
.zip files and with custom PEP 302 loaders that support the ``get_data()``
method.
"""
-__all__ = [
- 'register_loader_type', 'get_provider', 'IResourceProvider','PathMetadata',
- 'ResourceManager', 'AvailableDistributions', 'require', 'resource_string',
- 'resource_stream', 'resource_filename', 'set_extraction_path', 'EGG_DIST',
- 'cleanup_resources', 'parse_requirements', 'ensure_directory','SOURCE_DIST',
- 'compatible_platforms', 'get_platform', 'IMetadataProvider','parse_version',
- 'ResolutionError', 'VersionConflict', 'DistributionNotFound','EggMetadata',
- 'InvalidOption', 'Distribution', 'Requirement', 'yield_lines',
- 'get_importer', 'find_distributions', 'find_on_path', 'register_finder',
- 'split_sections', 'declare_namespace', 'register_namespace_handler',
- 'safe_name', 'safe_version', 'run_main', 'BINARY_DIST', 'run_script',
- 'get_default_cache', 'EmptyProvider', 'empty_provider', 'normalize_path',
- 'WorkingSet', 'working_set', 'add_activation_listener', 'CHECKOUT_DIST',
- 'list_resources', 'resource_exists', 'resource_isdir',
-]
import sys, os, zipimport, time, re, imp
from sets import ImmutableSet
@@ -39,6 +24,62 @@ from sets import ImmutableSet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+__all__ = [
+ # Basic resource access and distribution/entry point discovery
+ 'require', 'run_script', 'get_provider', 'get_distribution',
+ 'load_entry_point', 'get_entry_map', 'get_entry_info',
+ 'resource_string', 'resource_stream', 'resource_filename',
+ 'resource_listdir', 'resource_exists', 'resource_isdir',
+
+ # Environmental control
+ 'declare_namespace', 'working_set', 'add_activation_listener',
+ 'find_distributions', 'set_extraction_path', 'cleanup_resources',
+ 'get_default_cache',
+
+ # Primary implementation classes
+ 'AvailableDistributions', 'WorkingSet', 'ResourceManager',
+ 'Distribution', 'Requirement', 'EntryPoint',
+
+ # Exceptions
+ 'ResolutionError','VersionConflict','DistributionNotFound','UnknownExtra',
+
+ # Parsing functions and string utilities
+ 'parse_requirements', 'parse_version', 'safe_name', 'safe_version',
+ 'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections',
+
+ # filesystem utilities
+ 'ensure_directory', 'normalize_path',
+
+ # Distribution "precedence" constants
+ 'EGG_DIST', 'BINARY_DIST', 'SOURCE_DIST', 'CHECKOUT_DIST',
+
+ # "Provider" interfaces, implementations, and registration/lookup APIs
+ 'IMetadataProvider', 'IResourceProvider',
+ 'PathMetadata', 'EggMetadata', 'EmptyProvider', 'empty_provider',
+ 'NullProvider', 'EggProvider', 'DefaultProvider', 'ZipProvider',
+ 'register_finder', 'register_namespace_handler', 'register_loader_type',
+ 'fixup_namespace_packages', 'get_importer',
+
+ # Deprecated/backward compatibility only
+ 'run_main',
+]
+
+
class ResolutionError(Exception):
"""Abstract base for dependency resolution errors"""
@@ -48,8 +89,8 @@ class VersionConflict(ResolutionError):
class DistributionNotFound(ResolutionError):
"""A requested distribution was not found"""
-class InvalidOption(ResolutionError):
- """Invalid or unrecognized option name for a distribution"""
+class UnknownExtra(ResolutionError):
+ """Distribution doesn't have an "extra feature" of the given name"""
_provider_factories = {}
PY_MAJOR = sys.version[:3]
@@ -172,7 +213,6 @@ def compatible_platforms(provided,required):
return False
-
def run_script(dist_spec, script_name):
"""Locate distribution `dist_spec` and run its `script_name` script"""
ns = sys._getframe(1).f_globals
@@ -183,24 +223,25 @@ def run_script(dist_spec, script_name):
run_main = run_script # backward compatibility
+def get_distribution(dist):
+ """Return a current distribution object for a Requirement or string"""
+ if isinstance(dist,basestring): dist = Requirement.parse(dist)
+ if isinstance(dist,Requirement): dist = get_provider(dist)
+ if not isintance(dist,Distribution):
+ raise TypeError("Expected string, Requirement, or Distribution", dist)
+ return dist
+def load_entry_point(dist, kind, name):
+ """Return the `name` entry point of `kind` for dist or raise ImportError"""
+ return get_distribution(dist).load_entry_point(dist, kind, name)
+
+def get_entry_map(dist, kind=None):
+ """Return the entry point map for `kind`, or the full entry map"""
+ return get_distribution(dist).get_entry_map(dist, kind)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+def get_entry_info(dist, kind, name):
+ """Return the EntryPoint object for `kind`+`name`, or ``None``"""
+ return get_distribution(dist).get_entry_info(dist, kind, name)
class IMetadataProvider:
@@ -647,7 +688,7 @@ class ResourceManager:
self, resource_name
)
- def list_resources(self, package_name, resource_name):
+ def resource_listdir(self, package_name, resource_name):
return get_provider(package_name).resource_listdir(resource_name)
@@ -1008,9 +1049,9 @@ class ZipProvider(EggProvider):
return fspath[len(self.egg_root)+1:].split(os.sep)
raise AssertionError(
"%s is not a subpath of %s" % (fspath,self.egg_root)
- )
+ )
- def get_resource_filename(self, manager, resource_name):
+ def get_resource_filename(self, manager, resource_name):
if not self.egg_name:
raise NotImplementedError(
"resource_filename() only supported for .egg, not .zip"
@@ -1493,7 +1534,7 @@ VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|\.)+)").match # version info
COMMA = re.compile(r"\s*,").match # comma between items
OBRACKET = re.compile(r"\s*\[").match
CBRACKET = re.compile(r"\s*\]").match
-
+MODULE = re.compile(r"\w+(\.\w+)*$").match
EGG_NAME = re.compile(
r"(?P<name>[^-]+)"
r"( -(?P<ver>[^-]+) (-py(?P<pyver>[^-]+) (-(?P<plat>.+))? )? )?",
@@ -1556,6 +1597,131 @@ def parse_version(s):
+class EntryPoint(object):
+ """Object representing an importable location"""
+
+ def __init__(self, name, module_name, attrs=(), extras=()):
+ if not MODULE(module_name):
+ raise ValueError("Invalid module name", module_name)
+ self.name = name
+ self.module_name = module_name
+ self.attrs = tuple(attrs)
+ self.extras = Requirement.parse(
+ ("x[%s]" % ','.join(extras)).lower()
+ ).extras
+
+ def __str__(self):
+ s = "%s = %s" % (self.name, self.module_name)
+ if self.attrs:
+ s += ':' + '.'.join(self.attrs)
+ if self.extras:
+ s += ' [%s]' % ','.join(self.extras)
+ return s
+
+ def __repr__(self):
+ return "EntryPoint.parse(%r)" % str(self)
+
+ def load(self):
+ entry = __import__(self.module_name, globals(),globals(), ['__name__'])
+ for attr in self.attrs:
+ try:
+ entry = getattr(entry,attr)
+ except AttributeError:
+ raise ImportError("%r has no %r attribute" % (entry,attr))
+ return entry
+
+
+
+
+
+
+
+
+
+ #@classmethod
+ def parse(cls, src):
+ """Parse a single entry point from string `src`
+
+ Entry point syntax follows the form::
+
+ name = some.module:some.attr [extra1,extra2]
+
+ The entry name and module name are required, but the ``:attrs`` and
+ ``[extras]`` parts are optional
+ """
+ try:
+ attrs = extras = ()
+ name,value = src.split('=',1)
+ if '[' in value:
+ value,extras = value.split('[',1)
+ req = Requirement.parse("x["+extras)
+ if req.specs: raise ValueError
+ extras = req.extras
+ if ':' in value:
+ value,attrs = value.split(':',1)
+ if not MODULE(attrs.rstrip()):
+ raise ValueError
+ attrs = attrs.rstrip().split('.')
+ except ValueError:
+ raise ValueError(
+ "EntryPoint must be in 'name=module:attrs [extras]' format",
+ src
+ )
+ else:
+ return cls(name.strip(), value.lstrip(), attrs, extras)
+
+ parse = classmethod(parse)
+
+
+
+
+
+
+
+
+ #@classmethod
+ def parse_list(cls, section, contents):
+ if not MODULE(section):
+ raise ValueError("Invalid section name", section)
+ this = {}
+ for ep in map(cls.parse, yield_lines(contents)):
+ if ep.name in this:
+ raise ValueError("Duplicate entry point",section,ep.name)
+ this[ep.name]=ep
+ return this
+
+ parse_list = classmethod(parse_list)
+
+ #@classmethod
+ def parse_map(cls, data):
+ if isinstance(data,dict):
+ data = data.items()
+ else:
+ data = split_sections(data)
+ maps = {}
+ for section, contents in data:
+ if section is None:
+ if not contents:
+ continue
+ raise ValueError("Entry points must be listed in sections")
+ section = section.strip()
+ if section in maps:
+ raise ValueError("Duplicate section name", section)
+ maps[section] = cls.parse_list(section, contents)
+ return maps
+
+ parse_map = classmethod(parse_map)
+
+
+
+
+
+
+
+
+
+
+
class Distribution(object):
"""Wrap an actual or potential sys.path entry w/metadata"""
def __init__(self,
@@ -1660,7 +1826,7 @@ class Distribution(object):
try:
deps.extend(dm[ext.lower()])
except KeyError:
- raise InvalidOption(
+ raise UnknownExtra(
"%s has no such extra feature %r" % (self, ext)
)
return deps
@@ -1720,6 +1886,47 @@ class Distribution(object):
+ def load_entry_point(self, kind, name):
+ """Return the `name` entry point of `kind` or raise ImportError"""
+ ep = self.get_entry_info(kind,name)
+ if ep is None:
+ raise ImportError("Entry point %r not found" % ((kind,name),))
+ if ep.extras:
+ # Ensure any needed extras get added to the working set
+ map(working_set.add, working_set.resolve(self.requires(ep.extras)))
+ return ep.load()
+
+ def get_entry_map(self,kind=None):
+ """Return the entry point map for `kind`, or the full entry map"""
+ try:
+ ep_map = self._ep_map
+ except AttributeError:
+ ep_map = self._ep_map = EntryPoint.parse_map(
+ self._get_metadata('entry_points.txt')
+ )
+ if kind is not None:
+ return ep_map.get(kind,{})
+ return ep_map
+
+ def get_entry_info(self, kind, name):
+ """Return the EntryPoint object for `kind`+`name`, or ``None``"""
+ return self.get_entry_map(kind).get(name)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
def parse_requirements(strs):
"""Yield ``Requirement`` objects for each specification in `strs`
@@ -1885,6 +2092,7 @@ def _find_adapter(registry, ob):
def ensure_directory(path):
+ """Ensure that the parent directory of `path` exists"""
dirname = os.path.dirname(path)
if not os.path.isdir(dirname):
os.makedirs(dirname)
@@ -1924,7 +2132,6 @@ def split_sections(s):
-
# Set up global resource manager
_manager = ResourceManager()
diff --git a/setup.py b/setup.py
index b0bc3900..d2d2b008 100755
--- a/setup.py
+++ b/setup.py
@@ -18,6 +18,9 @@ def get_description():
VERSION = "0.6a0"
from setuptools import setup, find_packages
+import sys
+
+from setuptools.command import __all__ as SETUP_COMMANDS
setup(
name="setuptools",
@@ -35,9 +38,15 @@ setup(
packages = find_packages(),
py_modules = ['pkg_resources', 'easy_install'],
scripts = ['easy_install.py'],
+
zip_safe = False, # We want 'python -m easy_install' to work :(
-
+ entry_points = {
+ "distutils.commands" : [
+ "%(cmd)s = setuptools.command.%(cmd)s:%(cmd)s" % locals()
+ for cmd in SETUP_COMMANDS if cmd!="build_py" or sys.version>="2.4"
+ ],
+ },
classifiers = [f.strip() for f in """
Development Status :: 3 - Alpha
@@ -71,12 +80,3 @@ setup(
-
-
-
-
-
-
-
-
-
diff --git a/setuptools.egg-info/entry_points.txt b/setuptools.egg-info/entry_points.txt
new file mode 100755
index 00000000..8baf6137
--- /dev/null
+++ b/setuptools.egg-info/entry_points.txt
@@ -0,0 +1,17 @@
+[distutils.commands]
+rotate = setuptools.command.rotate:rotate
+develop = setuptools.command.develop:develop
+setopt = setuptools.command.setopt:setopt
+saveopts = setuptools.command.saveopts:saveopts
+egg_info = setuptools.command.egg_info:egg_info
+depends = setuptools.command.depends:depends
+upload = setuptools.command.upload:upload
+alias = setuptools.command.alias:alias
+easy_install = setuptools.command.easy_install:easy_install
+bdist_egg = setuptools.command.bdist_egg:bdist_egg
+install = setuptools.command.install:install
+test = setuptools.command.test:test
+install_lib = setuptools.command.install_lib:install_lib
+build_ext = setuptools.command.build_ext:build_ext
+sdist = setuptools.command.sdist:sdist
+
diff --git a/setuptools.txt b/setuptools.txt
index 1ddfa31c..50bc3b11 100755
--- a/setuptools.txt
+++ b/setuptools.txt
@@ -153,21 +153,20 @@ unless you need the associated ``setuptools`` feature.
A string or list of strings specifying what other distributions need to
be installed when this one is. See the section below on `Declaring
Dependencies`_ for details and examples of the format of this argument.
-
+
+``entry_points``
+ A dictionary mapping entry point group names to strings or lists of strings
+ defining the entry points. Entry points are used to support dynamic
+ discovery of services or plugins provided by a project. See `Dynamic
+ Discovery of Services and Plugins`_ for details and examples of the format
+ of this argument.
+
``extras_require``
A dictionary mapping names of "extras" (optional features of your project)
to strings or lists of strings specifying what other distributions must be
installed to support those features. See the section below on `Declaring
Dependencies`_ for details and examples of the format of this argument.
-``test_suite``
- A string naming a ``unittest.TestCase`` subclass (or a module containing
- one or more of them, or a method of such a subclass), or naming a function
- that can be called with no arguments and returns a ``unittest.TestSuite``.
- Specifying this argument enables use of the `test`_ command to run the
- specified test suite, e.g. via ``setup.py test``. See the section on the
- `test`_ command below for more details.
-
``namespace_packages``
A list of strings naming the project's "namespace packages". A namespace
package is a package that may be split across multiple project
@@ -180,6 +179,14 @@ unless you need the associated ``setuptools`` feature.
does not contain any code. See the section below on `Namespace Packages`_
for more information.
+``test_suite``
+ A string naming a ``unittest.TestCase`` subclass (or a module containing
+ one or more of them, or a method of such a subclass), or naming a function
+ that can be called with no arguments and returns a ``unittest.TestSuite``.
+ Specifying this argument enables use of the `test`_ command to run the
+ specified test suite, e.g. via ``setup.py test``. See the section on the
+ `test`_ command below for more details.
+
``eager_resources``
A list of strings naming resources that should be extracted together, if
any of them is needed, or if any C extensions included in the project are
@@ -516,7 +523,72 @@ either C or an external program that needs "real" files in your project before
there's any possibility of ``eager_resources`` being relevant to your project.
+Extensible Applications and Frameworks
+======================================
+
+
+Dynamic Discovery of Services and Plugins
+-----------------------------------------
+
+``setuptools`` supports creating libraries that "plug in" to extensible
+applications and frameworks, by letting you register "entry points" in your
+project that can be imported by the application or framework.
+
+For example, suppose that a blogging tool wants to support plugins
+that provide translation for various file types to the blog's output format.
+The framework might define an "entry point group" called ``blogtool.parsers``,
+and then allow plugins to register entry points for the file extensions they
+support.
+
+This would allow people to create distributions that contain one or more
+parsers for different file types, and then the blogging tool would be able to
+find the parsers at runtime by looking up an entry point for the file
+extension (or mime type, or however it wants to).
+
+Note that if the blogging tool includes parsers for certain file formats, it
+can register these as entry points in its own setup script, which means it
+doesn't have to special-case its built-in formats. They can just be treated
+the same as any other plugin's entry points would be.
+If you're creating a project that plugs in to an existing application or
+framework, you'll need to know what entry points or entry point groups are
+defined by that application or framework. Then, you can register entry points
+in your setup script. Here are a few examples of ways you might register an
+``.rst`` file parser entry point in the ``blogtool.parsers`` entry point group,
+for our hypothetical blogging tool::
+
+ setup(
+ # ...
+ entry_points = {'blogtool.parsers': '.rst = some_module:SomeClass'}
+ )
+
+ setup(
+ # ...
+ entry_points = {'blogtool.parsers': ['.rst = some_module:a_func']}
+ )
+
+ setup(
+ # ...
+ entry_points = """
+ [blogtool.parsers]
+ .rst = some.nested.module:SomeClass.some_classmethod [reST]
+ """,
+ extras_require = dict(reST = "Docutils>=0.3.5")
+ )
+
+The ``entry_points`` argument to ``setup()`` accepts either a string with
+``.ini``-style sections, or a dictionary mapping entry point group names to
+either strings or lists of strings containing entry point specifiers. An
+entry point specifier consists of a name and value, separated by an ``=``
+sign. The value consists of a dotted module name, optionally followed by a
+``:`` and a dotted identifier naming an object within the module. It can
+also include a bracketed list of "extras" that are required for the entry
+point to be used. When the invoking application or framework requests loading
+of an entry point, any requirements implied by the associated extras will be
+passed to ``pkg_resources.require()``, so that an appropriate error message
+can be displayed if the needed package(s) are missing. (Of course, the
+invoking app or framework can ignore such errors if it wants to make an entry
+point optional if a requirement isn't installed.)
"Development Mode"
@@ -1072,12 +1144,13 @@ Last, but not least, the ``develop`` command invokes the ``build_ext -i``
command to ensure any C extensions in the project have been built and are
up-to-date, and the ``egg_info`` command to ensure the project's metadata is
updated (so that the runtime and wrappers know what the project's dependencies
-are). If you make changes to the project's metadata or C extensions, you
-should rerun the ``develop`` command (or ``egg_info``, or ``build_ext -i``) in
-order to keep the project up-to-date. If you add or rename any of the
-project's scripts, you should re-run ``develop`` against all relevant staging
-areas to update the wrapper scripts. Most other kinds of changes to your
-project should not require any build operations or rerunning ``develop``.
+are). If you make any changes to the project's setup script or C extensions,
+you should rerun the ``develop`` command against all relevant staging areas to
+keep the project's scripts, metadata and extensions up-to-date. Most other
+kinds of changes to your project should not require any build operations or
+rerunning ``develop``, but keep in mind that even minor changes to the setup
+script (e.g. changing an entry point definition) require you to re-run the
+``develop`` or ``test`` commands to keep the distribution updated.
Here are the options that the ``develop`` command accepts. Note that they
affect the project's dependencies as well as the project itself, so if you have
@@ -1442,8 +1515,39 @@ in this page <setuptools?action=subscribe>`_ to see when new documentation is
added or updated.
+Adding Commands
+===============
+
+You can create add-on packages that extend setuptools with additional commands
+by defining entry points in the ``distutils.commands`` group. For example, if
+you wanted to add a ``foo`` command, you might add something like this to your
+setup script::
+
+ setup(
+ # ...
+ entry_points = {
+ "distutils.commands": [
+ "foo = mypackage.some_module:foo",
+ ],
+ },
+ )
+
+Assuming, of course, that the ``foo`` class in ``mypackage.some_module`` is
+a ``setuptools.Command`` subclass.
+
+Once a project containing such entry points has been activated on ``sys.path``,
+(e.g. by running "install" or "develop" with a site-packages installation
+directory) the command(s) will be available to any ``setuptools``-based setup
+scripts. It is not necessary to use the ``--command-packages`` option or
+to monkeypatch the ``distutils.command`` package to install your commands;
+``setuptools`` automatically adds a wrapper to the distutils to search for
+entry points in the active distributions on ``sys.path``. In fact, this is
+how setuptools' own commands are installed: the setuptools setup script defines
+entry points for them.
+
+
Subclassing ``Command``
-=======================
+-----------------------
XXX
@@ -1492,9 +1596,15 @@ Release Notes/Change History
* Fixed ``pkg_resources.resource_exists()`` not working correctly, along with
some other resource API bugs.
+ * Added ``entry_points`` argument to ``setup()``
* Many ``pkg_resources`` API changes and enhancements:
+ * Added ``EntryPoint``, ``get_entry_map``, ``load_entry_point``, and
+ ``get_entry_info`` APIs for dynamic plugin discovery.
+
+ * ``list_resources`` is now ``resource_listdir`` (and it actually works)
+
* Resource API functions like ``resource_string()`` that accepted a package
name and resource name, will now also accept a ``Requirement`` object in
place of the package name (to allow access to non-package data files in
@@ -1532,7 +1642,8 @@ Release Notes/Change History
* Distribution objects no longer have an ``installed_on()`` method, and the
``install_on()`` method is now ``activate()`` (but may go away altogether
- soon). The ``depends()`` method has also been renamed to ``requires()``.
+ soon). The ``depends()`` method has also been renamed to ``requires()``,
+ and ``InvalidOption`` is now ``UnknownExtra``.
* ``find_distributions()`` now takes an additional argument called ``only``,
that tells it to only yield distributions whose location is the passed-in
diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py
index 29f3000d..a58b5344 100644
--- a/setuptools/command/__init__.py
+++ b/setuptools/command/__init__.py
@@ -1,17 +1,10 @@
-import distutils.command
-
__all__ = [
- 'test', 'develop', 'bdist_egg', 'saveopts', 'setopt', 'rotate', 'alias'
+ 'alias', 'bdist_egg', 'build_ext', 'build_py', 'depends', 'develop',
+ 'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts',
+ 'sdist', 'setopt', 'test', 'upload',
]
-# Make our commands available as though they were part of the distutils
-
-distutils.command.__path__.extend(__path__)
-distutils.command.__all__.extend(
- [cmd for cmd in __all__ if cmd not in distutils.command.__all__]
- )
-
from distutils.command.bdist import bdist
if 'egg' not in bdist.format_commands:
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index a5418568..8577230f 100755
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -8,7 +8,7 @@ from setuptools import Command
from distutils.errors import *
from distutils import log
from pkg_resources import parse_requirements, safe_name, \
- safe_version, yield_lines
+ safe_version, yield_lines, EntryPoint
from setuptools.dist import iter_distribution_names
class egg_info(Command):
@@ -95,7 +95,7 @@ class egg_info(Command):
metadata.write_pkg_info(self.egg_info)
finally:
metadata.name, metadata.version = oldname, oldver
-
+ self.write_entry_points()
self.write_requirements()
self.write_toplevel_names()
self.write_or_delete_dist_arg('namespace_packages')
@@ -183,23 +183,23 @@ class egg_info(Command):
if not self.dry_run:
os.unlink(filename)
+ def write_entry_points(self):
+ ep = getattr(self.distribution,'entry_points',None)
+ if ep is None:
+ return
+ epname = os.path.join(self.egg_info,"entry_points.txt")
+ log.info("writing %s", epname)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ if not self.dry_run:
+ f = open(epname, 'wt')
+ if isinstance(ep,basestring):
+ f.write(ep)
+ else:
+ for section, contents in ep.items():
+ if not isinstance(contents,basestring):
+ contents = EntryPoint.parse_list(section, contents)
+ contents = '\n'.join(map(str,contents.values()))
+ f.write('[%s]\n%s\n\n' % (section,contents))
+ f.close()
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 3c7ff852..a603ade0 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -11,6 +11,32 @@ from distutils.errors import DistutilsOptionError, DistutilsPlatformError
from distutils.errors import DistutilsSetupError
import setuptools, pkg_resources
+def get_command_class(self, command):
+ """Pluggable version of get_command_class()"""
+ if command in self.cmdclass:
+ return self.cmdclass[command]
+
+ for dist in pkg_resources.working_set:
+ if dist.get_entry_info('distutils.commands',command):
+ cmdclass = dist.load_entry_point('distutils.commands',command)
+ self.cmdclass[command] = cmdclass
+ return cmdclass
+ else:
+ return _old_get_command_class(self, command)
+
+def print_commands(self):
+ for dist in pkg_resources.working_set:
+ for cmd,ep in dist.get_entry_map('distutils.commands').items():
+ if cmd not in self.cmdclass:
+ cmdclass = ep.load() # don't require extras, we're not running
+ self.cmdclass[cmd] = cmdclass
+ return _old_print_commands(self)
+
+for meth in 'print_commands', 'get_command_class':
+ if getattr(_Distribution,meth).im_func.func_globals is not globals():
+ globals()['_old_'+meth] = getattr(_Distribution,meth)
+ setattr(_Distribution, meth, globals()[meth])
+
sequence = tuple, list
class Distribution(_Distribution):
@@ -80,6 +106,21 @@ class Distribution(_Distribution):
distribution for the included and excluded features.
"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
def __init__ (self, attrs=None):
have_package_data = hasattr(self, "package_data")
if not have_package_data:
@@ -93,15 +134,9 @@ class Distribution(_Distribution):
self.zip_safe = None
self.namespace_packages = None
self.eager_resources = None
+ self.entry_points = None
_Distribution.__init__(self,attrs)
- if not have_package_data:
- from setuptools.command.build_py import build_py
- self.cmdclass.setdefault('build_py',build_py)
- self.cmdclass.setdefault('build_ext',build_ext)
- self.cmdclass.setdefault('install',install)
- self.cmdclass.setdefault('install_lib',install_lib)
- self.cmdclass.setdefault('sdist',sdist)
def parse_command_line(self):
"""Process features after parsing command line options"""
@@ -121,6 +156,12 @@ class Distribution(_Distribution):
+
+
+
+
+
+
def finalize_options(self):
_Distribution.finalize_options(self)
@@ -171,6 +212,12 @@ class Distribution(_Distribution):
"namespace package %r" % nsp
)
+ if self.entry_points is not None:
+ try:
+ pkg_resources.EntryPoint.parse_map(self.entry_points)
+ except ValueError, e:
+ raise DistutilsSetupError(e)
+
def _set_global_opts_from_features(self):
"""Add --with-X/--without-X options based on optional features"""
@@ -197,12 +244,6 @@ class Distribution(_Distribution):
-
-
-
-
-
-
def _finalize_features(self):
"""Add/remove features and resolve dependencies between them"""
@@ -420,7 +461,7 @@ class Distribution(_Distribution):
src,alias = aliases[command]
del aliases[command] # ensure each alias can expand only once!
import shlex
- args[:1] = shlex.split(alias,True)
+ args[:1] = shlex.split(alias,True)
command = args[0]
nargs = _Distribution._parse_command_opts(self, parser, args)
diff --git a/setuptools/tests/test_resources.py b/setuptools/tests/test_resources.py
index 3345311a..5392e59f 100644
--- a/setuptools/tests/test_resources.py
+++ b/setuptools/tests/test_resources.py
@@ -185,7 +185,7 @@ class DistroTests(TestCase):
d,"Twisted>=1.5 fcgiapp>=0.1 ZConfig>=2.0 docutils>=0.3".split(),
["fastcgi", "docgen"]
)
- self.assertRaises(InvalidOption, d.requires, ["foo"])
+ self.assertRaises(UnknownExtra, d.requires, ["foo"])
@@ -203,6 +203,88 @@ class DistroTests(TestCase):
+class EntryPointTests(TestCase):
+
+ def assertfields(self, ep):
+ self.assertEqual(ep.name,"foo")
+ self.assertEqual(ep.module_name,"setuptools.tests.test_resources")
+ self.assertEqual(ep.attrs, ("EntryPointTests",))
+ self.assertEqual(ep.extras, ("x",))
+ self.failUnless(ep.load() is EntryPointTests)
+ self.assertEqual(
+ str(ep),
+ "foo = setuptools.tests.test_resources:EntryPointTests [x]"
+ )
+
+ def testBasics(self):
+ ep = EntryPoint(
+ "foo", "setuptools.tests.test_resources", ["EntryPointTests"],
+ ["x"]
+ )
+ self.assertfields(ep)
+
+ def testParse(self):
+ s = "foo = setuptools.tests.test_resources:EntryPointTests [x]"
+ ep = EntryPoint.parse(s)
+ self.assertfields(ep)
+
+ ep = EntryPoint.parse("bar baz= spammity[PING]")
+ self.assertEqual(ep.name,"bar baz")
+ self.assertEqual(ep.module_name,"spammity")
+ self.assertEqual(ep.attrs, ())
+ self.assertEqual(ep.extras, ("ping",))
+
+ ep = EntryPoint.parse(" fizzly = wocka:foo")
+ self.assertEqual(ep.name,"fizzly")
+ self.assertEqual(ep.module_name,"wocka")
+ self.assertEqual(ep.attrs, ("foo",))
+ self.assertEqual(ep.extras, ())
+
+
+
+
+
+ def testRejects(self):
+ for ep in [
+ "foo", "x=1=2", "x=a:b:c", "q=x/na", "fez=pish:tush-z", "x=f[a]>2",
+ ]:
+ try: EntryPoint.parse(ep)
+ except ValueError: pass
+ else: raise AssertionError("Should've been bad", ep)
+
+ def checkSubMap(self, m):
+ self.assertEqual(str(m),
+ "{"
+ "'feature2': EntryPoint.parse("
+ "'feature2 = another.module:SomeClass [extra1,extra2]'), "
+ "'feature1': EntryPoint.parse("
+ "'feature1 = somemodule:somefunction')"
+ "}"
+ )
+
+ submap_str = """
+ # define features for blah blah
+ feature1 = somemodule:somefunction
+ feature2 = another.module:SomeClass [extra1,extra2]
+ """
+
+ def testParseList(self):
+ self.checkSubMap(EntryPoint.parse_list("xyz", self.submap_str))
+ self.assertRaises(ValueError, EntryPoint.parse_list, "x a", "foo=bar")
+ self.assertRaises(ValueError, EntryPoint.parse_list, "x",
+ ["foo=baz", "foo=bar"])
+
+ def testParseMap(self):
+ m = EntryPoint.parse_map({'xyz':self.submap_str})
+ self.checkSubMap(m['xyz'])
+ self.assertEqual(m.keys(),['xyz'])
+ m = EntryPoint.parse_map("[xyz]\n"+self.submap_str)
+ self.checkSubMap(m['xyz'])
+ self.assertEqual(m.keys(),['xyz'])
+ self.assertRaises(ValueError, EntryPoint.parse_map, ["[xyz]", "[xyz]"])
+ self.assertRaises(ValueError, EntryPoint.parse_map, self.submap_str)
+
+
class RequirementsTests(TestCase):
def testBasics(self):