2023-01-12 01:04:47 +00:00
|
|
|
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 == '{}.{}'.format(*sys.version_info)
|
|
|
|
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',version="") 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...']
|
|
|
|
|
|
|
|
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 = WorkingSet([])
|
|
|
|
>>> ws.add(dist,"foo")
|
|
|
|
>>> ws.entries
|
|
|
|
['foo']
|
|
|
|
|
|
|
|
But even if a distribution is found under multiple path entries, it still only
|
|
|
|
shows up once when iterating the working set:
|
|
|
|
|
|
|
|
>>> ws.add_entry(ws.entries[0])
|
|
|
|
>>> 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:
|
|
|
|
|
|
|
|
>>> try:
|
|
|
|
... ws.find(Requirement.parse("Bar==1.0"))
|
|
|
|
... except pkg_resources.VersionConflict as exc:
|
|
|
|
... print(str(exc))
|
|
|
|
... else:
|
|
|
|
... raise AssertionError("VersionConflict was not raised")
|
|
|
|
(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 %s" % dist)
|
|
|
|
>>> ws.subscribe(added)
|
|
|
|
Added Bar 0.9
|
|
|
|
>>> foo12 = Distribution(project_name="Foo", version="1.2", location="f12")
|
|
|
|
>>> 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", location="f14")
|
|
|
|
>>> 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
|
|
|
|
>>> just_a_test = Distribution(project_name="JustATest", version="0.99")
|
|
|
|
>>> ws.add(just_a_test)
|
|
|
|
Added JustATest 0.99
|
|
|
|
|
|
|
|
|
|
|
|
Finding Plugins
|
|
|
|
---------------
|
|
|
|
|
|
|
|
``WorkingSet`` objects can be used to figure out what plugins in an
|
|
|
|
``Environment`` can be loaded without any resolution errors::
|
|
|
|
|
|
|
|
>>> from pkg_resources import Environment
|
|
|
|
|
|
|
|
>>> plugins = Environment([]) # normally, a list of plugin directories
|
|
|
|
>>> plugins.add(foo12)
|
|
|
|
>>> plugins.add(foo14)
|
|
|
|
>>> plugins.add(just_a_test)
|
|
|
|
|
|
|
|
In the simplest case, we just get the newest version of each distribution in
|
|
|
|
the plugin environment::
|
|
|
|
|
|
|
|
>>> ws = WorkingSet([])
|
|
|
|
>>> ws.find_plugins(plugins)
|
|
|
|
([JustATest 0.99, Foo 1.4 (f14)], {})
|
|
|
|
|
|
|
|
But if there's a problem with a version conflict or missing requirements, the
|
|
|
|
method falls back to older versions, and the error info dict will contain an
|
|
|
|
exception instance for each unloadable plugin::
|
|
|
|
|
|
|
|
>>> ws.add(foo12) # this will conflict with Foo 1.4
|
|
|
|
>>> ws.find_plugins(plugins)
|
|
|
|
([JustATest 0.99, Foo 1.2 (f12)], {Foo 1.4 (f14): VersionConflict(...)})
|
|
|
|
|
|
|
|
But if you disallow fallbacks, the failed plugin will be skipped instead of
|
|
|
|
trying older versions::
|
|
|
|
|
|
|
|
>>> ws.find_plugins(plugins, fallback=False)
|
|
|
|
([JustATest 0.99], {Foo 1.4 (f14): VersionConflict(...)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Platform Compatibility Rules
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
On the Mac, there are potential compatibility issues for modules compiled
|
|
|
|
on newer versions of macOS than what the user is running. Additionally,
|
|
|
|
macOS will soon have two platforms to contend with: Intel and PowerPC.
|
|
|
|
|
|
|
|
Basic equality works as on other platforms::
|
|
|
|
|
|
|
|
>>> from pkg_resources import compatible_platforms as cp
|
|
|
|
>>> reqd = 'macosx-10.4-ppc'
|
|
|
|
>>> cp(reqd, reqd)
|
|
|
|
True
|
|
|
|
>>> cp("win32", reqd)
|
|
|
|
False
|
|
|
|
|
|
|
|
Distributions made on other machine types are not compatible::
|
|
|
|
|
|
|
|
>>> cp("macosx-10.4-i386", reqd)
|
|
|
|
False
|
|
|
|
|
|
|
|
Distributions made on earlier versions of the OS are compatible, as
|
|
|
|
long as they are from the same top-level version. The patchlevel version
|
|
|
|
number does not matter::
|
|
|
|
|
|
|
|
>>> cp("macosx-10.4-ppc", reqd)
|
|
|
|
True
|
|
|
|
>>> cp("macosx-10.3-ppc", reqd)
|
|
|
|
True
|
|
|
|
>>> cp("macosx-10.5-ppc", reqd)
|
|
|
|
False
|
|
|
|
>>> cp("macosx-9.5-ppc", reqd)
|
|
|
|
False
|
|
|
|
|
|
|
|
Backwards compatibility for packages made via earlier versions of
|
|
|
|
setuptools is provided as well::
|
|
|
|
|
|
|
|
>>> cp("darwin-8.2.0-Power_Macintosh", reqd)
|
|
|
|
True
|
|
|
|
>>> cp("darwin-7.2.0-Power_Macintosh", reqd)
|
|
|
|
True
|
|
|
|
>>> cp("darwin-8.2.0-Power_Macintosh", "macosx-10.3-ppc")
|
|
|
|
False
|
|
|
|
|
|
|
|
|
|
|
|
Environment Markers
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
>>> from pkg_resources import invalid_marker as im, evaluate_marker as em
|
|
|
|
>>> import os
|
|
|
|
|
|
|
|
>>> print(im("sys_platform"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
|
|
sys_platform
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("sys_platform=="))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
sys_platform==
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("sys_platform=='win32'"))
|
|
|
|
False
|
|
|
|
|
|
|
|
>>> print(im("sys=='x'"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
sys=='x'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("(extra)"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
|
|
(extra)
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("(extra"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
|
|
(extra
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("os.open('foo')=='y'"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
os.open('foo')=='y'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("'x'=='y' and os.open('foo')=='y'")) # no short-circuit!
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
'x'=='y' and os.open('foo')=='y'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("'x'=='x' or os.open('foo')=='y'")) # no short-circuit!
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
'x'=='x' or os.open('foo')=='y'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("r'x'=='x'"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
r'x'=='x'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("'''x'''=='x'"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
|
|
'''x'''=='x'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im('"""x"""=="x"'))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
|
|
"""x"""=="x"
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im(r"x\n=='x'"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
x\n=='x'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> print(im("os.open=='y'"))
|
2023-02-17 01:17:58 +00:00
|
|
|
Expected a marker variable or quoted string
|
|
|
|
os.open=='y'
|
|
|
|
^
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
>>> em("sys_platform=='win32'") == (sys.platform=='win32')
|
|
|
|
True
|
|
|
|
|
|
|
|
>>> em("python_version >= '2.7'")
|
|
|
|
True
|
|
|
|
|
|
|
|
>>> em("python_version > '2.6'")
|
|
|
|
True
|
|
|
|
|
|
|
|
>>> im("implementation_name=='cpython'")
|
|
|
|
False
|
|
|
|
|
|
|
|
>>> im("platform_python_implementation=='CPython'")
|
|
|
|
False
|
|
|
|
|
|
|
|
>>> im("implementation_version=='3.5.1'")
|
|
|
|
False
|