• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import sys
2import marshal
3import contextlib
4import dis
5
6from setuptools.extern.packaging import version
7
8from ._imp import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE
9from . import _imp
10
11
12__all__ = [
13    'Require', 'find_module', 'get_module_constant', 'extract_constant'
14]
15
16
17class Require:
18    """A prerequisite to building or installing a distribution"""
19
20    def __init__(
21            self, name, requested_version, module, homepage='',
22            attribute=None, format=None):
23
24        if format is None and requested_version is not None:
25            format = version.Version
26
27        if format is not None:
28            requested_version = format(requested_version)
29            if attribute is None:
30                attribute = '__version__'
31
32        self.__dict__.update(locals())
33        del self.self
34
35    def full_name(self):
36        """Return full package/distribution name, w/version"""
37        if self.requested_version is not None:
38            return '%s-%s' % (self.name, self.requested_version)
39        return self.name
40
41    def version_ok(self, version):
42        """Is 'version' sufficiently up-to-date?"""
43        return self.attribute is None or self.format is None or \
44            str(version) != "unknown" and self.format(version) >= self.requested_version
45
46    def get_version(self, paths=None, default="unknown"):
47        """Get version number of installed module, 'None', or 'default'
48
49        Search 'paths' for module.  If not found, return 'None'.  If found,
50        return the extracted version attribute, or 'default' if no version
51        attribute was specified, or the value cannot be determined without
52        importing the module.  The version is formatted according to the
53        requirement's version format (if any), unless it is 'None' or the
54        supplied 'default'.
55        """
56
57        if self.attribute is None:
58            try:
59                f, p, i = find_module(self.module, paths)
60                if f:
61                    f.close()
62                return default
63            except ImportError:
64                return None
65
66        v = get_module_constant(self.module, self.attribute, default, paths)
67
68        if v is not None and v is not default and self.format is not None:
69            return self.format(v)
70
71        return v
72
73    def is_present(self, paths=None):
74        """Return true if dependency is present on 'paths'"""
75        return self.get_version(paths) is not None
76
77    def is_current(self, paths=None):
78        """Return true if dependency is present and up-to-date on 'paths'"""
79        version = self.get_version(paths)
80        if version is None:
81            return False
82        return self.version_ok(str(version))
83
84
85def maybe_close(f):
86    @contextlib.contextmanager
87    def empty():
88        yield
89        return
90    if not f:
91        return empty()
92
93    return contextlib.closing(f)
94
95
96def get_module_constant(module, symbol, default=-1, paths=None):
97    """Find 'module' by searching 'paths', and extract 'symbol'
98
99    Return 'None' if 'module' does not exist on 'paths', or it does not define
100    'symbol'.  If the module defines 'symbol' as a constant, return the
101    constant.  Otherwise, return 'default'."""
102
103    try:
104        f, path, (suffix, mode, kind) = info = find_module(module, paths)
105    except ImportError:
106        # Module doesn't exist
107        return None
108
109    with maybe_close(f):
110        if kind == PY_COMPILED:
111            f.read(8)  # skip magic & date
112            code = marshal.load(f)
113        elif kind == PY_FROZEN:
114            code = _imp.get_frozen_object(module, paths)
115        elif kind == PY_SOURCE:
116            code = compile(f.read(), path, 'exec')
117        else:
118            # Not something we can parse; we'll have to import it.  :(
119            imported = _imp.get_module(module, paths, info)
120            return getattr(imported, symbol, None)
121
122    return extract_constant(code, symbol, default)
123
124
125def extract_constant(code, symbol, default=-1):
126    """Extract the constant value of 'symbol' from 'code'
127
128    If the name 'symbol' is bound to a constant value by the Python code
129    object 'code', return that value.  If 'symbol' is bound to an expression,
130    return 'default'.  Otherwise, return 'None'.
131
132    Return value is based on the first assignment to 'symbol'.  'symbol' must
133    be a global, or at least a non-"fast" local in the code block.  That is,
134    only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol'
135    must be present in 'code.co_names'.
136    """
137    if symbol not in code.co_names:
138        # name's not there, can't possibly be an assignment
139        return None
140
141    name_idx = list(code.co_names).index(symbol)
142
143    STORE_NAME = 90
144    STORE_GLOBAL = 97
145    LOAD_CONST = 100
146
147    const = default
148
149    for byte_code in dis.Bytecode(code):
150        op = byte_code.opcode
151        arg = byte_code.arg
152
153        if op == LOAD_CONST:
154            const = code.co_consts[arg]
155        elif arg == name_idx and (op == STORE_NAME or op == STORE_GLOBAL):
156            return const
157        else:
158            const = default
159
160
161def _update_globals():
162    """
163    Patch the globals to remove the objects not available on some platforms.
164
165    XXX it'd be better to test assertions about bytecode instead.
166    """
167
168    if not sys.platform.startswith('java') and sys.platform != 'cli':
169        return
170    incompatible = 'extract_constant', 'get_module_constant'
171    for name in incompatible:
172        del globals()[name]
173        __all__.remove(name)
174
175
176_update_globals()
177