1Pluggable Distributions of Python Software
2==========================================
3
4Distributions
5-------------
6
7A "Distribution" is a collection of files that represent a "Release" of a
8"Project" as of a particular point in time, denoted by a
9"Version"::
10
11    >>> import sys, pkg_resources
12    >>> from pkg_resources import Distribution
13    >>> Distribution(project_name="Foo", version="1.2")
14    Foo 1.2
15
16Distributions have a location, which can be a filename, URL, or really anything
17else you care to use::
18
19    >>> dist = Distribution(
20    ...     location="http://example.com/something",
21    ...     project_name="Bar", version="0.9"
22    ... )
23
24    >>> dist
25    Bar 0.9 (http://example.com/something)
26
27
28Distributions have various introspectable attributes::
29
30    >>> dist.location
31    'http://example.com/something'
32
33    >>> dist.project_name
34    'Bar'
35
36    >>> dist.version
37    '0.9'
38
39    >>> dist.py_version == sys.version[:3]
40    True
41
42    >>> print(dist.platform)
43    None
44
45Including various computed attributes::
46
47    >>> from pkg_resources import parse_version
48    >>> dist.parsed_version == parse_version(dist.version)
49    True
50
51    >>> dist.key    # case-insensitive form of the project name
52    'bar'
53
54Distributions are compared (and hashed) by version first::
55
56    >>> Distribution(version='1.0') == Distribution(version='1.0')
57    True
58    >>> Distribution(version='1.0') == Distribution(version='1.1')
59    False
60    >>> Distribution(version='1.0') <  Distribution(version='1.1')
61    True
62
63but also by project name (case-insensitive), platform, Python version,
64location, etc.::
65
66    >>> Distribution(project_name="Foo",version="1.0") == \
67    ... Distribution(project_name="Foo",version="1.0")
68    True
69
70    >>> Distribution(project_name="Foo",version="1.0") == \
71    ... Distribution(project_name="foo",version="1.0")
72    True
73
74    >>> Distribution(project_name="Foo",version="1.0") == \
75    ... Distribution(project_name="Foo",version="1.1")
76    False
77
78    >>> Distribution(project_name="Foo",py_version="2.3",version="1.0") == \
79    ... Distribution(project_name="Foo",py_version="2.4",version="1.0")
80    False
81
82    >>> Distribution(location="spam",version="1.0") == \
83    ... Distribution(location="spam",version="1.0")
84    True
85
86    >>> Distribution(location="spam",version="1.0") == \
87    ... Distribution(location="baz",version="1.0")
88    False
89
90
91
92Hash and compare distribution by prio/plat
93
94Get version from metadata
95provider capabilities
96egg_name()
97as_requirement()
98from_location, from_filename (w/path normalization)
99
100Releases may have zero or more "Requirements", which indicate
101what releases of another project the release requires in order to
102function.  A Requirement names the other project, expresses some criteria
103as to what releases of that project are acceptable, and lists any "Extras"
104that the requiring release may need from that project.  (An Extra is an
105optional feature of a Release, that can only be used if its additional
106Requirements are satisfied.)
107
108
109
110The Working Set
111---------------
112
113A collection of active distributions is called a Working Set.  Note that a
114Working Set can contain any importable distribution, not just pluggable ones.
115For example, the Python standard library is an importable distribution that
116will usually be part of the Working Set, even though it is not pluggable.
117Similarly, when you are doing development work on a project, the files you are
118editing are also a Distribution.  (And, with a little attention to the
119directory names used,  and including some additional metadata, such a
120"development distribution" can be made pluggable as well.)
121
122    >>> from pkg_resources import WorkingSet
123
124A working set's entries are the sys.path entries that correspond to the active
125distributions.  By default, the working set's entries are the items on
126``sys.path``::
127
128    >>> ws = WorkingSet()
129    >>> ws.entries == sys.path
130    True
131
132But you can also create an empty working set explicitly, and add distributions
133to it::
134
135    >>> ws = WorkingSet([])
136    >>> ws.add(dist)
137    >>> ws.entries
138    ['http://example.com/something']
139    >>> dist in ws
140    True
141    >>> Distribution('foo',version="") in ws
142    False
143
144And you can iterate over its distributions::
145
146    >>> list(ws)
147    [Bar 0.9 (http://example.com/something)]
148
149Adding the same distribution more than once is a no-op::
150
151    >>> ws.add(dist)
152    >>> list(ws)
153    [Bar 0.9 (http://example.com/something)]
154
155For that matter, adding multiple distributions for the same project also does
156nothing, because a working set can only hold one active distribution per
157project -- the first one added to it::
158
159    >>> ws.add(
160    ...     Distribution(
161    ...         'http://example.com/something', project_name="Bar",
162    ...         version="7.2"
163    ...     )
164    ... )
165    >>> list(ws)
166    [Bar 0.9 (http://example.com/something)]
167
168You can append a path entry to a working set using ``add_entry()``::
169
170    >>> ws.entries
171    ['http://example.com/something']
172    >>> ws.add_entry(pkg_resources.__file__)
173    >>> ws.entries
174    ['http://example.com/something', '...pkg_resources...']
175
176Multiple additions result in multiple entries, even if the entry is already in
177the working set (because ``sys.path`` can contain the same entry more than
178once)::
179
180    >>> ws.add_entry(pkg_resources.__file__)
181    >>> ws.entries
182    ['...example.com...', '...pkg_resources...', '...pkg_resources...']
183
184And you can specify the path entry a distribution was found under, using the
185optional second parameter to ``add()``::
186
187    >>> ws = WorkingSet([])
188    >>> ws.add(dist,"foo")
189    >>> ws.entries
190    ['foo']
191
192But even if a distribution is found under multiple path entries, it still only
193shows up once when iterating the working set:
194
195    >>> ws.add_entry(ws.entries[0])
196    >>> list(ws)
197    [Bar 0.9 (http://example.com/something)]
198
199You can ask a WorkingSet to ``find()`` a distribution matching a requirement::
200
201    >>> from pkg_resources import Requirement
202    >>> print(ws.find(Requirement.parse("Foo==1.0")))   # no match, return None
203    None
204
205    >>> ws.find(Requirement.parse("Bar==0.9"))  # match, return distribution
206    Bar 0.9 (http://example.com/something)
207
208Note that asking for a conflicting version of a distribution already in a
209working set triggers a ``pkg_resources.VersionConflict`` error:
210
211    >>> try:
212    ...     ws.find(Requirement.parse("Bar==1.0"))
213    ... except pkg_resources.VersionConflict as exc:
214    ...     print(str(exc))
215    ... else:
216    ...     raise AssertionError("VersionConflict was not raised")
217    (Bar 0.9 (http://example.com/something), Requirement.parse('Bar==1.0'))
218
219You can subscribe a callback function to receive notifications whenever a new
220distribution is added to a working set.  The callback is immediately invoked
221once for each existing distribution in the working set, and then is called
222again for new distributions added thereafter::
223
224    >>> def added(dist): print("Added %s" % dist)
225    >>> ws.subscribe(added)
226    Added Bar 0.9
227    >>> foo12 = Distribution(project_name="Foo", version="1.2", location="f12")
228    >>> ws.add(foo12)
229    Added Foo 1.2
230
231Note, however, that only the first distribution added for a given project name
232will trigger a callback, even during the initial ``subscribe()`` callback::
233
234    >>> foo14 = Distribution(project_name="Foo", version="1.4", location="f14")
235    >>> ws.add(foo14)   # no callback, because Foo 1.2 is already active
236
237    >>> ws = WorkingSet([])
238    >>> ws.add(foo12)
239    >>> ws.add(foo14)
240    >>> ws.subscribe(added)
241    Added Foo 1.2
242
243And adding a callback more than once has no effect, either::
244
245    >>> ws.subscribe(added)     # no callbacks
246
247    # and no double-callbacks on subsequent additions, either
248    >>> just_a_test = Distribution(project_name="JustATest", version="0.99")
249    >>> ws.add(just_a_test)
250    Added JustATest 0.99
251
252
253Finding Plugins
254---------------
255
256``WorkingSet`` objects can be used to figure out what plugins in an
257``Environment`` can be loaded without any resolution errors::
258
259    >>> from pkg_resources import Environment
260
261    >>> plugins = Environment([])   # normally, a list of plugin directories
262    >>> plugins.add(foo12)
263    >>> plugins.add(foo14)
264    >>> plugins.add(just_a_test)
265
266In the simplest case, we just get the newest version of each distribution in
267the plugin environment::
268
269    >>> ws = WorkingSet([])
270    >>> ws.find_plugins(plugins)
271    ([JustATest 0.99, Foo 1.4 (f14)], {})
272
273But if there's a problem with a version conflict or missing requirements, the
274method falls back to older versions, and the error info dict will contain an
275exception instance for each unloadable plugin::
276
277    >>> ws.add(foo12)   # this will conflict with Foo 1.4
278    >>> ws.find_plugins(plugins)
279    ([JustATest 0.99, Foo 1.2 (f12)], {Foo 1.4 (f14): VersionConflict(...)})
280
281But if you disallow fallbacks, the failed plugin will be skipped instead of
282trying older versions::
283
284    >>> ws.find_plugins(plugins, fallback=False)
285    ([JustATest 0.99], {Foo 1.4 (f14): VersionConflict(...)})
286
287
288
289Platform Compatibility Rules
290----------------------------
291
292On the Mac, there are potential compatibility issues for modules compiled
293on newer versions of Mac OS X than what the user is running. Additionally,
294Mac OS X will soon have two platforms to contend with: Intel and PowerPC.
295
296Basic equality works as on other platforms::
297
298    >>> from pkg_resources import compatible_platforms as cp
299    >>> reqd = 'macosx-10.4-ppc'
300    >>> cp(reqd, reqd)
301    True
302    >>> cp("win32", reqd)
303    False
304
305Distributions made on other machine types are not compatible::
306
307    >>> cp("macosx-10.4-i386", reqd)
308    False
309
310Distributions made on earlier versions of the OS are compatible, as
311long as they are from the same top-level version. The patchlevel version
312number does not matter::
313
314    >>> cp("macosx-10.4-ppc", reqd)
315    True
316    >>> cp("macosx-10.3-ppc", reqd)
317    True
318    >>> cp("macosx-10.5-ppc", reqd)
319    False
320    >>> cp("macosx-9.5-ppc", reqd)
321    False
322
323Backwards compatibility for packages made via earlier versions of
324setuptools is provided as well::
325
326    >>> cp("darwin-8.2.0-Power_Macintosh", reqd)
327    True
328    >>> cp("darwin-7.2.0-Power_Macintosh", reqd)
329    True
330    >>> cp("darwin-8.2.0-Power_Macintosh", "macosx-10.3-ppc")
331    False
332
333
334Environment Markers
335-------------------
336
337    >>> from pkg_resources import invalid_marker as im, evaluate_marker as em
338    >>> import os
339
340    >>> print(im("sys_platform"))
341    Invalid marker: 'sys_platform', parse error at ''
342
343    >>> print(im("sys_platform=="))
344    Invalid marker: 'sys_platform==', parse error at ''
345
346    >>> print(im("sys_platform=='win32'"))
347    False
348
349    >>> print(im("sys=='x'"))
350    Invalid marker: "sys=='x'", parse error at "sys=='x'"
351
352    >>> print(im("(extra)"))
353    Invalid marker: '(extra)', parse error at ')'
354
355    >>> print(im("(extra"))
356    Invalid marker: '(extra', parse error at ''
357
358    >>> print(im("os.open('foo')=='y'"))
359    Invalid marker: "os.open('foo')=='y'", parse error at 'os.open('
360
361    >>> print(im("'x'=='y' and os.open('foo')=='y'"))   # no short-circuit!
362    Invalid marker: "'x'=='y' and os.open('foo')=='y'", parse error at 'and os.o'
363
364    >>> print(im("'x'=='x' or os.open('foo')=='y'"))   # no short-circuit!
365    Invalid marker: "'x'=='x' or os.open('foo')=='y'", parse error at 'or os.op'
366
367    >>> print(im("'x' < 'y' < 'z'"))
368    Invalid marker: "'x' < 'y' < 'z'", parse error at "< 'z'"
369
370    >>> print(im("r'x'=='x'"))
371    Invalid marker: "r'x'=='x'", parse error at "r'x'=='x"
372
373    >>> print(im("'''x'''=='x'"))
374    Invalid marker: "'''x'''=='x'", parse error at "'x'''=='"
375
376    >>> print(im('"""x"""=="x"'))
377    Invalid marker: '"""x"""=="x"', parse error at '"x"""=="'
378
379    >>> print(im(r"x\n=='x'"))
380    Invalid marker: "x\\n=='x'", parse error at "x\\n=='x'"
381
382    >>> print(im("os.open=='y'"))
383    Invalid marker: "os.open=='y'", parse error at 'os.open='
384
385    >>> em("sys_platform=='win32'") == (sys.platform=='win32')
386    True
387
388    >>> em("python_version >= '2.7'")
389    True
390
391    >>> em("python_version > '2.6'")
392    True
393
394    >>> im("implementation_name=='cpython'")
395    False
396
397    >>> im("platform_python_implementation=='CPython'")
398    False
399
400    >>> im("implementation_version=='3.5.1'")
401    False
402