Source code for spyder.config.base

# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
Spyder base configuration management

This file only deals with non-GUI configuration features
(in other words, we won't import any PyQt object here, avoiding any
sip API incompatibility issue in spyder's non-gui modules)
"""

from glob import glob
import locale
import os
import os.path as osp
import re
import shutil
import sys
import tempfile
import uuid
import warnings

# Local imports
from spyder import __version__
from spyder.utils import encoding

#==============================================================================
# Only for development
#==============================================================================
# To activate/deactivate certain things for development
# SPYDER_DEV is (and *only* has to be) set in bootstrap.py
DEV = os.environ.get('SPYDER_DEV')

# Manually override whether the dev configuration directory is used.
USE_DEV_CONFIG_DIR = os.environ.get('SPYDER_USE_DEV_CONFIG_DIR')

# Get a random id for the safe-mode config dir
CLEAN_DIR_ID = str(uuid.uuid4()).split('-')[-1]


def get_safe_mode():
    """
    Make Spyder use a temp clean configuration directory for testing
    purposes SPYDER_SAFE_MODE can be set using the --safe-mode option.
    """
    return bool(os.environ.get('SPYDER_SAFE_MODE'))


def running_under_pytest():
    """
    Return True if currently running under pytest.

    This function is used to do some adjustment for testing. The environment
    variable SPYDER_PYTEST is defined in conftest.py.
    """
    return bool(os.environ.get('SPYDER_PYTEST'))


def running_remoteclient_tests():
    """
    Return True if currently running the remoteclient tests.

    This function is used to do some adjustment for testing. The environment
    variable SPYDER_TEST_REMOTE_CLIENT is 'true' in conftest.py.
    """
    return bool(os.environ.get("SPYDER_TEST_REMOTE_CLIENT") == "true")


def running_in_ci():
    """Return True if currently running under CI."""
    return bool(os.environ.get('CI'))


def running_in_ci_with_conda():
    """Return True if currently running under CI with conda packages."""
    return running_in_ci() and os.environ.get('USE_CONDA', None) == 'true'


def running_in_binder():
    """Return True if currently running in Binder."""
    return (
        bool(os.environ.get("BINDER_REPO_URL"))
        and "spyder-ide/binder-environments" in os.environ["BINDER_REPO_URL"]
    )


def is_stable_version(version):
    """
    Return true if version is stable, i.e. with letters in the final component.

    Stable version examples: ``1.2``, ``1.3.4``, ``1.0.5``.
    Non-stable version examples: ``1.3.4beta``, ``0.1.0rc1``, ``3.0.0dev0``.
    """
    if not isinstance(version, tuple):
        version = version.split('.')
    last_part = version[-1]

    if not re.search(r'[a-zA-Z]', last_part):
        return True
    else:
        return False


def use_dev_config_dir(use_dev_config_dir=USE_DEV_CONFIG_DIR):
    """Return whether the dev configuration directory should used."""
    if use_dev_config_dir is not None:
        if use_dev_config_dir.lower() in {'false', '0'}:
            use_dev_config_dir = False
    else:
        use_dev_config_dir = DEV or not is_stable_version(__version__)

    return use_dev_config_dir


#==============================================================================
# Debug helpers
#==============================================================================
# This is needed after restarting and using debug_print
STDOUT = sys.stdout
STDERR = sys.stderr


def get_debug_level():
    debug_env = os.environ.get('SPYDER_DEBUG', '')
    if not debug_env.isdigit():
        debug_env = bool(debug_env)
    return int(debug_env)


def debug_print(*message):
    """Output debug messages to stdout"""
    warnings.warn("debug_print is deprecated; use the logging module instead.")
    if get_debug_level():
        ss = STDOUT
        # This is needed after restarting and using debug_print
        for m in message:
            ss.buffer.write(str(m).encode('utf-8'))
        print('', file=ss)


#==============================================================================
# Configuration paths
#==============================================================================
def get_conf_subfolder():
    """Return the configuration subfolder for different ooperating systems."""
    # Spyder settings dir
    # NOTE: During the 2.x.x series this dir was named .spyder2, but
    # since 3.0+ we've reverted back to use .spyder to simplify major
    # updates in version (required when we change APIs by Linux
    # packagers)
    if sys.platform.startswith('linux'):
        SUBFOLDER = 'spyder'
    else:
        SUBFOLDER = '.spyder'

    # We can't have PY2 and PY3 settings in the same dir because:
    # 1. This leads to ugly crashes and freezes (e.g. by trying to
    #    embed a PY2 interpreter in PY3)
    # 2. We need to save the list of installed modules (for code
    #    completion) separately for each version
    SUBFOLDER = SUBFOLDER + '-py3'

    # If running a development/beta version, save config in a separate
    # directory to avoid wiping or contaiminating the user's saved stable
    # configuration.
    if use_dev_config_dir():
        SUBFOLDER = SUBFOLDER + '-dev'

    return SUBFOLDER


def get_project_config_folder():
    """Return the default project configuration folder."""
    return '.spyproject'


def get_home_dir():
    """Return user home directory."""
    try:
        # expanduser() returns a raw byte string which needs to be
        # decoded with the codec that the OS is using to represent
        # file paths.
        path = encoding.to_unicode_from_fs(osp.expanduser('~'))
    except Exception:
        path = ''

    if osp.isdir(path):
        return path
    else:
        # Get home from alternative locations
        for env_var in ('HOME', 'USERPROFILE', 'TMP'):
            # os.environ.get() returns a raw byte string which needs to be
            # decoded with the codec that the OS is using to represent
            # environment variables.
            path = encoding.to_unicode_from_fs(os.environ.get(env_var, ''))
            if osp.isdir(path):
                return path
            else:
                path = ''

        if not path:
            raise RuntimeError('Please set the environment variable HOME to '
                               'your user/home directory path so Spyder can '
                               'start properly.')


def get_clean_conf_dir():
    """
    Return the path to a temp clean configuration dir, for tests and safe mode.
    """
    conf_dir = osp.join(
        tempfile.gettempdir(),
        'spyder-clean-conf-dirs',
        CLEAN_DIR_ID,
    )
    return conf_dir


def get_custom_conf_dir():
    """
    Use a custom configuration directory, passed through our command
    line options or by setting the env var below.
    """
    custom_dir = os.environ.get('SPYDER_CONFDIR')
    if custom_dir:
        custom_dir = osp.abspath(custom_dir)

        # Set env var to not lose its value in future calls when the cwd
        # is changed by Spyder.
        os.environ['SPYDER_CONFDIR'] = custom_dir
        return custom_dir


def get_conf_path(filename=None):
    """Return absolute path to the config file with the specified filename."""
    # Define conf_dir
    if running_under_pytest() or get_safe_mode():
        # Use clean config dir if running tests or the user requests it.
        conf_dir = get_clean_conf_dir()
    elif get_custom_conf_dir():
        # Use a custom directory if the user decided to do it through
        # our command line options.
        conf_dir = get_custom_conf_dir()
    elif sys.platform.startswith('linux'):
        # This makes us follow the XDG standard to save our settings
        # on Linux, as it was requested on spyder-ide/spyder#2629.
        xdg_config_home = os.environ.get('XDG_CONFIG_HOME', '')
        if not xdg_config_home:
            xdg_config_home = osp.join(get_home_dir(), '.config')

        if not osp.isdir(xdg_config_home):
            os.makedirs(xdg_config_home)

        conf_dir = osp.join(xdg_config_home, get_conf_subfolder())
    else:
        conf_dir = osp.join(get_home_dir(), get_conf_subfolder())

    # Create conf_dir
    if not osp.isdir(conf_dir):
        if running_under_pytest() or get_safe_mode() or get_custom_conf_dir():
            os.makedirs(conf_dir)
        else:
            os.mkdir(conf_dir)

    if filename is None:
        return conf_dir
    else:
        return osp.join(conf_dir, filename)


def get_conf_paths():
    """Return the files that can update system configuration defaults."""
    CONDA_PREFIX = os.environ.get('CONDA_PREFIX', None)

    if os.name == 'nt':
        SEARCH_PATH = (
            'C:/ProgramData/spyder',
        )
    else:
        SEARCH_PATH = (
            '/etc/spyder',
            '/usr/local/etc/spyder',
        )

    if CONDA_PREFIX is not None:
        CONDA_PREFIX = CONDA_PREFIX.replace('\\', '/')
        SEARCH_PATH += (
            '{}/etc/spyder'.format(CONDA_PREFIX),
        )

    SEARCH_PATH += (
        '{}/etc/spyder'.format(sys.prefix),
    )

    if running_under_pytest():
        search_paths = []
        tmpfolder = str(tempfile.gettempdir())
        for i in range(3):
            path = os.path.join(tmpfolder, 'site-config-' + str(i))
            if not os.path.isdir(path):
                os.makedirs(path)
            search_paths.append(path)
        SEARCH_PATH = tuple(search_paths)

    return SEARCH_PATH


def get_module_path(modname):
    """Return module *modname* base path"""
    return osp.abspath(osp.dirname(sys.modules[modname].__file__))


def get_module_data_path(modname, relpath=None, attr_name='DATAPATH'):
    """Return module *modname* data path
    Note: relpath is ignored if module has an attribute named *attr_name*

    Handles py2exe/cx_Freeze distributions"""
    datapath = getattr(sys.modules[modname], attr_name, '')
    if datapath:
        return datapath
    else:
        datapath = get_module_path(modname)
        parentdir = osp.join(datapath, osp.pardir)
        if osp.isfile(parentdir):
            # Parent directory is not a directory but the 'library.zip' file:
            # this is either a py2exe or a cx_Freeze distribution
            datapath = osp.abspath(osp.join(osp.join(parentdir, osp.pardir),
                                            modname))
        if relpath is not None:
            datapath = osp.abspath(osp.join(datapath, relpath))
        return datapath


def get_module_source_path(modname, basename=None):
    """Return module *modname* source path
    If *basename* is specified, return *modname.basename* path where
    *modname* is a package containing the module *basename*

    *basename* is a filename (not a module name), so it must include the
    file extension: .py or .pyw

    Handles py2exe/cx_Freeze distributions"""
    srcpath = get_module_path(modname)
    parentdir = osp.join(srcpath, osp.pardir)
    if osp.isfile(parentdir):
        # Parent directory is not a directory but the 'library.zip' file:
        # this is either a py2exe or a cx_Freeze distribution
        srcpath = osp.abspath(osp.join(osp.join(parentdir, osp.pardir),
                                       modname))
    if basename is not None:
        srcpath = osp.abspath(osp.join(srcpath, basename))
    return srcpath


def is_py2exe_or_cx_Freeze():
    """Return True if this is a py2exe/cx_Freeze distribution of Spyder"""
    return osp.isfile(osp.join(get_module_path('spyder'), osp.pardir))


#==============================================================================
# Translations
#==============================================================================
LANG_FILE = get_conf_path('langconfig')
DEFAULT_LANGUAGE = 'en'

# This needs to be updated every time a new language is added to spyder, and is
# used by the Preferences configuration to populate the Language QComboBox
LANGUAGE_CODES = {
    'en': 'English',
    'fr': 'Français',
    'es': 'Español',
    'hu': 'Magyar',
    'pt_BR': 'Português',
    'ru': 'Русский',
    'zh_CN': '简体中文',
    'ja': '日本語',
    'de': 'Deutsch',
    'pl': 'Polski',
    'fa': 'Persian',
    'hr': 'Croatian',
    'te': 'Telugu',
    'uk': 'Ukrainian',
}

# Disabled languages because their translations are outdated or incomplete
DISABLED_LANGUAGES = ['fa', 'hr', 'hu', 'pl', 'te', 'uk', 'ru']


def get_available_translations():
    """
    List available translations for spyder based on the folders found in the
    locale folder. This function checks if LANGUAGE_CODES contain the same
    information that is found in the 'locale' folder to ensure that when a new
    language is added, LANGUAGE_CODES is updated.
    """
    locale_path = get_module_data_path("spyder", relpath="locale",
                                       attr_name='LOCALEPATH')
    listdir = os.listdir(locale_path)
    langs = [d for d in listdir if osp.isdir(osp.join(locale_path, d))]
    langs = [DEFAULT_LANGUAGE] + langs

    # Remove disabled languages
    langs = list(set(langs) - set(DISABLED_LANGUAGES))

    # Check that there is a language code available in case a new translation
    # is added, to ensure LANGUAGE_CODES is updated.
    retlangs = []
    for lang in langs:
        if lang not in LANGUAGE_CODES:
            if DEV:
                error = ('Update LANGUAGE_CODES (inside config/base.py) if a '
                         'new translation has been added to Spyder. '
                         'Currently missing ' + lang)
                print(error)  # spyder: test-skip
                return ['en']
        else:
            retlangs.append(lang)

    return retlangs


def get_interface_language():
    """
    If Spyder has a translation available for the locale language, it will
    return the version provided by Spyder adjusted for language subdifferences,
    otherwise it will return DEFAULT_LANGUAGE.

    Example:
    1.) Spyder provides ('en', 'de', 'fr', 'es' 'hu' and 'pt_BR'), if the
    locale is either 'en_US' or 'en' or 'en_UK', this function will return 'en'

    2.) Spyder provides ('en', 'de', 'fr', 'es' 'hu' and 'pt_BR'), if the
    locale is either 'pt' or 'pt_BR', this function will return 'pt_BR'
    """
    
    if os.name == "nt":
        # Changing to locale.getlocale from locale.getdefaultlocale caused some
        # Windows machines to return non BCP47 locale codes. Instead use
        # win32 GetUserDefaultLocaleName which does seem to give BCP47 locale.
        # Fixes spyder-ide/spyder#23318.
        from ctypes import create_unicode_buffer, windll
        bufsize = 85  # LOCALE_NAME_MAX_LENGTH
        buf = create_unicode_buffer(bufsize)
        if windll.kernel32.GetUserDefaultLocaleName(buf, bufsize):
            locale_language = buf.value
        else:
            locale_language = DEFAULT_LANGUAGE
    else:
        # Fixes spyder-ide/spyder#3627.
        try:
            locale_language = locale.getlocale()[0]
        except ValueError:
            locale_language = DEFAULT_LANGUAGE

    # Tests expect English as the interface language
    if running_under_pytest():
        locale_language = DEFAULT_LANGUAGE

    language = DEFAULT_LANGUAGE

    if locale_language is not None:
        spyder_languages = get_available_translations()
        for lang in spyder_languages:
            if locale_language == lang:
                language = locale_language
                break
            elif (locale_language.startswith(lang) or
                    lang.startswith(locale_language)):
                language = lang
                break

    return language


def save_lang_conf(value):
    """Save language setting to language config file"""
    # Needed to avoid an error when trying to save LANG_FILE
    # but the operation fails for some reason.
    # See spyder-ide/spyder#8807.
    try:
        with open(LANG_FILE, 'w') as f:
            f.write(value)
    except EnvironmentError:
        pass


def load_lang_conf():
    """
    Load language setting from language config file if it exists, otherwise
    try to use the local settings if Spyder provides a translation, or
    return the default if no translation provided.
    """
    if osp.isfile(LANG_FILE):
        with open(LANG_FILE, 'r') as f:
            lang = f.read()
    else:
        lang = get_interface_language()
        save_lang_conf(lang)

    # Save language again if it's been disabled
    if lang.strip('\n') in DISABLED_LANGUAGES:
        lang = DEFAULT_LANGUAGE
        save_lang_conf(lang)

    return lang


[docs] def get_translation(modname, dirname=None): """Return translation callback for module *modname*""" if dirname is None: dirname = modname def translate_dumb(x): """Dumb function to not use translations.""" return x locale_path = get_module_data_path(dirname, relpath="locale", attr_name='LOCALEPATH') # If LANG is defined in Ubuntu, a warning message is displayed, # so in Unix systems we define the LANGUAGE variable. language = load_lang_conf() if os.name == 'nt': # Trying to set LANG on Windows can fail when Spyder is # run with admin privileges. # Fixes spyder-ide/spyder#6886. try: os.environ["LANG"] = language # Works on Windows except Exception: return translate_dumb else: os.environ["LANGUAGE"] = language # Works on Linux if language == "en": return translate_dumb import gettext try: _trans = gettext.translation(modname, locale_path) def translate_gettext(x): return _trans.gettext(x) return translate_gettext except Exception as exc: # logging module is not yet initialised at this point print( f"Could not load translations for {language} due to: " f"{exc.__class__.__name__} - {exc}", file=sys.stderr ) return translate_dumb
# Translation callback _ = get_translation("spyder") #============================================================================== # Namespace Browser (Variable Explorer) configuration management #============================================================================== # Variable explorer display / check all elements data types for sequences: # (when saving the variable explorer contents, check_all is True, CHECK_ALL = False # XXX: If True, this should take too much to compute... EXCLUDED_NAMES = ['nan', 'inf', 'infty', 'little_endian', 'colorbar_doc', 'typecodes', '__builtins__', '__main__', '__doc__', 'NaN', 'Inf', 'Infinity', 'sctypes', 'rcParams', 'rcParamsDefault', 'sctypeNA', 'typeNA', 'False_', 'True_'] #============================================================================== # Conda-based installer application utilities #============================================================================== def is_conda_based_app(pyexec=sys.executable): """ Check if Spyder is running from the conda-based installer by looking for the `spyder-menu.json` file. If a Python executable is provided, checks if it is in a conda-based installer environment or the root environment thereof. """ real_pyexec = osp.realpath(pyexec) # pyexec may be symlink if os.name == 'nt': env_path = osp.dirname(real_pyexec) else: env_path = osp.dirname(osp.dirname(real_pyexec)) menu_rel_path = '/Menu/conda-based-app' if ( osp.exists(env_path + menu_rel_path) or glob(env_path + '/envs/*' + menu_rel_path) ): return True else: return False #============================================================================== # Reset config files #============================================================================== SAVED_CONFIG_FILES = ('help', 'onlinehelp', 'path', 'pylint.results', 'spyder.ini', 'temp.py', 'temp.spydata', 'template.py', 'history.py', 'history_internal.py', 'workingdir', '.projects', '.spyproject', '.ropeproject', 'monitor.log', 'monitor_debug.log', 'rope.log', 'langconfig', 'spyder.lock', 'config{}spyder.ini'.format(os.sep), 'config{}transient.ini'.format(os.sep), 'lsp_root_path', 'plugins') def reset_config_files(): """Remove all config files""" print("*** Reset Spyder settings to defaults ***", file=STDERR) for fname in SAVED_CONFIG_FILES: cfg_fname = get_conf_path(fname) if osp.isfile(cfg_fname) or osp.islink(cfg_fname): os.remove(cfg_fname) elif osp.isdir(cfg_fname): shutil.rmtree(cfg_fname) else: continue print("removing:", cfg_fname, file=STDERR)