# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Module checking Spyder runtime dependencies""" # Standard library imports import os import os.path as osp import sys # Local imports from spyder.config.base import ( _, DEV, is_pynsist, running_in_ci, running_under_pytest) from spyder.utils import programs HERE = osp.dirname(osp.abspath(__file__)) # ============================================================================= # Kind of dependency # ============================================================================= MANDATORY = 'mandatory' OPTIONAL = 'optional' PLUGIN = 'spyder plugins' # ============================================================================= # Versions # ============================================================================= # Hard dependencies APPLAUNCHSERVICES_REQVER = '>=0.1.7' ATOMICWRITES_REQVER = '>=1.2.0' CHARDET_REQVER = '>=2.0.0' CLOUDPICKLE_REQVER = '>=0.5.0' COOKIECUTTER_REQVER = '>=1.6.0' DIFF_MATCH_PATCH_REQVER = '>=20181111' # None for pynsist install for now # (check way to add dist.info/egg.info from packages without wheels available) INTERVALTREE_REQVER = None if is_pynsist() else '>=3.0.2' IPYTHON_REQVER = ">=7.6.0" JEDI_REQVER = '>=0.17.2;<0.19.0' JSONSCHEMA_REQVER = '>=3.2.0' KEYRING_REQVER = '>=17.0.0' NBCONVERT_REQVER = '>=4.0' NUMPYDOC_REQVER = '>=0.6.0' PARAMIKO_REQVER = '>=2.4.0' PARSO_REQVER = '>=0.7.0;<0.9.0' PEXPECT_REQVER = '>=4.4.0' PICKLESHARE_REQVER = '>=0.4' PSUTIL_REQVER = '>=5.3' PYGMENTS_REQVER = '>=2.0' PYLINT_REQVER = '>=2.5.0;<2.10.0' PYLSP_REQVER = '>=1.2.2;<1.3.0' PYLSP_BLACK_REQVER = '>=1.0.0' PYLS_SPYDER_REQVER = '>=0.4.0' PYXDG_REQVER = '>=0.26' PYZMQ_REQVER = '>=17' QDARKSTYLE_REQVER = '=3.0.2' QSTYLIZER_REQVER = '>=0.1.10' QTAWESOME_REQVER = '>=1.0.2' QTCONSOLE_REQVER = '>=5.1.0' QTPY_REQVER = '>=1.5.0' RTREE_REQVER = '>=0.9.7' SETUPTOOLS_REQVER = '>=49.6.0' SPHINX_REQVER = '>=0.6.6' SPYDER_KERNELS_REQVER = '>=2.1.1;<2.2.0' TEXTDISTANCE_REQVER = '>=4.2.0' THREE_MERGE_REQVER = '>=0.1.1' # None for pynsist install for now # (check way to add dist.info/egg.info from packages without wheels available) WATCHDOG_REQVER = None if is_pynsist() else '>=0.10.3' # Optional dependencies CYTHON_REQVER = '>=0.21' MATPLOTLIB_REQVER = '>=2.0.0' NUMPY_REQVER = '>=1.7' PANDAS_REQVER = '>=1.1.1' SCIPY_REQVER = '>=0.17.0' SYMPY_REQVER = '>=0.7.3' # ============================================================================= # Descriptions # NOTE: We declare our dependencies in **alphabetical** order # If some dependencies are limited to some systems only, add a 'display' key. # See 'applaunchservices' for an example. # ============================================================================= # List of descriptions DESCRIPTIONS = [ {'modname': "applaunchservices", 'package_name': "applaunchservices", 'features': _("Notify macOS that Spyder can open Python files"), 'required_version': APPLAUNCHSERVICES_REQVER, 'display': sys.platform == "darwin"}, {'modname': "atomicwrites", 'package_name': "atomicwrites", 'features': _("Atomic file writes in the Editor"), 'required_version': ATOMICWRITES_REQVER}, {'modname': "chardet", 'package_name': "chardet", 'features': _("Character encoding auto-detection for the Editor"), 'required_version': CHARDET_REQVER}, {'modname': "cloudpickle", 'package_name': "cloudpickle", 'features': _("Handle communications between kernel and frontend"), 'required_version': CLOUDPICKLE_REQVER}, {'modname': "cookiecutter", 'package_name': "cookiecutter", 'features': _("Create projects from cookiecutter templates"), 'required_version': COOKIECUTTER_REQVER}, {'modname': "diff_match_patch", 'package_name': "diff-match-patch", 'features': _("Compute text file diff changes during edition"), 'required_version': DIFF_MATCH_PATCH_REQVER}, {'modname': "intervaltree", 'package_name': "intervaltree", 'features': _("Compute folding range nesting levels"), 'required_version': INTERVALTREE_REQVER}, {'modname': "IPython", 'package_name': "IPython", 'features': _("IPython interactive python environment"), 'required_version': IPYTHON_REQVER}, {'modname': "jedi", 'package_name': "jedi", 'features': _("Main backend for the Python Language Server"), 'required_version': JEDI_REQVER}, {'modname': 'jsonschema', 'package_name': 'jsonschema', 'features': _('Verify if snippets files are valid'), 'required_version': JSONSCHEMA_REQVER}, {'modname': "keyring", 'package_name': "keyring", 'features': _("Save Github credentials to report internal " "errors securely"), 'required_version': KEYRING_REQVER}, {'modname': "nbconvert", 'package_name': "nbconvert", 'features': _("Manipulate Jupyter notebooks in the Editor"), 'required_version': NBCONVERT_REQVER}, {'modname': "numpydoc", 'package_name': "numpydoc", 'features': _("Improve code completion for objects that use Numpy docstrings"), 'required_version': NUMPYDOC_REQVER}, {'modname': "paramiko", 'package_name': "paramiko", 'features': _("Connect to remote kernels through SSH"), 'required_version': PARAMIKO_REQVER, 'display': os.name == 'nt'}, {'modname': "parso", 'package_name': "parso", 'features': _("Python parser that supports error recovery and " "round-trip parsing"), 'required_version': PARSO_REQVER}, {'modname': "pexpect", 'package_name': "pexpect", 'features': _("Stdio support for our language server client"), 'required_version': PEXPECT_REQVER}, {'modname': "pickleshare", 'package_name': "pickleshare", 'features': _("Cache the list of installed Python modules"), 'required_version': PICKLESHARE_REQVER}, {'modname': "psutil", 'package_name': "psutil", 'features': _("CPU and memory usage info in the status bar"), 'required_version': PSUTIL_REQVER}, {'modname': "pygments", 'package_name': "pygments", 'features': _("Syntax highlighting for a lot of file types in the Editor"), 'required_version': PYGMENTS_REQVER}, {'modname': "pylint", 'package_name': "pylint", 'features': _("Static code analysis"), 'required_version': PYLINT_REQVER}, {'modname': 'pylsp', 'package_name': 'python-lsp-server', 'features': _("Code completion and linting for the Editor"), 'required_version': PYLSP_REQVER}, {'modname': 'pylsp_black', 'package_name': 'python-lsp-black', 'features': _("Autoformat Python files in the Editor with the Black " "package"), 'required_version': PYLSP_BLACK_REQVER}, {'modname': 'pyls_spyder', 'package_name': 'pyls-spyder', 'features': _('Spyder plugin for the Python LSP Server'), 'required_version': PYLS_SPYDER_REQVER}, {'modname': "xdg", 'package_name': "pyxdg", 'features': _("Parse desktop files on Linux"), 'required_version': PYXDG_REQVER, 'display': sys.platform.startswith('linux')}, {'modname': "zmq", 'package_name': "pyzmq", 'features': _("Client for the language server protocol (LSP)"), 'required_version': PYZMQ_REQVER}, {'modname': "qdarkstyle", 'package_name': "qdarkstyle", 'features': _("Dark style for the entire interface"), 'required_version': QDARKSTYLE_REQVER}, {'modname': "qstylizer", 'package_name': "qstylizer", 'features': _("Customize Qt stylesheets"), 'required_version': QSTYLIZER_REQVER}, {'modname': "qtawesome", 'package_name': "qtawesome", 'features': _("Icon theme based on FontAwesome and Material Design icons"), 'required_version': QTAWESOME_REQVER}, {'modname': "qtconsole", 'package_name': "qtconsole", 'features': _("Main package for the IPython console"), 'required_version': QTCONSOLE_REQVER}, {'modname': "qtpy", 'package_name': "qtpy", 'features': _("Abstraction layer for Python Qt bindings."), 'required_version': QTPY_REQVER}, {'modname': "rtree", 'package_name': "rtree", 'features': _("Fast access to code snippets regions"), 'required_version': RTREE_REQVER}, {'modname': "setuptools", 'package_name': "setuptools", 'features': _("Determine package version"), 'required_version': SETUPTOOLS_REQVER}, {'modname': "sphinx", 'package_name': "sphinx", 'features': _("Show help for objects in the Editor and Consoles in a dedicated pane"), 'required_version': SPHINX_REQVER}, {'modname': "spyder_kernels", 'package_name': "spyder-kernels", 'features': _("Jupyter kernels for the Spyder console"), 'required_version': SPYDER_KERNELS_REQVER}, {'modname': 'textdistance', 'package_name': "textdistance", 'features': _('Compute distances between strings'), 'required_version': TEXTDISTANCE_REQVER}, {'modname': "three_merge", 'package_name': "three-merge", 'features': _("3-way merge algorithm to merge document changes"), 'required_version': THREE_MERGE_REQVER}, {'modname': "watchdog", 'package_name': "watchdog", 'features': _("Watch file changes on project directories"), 'required_version': WATCHDOG_REQVER}, ] # Optional dependencies DESCRIPTIONS += [ {'modname': "cython", 'package_name': "cython", 'features': _("Run Cython files in the IPython Console"), 'required_version': CYTHON_REQVER, 'kind': OPTIONAL}, {'modname': "matplotlib", 'package_name': "matplotlib", 'features': _("2D/3D plotting in the IPython console"), 'required_version': MATPLOTLIB_REQVER, 'kind': OPTIONAL}, {'modname': "numpy", 'package_name': "numpy", 'features': _("View and edit two and three dimensional arrays in the Variable Explorer"), 'required_version': NUMPY_REQVER, 'kind': OPTIONAL}, {'modname': 'pandas', 'package_name': 'pandas', 'features': _("View and edit DataFrames and Series in the Variable Explorer"), 'required_version': PANDAS_REQVER, 'kind': OPTIONAL}, {'modname': "scipy", 'package_name': "scipy", 'features': _("Import Matlab workspace files in the Variable Explorer"), 'required_version': SCIPY_REQVER, 'kind': OPTIONAL}, {'modname': "sympy", 'package_name': "sympy", 'features': _("Symbolic mathematics in the IPython Console"), 'required_version': SYMPY_REQVER, 'kind': OPTIONAL} ] # ============================================================================= # Code # ============================================================================= class Dependency(object): """Spyder's dependency version may starts with =, >=, > or < to specify the exact requirement ; multiple conditions may be separated by ';' (e.g. '>=0.13;<1.0')""" OK = 'OK' NOK = 'NOK' def __init__(self, modname, package_name, features, required_version, installed_version=None, kind=MANDATORY): self.modname = modname self.package_name = package_name self.features = features self.required_version = required_version self.kind = kind if installed_version is None: try: self.installed_version = programs.get_module_version(modname) except Exception: # NOTE: Don't add any exception type here! # Modules can fail to import in several ways besides # ImportError self.installed_version = None else: self.installed_version = installed_version def check(self): """Check if dependency is installed""" if self.required_version: return programs.is_module_installed(self.modname, self.required_version) else: return True def get_installed_version(self): """Return dependency status (string)""" if self.check(): return '%s (%s)' % (self.installed_version, self.OK) else: return '%s (%s)' % (self.installed_version, self.NOK) def get_status(self): """Return dependency status (string)""" if self.check(): return self.OK else: return self.NOK DEPENDENCIES = [] def add(modname, package_name, features, required_version, installed_version=None, kind=MANDATORY): """Add Spyder dependency""" global DEPENDENCIES for dependency in DEPENDENCIES: if dependency.modname == modname: raise ValueError("Dependency has already been registered: %s"\ % modname) DEPENDENCIES += [Dependency(modname, package_name, features, required_version, installed_version, kind)] def check(modname): """Check if required dependency is installed""" for dependency in DEPENDENCIES: if dependency.modname == modname: return dependency.check() else: raise RuntimeError("Unknown dependency %s" % modname) def status(deps=DEPENDENCIES, linesep=os.linesep): """Return a status of dependencies.""" maxwidth = 0 data = [] # Find maximum width for dep in deps: title = dep.modname if dep.required_version is not None: title += ' ' + dep.required_version maxwidth = max([maxwidth, len(title)]) dep_order = {MANDATORY: '0', OPTIONAL: '1', PLUGIN: '2'} order_dep = {'0': MANDATORY, '1': OPTIONAL, '2': PLUGIN} data.append([dep_order[dep.kind], title, dep.get_installed_version()]) # Construct text and sort by kind and name maxwidth += 1 text = "" prev_order = '-1' for order, title, version in sorted(data, key=lambda x: x[0] + x[1].lower()): if order != prev_order: text += '{sep}# {name}:{sep}'.format( sep=linesep, name=order_dep[order].capitalize()) prev_order = order text += '{title}: {version}{linesep}'.format( title=title.ljust(maxwidth), version=version, linesep=linesep) # Remove spurious linesep when reporting deps to Github if not linesep == '
': text = text[:-1] return text def missing_dependencies(): """Return the status of missing dependencies (if any)""" missing_deps = [] for dependency in DEPENDENCIES: # Skip checking dependencies for which we have subrepos if (DEV or running_under_pytest()) and not running_in_ci(): repo_path = osp.normpath(osp.join(HERE, '..')) subrepos_path = osp.join(repo_path, 'external-deps') subrepos = os.listdir(subrepos_path) if dependency.package_name in subrepos: continue if dependency.kind != OPTIONAL and not dependency.check(): missing_deps.append(dependency) if missing_deps: return status(deps=missing_deps, linesep='
') else: return "" def declare_dependencies(): for dep in DESCRIPTIONS: if dep.get('display', True): add(dep['modname'], dep['package_name'], dep['features'], dep['required_version'], kind=dep.get('kind', MANDATORY))