[Python-checkins] python/nondist/sandbox/setuptools api_tests.txt, NONE, 1.1 EasyInstall.txt, 1.43, 1.44 pkg_resources.py, 1.45, 1.46 setuptools.txt, 1.18, 1.19

pje@users.sourceforge.net pje at users.sourceforge.net
Mon Jul 18 03:39:47 CEST 2005


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

Modified Files:
	EasyInstall.txt pkg_resources.py setuptools.txt 
Added Files:
	api_tests.txt 
Log Message:
Massive API refactoring; see setuptools.txt changelog for details.  Also,
add ``#egg=project-version`` link support, and docs on how to make your
package available for EasyInstall to find.


--- NEW FILE: api_tests.txt ---
Pluggable Distributions of Python Software
==========================================

Distributions
-------------

A "Distribution" is a collection of files that represent a "Release" of a
"Project" as of a particular point in time, denoted by a
"Version"::

    >>> import sys, pkg_resources
    >>> from pkg_resources import Distribution
    >>> Distribution(project_name="Foo", version="1.2")
    Foo 1.2

Distributions have a location, which can be a filename, URL, or really anything
else you care to use::

    >>> dist = Distribution(
    ...     location="http://example.com/something",
    ...     project_name="Bar", version="0.9"
    ... )

    >>> dist
    Bar 0.9 (http://example.com/something)


Distributions have various introspectable attributes::

    >>> dist.location
    'http://example.com/something'

    >>> dist.project_name
    'Bar'

    >>> dist.version
    '0.9'

    >>> dist.py_version == sys.version[:3]
    True

    >>> print dist.platform
    None

Including various computed attributes::

    >>> from pkg_resources import parse_version
    >>> dist.parsed_version == parse_version(dist.version)
    True

    >>> dist.key    # case-insensitive form of the project name
    'bar'

Distributions are compared (and hashed) by version first::

    >>> Distribution(version='1.0') == Distribution(version='1.0')
    True
    >>> Distribution(version='1.0') == Distribution(version='1.1')
    False
    >>> Distribution(version='1.0') <  Distribution(version='1.1')
    True

but also by project name (case-insensitive), platform, Python version,
location, etc.::

    >>> Distribution(project_name="Foo",version="1.0") == \
    ... Distribution(project_name="Foo",version="1.0")
    True

    >>> Distribution(project_name="Foo",version="1.0") == \
    ... Distribution(project_name="foo",version="1.0")
    True

    >>> Distribution(project_name="Foo",version="1.0") == \
    ... Distribution(project_name="Foo",version="1.1")
    False

    >>> Distribution(project_name="Foo",py_version="2.3",version="1.0") == \
    ... Distribution(project_name="Foo",py_version="2.4",version="1.0")
    False

    >>> Distribution(location="spam",version="1.0") == \
    ... Distribution(location="spam",version="1.0")
    True

    >>> Distribution(location="spam",version="1.0") == \
    ... Distribution(location="baz",version="1.0")
    False



Hash and compare distribution by prio/plat

Get version from metadata
provider capabilities
egg_name()
as_requirement()
from_location, from_filename (w/path normalization)

Releases may have zero or more "Requirements", which indicate
what releases of another project the release requires in order to
function.  A Requirement names the other project, expresses some criteria
as to what releases of that project are acceptable, and lists any "Extras"
that the requiring release may need from that project.  (An Extra is an
optional feature of a Release, that can only be used if its additional
Requirements are satisfied.)



The Working Set
---------------

A collection of active distributions is called a Working Set.  Note that a
Working Set can contain any importable distribution, not just pluggable ones.
For example, the Python standard library is an importable distribution that
will usually be part of the Working Set, even though it is not pluggable.
Similarly, when you are doing development work on a project, the files you are
editing are also a Distribution.  (And, with a little attention to the
directory names used,  and including some additional metadata, such a
"development distribution" can be made pluggable as well.)

    >>> from pkg_resources import WorkingSet

A working set's entries are the sys.path entries that correspond to the active
distributions.  By default, the working set's entries are the items on
``sys.path``::

    >>> ws = WorkingSet()
    >>> ws.entries == sys.path
    True

But you can also create an empty working set explicitly, and add distributions
to it::

    >>> ws = WorkingSet([])
    >>> ws.add(dist)
    >>> ws.entries
    ['http://example.com/something']
    >>> dist in ws
    True
    >>> Distribution('foo') in ws
    False

And you can iterate over its distributions::

    >>> list(ws)
    [Bar 0.9 (http://example.com/something)]

Adding the same distribution more than once is a no-op::

    >>> ws.add(dist)
    >>> list(ws)
    [Bar 0.9 (http://example.com/something)]

For that matter, adding multiple distributions for the same project also does
nothing, because a working set can only hold one active distribution per
project -- the first one added to it::

    >>> ws.add(
    ...     Distribution(
    ...         'http://example.com/something', project_name="Bar",
    ...         version="7.2"
    ...     )
    ... )
    >>> list(ws)
    [Bar 0.9 (http://example.com/something)]

You can append a path entry to a working set using ``add_entry()``::

    >>> ws.entries
    ['http://example.com/something']
    >>> ws.add_entry(pkg_resources.__file__)
    >>> ws.entries
    ['http://example.com/something', '...pkg_resources.py...']

Multiple additions result in multiple entries, even if the entry is already in
the working set (because ``sys.path`` can contain the same entry more than
once)::

    >>> ws.add_entry(pkg_resources.__file__)
    >>> ws.entries
    ['...example.com...', '...pkg_resources...', '...pkg_resources...']

And you can specify the path entry a distribution was found under, using the
optional second parameter to ``add()``

    >>> ws.add(dist,"foo")
    >>> ws.add(dist,"bar")
    >>> ws.entries
    ['http://example.com/something', ..., 'foo', 'bar']

But even if a distribution is found under multiple path entries, it still only
shows up once when iterating the working set:

    >>> list(ws)
    [Bar 0.9 (http://example.com/something)]

You can ask a WorkingSet to ``find()`` a distribution matching a requirement::

    >>> from pkg_resources import Requirement
    >>> print ws.find(Requirement.parse("Foo==1.0"))    # no match, return None
    None

    >>> ws.find(Requirement.parse("Bar==0.9"))  # match, return distribution
    Bar 0.9 (http://example.com/something)

Note that asking for a conflicting version of a distribution already in a
working set triggers a ``pkg_resources.VersionConflict`` error:

    >>> ws.find(Requirement.parse("Bar==1.0")) # doctest: +NORMALIZE_WHITESPACE
    Traceback (most recent call last):
      ...
    VersionConflict: (Bar 0.9 (http://example.com/something),
                      Requirement.parse('Bar==1.0'))

You can subscribe a callback function to receive notifications whenever a new
distribution is added to a working set.  The callback is immediately invoked
once for each existing distribution in the working set, and then is called
again for new distributions added thereafter::

    >>> def added(dist): print "Added", dist
    >>> ws.subscribe(added)
    Added Bar 0.9
    >>> foo12 = Distribution(project_name="Foo", version="1.2") 
    >>> ws.add(foo12)
    Added Foo 1.2

Note, however, that only the first distribution added for a given project name
will trigger a callback, even during the initial ``subscribe()`` callback::

    >>> foo14 = Distribution(project_name="Foo", version="1.4") 
    >>> ws.add(foo14)   # no callback, because Foo 1.2 is already active

    >>> ws = WorkingSet([])
    >>> ws.add(foo12)
    >>> ws.add(foo14)
    >>> ws.subscribe(added)
    Added Foo 1.2
    
And adding a callback more than once has no effect, either::

    >>> ws.subscribe(added)     # no callbacks

    # and no double-callbacks on subsequent additions, either
    >>> ws.add(Distribution(project_name="JustATest", version="0.99"))
    Added JustATest 0.99


Index: EasyInstall.txt
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/setuptools/EasyInstall.txt,v
retrieving revision 1.43
retrieving revision 1.44
diff -u -d -r1.43 -r1.44
--- EasyInstall.txt	16 Jul 2005 21:57:50 -0000	1.43
+++ EasyInstall.txt	18 Jul 2005 01:39:45 -0000	1.44
@@ -623,6 +623,22 @@
    Made ``easy-install.pth`` work in platform-specific alternate site
    directories (e.g. ``~/Library/Python/2.x/site-packages``).
 
+ * If you manually delete the current version of a package, the next run of
+   EasyInstall against the target directory will now remove the stray entry
+   from the ``easy-install.pth``file.
+
+ * EasyInstall now recognizes URLs with a ``#egg=project_name`` fragment ID
+   as pointing to the named project's source checkout.  Such URLs have a lower
+   match precedence than any other kind of distribution, so they'll only be
+   used if they have a higher version number than any other available
+   distribution.  (Future versions may allow you to specify that you want to
+   use source checkouts instead of other kinds of distributions.)  The ``#egg``
+   fragment can contain a version if it's formatted as ``#egg=proj-ver``,
+   where ``proj`` is the project name, and ``ver`` is the version number.  You
+   *must* use the format for these values that the ``bdist_egg`` command uses;
+   i.e., all non-alphanumeric runs must be condensed to single underscore
+   characters.
+
 0.5a12
  * Fix ``python -m easy_install`` not working due to setuptools being installed
    as a zipfile.  Update safety scanner to check for modules that might be used

Index: pkg_resources.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/setuptools/pkg_resources.py,v
retrieving revision 1.45
retrieving revision 1.46
diff -u -d -r1.45 -r1.46
--- pkg_resources.py	17 Jul 2005 19:54:37 -0000	1.45
+++ pkg_resources.py	18 Jul 2005 01:39:45 -0000	1.46
@@ -23,7 +23,8 @@
     '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',
+    'get_default_cache', 'EmptyProvider', 'empty_provider', 'normalize_path',
+    'WorkingSet', 'working_set', 'add_activation_listener', 'CHECKOUT_DIST',
 ]
 
 import sys, os, zipimport, time, re, imp
@@ -38,7 +39,6 @@
 
 
 
-
 class ResolutionError(Exception):
     """Abstract base for dependency resolution errors"""
 
@@ -57,6 +57,7 @@
 EGG_DIST    = 3
 BINARY_DIST = 2
 SOURCE_DIST = 1
+CHECKOUT_DIST = 0
 
 def register_loader_type(loader_type, provider_factory):
     """Register `provider_factory` to make providers for `loader_type`
@@ -79,7 +80,6 @@
 
 
 
-
 def get_platform():
     """Return this platform's string for platform-specific distributions
 
@@ -203,6 +203,211 @@
 
 
 
+class WorkingSet(object):
+    """A collection of active distributions on sys.path (or a similar list)"""
+
+    def __init__(self, entries=None):
+        """Create working set from list of path entries (default=sys.path)"""
+        self.entries = []
+        self.entry_keys = {}
+        self.by_key = {}
+        self.callbacks = []
+
+        if entries is None:
+            entries = sys.path
+
+        for entry in entries:
+            self.add_entry(entry)
+
+
+    def add_entry(self, entry):
+        """Add a path item to ``.entries``, finding any distributions on it
+
+        ``find_distributions(entry,False)`` is used to find distributions
+        corresponding to the path entry, and they are added.  `entry` is
+        always appended to ``.entries``, even if it is already present.
+        (This is because ``sys.path`` can contain the same value more than
+        once, and the ``.entries`` of the ``sys.path`` WorkingSet should always
+        equal ``sys.path``.)
+        """
+        self.entry_keys.setdefault(entry, [])
+        self.entries.append(entry)
+        for dist in find_distributions(entry, False):
+            self.add(dist, entry)
+
+
+    def __contains__(self,dist):
+        """True if `dist` is the active distribution for its project"""
+        return self.by_key.get(dist.key) == dist
+
+
+
+
+
+    def __iter__(self):
+        """Yield distributions for non-duplicate projects in the working set
+
+        The yield order is the order in which the items' path entries were
+        added to the working set.
+        """
+        for item in self.entries:
+            for key in self.entry_keys[item]:
+                yield self.by_key[key]
+
+
+    def find(self, req):
+        """Find a distribution matching requirement `req`
+
+        If there is an active distribution for the requested project, this
+        returns it as long as it meets the version requirement specified by
+        `req`.  But, if there is an active distribution for the project and it
+        does *not* meet the `req` requirement, ``VersionConflict`` is raised.
+        If there is no active distribution for the requested project, ``None``
+        is returned.
+        """
+        dist = self.by_key.get(req.key)
+        if dist is not None and dist not in req:
+            raise VersionConflict(dist,req)     # XXX add more info
+        else:
+            return dist
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+    def add(self, dist, entry=None):
+        """Add `dist` to working set, associated with `entry`
+
+        If `entry` is unspecified, it defaults to the ``.location`` of `dist`.
+        On exit from this routine, `entry` is added to the end of the working
+        set's ``.entries`` (if it wasn't already present).
+
+        `dist` is only added to the working set if it's for a project that
+        doesn't already have a distribution in the set.  If it's added, any
+        callbacks registered with the ``subscribe()`` method will be called.
+        """
+        if entry is None:
+            entry = dist.location
+
+        if entry not in self.entry_keys:
+            self.entries.append(entry)
+            self.entry_keys[entry] = []
+
+        if dist.key in self.by_key:
+            return      # ignore hidden distros
+
+        self.by_key[dist.key] = dist
+        keys = self.entry_keys[entry]
+
+        if dist.key not in keys:
+            keys.append(dist.key)
+
+        self._added_new(dist)
+
+
+
+
+
+
+
+
+
+
+
+
+
+    def resolve(self, requirements, env=None, installer=None):
+        """List all distributions needed to (recursively) meet `requirements`
+
+        `requirements` must be a sequence of ``Requirement`` objects.  `env`,
+        if supplied, should be an ``AvailableDistributions`` instance.  If
+        not supplied, it defaults to all distributions available within any
+        entry or distribution in the working set.  `installer`, if supplied,
+        will be invoked with each requirement that cannot be met by an
+        already-installed distribution; it should return a ``Distribution`` or
+        ``None``.
+        """
+        if env is None:
+            env = AvailableDistributions(self.entries)
+
+        requirements = list(requirements)[::-1]  # set up the stack
+        processed = {}  # set of processed requirements
+        best = {}       # key -> dist
+        to_activate = []
+
+        while requirements:
+            req = requirements.pop()
+            if req in processed:
+                # Ignore cyclic or redundant dependencies
+                continue
+
+            dist = best.get(req.key)
+            if dist is None:
+                # Find the best distribution and add it to the map
+                dist = best[req.key] = env.best_match(req, self, installer)
+                if dist is None:
+                    raise DistributionNotFound(req)  # XXX put more info here
+                to_activate.append(dist)
+            elif dist not in req:
+                # Oops, the "best" so far conflicts with a dependency
+                raise VersionConflict(dist,req) # XXX put more info here
+
+            requirements.extend(dist.depends(req.extras)[::-1])
+            processed[req] = True
+
+        return to_activate    # return list of distros to activate
+
+    def require(self, *requirements):
+        """Ensure that distributions matching `requirements` are activated
+
+        `requirements` must be a string or a (possibly-nested) sequence
+        thereof, specifying the distributions and versions required.  The
+        return value is a sequence of the distributions that needed to be
+        activated to fulfill the requirements; all relevant distributions are
+        included, even if they were already activated in this working set.
+        """
+
+        needed = self.resolve(parse_requirements(requirements))
+
+        for dist in needed:
+            self.add(dist)
+
+        return needed
+
+
+    def subscribe(self, callback):
+        """Invoke `callback` for all distributions (including existing ones)"""
+        if callback in self.callbacks:
+            return
+        self.callbacks.append(callback)
+        for dist in self:
+            callback(dist)
+
+
+    def _added_new(self, dist):
+        for callback in self.callbacks:
+            callback(dist)
+
+
+
+
+
+
+
+
+
+
+
 class AvailableDistributions(object):
     """Searchable snapshot of distributions on a search path"""
 
@@ -296,69 +501,27 @@
         """Remove `dist` from the distribution map"""
         self._distmap[dist.key].remove(dist)
 
-    def best_match(self, requirement, path=None, installer=None):
-        """Find distribution best matching `requirement` and usable on `path`
+    def best_match(self, req, working_set, installer=None):
+        """Find distribution best matching `req` and usable on `working_set`
 
-        If a distribution that's already installed on `path` is unsuitable,
+        If a distribution that's already active in `working_set` is unsuitable,
         a VersionConflict is raised.  If one or more suitable distributions are
-        already installed, the leftmost distribution (i.e., the one first in
+        already active, the leftmost distribution (i.e., the one first in
         the search path) is returned.  Otherwise, the available distribution
-        with the highest version number is returned, or a deferred distribution
-        object is returned if a suitable ``obtain()`` method exists.  If there
-        is no way to meet the requirement, None is returned.
+        with the highest version number is returned.  If nothing is available,
+        returns ``obtain(req,installer)`` or ``None`` if no distribution can
+        be obtained.
         """
-        if path is None:
-            path = sys.path
-
-        distros = self.get(requirement.key, ())
-        find = dict([(dist.location,dist) for dist in distros]).get
-
-        for item in path:
-            dist = find(item)
-            if dist is not None:
-                if dist in requirement:
-                    return dist
-                else:
-                    raise VersionConflict(dist,requirement) # XXX add more info
+        dist = working_set.find(req)
+        if dist is not None:
+            return dist
 
-        for dist in distros:
-            if dist in requirement:
+        for dist in self.get(req.key, ()):
+            if dist in req:
                 return dist
-        return self.obtain(requirement, installer) # try and download/install
 
-    def resolve(self, requirements, path=None, installer=None):
-        """List all distributions needed to (recursively) meet requirements"""
-
-        if path is None:
-            path = sys.path
-
-        requirements = list(requirements)[::-1]  # set up the stack
-        processed = {}  # set of processed requirements
-        best = {}       # key -> dist
-        to_install = []
-
-        while requirements:
-            req = requirements.pop()
-            if req in processed:
-                # Ignore cyclic or redundant dependencies
-                continue
-
-            dist = best.get(req.key)
-            if dist is None:
-                # Find the best distribution and add it to the map
-                dist = best[req.key] = self.best_match(req, path, installer)
-                if dist is None:
-                    raise DistributionNotFound(req)  # XXX put more info here
-                to_install.append(dist)
-
-            elif dist not in req:
-                # Oops, the "best" so far conflicts with a dependency
-                raise VersionConflict(dist,req) # XXX put more info here
-
-            requirements.extend(dist.depends(req.extras)[::-1])
-            processed[req] = True
+        return self.obtain(req, installer) # try and download/install
 
-        return to_install    # return list of distros to install
 
     def obtain(self, requirement, installer=None):
         """Obtain a distro that matches requirement (e.g. via download)"""
@@ -367,6 +530,7 @@
 
     def __len__(self): return len(self._distmap)
 
+
 class ResourceManager:
     """Manage resource extraction and packages"""
 
@@ -531,23 +695,6 @@
             "Please set the PYTHON_EGG_CACHE enviroment variable"
         )
 
-def require(*requirements):
-    """Ensure that distributions matching `requirements` are on ``sys.path``
-
-    `requirements` must be a string or a (possibly-nested) sequence
-    thereof, specifying the distributions and versions required.
-
-    XXX This doesn't support arbitrary PEP 302 sys.path items yet, because
-    ``find_distributions()`` is hardcoded at the moment.
-    """
-
-    requirements = parse_requirements(requirements)
-    to_install = AvailableDistributions().resolve(requirements)
-    for dist in to_install:
-        dist.install_on(sys.path)
-    return to_install
-
-
 def safe_name(name):
     """Convert an arbitrary string to a standard distribution name
 
@@ -572,6 +719,23 @@
 
 
 
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
 class NullProvider:
     """Try to implement resources and metadata for arbitrary PEP 302 loaders"""
 
@@ -1035,24 +1199,25 @@
     _distribution_finders[importer_type] = distribution_finder
 
 
-def find_distributions(path_item):
+def find_distributions(path_item, only=False):
     """Yield distributions accessible via `path_item`"""
     importer = get_importer(path_item)
     finder = _find_adapter(_distribution_finders, importer)
-    return finder(importer,path_item)
+    return finder(importer, path_item, only)
 
-def find_in_zip(importer,path_item):
+def find_in_zip(importer, path_item, only=False):
     metadata = EggMetadata(importer)
     if metadata.has_metadata('PKG-INFO'):
         yield Distribution.from_filename(path_item, metadata=metadata)
+    if only:
+        return  # don't yield nested distros
     for subitem in metadata.resource_listdir('/'):
         if subitem.endswith('.egg'):
             subpath = os.path.join(path_item, subitem)
             for dist in find_in_zip(zipimport.zipimporter(subpath), subpath):
                 yield dist
 
-register_finder(zipimport.zipimporter,find_in_zip)
-
+register_finder(zipimport.zipimporter, find_in_zip)
 
 def StringIO(*args, **kw):
     """Thunk to load the real StringIO on demand"""
@@ -1063,22 +1228,21 @@
         from StringIO import StringIO
     return StringIO(*args,**kw)
 
-
-def find_nothing(importer,path_item):
+def find_nothing(importer, path_item, only=False):
     return ()
-
 register_finder(object,find_nothing)
 
-def find_on_path(importer,path_item):
+def find_on_path(importer, path_item, only=False):
     """Yield distributions accessible on a sys.path directory"""
     if not os.path.exists(path_item):
         return
-    elif os.path.isdir(path_item):
+    path_item = normalize_path(path_item)
+    if os.path.isdir(path_item):
         if path_item.lower().endswith('.egg'):
             # unpacked egg
             yield Distribution.from_filename(
                 path_item, metadata=PathMetadata(
-                    path_item,os.path.join(path_item,'EGG-INFO')
+                    path_item, os.path.join(path_item,'EGG-INFO')
                 )
             )
         else:
@@ -1086,16 +1250,18 @@
             for entry in os.listdir(path_item):
                 fullpath = os.path.join(path_item, entry)
                 lower = entry.lower()
-                if lower.endswith('.egg'):
-                    for dist in find_distributions(fullpath):
-                        yield dist
-                elif lower.endswith('.egg-info'):
+                if lower.endswith('.egg-info'):
                     if os.path.isdir(fullpath):
                         # development egg
                         metadata = PathMetadata(path_item, fullpath)
                         dist_name = os.path.splitext(entry)[0]
-                        yield Distribution(path_item,metadata,project_name=dist_name)
-                elif lower.endswith('.egg-link'):
+                        yield Distribution(
+                            path_item, metadata, project_name=dist_name
+                        )
+                elif not only and lower.endswith('.egg'):
+                    for dist in find_distributions(fullpath):
+                        yield dist
+                elif not only and lower.endswith('.egg-link'):
                     for line in file(fullpath):
                         if not line.strip(): continue
                         for item in find_distributions(line.rstrip()):
@@ -1103,8 +1269,6 @@
 
 register_finder(ImpWrapper,find_on_path)
 
-
-
 _namespace_handlers = {}
 _namespace_packages = {}
 
@@ -1209,9 +1373,9 @@
 register_namespace_handler(object,null_ns_handler)
 
 
-
-
-
+def normalize_path(filename):
+    """Normalize a file/dir name for comparison purposes"""
+    return os.path.normcase(os.path.realpath(filename))
 
 
 
@@ -1312,10 +1476,9 @@
 
 class Distribution(object):
     """Wrap an actual or potential sys.path entry w/metadata"""
-
     def __init__(self,
-        location, metadata=None, project_name=None, version=None,
-        py_version=PY_MAJOR, platform=None, distro_type = EGG_DIST
+        location=None, metadata=None, project_name=None, version=None,
+        py_version=PY_MAJOR, platform=None, precedence = EGG_DIST
     ):
         self.project_name = safe_name(project_name or 'Unknown')
         if version is not None:
@@ -1323,15 +1486,9 @@
         self.py_version = py_version
         self.platform = platform
         self.location = location
-        self.distro_type = distro_type
+        self.precedence = precedence
         self._provider = metadata or empty_provider
 
-    def installed_on(self,path=None):
-        """Is this distro installed on `path`? (defaults to ``sys.path``)"""
-        if path is None:
-            path = sys.path
-        return self.location in path
-
     #@classmethod
     def from_location(cls,location,basename,metadata=None):
         project_name, version, py_version, platform = [None]*4
@@ -1348,7 +1505,14 @@
         )
     from_location = classmethod(from_location)
 
-
+    hashcmp = property(
+        lambda self: (
+            getattr(self,'parsed_version',()), self.precedence, self.key,
+            self.location, self.py_version, self.platform
+        )
+    )
+    def __cmp__(self, other): return cmp(self.hashcmp, other)
+    def __hash__(self): return hash(self.hashcmp)
 
 
     # These properties have to be lazy so that we don't have to load any
@@ -1389,7 +1553,7 @@
                 )
     version = property(version)
 
-        
+
 
 
     #@property
@@ -1424,7 +1588,7 @@
             for line in self.get_metadata_lines(name):
                 yield line
 
-    def install_on(self,path=None):
+    def activate(self,path=None):
         """Ensure distribution is importable on `path` (default=sys.path)"""
         if path is None: path = sys.path
         if self.location not in path:
@@ -1446,7 +1610,10 @@
         return filename
 
     def __repr__(self):
-        return "%s (%s)" % (self,self.location)
+        if self.location:
+            return "%s (%s)" % (self,self.location)
+        else:
+            return str(self)
 
     def __str__(self):
         version = getattr(self,'version',None) or "[unknown version]"
@@ -1460,19 +1627,13 @@
 
     #@classmethod
     def from_filename(cls,filename,metadata=None):
-        return cls.from_location(filename, os.path.basename(filename), metadata)
+        return cls.from_location(
+            normalize_path(filename), os.path.basename(filename), metadata
+        )
     from_filename = classmethod(from_filename)
 
     def as_requirement(self):
-        return Requirement.parse('%s==%s' % (dist.project_name, dist.version))
-
-
-
-
-
-
-
-
+        return Requirement.parse('%s==%s' % (self.project_name, self.version))
 
 
 
@@ -1538,9 +1699,9 @@
 
 
 def _sort_dists(dists):
-    tmp = [(dist.parsed_version,dist.distro_type,dist) for dist in dists]
+    tmp = [(dist.hashcmp,dist) for dist in dists]
     tmp.sort()
-    dists[::-1] = [d for v,t,d in tmp]
+    dists[::-1] = [d for hc,d in tmp]
 
 
 
@@ -1560,7 +1721,6 @@
 
 
 class Requirement:
-
     def __init__(self, project_name, specs=(), extras=()):
         self.project_name = project_name
         self.key = project_name.lower()
@@ -1575,11 +1735,12 @@
         self.__hash = hash(self.hashCmp)
 
     def __str__(self):
-        return self.project_name + ','.join([''.join(s) for s in self.specs])
+        specs = ','.join([''.join(s) for s in self.specs])
+        extras = ','.join(self.extras)
+        if extras: extras = '[%s]' % extras
+        return '%s%s%s' % (self.project_name, extras, specs)
 
-    def __repr__(self):
-        return "Requirement(%r, %r, %r)" % \
-            (self.project_name,self.specs,self.extras)
+    def __repr__(self): return "Requirement.parse(%r)" % str(self)
 
     def __eq__(self,other):
         return isinstance(other,Requirement) and self.hashCmp==other.hashCmp
@@ -1693,17 +1854,17 @@
 _initialize(globals())
 
 
+# Prepare the master working set and make the ``require()`` API available
 
+working_set = WorkingSet()
+require = working_set.require
+add_activation_listener = working_set.subscribe
 
-
-
-
-
-
-
-
-
-
+# Activate all distributions already on sys.path, and ensure that
+# all distributions added to the working set in the future (e.g. by
+# calling ``require()``) will get activated as well.
+#
+add_activation_listener(lambda dist: dist.activate())
 
 
 

Index: setuptools.txt
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/setuptools/setuptools.txt,v
retrieving revision 1.18
retrieving revision 1.19
diff -u -d -r1.18 -r1.19
--- setuptools.txt	17 Jul 2005 19:54:38 -0000	1.18
+++ setuptools.txt	18 Jul 2005 01:39:45 -0000	1.19
@@ -732,6 +732,58 @@
 so we can include support for it, too.)
 
 
+Making your package available for EasyInstall
+---------------------------------------------
+
+If you use the ``register`` command (``setup.py register``) to register your
+package with PyPI, that's most of the battle right there.  (See the
+`docs for the register command`_ for more details.)
+
+.. _docs for the register command: http://docs.python.org/dist/package-index.html
+
+If you also use the `upload`_ command to upload actual distributions of your
+package, that's even better, because EasyInstall will be able to find and
+download them directly from your project's PyPI page.
+
+However, there may be reasons why you don't want to upload distributions to
+PyPI, and just want your existing distributions (or perhaps a Subversion
+checkout) to be used instead.
+
+So here's what you need to do before running the ``register`` command.  There
+are three ``setup()`` arguments that affect EasyInstall:
+
+``url`` and ``download_url``
+   These become links on your project's PyPI page.  EasyInstall will examine
+   them to see if they link to a package ("primary links"), or whether they are
+   HTML pages.  If they're HTML pages, EasyInstall scans all HREF's on the
+   page for primary links
+
+``long_description``
+   EasyInstall will check any URLs contained in this argument to see if they
+   are primary links.
+
+A URL is considered a "primary link" if it is a link to a .tar.gz, .tgz, .zip,
+.egg, .egg.zip, .tar.bz2, or .exe file, or if it has an ``#egg=project`` or
+``#egg=project-version`` fragment identifier attached to it.  EasyInstall
+attempts to determine a project name and optional version number from the text
+of a primary link *without* downloading it.  When it has found all the primary
+links, EasyInstall will select the best match based on requested version,
+platform compatibility, and other criteria.
+
+So, if your ``url`` or ``download_url`` point either directly to a downloadable
+source distribution, or to HTML page(s) that have direct links to such, then
+EasyInstall will be able to locate downloads automatically.  If you want to
+make Subversion checkouts available, then you should create links with either
+``#egg=project`` or ``#egg=project-version`` added to the URL (replacing
+``project`` and ``version`` with appropriate values).
+
+Note that Subversion checkout links are of lower precedence than other kinds
+of distributions, so EasyInstall will not select a Subversion checkout for
+downloading unless it has a version included in the ``#egg=`` suffix, and
+it's a higher version than EasyInstall has seen in any other links for your
+project.
+
+
 Distributing Extensions compiled with Pyrex
 -------------------------------------------
 
@@ -1342,7 +1394,7 @@
    
  * Fixed ``pkg_resources.resource_exists()`` not working correctly.
 
- * Many ``pkg_resources`` API changes:
+ * Many ``pkg_resources`` API changes and enhancements:
 
    * ``Distribution`` objects now implement the ``IResourceProvider`` and
      ``IMetadataProvider`` interfaces, so you don't need to reference the (no
@@ -1354,11 +1406,35 @@
 
    * The ``path`` attribute of ``Distribution`` objects is now ``location``,
      because it isn't necessarily a filesystem path (and hasn't been for some
-     time now).
+     time now).  The ``location`` of ``Distribution`` objects in the filesystem
+     should always be normalized using ``pkg_resources.normalize_path()``; all
+     of the setuptools and EasyInstall code that generates distributions from
+     the filesystem (including ``Distribution.from_filename()``) ensure this
+     invariant, but if you use a more generic API like ``Distribution()`` or
+     ``Distribution.from_location()`` you should take care that you don't
+     create a distribution with an un-normalized filesystem path.
 
    * ``Distribution`` objects now have an ``as_requirement()`` method that
      returns a ``Requirement`` for the distribution's project name and version.
 
+   * Distribution objects no longer have an ``installed_on()`` method, and the
+     ``install_on()`` method is now ``activate()`` (but may go away altogether
+     soon).
+
+   * ``find_distributions()`` now takes an additional argument called ``only``,
+     that tells it to only yield distributions whose location is the passed-in
+     path.  (It defaults to False, so that the default behavior is unchanged.)
+
+   * The ``resolve()`` method of ``AvailableDistributions`` is now a method of
+     ``WorkingSet`` instead, and the ``best_match()`` method now uses a working
+     set instead of a path list as its second argument.
+
+   * There is a new ``pkg_resources.add_activation_listener()`` API that lets
+     you register a callback for notifications about distributions added to
+     ``sys.path`` (including the distributions already on it).  This is
+     basically a hook for extensible applications and frameworks to be able to
+     search for plugin metadata in distributions added at runtime.
+     
 0.5a13
  * Fixed a bug in resource extraction from nested packages in a zipped egg.
 



More information about the Python-checkins mailing list