1import contextlib
2import dis
3import marshal
4import sys
5
6from packaging.version import Version
7
8from . import _imp
9from ._imp import PY_COMPILED, PY_FROZEN, PY_SOURCE, find_module
10
11__all__ = ['Require', 'find_module']
12
13
14class Require:
15 """A prerequisite to building or installing a distribution"""
16
17 def __init__(
18 self, name, requested_version, module, homepage='', attribute=None, format=None
19 ):
20 if format is None and requested_version is not None:
21 format = Version
22
23 if format is not None:
24 requested_version = format(requested_version)
25 if attribute is None:
26 attribute = '__version__'
27
28 self.__dict__.update(locals())
29 del self.self
30
31 def full_name(self):
32 """Return full package/distribution name, w/version"""
33 if self.requested_version is not None:
34 return '%s-%s' % (self.name, self.requested_version)
35 return self.name
36
37 def version_ok(self, version):
38 """Is 'version' sufficiently up-to-date?"""
39 return (
40 self.attribute is None
41 or self.format is None
42 or str(version) != "unknown"
43 and self.format(version) >= self.requested_version
44 )
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 except ImportError:
61 return None
62 if f:
63 f.close()
64 return default
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
91 if not f:
92 return empty()
93
94 return contextlib.closing(f)
95
96
97# Some objects are not available on some platforms.
98# XXX it'd be better to test assertions about bytecode instead.
99if not sys.platform.startswith('java') and sys.platform != 'cli':
100
101 def 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) = info = find_module(module, paths)
110 except ImportError:
111 # Module doesn't exist
112 return None
113
114 with maybe_close(f):
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, paths)
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 imported = _imp.get_module(module, paths, info)
125 return getattr(imported, symbol, None)
126
127 return extract_constant(code, symbol, default)
128
129 def extract_constant(code, symbol, default=-1):
130 """Extract the constant value of 'symbol' from 'code'
131
132 If the name 'symbol' is bound to a constant value by the Python code
133 object 'code', return that value. If 'symbol' is bound to an expression,
134 return 'default'. Otherwise, return 'None'.
135
136 Return value is based on the first assignment to 'symbol'. 'symbol' must
137 be a global, or at least a non-"fast" local in the code block. That is,
138 only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol'
139 must be present in 'code.co_names'.
140 """
141 if symbol not in code.co_names:
142 # name's not there, can't possibly be an assignment
143 return None
144
145 name_idx = list(code.co_names).index(symbol)
146
147 STORE_NAME = dis.opmap['STORE_NAME']
148 STORE_GLOBAL = dis.opmap['STORE_GLOBAL']
149 LOAD_CONST = dis.opmap['LOAD_CONST']
150
151 const = default
152
153 for byte_code in dis.Bytecode(code):
154 op = byte_code.opcode
155 arg = byte_code.arg
156
157 if op == LOAD_CONST:
158 const = code.co_consts[arg]
159 elif arg == name_idx and (op == STORE_NAME or op == STORE_GLOBAL):
160 return const
161 else:
162 const = default
163
164 return None
165
166 __all__ += ['get_module_constant', 'extract_constant']