Commit 1c40632b authored by PJ Eby's avatar PJ Eby

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.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4041152
parent 8618cfa8
......@@ -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()
......
......@@ -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
......@@ -63,15 +72,6 @@ setup(
......
[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
......@@ -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
......
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:
......
......@@ -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()
......@@ -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)
......
......@@ -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):
......
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