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