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