jpayne@68: from __future__ import annotations jpayne@68: jpayne@68: import contextlib jpayne@68: import dis jpayne@68: import marshal jpayne@68: import sys jpayne@68: jpayne@68: from packaging.version import Version jpayne@68: jpayne@68: from . import _imp jpayne@68: from ._imp import PY_COMPILED, PY_FROZEN, PY_SOURCE, find_module jpayne@68: jpayne@68: __all__ = ['Require', 'find_module'] jpayne@68: jpayne@68: jpayne@68: class Require: jpayne@68: """A prerequisite to building or installing a distribution""" jpayne@68: jpayne@68: def __init__( jpayne@68: self, jpayne@68: name, jpayne@68: requested_version, jpayne@68: module, jpayne@68: homepage: str = '', jpayne@68: attribute=None, jpayne@68: format=None, jpayne@68: ): jpayne@68: if format is None and requested_version is not None: jpayne@68: format = Version jpayne@68: jpayne@68: if format is not None: jpayne@68: requested_version = format(requested_version) jpayne@68: if attribute is None: jpayne@68: attribute = '__version__' jpayne@68: jpayne@68: self.__dict__.update(locals()) jpayne@68: del self.self jpayne@68: jpayne@68: def full_name(self): jpayne@68: """Return full package/distribution name, w/version""" jpayne@68: if self.requested_version is not None: jpayne@68: return '%s-%s' % (self.name, self.requested_version) jpayne@68: return self.name jpayne@68: jpayne@68: def version_ok(self, version): jpayne@68: """Is 'version' sufficiently up-to-date?""" jpayne@68: return ( jpayne@68: self.attribute is None jpayne@68: or self.format is None jpayne@68: or str(version) != "unknown" jpayne@68: and self.format(version) >= self.requested_version jpayne@68: ) jpayne@68: jpayne@68: def get_version(self, paths=None, default: str = "unknown"): jpayne@68: """Get version number of installed module, 'None', or 'default' jpayne@68: jpayne@68: Search 'paths' for module. If not found, return 'None'. If found, jpayne@68: return the extracted version attribute, or 'default' if no version jpayne@68: attribute was specified, or the value cannot be determined without jpayne@68: importing the module. The version is formatted according to the jpayne@68: requirement's version format (if any), unless it is 'None' or the jpayne@68: supplied 'default'. jpayne@68: """ jpayne@68: jpayne@68: if self.attribute is None: jpayne@68: try: jpayne@68: f, p, i = find_module(self.module, paths) jpayne@68: except ImportError: jpayne@68: return None jpayne@68: if f: jpayne@68: f.close() jpayne@68: return default jpayne@68: jpayne@68: v = get_module_constant(self.module, self.attribute, default, paths) jpayne@68: jpayne@68: if v is not None and v is not default and self.format is not None: jpayne@68: return self.format(v) jpayne@68: jpayne@68: return v jpayne@68: jpayne@68: def is_present(self, paths=None): jpayne@68: """Return true if dependency is present on 'paths'""" jpayne@68: return self.get_version(paths) is not None jpayne@68: jpayne@68: def is_current(self, paths=None): jpayne@68: """Return true if dependency is present and up-to-date on 'paths'""" jpayne@68: version = self.get_version(paths) jpayne@68: if version is None: jpayne@68: return False jpayne@68: return self.version_ok(str(version)) jpayne@68: jpayne@68: jpayne@68: def maybe_close(f): jpayne@68: @contextlib.contextmanager jpayne@68: def empty(): jpayne@68: yield jpayne@68: return jpayne@68: jpayne@68: if not f: jpayne@68: return empty() jpayne@68: jpayne@68: return contextlib.closing(f) jpayne@68: jpayne@68: jpayne@68: # Some objects are not available on some platforms. jpayne@68: # XXX it'd be better to test assertions about bytecode instead. jpayne@68: if not sys.platform.startswith('java') and sys.platform != 'cli': jpayne@68: jpayne@68: def get_module_constant(module, symbol, default: str | int = -1, paths=None): jpayne@68: """Find 'module' by searching 'paths', and extract 'symbol' jpayne@68: jpayne@68: Return 'None' if 'module' does not exist on 'paths', or it does not define jpayne@68: 'symbol'. If the module defines 'symbol' as a constant, return the jpayne@68: constant. Otherwise, return 'default'.""" jpayne@68: jpayne@68: try: jpayne@68: f, path, (suffix, mode, kind) = info = find_module(module, paths) jpayne@68: except ImportError: jpayne@68: # Module doesn't exist jpayne@68: return None jpayne@68: jpayne@68: with maybe_close(f): jpayne@68: if kind == PY_COMPILED: jpayne@68: f.read(8) # skip magic & date jpayne@68: code = marshal.load(f) jpayne@68: elif kind == PY_FROZEN: jpayne@68: code = _imp.get_frozen_object(module, paths) jpayne@68: elif kind == PY_SOURCE: jpayne@68: code = compile(f.read(), path, 'exec') jpayne@68: else: jpayne@68: # Not something we can parse; we'll have to import it. :( jpayne@68: imported = _imp.get_module(module, paths, info) jpayne@68: return getattr(imported, symbol, None) jpayne@68: jpayne@68: return extract_constant(code, symbol, default) jpayne@68: jpayne@68: def extract_constant(code, symbol, default: str | int = -1): jpayne@68: """Extract the constant value of 'symbol' from 'code' jpayne@68: jpayne@68: If the name 'symbol' is bound to a constant value by the Python code jpayne@68: object 'code', return that value. If 'symbol' is bound to an expression, jpayne@68: return 'default'. Otherwise, return 'None'. jpayne@68: jpayne@68: Return value is based on the first assignment to 'symbol'. 'symbol' must jpayne@68: be a global, or at least a non-"fast" local in the code block. That is, jpayne@68: only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol' jpayne@68: must be present in 'code.co_names'. jpayne@68: """ jpayne@68: if symbol not in code.co_names: jpayne@68: # name's not there, can't possibly be an assignment jpayne@68: return None jpayne@68: jpayne@68: name_idx = list(code.co_names).index(symbol) jpayne@68: jpayne@68: STORE_NAME = dis.opmap['STORE_NAME'] jpayne@68: STORE_GLOBAL = dis.opmap['STORE_GLOBAL'] jpayne@68: LOAD_CONST = dis.opmap['LOAD_CONST'] jpayne@68: jpayne@68: const = default jpayne@68: jpayne@68: for byte_code in dis.Bytecode(code): jpayne@68: op = byte_code.opcode jpayne@68: arg = byte_code.arg jpayne@68: jpayne@68: if op == LOAD_CONST: jpayne@68: const = code.co_consts[arg] jpayne@68: elif arg == name_idx and (op == STORE_NAME or op == STORE_GLOBAL): jpayne@68: return const jpayne@68: else: jpayne@68: const = default jpayne@68: jpayne@68: return None jpayne@68: jpayne@68: __all__ += ['get_module_constant', 'extract_constant']