| 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")) | |
| Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in | |
| sys_platform | |
| ^ | |
| >>> print(im("sys_platform==")) | |
| Expected a marker variable or quoted string | |
| sys_platform== | |
| ^ | |
| >>> print(im("sys_platform=='win32'")) | |
| False | |
| >>> print(im("sys=='x'")) | |
| Expected a marker variable or quoted string | |
| sys=='x' | |
| ^ | |
| >>> print(im("(extra)")) | |
| Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in | |
| (extra) | |
| ^ | |
| >>> print(im("(extra")) | |
| Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in | |
| (extra | |
| ^ | |
| >>> print(im("os.open('foo')=='y'")) | |
| Expected a marker variable or quoted string | |
| os.open('foo')=='y' | |
| ^ | |
| >>> print(im("'x'=='y' and os.open('foo')=='y'")) # no short-circuit! | |
| Expected a marker variable or quoted string | |
| 'x'=='y' and os.open('foo')=='y' | |
| ^ | |
| >>> print(im("'x'=='x' or os.open('foo')=='y'")) # no short-circuit! | |
| Expected a marker variable or quoted string | |
| 'x'=='x' or os.open('foo')=='y' | |
| ^ | |
| >>> print(im("r'x'=='x'")) | |
| Expected a marker variable or quoted string | |
| r'x'=='x' | |
| ^ | |
| >>> print(im("'''x'''=='x'")) | |
| Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in | |
| '''x'''=='x' | |
| ^ | |
| >>> print(im('"""x"""=="x"')) | |
| Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in | |
| """x"""=="x" | |
| ^ | |
| >>> print(im(r"x\n=='x'")) | |
| Expected a marker variable or quoted string | |
| x\n=='x' | |
| ^ | |
| >>> print(im("os.open=='y'")) | |
| Expected a marker variable or quoted string | |
| os.open=='y' | |
| ^ | |
| >>> 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 | |