summaryrefslogtreecommitdiff
path: root/pkg_resources.py
diff options
context:
space:
mode:
Diffstat (limited to 'pkg_resources.py')
-rw-r--r--pkg_resources.py208
1 files changed, 173 insertions, 35 deletions
diff --git a/pkg_resources.py b/pkg_resources.py
index 2ba0febf..d14104a8 100644
--- a/pkg_resources.py
+++ b/pkg_resources.py
@@ -152,7 +152,7 @@ __all__ = [
# Parsing functions and string utilities
'parse_requirements', 'parse_version', 'safe_name', 'safe_version',
'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections',
- 'safe_extra', 'to_filename',
+ 'safe_extra', 'to_filename', 'invalid_marker', 'evaluate_marker',
# filesystem utilities
'ensure_directory', 'normalize_path',
@@ -1170,6 +1170,129 @@ def to_filename(name):
+_marker_names = {
+ 'os': ['name'], 'sys': ['platform'],
+ 'platform': ['version','machine','python_implementation'],
+ 'python_version': [], 'python_full_version': [], 'extra':[],
+}
+
+_marker_values = {
+ 'os_name': lambda: os.name,
+ 'sys_platform': lambda: sys.platform,
+ 'python_full_version': lambda: sys.version.split()[0],
+ 'python_version': lambda:'%s.%s' % (sys.version_info[0], sys.version_info[1]),
+ 'platform_version': lambda: _platinfo('version'),
+ 'platform_machine': lambda: _platinfo('machine'),
+ 'python_implementation': lambda: _platinfo('python_implementation') or _pyimp(),
+}
+
+def _platinfo(attr):
+ try:
+ import platform
+ except ImportError:
+ return ''
+ return getattr(platform, attr, lambda:'')()
+
+def _pyimp():
+ if sys.platform=='cli':
+ return 'IronPython'
+ elif sys.platform.startswith('java'):
+ return 'Jython'
+ elif '__pypy__' in sys.builtin_module_names:
+ return 'PyPy'
+ else:
+ return 'CPython'
+
+def invalid_marker(text):
+ """Validate text as a PEP 426 environment marker; return exception or False"""
+ try:
+ evaluate_marker(text)
+ except SyntaxError:
+ return sys.exc_info()[1]
+ return False
+
+def evaluate_marker(text, extra=None, _ops={}):
+ """Evaluate a PEP 426 environment marker; SyntaxError if marker is invalid"""
+
+ if not _ops:
+
+ from token import NAME, STRING
+ import token, symbol, operator
+
+ def and_test(nodelist):
+ # MUST NOT short-circuit evaluation, or invalid syntax can be skipped!
+ return reduce(operator.and_, [interpret(nodelist[i]) for i in range(1,len(nodelist),2)])
+
+ def test(nodelist):
+ # MUST NOT short-circuit evaluation, or invalid syntax can be skipped!
+ return reduce(operator.or_, [interpret(nodelist[i]) for i in range(1,len(nodelist),2)])
+
+ def atom(nodelist):
+ t = nodelist[1][0]
+ if t == token.LPAR:
+ if nodelist[2][0] == token.RPAR:
+ raise SyntaxError("Empty parentheses")
+ return interpret(nodelist[2])
+ raise SyntaxError("Language feature not supported in environment markers")
+
+ def comparison(nodelist):
+ if len(nodelist)>4:
+ raise SyntaxError("Chained comparison not allowed in environment markers")
+ comp = nodelist[2][1]
+ cop = comp[1]
+ if comp[0] == NAME:
+ if len(nodelist[2]) == 3:
+ if cop == 'not':
+ cop = 'not in'
+ else:
+ cop = 'is not'
+ try:
+ cop = _ops[cop]
+ except KeyError:
+ raise SyntaxError(repr(cop)+" operator not allowed in environment markers")
+ return cop(evaluate(nodelist[1]), evaluate(nodelist[3]))
+
+ _ops.update({
+ symbol.test: test, symbol.and_test: and_test, symbol.atom: atom,
+ symbol.comparison: comparison, 'not in': lambda x,y: x not in y,
+ 'in': lambda x,y: x in y, '==': operator.eq, '!=': operator.ne,
+ })
+ if hasattr(symbol,'or_test'):
+ _ops[symbol.or_test] = test
+
+ def interpret(nodelist):
+ while len(nodelist)==2: nodelist = nodelist[1]
+ try:
+ op = _ops[nodelist[0]]
+ except KeyError:
+ raise SyntaxError("Comparison or logical expression expected")
+ raise SyntaxError("Language feature not supported in environment markers: "+symbol.sym_name[nodelist[0]])
+ return op(nodelist)
+
+ def evaluate(nodelist):
+ while len(nodelist)==2: nodelist = nodelist[1]
+ kind = nodelist[0]
+ name = nodelist[1]
+ #while len(name)==2: name = name[1]
+ if kind==NAME:
+ try:
+ op = _marker_values[name]
+ except KeyError:
+ raise SyntaxError("Unknown name %r" % name)
+ return op()
+ if kind==STRING:
+ s = nodelist[1]
+ if s[:1] not in "'\"" or s.startswith('"""') or s.startswith("'''") \
+ or '\\' in s:
+ raise SyntaxError(
+ "Only plain strings allowed in environment markers")
+ return s[1:-1]
+ raise SyntaxError("Language feature not supported in environment markers")
+
+ import parser
+ return interpret(parser.expr(text).totuple(1)[1])
+
+
class NullProvider:
"""Try to implement resources and metadata for arbitrary PEP 302 loaders"""
@@ -1384,6 +1507,16 @@ class ZipProvider(EggProvider):
self._extract_resource(manager, self._eager_to_zip(name))
return self._extract_resource(manager, zip_path)
+ @staticmethod
+ def _get_date_and_size(zip_stat):
+ 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)
+ return timestamp, size
+
def _extract_resource(self, manager, zip_path):
if zip_path in self._index():
@@ -1393,28 +1526,19 @@ class ZipProvider(EggProvider):
)
return os.path.dirname(last) # return the extracted directory name
- zip_stat = self.zipinfo[zip_path]
- 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)
+ timestamp, size = self._get_date_and_size(self.zipinfo[zip_path])
+ if not WRITE_SUPPORT:
+ raise IOError('"os.rename" and "os.unlink" are not supported '
+ 'on this platform')
try:
- if not WRITE_SUPPORT:
- raise IOError('"os.rename" and "os.unlink" are not supported '
- 'on this platform')
real_path = manager.get_cache_path(
self.egg_name, self._parts(zip_path)
)
- 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
+ if self._is_current(real_path, zip_path):
+ return real_path
outf, tmpnam = _mkstemp(".$extract", dir=os.path.dirname(real_path))
os.write(outf, self.loader.get_data(zip_path))
@@ -1427,11 +1551,9 @@ class ZipProvider(EggProvider):
except os.error:
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, somebody did it just ahead of
- # us, so we're done
+ if self._is_current(real_path, zip_path):
+ # the file became current since it was checked above,
+ # so proceed.
return real_path
elif os.name=='nt': # Windows, del old file and retry
unlink(real_path)
@@ -1444,6 +1566,23 @@ class ZipProvider(EggProvider):
return real_path
+ def _is_current(self, file_path, zip_path):
+ """
+ Return True if the file_path is current for this zip_path
+ """
+ timestamp, size = self._get_date_and_size(self.zipinfo[zip_path])
+ if not os.path.isfile(file_path):
+ return False
+ stat = os.stat(file_path)
+ if stat.st_size!=size or stat.st_mtime!=timestamp:
+ return False
+ # check that the contents match
+ zip_contents = self.loader.get_data(zip_path)
+ f = open(file_path, 'rb')
+ file_contents = f.read()
+ f.close()
+ return zip_contents == file_contents
+
def _get_eager_resources(self):
if self.eagers is None:
eagers = []
@@ -1978,7 +2117,6 @@ def parse_version(s):
parts.pop()
parts.append(part)
return tuple(parts)
-
class EntryPoint(object):
"""Object representing an advertised importable object"""
@@ -2219,7 +2357,14 @@ class Distribution(object):
dm = self.__dep_map = {None: []}
for name in 'requires.txt', 'depends.txt':
for extra,reqs in split_sections(self._get_metadata(name)):
- if extra: extra = safe_extra(extra)
+ if extra:
+ if ':' in extra:
+ extra, marker = extra.split(':',1)
+ if invalid_marker(marker):
+ reqs=[] # XXX warn
+ elif not evaluate_marker(marker):
+ reqs=[]
+ extra = safe_extra(extra) or None
dm.setdefault(extra,[]).extend(parse_requirements(reqs))
return dm
_dep_map = property(_dep_map)
@@ -2243,6 +2388,8 @@ class Distribution(object):
for line in self.get_metadata_lines(name):
yield line
+
+
def activate(self,path=None):
"""Ensure distribution is importable on `path` (default=sys.path)"""
if path is None: path = sys.path
@@ -2281,6 +2428,9 @@ class Distribution(object):
raise AttributeError,attr
return getattr(self._provider, attr)
+
+
+
#@classmethod
def from_filename(cls,filename,metadata=None, **kw):
return cls.from_location(
@@ -2322,18 +2472,6 @@ class Distribution(object):
-
-
-
-
-
-
-
-
-
-
-
-
def insert_on(self, path, loc = None):
"""Insert self.location in path before its nearest parent directory"""