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