[Python-checkins] python/nondist/sandbox/setuptools pkg_resources.py, 1.52, 1.53 setup.py, 1.32, 1.33 setuptools.txt, 1.22, 1.23

pje@users.sourceforge.net pje at users.sourceforge.net
Mon Jul 25 00:47:08 CEST 2005


Update of /cvsroot/python/python/nondist/sandbox/setuptools
In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv6325

Modified Files:
	pkg_resources.py setup.py setuptools.txt 
Log Message:
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.


Index: pkg_resources.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/setuptools/pkg_resources.py,v
retrieving revision 1.52
retrieving revision 1.53
diff -u -d -r1.52 -r1.53
--- pkg_resources.py	24 Jul 2005 17:59:26 -0000	1.52
+++ pkg_resources.py	24 Jul 2005 22:47:05 -0000	1.53
@@ -12,21 +12,6 @@
 .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 @@
 
 
 
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+__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 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 @@
     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 @@
 
 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 @@
             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 @@
             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 @@
 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 @@
 
 
 
+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 @@
             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 @@
 
 
 
+    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 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 @@
 
 
 
-
 # Set up global resource manager
 
 _manager = ResourceManager()

Index: setup.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/setuptools/setup.py,v
retrieving revision 1.32
retrieving revision 1.33
diff -u -d -r1.32 -r1.33
--- setup.py	18 Jul 2005 02:06:15 -0000	1.32
+++ setup.py	24 Jul 2005 22:47:05 -0000	1.33
@@ -18,6 +18,9 @@
 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 @@
     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 @@
 
 
 
-
-
-
-
-
-
-
-
-

Index: setuptools.txt
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/setuptools/setuptools.txt,v
retrieving revision 1.22
retrieving revision 1.23
diff -u -d -r1.22 -r1.23
--- setuptools.txt	24 Jul 2005 17:59:26 -0000	1.22
+++ setuptools.txt	24 Jul 2005 22:47:05 -0000	1.23
@@ -153,21 +153,20 @@
     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 @@
     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 @@
 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 @@
 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 @@
 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 @@
  * 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 @@
 
    * 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



More information about the Python-checkins mailing list