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