1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
|
"""\
Package resource API
--------------------
A resource is a logical file contained within a package, or a logical
subdirectory thereof. The package resource API expects resource names
to have their path parts separated with ``/``, *not* whatever the local
path separator is. Do not use os.path operations to manipulate resource
names being passed into the API.
The package resource API is designed to work with normal filesystem packages,
.egg files, and unpacked .egg files. It can also work in a limited way with
.zip files and with custom PEP 302 loaders that support the ``get_data()``
method.
"""
__all__ = [
'register_loader_type', 'get_provider', 'IResourceProvider',
'ResourceManager', 'AvailableDistributions', 'require', 'resource_string',
'resource_stream', 'resource_filename', 'set_extraction_path',
'cleanup_resources', 'parse_requirements', 'parse_version',
'compatible_platforms', 'get_platform', 'IMetadataProvider',
'ResolutionError', 'VersionConflict', 'DistributionNotFound',
'InvalidOption', 'Distribution', 'Requirement', 'yield_lines',
'split_sections', # 'glob_resources'
]
import sys, os, zipimport, time, re
class ResolutionError(Exception):
"""Abstract base for dependency resolution errors"""
class VersionConflict(ResolutionError):
"""An already-installed version conflicts with the requested version"""
class DistributionNotFound(ResolutionError):
"""A requested distribution was not found"""
class InvalidOption(ResolutionError):
"""Invalid or unrecognized option name for a distribution"""
_provider_factories = {}
def register_loader_type(loader_type, provider_factory):
"""Register `provider_factory` to make providers for `loader_type`
`loader_type` is the type or class of a PEP 302 ``module.__loader__``,
and `provider_factory` is a function that, passed a *module* object,
returns an ``IResourceProvider`` for that module.
"""
_provider_factories[loader_type] = provider_factory
def get_provider(moduleName):
"""Return an IResourceProvider for the named module"""
module = sys.modules[moduleName]
loader = getattr(module, '__loader__', None)
return _find_adapter(_provider_factories, loader)(module)
def get_platform():
"""Return this platform's string for platform-specific distributions
XXX Currently this is the same as ``distutils.util.get_platform()``, but it
needs some hacks for Linux and Mac OS X.
"""
from distutils.util import get_platform
return get_platform()
def compatible_platforms(provided,required):
"""Can code for the `provided` platform run on the `required` platform?
Returns true if either platform is ``None``, or the platforms are equal.
XXX Needs compatibility checks for Linux and Mac OS X.
"""
if provided is None or required is None or provided==required:
return True # easy case
# XXX all the tricky cases go here
return False
class IMetadataProvider:
def has_metadata(name):
"""Does the package's distribution contain the named metadata?"""
def get_metadata(name):
"""The named metadata resource as a string"""
def get_metadata_lines(name):
"""Yield named metadata resource as list of non-blank non-comment lines
Leading and trailing whitespace is stripped from each line, and lines
with ``#`` as the first non-blank character are omitted.
"""
class IResourceProvider(IMetadataProvider):
"""An object that provides access to package resources"""
def get_resource_filename(manager, resource_name):
"""Return a true filesystem path for `resource_name`
`manager` must be an ``IResourceManager``"""
def get_resource_stream(manager, resource_name):
"""Return a readable file-like object for `resource_name`
`manager` must be an ``IResourceManager``"""
def get_resource_string(manager, resource_name):
"""Return a string containing the contents of `resource_name`
`manager` must be an ``IResourceManager``"""
def has_resource(resource_name):
"""Does the package contain the named resource?"""
# XXX list_resources? glob_resources?
class AvailableDistributions(object):
"""Searchable snapshot of distributions on a search path"""
def __init__(self, search_path=None, platform=get_platform()):
"""Snapshot distributions available on a search path
`search_path` should be a sequence of ``sys.path`` items. If not
supplied, ``sys.path`` is used.
The `platform` is an optional string specifying the name of the
platform that platform-specific distributions must be compatible
with. If not specified, it defaults to the current platform
(as defined by the result of ``get_platform()`` when ``pkg_resources``
was first imported.)
You may explicitly set `platform` to ``None`` if you wish to map *all*
distributions, not just those compatible with a single platform.
"""
self._distmap = {}
self._cache = {}
self.scan(search_path,platform)
def __iter__(self):
"""Iterate over distribution keys"""
return iter(self._distmap.keys())
def __contains__(self,name):
"""Has a distribution named `name` ever been added to this map?"""
return name.lower() in self._distmap
def __len__(self):
return len(self._distmap)
def get(self,key,default=None):
"""Return ``self[key]`` if `key` in self, otherwise return `default`"""
if key in self:
return self[key]
else:
return default
def scan(self, search_path=None, platform=get_platform()):
"""Scan `search_path` for distributions usable on `platform`
Any distributions found are added to the distribution map.
`search_path` should be a sequence of ``sys.path`` items. If not
supplied, ``sys.path`` is used. `platform` is an optional string
specifying the name of the platform that platform-specific
distributions must be compatible with. If unspecified, it defaults to
the current platform.
You may explicitly set `platform` to ``None`` if you wish to map *all*
distributions, not just those compatible with the running platform.
"""
if search_path is None:
search_path = sys.path
add = self.add
for item in search_path:
source = get_distro_source(item)
for dist in source.iter_distributions(requirement):
if compatible_platforms(dist.platform, platform):
add(dist) # XXX should also check python version!
def __getitem__(self,key):
"""Return a newest-to-oldest list of distributions for the given key
The returned list may be modified in-place, e.g. for narrowing down
usable distributions.
"""
try:
return self._cache[key]
except KeyError:
key = key.lower()
if key not in self._distmap:
raise
if key not in self._cache:
dists = self._cache[key] = self._distmap[key]
_sort_dists(dists)
return self._cache[key]
def add(self,dist):
"""Add `dist` to the distribution map"""
self._distmap.setdefault(dist.key,[]).append(dist)
if dist.key in self._cache:
_sort_dists(self._cache[dist.key])
def remove(self,dist):
"""Remove `dist` from the distribution map"""
self._distmap[dist.key].remove(dist)
def best_match(self,requirement,path=None):
"""Find distribution best matching `requirement` and usable on `path`
If a distribution that's already installed on `path` is unsuitable,
a VersionConflict is raised. If one or more suitable distributions are
already installed, 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.
"""
if path is None:
path = sys.path
distros = self.get(requirement.key, ())
find = dict([(dist.path,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
for dist in distros:
if dist in requirement:
return dist
return self.obtain(requirement) # as a last resort, try and download
def resolve(self, requirements, path=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
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)
if dist is None:
raise DistributionNotFound(req) # XXX put more info here
elif dist not in requirement:
# Oops, the "best" so far conflicts with a dependency
raise VersionConflict(req,dist) # XXX put more info here
requirements.extend(dist.depends(req.options)[::-1])
processed[req] = True
return best.values() # return list of distros to install
def obtain(self, requirement):
"""Obtain a distro that matches requirement (e.g. via download)"""
return None # override this in subclasses
class ResourceManager:
"""Manage resource extraction and packages"""
extraction_path = None
def __init__(self):
self.cached_files = []
def resource_exists(self, package_name, resource_name):
"""Does the named resource exist in the named package?"""
return get_provider(package_name).has_resource(self, resource_name)
def resource_filename(self, package_name, resource_name):
"""Return a true filesystem path for specified resource"""
return get_provider(package_name).get_resource_filename(
self,resource_name
)
def resource_stream(self, package_name, resource_name):
"""Return a readable file-like object for specified resource"""
return get_provider(package_name).get_resource_stream(
self, resource_name
)
def resource_string(self, package_name, resource_name):
"""Return specified resource as a string"""
return get_provider(package_name).get_resource_string(
self, resource_name
)
def get_cache_path(self, archive_name, names=()):
"""Return absolute location in cache for `archive_name` and `names`
The parent directory of the resulting path will be created if it does
not already exist. `archive_name` should be the base filename of the
enclosing egg (which may not be the name of the enclosing zipfile!),
including the ".egg" extension. `names`, if provided, should be a
sequence of path name parts "under" the egg's extraction location.
This method should only be called by resource providers that need to
obtain an extraction location, and only for names they intend to
extract, as it tracks the generated names for possible cleanup later.
"""
extract_path = self.extraction_path
extract_path = extract_path or os.path.expanduser('~/.python-eggs')
target_path = os.path.join(extract_path, archive_name, *names)
_ensure_directory(target_path)
self.cached_files.append(target_path)
return target_path
def postprocess(self, filename):
"""Perform any platform-specific postprocessing of file `filename`
This is where Mac header rewrites should be done; other platforms don't
have anything special they should do.
Resource providers should call this method ONLY after successfully
extracting a compressed resource. They must NOT call it on resources
that are already in the filesystem.
"""
# XXX
def set_extraction_path(self, path):
"""Set the base path where resources will be extracted to, if needed.
If not set, this defaults to ``os.expanduser("~/.python-eggs")``.
Resources are extracted to subdirectories of this path based upon
information given by the ``IResourceProvider``. You may set this to a
temporary directory, but then you must call ``cleanup_resources()`` to
delete the extracted files when done. There is no guarantee that
``cleanup_resources()`` will be able to remove all extracted files.
(Note: you may not change the extraction path for a given resource
manager once resources have been extracted, unless you first call
``cleanup_resources()``.)
"""
if self.cached_files:
raise ValueError(
"Can't change extraction path, files already extracted"
)
self.extraction_path = path
def cleanup_resources(self, force=False):
"""
Delete all extracted resource files and directories, returning a list
of the file and directory names that could not be successfully removed.
This function does not have any concurrency protection, so it should
generally only be called when the extraction path is a temporary
directory exclusive to a single process. This method is not
automatically called; you must call it explicitly or register it as an
``atexit`` function if you wish to ensure cleanup of a temporary
directory used for extractions.
"""
# XXX
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 work yet, because:
* get_distro_source() isn't implemented
* Distribution.install_on() isn't implemented
* Requirement.options isn't implemented
* AvailableDistributions.resolve() is untested
* AvailableDistributions.scan() is untested
There may be other things missing as well, but this definitely won't work
as long as any of the above items remain unimplemented.
"""
requirements = parse_requirements(requirements)
for dist in AvailableDistributions().resolve(requirements):
dist.install_on(sys.path)
class DefaultProvider:
"""Provides access to package resources in the filesystem"""
egg_info = None
def __init__(self, module):
self.module = module
self.loader = getattr(module, '__loader__', None)
self.module_path = os.path.dirname(module.__file__)
def get_resource_filename(self, manager, resource_name):
return self._fn(resource_name)
def get_resource_stream(self, manager, resource_name):
return open(self._fn(resource_name), 'rb')
def get_resource_string(self, manager, resource_name):
return self._get(self._fn(resource_name))
def has_resource(self, resource_name):
return self._has(self._fn(resource_name))
def has_metadata(self, name):
if not self.egg_info:
raise NotImplementedError("Only .egg supports metadata")
return self._has(os.path.join(self.egg_info, *name.split('/')))
def get_metadata(self, name):
if not self.egg_info:
raise NotImplementedError("Only .egg supports metadata")
return self._get(os.path.join(self.egg_info, *name.split('/')))
def get_metadata_lines(self, name):
return yield_lines(self.get_metadata(name))
def _has(self, path):
return os.path.exists(path)
def _get(self, path):
stream = open(path, 'rb')
try:
return stream.read()
finally:
stream.close()
def _fn(self, resource_name):
return os.path.join(self.module_path, *resource_name.split('/'))
register_loader_type(type(None), DefaultProvider)
class NullProvider(DefaultProvider):
"""Try to implement resource support for arbitrary PEP 302 loaders"""
def _has(self, path):
raise NotImplementedError(
"Can't perform this operation for unregistered loader type"
)
def _get(self, path):
if hasattr(self.loader, 'get_data'):
return self.loader.get_data(path)
raise NotImplementedError(
"Can't perform this operation for loaders without 'get_data()'"
)
register_loader_type(object, NullProvider)
class ZipProvider(DefaultProvider):
"""Resource support for zips and eggs"""
egg_name = None
eagers = None
def __init__(self, module):
self.module = module
self.loader = module.__loader__
self.zipinfo = zipimport._zip_directory_cache[self.loader.archive]
self.zip_pre = self.loader.archive+os.sep
path = self.module_path = os.path.dirname(module.__file__)
old = None
self.prefix = []
while path!=old:
if path.lower().endswith('.egg'):
self.egg_name = os.path.basename(path)
self.egg_info = os.path.join(path, 'EGG-INFO')
break
old = path
path, base = os.path.split(path)
self.prefix.append(base)
def _short_name(self, path):
if path.startswith(self.zip_pre):
return path[len(self.zip_pre):]
return path
def _has(self, path):
return self._short_name(path) in self.zipinfo
def _get(self, path):
return self.loader.get_data(path)
def get_resource_stream(self, manager, resource_name):
return StringIO(self.get_resource_string(manager, resource_name))
def _extract_resource(self, manager, resource_name):
parts = resource_name.split('/')
zip_path = os.path.join(self.module_path, *parts)
zip_stat = self.zipinfo[os.path.join(*self.prefix+parts)]
t,d,size = zip_stat[5], zip_stat[6], zip_stat[3]
date_time = (
(d>>9)+1980, (d>>5)&0xF, d&0x1F, # ymd
(t&0xFFFF)>>11, (t>>5)&0x3F, (t&0x1F) * 2, 0, 0, -1 # hms, etc.
)
timestamp = time.mktime(date_time)
real_path = manager.get_cache_path(self.egg_name, self.prefix+parts)
if os.path.isfile(real_path):
stat = os.stat(real_path)
if stat.st_size==size and stat.st_mtime==timestamp:
# size and stamp match, don't bother extracting
return real_path
# print "extracting", zip_path
data = self.loader.get_data(zip_path)
open(real_path, 'wb').write(data)
os.utime(real_path, (timestamp,timestamp))
manager.postprocess(real_path)
return real_path
def _get_eager_resources(self):
if self.eagers is None:
eagers = []
for name in ('native_libs.txt', 'eager_resources.txt'):
if self.has_metadata(name):
eagers.extend(self.get_metadata_lines(name))
self.eagers = eagers
return self.eagers
def get_resource_filename(self, manager, resource_name):
if not self.egg_name:
raise NotImplementedError(
"resource_filename() only supported for .egg, not .zip"
)
# should lock for extraction here
eagers = self._get_eager_resources()
if resource_name in eagers:
for name in eagers:
self._extract_resource(manager, name)
return self._extract_resource(manager, resource_name)
register_loader_type(zipimport.zipimporter, ZipProvider)
def StringIO(*args, **kw):
"""Thunk to load the real StringIO on demand"""
global StringIO
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
return StringIO(*args,**kw)
def get_distro_source(path_item):
pass # XXX
def yield_lines(strs):
"""Yield non-empty/non-comment lines of a ``basestring`` or sequence"""
if isinstance(strs,basestring):
for s in strs.splitlines():
s = s.strip()
if s and not s.startswith('#'): # skip blank lines/comments
yield s
else:
for ss in strs:
for s in yield_lines(ss):
yield s
LINE_END = re.compile(r"\s*(#.*)?$").match # whitespace and comment
CONTINUE = re.compile(r"\s*\\\s*(#.*)?$").match # line continuation
DISTRO = re.compile(r"\s*(\w+)").match # Distribution name
VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|\.)+)").match # version info
COMMA = re.compile(r"\s*,").match # comma between items
EGG_NAME = re.compile(
r"(?P<name>[^-]+)"
r"( -(?P<ver>[^-]+) (-py(?P<pyver>[^-]+) (-(?P<plat>.+))? )? )?",
re.VERBOSE | re.IGNORECASE
).match
component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE)
replace = {'pre':'c', 'preview':'c','-':'final-','rc':'c'}.get
def _parse_version_parts(s):
for part in component_re.split(s):
part = replace(part,part)
if not part or part=='.':
continue
if part[:1] in '0123456789':
yield part.zfill(8) # pad for numeric comparison
else:
yield '*'+part
yield '*final' # ensure that alpha/beta/candidate are before final
def parse_version(s):
"""Convert a version string to a sortable key
This is a rough cross between distutils' StrictVersion and LooseVersion;
if you give it versions that would work with StrictVersion, then it behaves
the same; otherwise it acts like a slightly-smarter LooseVersion.
The returned value will be a tuple of strings. Numeric portions of the
version are padded to 8 digits so they will compare numerically, but
without relying on how numbers compare relative to strings. Dots are
dropped, but dashes are retained. Trailing zeros between alpha segments
or dashes are suppressed, so that e.g. 2.4.0 is considered the same as 2.4.
Alphanumeric parts are lower-cased.
The algorithm assumes that strings like '-' and any alpha string > "final"
represents a "patch level". So, "2.4-1" is assumed to be a branch or patch
of "2.4", and therefore "2.4.1" is considered newer than "2.4-1".
Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
come before "final" alphabetically) are assumed to be pre-release versions,
and so the version "2.4" is considered newer than "2.4a1".
Finally, to handle miscellaneous cases, the strings "pre", "preview", and
"rc" are treated as if they were "c", i.e. as though they were release
candidates, and therefore are not as new as a version string that does not
contain them.
"""
parts = []
for part in _parse_version_parts(s.lower()):
if part.startswith('*'):
# remove trailing zeros from each series of numeric parts
while parts and parts[-1]=='00000000':
parts.pop()
parts.append(part)
return tuple(parts)
class Distribution(object):
"""Wrap an actual or potential sys.path entry w/metadata"""
def __init__(self,
path_str, metadata=None, name=None, version=None,
py_version=sys.version[:3], platform=None
):
if name:
self.name = name.replace('_','-')
if version:
self._version = version.replace('_','-')
self.py_version = py_version
self.platform = platform
self.path = path_str
self.metadata = metadata
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.path in path
#@classmethod
def from_filename(cls,filename,metadata=None):
name,version,py_version,platform = [None]*4
basename,ext = os.path.splitext(os.path.basename(filename))
if ext.lower()==".egg":
match = EGG_NAME(basename)
if match:
name,version,py_version,platform = match.group(
'name','ver','pyver','plat'
)
return cls(
filename, metadata, name=name, version=version,
py_version=py_version, platform=platform
)
from_filename = classmethod(from_filename)
# These properties have to be lazy so that we don't have to load any
# metadata until/unless it's actually needed. (i.e., some distributions
# may not know their name or version without loading PKG-INFO)
#@property
def key(self):
try:
return self._key
except AttributeError:
self._key = key = self.name.lower()
return key
key = property(key)
#@property
def parsed_version(self):
try:
return self._parsed_version
except AttributeError:
self._parsed_version = pv = parse_version(self.version)
return pv
parsed_version = property(parsed_version)
#@property
def version(self):
try:
return self._version
except AttributeError:
for line in self.metadata.get_metadata_lines('PKG-INFO'):
if line.lower().startswith('version:'):
self._version = line.split(':',1)[1].strip()
return self._version
else:
raise AttributeError(
"Missing Version: header in PKG-INFO", self
)
version = property(version)
#@property
def _dep_map(self):
try:
return self.__dep_map
except AttributeError:
dm = self.__dep_map = {None: []}
if self.metadata.has_metadata('depends.txt'):
for section,contents in split_sections(
self.metadata.get_metadata_lines('depends.txt')
):
dm[section] = list(parse_requirements(contents))
return dm
_dep_map = property(_dep_map)
def depends(self,options=()):
"""List of Requirements needed for this distro if `options` are used"""
dm = self._dep_map
deps = []
deps.extend(dm.get(None,()))
for opt in options:
try:
deps.extend(dm[opt.lower()])
except KeyError:
raise InvalidOption("No such option", self, opt)
return deps
def _sort_dists(dists):
tmp = [(dist.version,dist) for dist in dists]
tmp.sort()
dists[::-1] = [d for v,d in tmp]
def parse_requirements(strs):
"""Yield ``Requirement`` objects for each specification in `strs`
`strs` must be an instance of ``basestring``, or a (possibly-nested)
iterable thereof.
"""
# create a steppable iterator, so we can handle \-continuations
lines = iter(yield_lines(strs))
for line in lines:
line = line.replace('-','_')
match = DISTRO(line)
if not match:
raise ValueError("Missing distribution spec", line)
distname = match.group(1)
p = match.end()
specs = []
while not LINE_END(line,p):
if CONTINUE(line,p):
try:
line = lines.next().replace('-','_'); p = 0
except StopIteration:
raise ValueError(
"\\ must not appear on the last nonblank line"
)
match = VERSION(line,p)
if not match:
raise ValueError("Expected version spec in",line,"at",line[p:])
op,val = match.group(1,2)
specs.append((op,val.replace('_','-')))
p = match.end()
match = COMMA(line,p)
if match:
p = match.end() # skip the comma
elif not LINE_END(line,p):
raise ValueError("Expected ',' or EOL in",line,"at",line[p:])
yield Requirement(distname.replace('_','-'), specs)
class Requirement:
def __init__(self, distname, specs=()):
self.distname = distname
self.key = distname.lower()
index = [(parse_version(v),state_machine[op],op,v) for op,v in specs]
index.sort()
self.specs = [(op,ver) for parsed,trans,op,ver in index]
self.index = index
def __str__(self):
return self.distname + ','.join([''.join(s) for s in self.specs])
def __repr__(self):
return "Requirement(%r, %r)" % (self.distname, self.specs)
def __eq__(self,other):
return isinstance(other,Requirement) \
and self.key==other.key and self.specs==other.specs
def __contains__(self,item):
if isinstance(item,Distribution):
if item.key <> self.key:
return False
item = item.parsed_version
elif isinstance(item,basestring):
item = parse_version(item)
last = True
for parsed,trans,op,ver in self.index:
action = trans[cmp(item,parsed)]
if action=='F':
return False
elif action=='T':
return True
elif action=='+':
last = True
elif action=='-':
last = False
return last
#@staticmethod
def parse(s):
reqs = list(parse_requirements(s))
if reqs:
if len(reqs)==1:
return reqs[0]
raise ValueError("Expected only one requirement", s)
raise ValueError("No requirements found", s)
parse = staticmethod(parse)
state_machine = {
# =><
'<' : '--T',
'<=': 'T-T',
'>' : 'F+F',
'>=': 'T+F',
'==': 'T..',
'!=': 'F..',
}
def _get_mro(cls):
"""Get an mro for a type or classic class"""
if not isinstance(cls,type):
class cls(cls,object): pass
return cls.__mro__[1:]
return cls.__mro__
def _find_adapter(registry, ob):
"""Return an adapter factory for `ob` from `registry`"""
for t in _get_mro(getattr(ob, '__class__', type(ob))):
if t in registry:
return registry[t]
def _ensure_directory(path):
dirname = os.path.dirname(path)
if not os.path.isdir(dirname):
os.makedirs(dirname)
def split_sections(s):
"""Split a string or iterable thereof into (section,content) pairs
Each ``section`` is a lowercase version of the section header ("[section]")
and each ``content`` is a list of stripped lines excluding blank lines and
comment-only lines. If there are any such lines before the first section
header, they're returned in a first ``section`` of ``None``.
"""
section = None
content = []
for line in yield_lines(s):
if line.startswith("["):
if line.endswith("]"):
if content:
yield section, content
section = line[1:-1].strip().lower()
content = []
else:
raise ValueError("Invalid section heading", line)
else:
content.append(line)
# wrap up last segment
if content:
yield section, content
# Set up global resource manager
_manager = ResourceManager()
def _initialize(g):
for name in dir(_manager):
if not name.startswith('_'):
g[name] = getattr(_manager, name)
_initialize(globals())
|