CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
config.py992 linesDownload Raw Back to utils
1"""The config functions."""2 3# Authors: The MNE-Python contributors.4# License: BSD-3-Clause5# Copyright the MNE-Python contributors.6 7import atexit8import contextlib9import json10import multiprocessing11import os12import os.path as op13import platform14import shutil15import subprocess16import sys17import tempfile18from functools import lru_cache, partial19from importlib import import_module20from pathlib import Path21from urllib.error import URLError22from urllib.request import urlopen23 24from packaging.version import parse25 26from ._logging import logger, warn27from .check import (28    _check_fname,29    _check_option,30    _check_qt_version,31    _soft_import,32    _validate_type,33)34from .docs import fill_doc35from .misc import _pl36 37_temp_home_dir = None38 39 40class UnknownPlatformError(Exception):41    """Exception raised for unknown platforms."""42 43 44def set_cache_dir(cache_dir):45    """Set the directory to be used for temporary file storage.46 47    This directory is used by joblib to store memmapped arrays,48    which reduces memory requirements and speeds up parallel49    computation.50 51    Parameters52    ----------53    cache_dir : str or None54        Directory to use for temporary file storage. None disables55        temporary file storage.56    """57    if cache_dir is not None and not op.exists(cache_dir):58        raise OSError(f"Directory {cache_dir} does not exist")59 60    set_config("MNE_CACHE_DIR", cache_dir, set_env=False)61 62 63def set_memmap_min_size(memmap_min_size):64    """Set the minimum size for memmaping of arrays for parallel processing.65 66    Parameters67    ----------68    memmap_min_size : str or None69        Threshold on the minimum size of arrays that triggers automated memory70        mapping for parallel processing, e.g., '1M' for 1 megabyte.71        Use None to disable memmaping of large arrays.72    """73    _validate_type(memmap_min_size, (str, None), "memmap_min_size")74    if memmap_min_size is not None:75        if memmap_min_size[-1] not in ["K", "M", "G"]:76            raise ValueError(77                "The size has to be given in kilo-, mega-, or "78                f"gigabytes, e.g., 100K, 500M, 1G, got {repr(memmap_min_size)}"79            )80 81    set_config("MNE_MEMMAP_MIN_SIZE", memmap_min_size, set_env=False)82 83 84# List the known configuration values85_known_config_types = {86    "MNE_3D_OPTION_ANTIALIAS": (87        "bool, whether to use full-screen antialiasing in 3D plots"88    ),89    "MNE_3D_OPTION_DEPTH_PEELING": "bool, whether to use depth peeling in 3D plots",90    "MNE_3D_OPTION_MULTI_SAMPLES": (91        "int, number of samples to use for full-screen antialiasing"92    ),93    "MNE_3D_OPTION_SMOOTH_SHADING": ("bool, whether to use smooth shading in 3D plots"),94    "MNE_3D_OPTION_THEME": ("str, the color theme (light or dark) to use for 3D plots"),95    "MNE_BROWSE_RAW_SIZE": (96        "tuple, width and height of the raw browser window (in inches)"97    ),98    "MNE_BROWSER_BACKEND": (99        "str, the backend to use for the MNE Browse Raw window (qt or matplotlib)"100    ),101    "MNE_BROWSER_OVERVIEW_MODE": (102        "str, the overview mode to use in the MNE Browse Raw window )"103        "(see mne.viz.plot_raw for valid options)"104    ),105    "MNE_BROWSER_PRECOMPUTE": (106        "bool, whether to precompute raw data in the MNE Browse Raw window"107    ),108    "MNE_BROWSER_THEME": "str, the color theme (light or dark) to use for the browser",109    "MNE_BROWSER_USE_OPENGL": (110        "bool, whether to use OpenGL for rendering in the MNE Browse Raw window"111    ),112    "MNE_CACHE_DIR": "str, path to the cache directory for parallel execution",113    "MNE_COREG_ADVANCED_RENDERING": (114        "bool, whether to use advanced OpenGL rendering in mne coreg"115    ),116    "MNE_COREG_COPY_ANNOT": (117        "bool, whether to copy the annotation files during warping"118    ),119    "MNE_COREG_FULLSCREEN": "bool, whether to use full-screen mode in mne coreg",120    "MNE_COREG_GUESS_MRI_SUBJECT": (121        "bool, whether to guess the MRI subject in mne coreg"122    ),123    "MNE_COREG_HEAD_HIGH_RES": (124        "bool, whether to use high-res head surface in mne coreg"125    ),126    "MNE_COREG_HEAD_OPACITY": ("bool, the head surface opacity to use in mne coreg"),127    "MNE_COREG_HEAD_INSIDE": (128        "bool, whether to add an opaque inner scalp head surface to help "129        "occlude points behind the head in mne coreg"130    ),131    "MNE_COREG_INTERACTION": (132        "str, interaction style in mne coreg (trackball or terrain)"133    ),134    "MNE_COREG_MARK_INSIDE": (135        "bool, whether to mark points inside the head surface in mne coreg"136    ),137    "MNE_COREG_PREPARE_BEM": (138        "bool, whether to prepare the BEM solution after warping in mne coreg"139    ),140    "MNE_COREG_ORIENT_TO_SURFACE": (141        "bool, whether to orient the digitization markers to the head surface "142        "in mne coreg"143    ),144    "MNE_COREG_SCALE_LABELS": (145        "bool, whether to scale the MRI labels during warping in mne coreg"146    ),147    "MNE_COREG_SCALE_BY_DISTANCE": (148        "bool, whether to scale the digitization markers by their distance from "149        "the scalp in mne coreg"150    ),151    "MNE_COREG_SCENE_SCALE": (152        "float, the scale factor of the 3D scene in mne coreg (default 0.16)"153    ),154    "MNE_COREG_WINDOW_HEIGHT": "int, window height for mne coreg",155    "MNE_COREG_WINDOW_WIDTH": "int, window width for mne coreg",156    "MNE_COREG_SUBJECTS_DIR": "str, path to the subjects directory for mne coreg",157    "MNE_CUDA_DEVICE": "int, CUDA device to use for GPU processing",158    "MNE_DATA": "str, default data directory",159    "MNE_DATASETS_BRAINSTORM_PATH": "str, path for brainstorm data",160    "MNE_DATASETS_EEGBCI_PATH": "str, path for EEGBCI data",161    "MNE_DATASETS_EPILEPSY_ECOG_PATH": "str, path for epilepsy_ecog data",162    "MNE_DATASETS_HF_SEF_PATH": "str, path for HF_SEF data",163    "MNE_DATASETS_MEGSIM_PATH": "str, path for MEGSIM data",164    "MNE_DATASETS_MISC_PATH": "str, path for misc data",165    "MNE_DATASETS_MTRF_PATH": "str, path for MTRF data",166    "MNE_DATASETS_SAMPLE_PATH": "str, path for sample data",167    "MNE_DATASETS_SOMATO_PATH": "str, path for somato data",168    "MNE_DATASETS_MULTIMODAL_PATH": "str, path for multimodal data",169    "MNE_DATASETS_FNIRS_MOTOR_PATH": "str, path for fnirs_motor data",170    "MNE_DATASETS_OPM_PATH": "str, path for OPM data",171    "MNE_DATASETS_SPM_FACE_DATASETS_TESTS": "str, path for spm_face data",172    "MNE_DATASETS_SPM_FACE_PATH": "str, path for spm_face data",173    "MNE_DATASETS_TESTING_PATH": "str, path for testing data",174    "MNE_DATASETS_VISUAL_92_CATEGORIES_PATH": "str, path for visual_92_categories data",175    "MNE_DATASETS_KILOWORD_PATH": "str, path for kiloword data",176    "MNE_DATASETS_FIELDTRIP_CMC_PATH": "str, path for fieldtrip_cmc data",177    "MNE_DATASETS_PHANTOM_KIT_PATH": "str, path for phantom_kit data",178    "MNE_DATASETS_PHANTOM_4DBTI_PATH": "str, path for phantom_4dbti data",179    "MNE_DATASETS_PHANTOM_KERNEL_PATH": "str, path for phantom_kernel data",180    "MNE_DATASETS_LIMO_PATH": "str, path for limo data",181    "MNE_DATASETS_REFMEG_NOISE_PATH": "str, path for refmeg_noise data",182    "MNE_DATASETS_SSVEP_PATH": "str, path for ssvep data",183    "MNE_DATASETS_ERP_CORE_PATH": "str, path for erp_core data",184    "MNE_FORCE_SERIAL": "bool, force serial rather than parallel execution",185    "MNE_LOGGING_LEVEL": (186        "str or int, controls the level of verbosity of any function "187        "decorated with @verbose. See "188        "https://mne.tools/stable/auto_tutorials/intro/50_configure_mne.html#logging"189    ),190    "MNE_MEMMAP_MIN_SIZE": (191        "str, threshold on the minimum size of arrays passed to the workers that "192        "triggers automated memory mapping, e.g., 1M or 0.5G"193    ),194    "MNE_REPR_HTML": (195        "bool, represent some of our objects with rich HTML in a notebook environment"196    ),197    "MNE_SKIP_NETWORK_TESTS": (198        "bool, used in a test decorator (@requires_good_network) to skip "199        "tests that include large downloads"200    ),201    "MNE_SKIP_TESTING_DATASET_TESTS": (202        "bool, used in test decorators (@requires_spm_data, "203        "@requires_bstraw_data) to skip tests that require specific datasets"204    ),205    "MNE_STIM_CHANNEL": "string, the default channel name for mne.find_events",206    "MNE_TQDM": (207        'str, either "tqdm", "tqdm.auto", or "off". Controls presence/absence '208        "of progress bars"209    ),210    "MNE_USE_CUDA": "bool, use GPU for filtering/resampling",211    "MNE_USE_NUMBA": (212        "bool, use Numba just-in-time compiler for some of our intensive computations"213    ),214    "SUBJECTS_DIR": "path-like, directory of freesurfer MRI files for each subject",215}216 217# These allow for partial matches, e.g. 'MNE_STIM_CHANNEL_1' is okay key218_known_config_wildcards = (219    "MNE_STIM_CHANNEL",  # can have multiple stim channels220    "MNE_DATASETS_FNIRS",  # mne-nirs221    "MNE_NIRS",  # mne-nirs222    "MNE_KIT2FIFF",  # mne-kit-gui223    "MNE_ICALABEL",  # mne-icalabel224    "MNE_LSL",  # mne-lsl225)226 227 228@contextlib.contextmanager229def _open_lock(path, *args, **kwargs):230    """231    Context manager that opens a file with an optional file lock.232 233    If the `filelock` package is available, a lock is acquired on a lock file234    based on the given path (by appending '.lock').235 236    Otherwise, a null context is used. The path is then opened in the237    specified mode.238 239    Parameters240    ----------241    path : str242        The path to the file to be opened.243    *args, **kwargs : optional244        Additional arguments and keyword arguments to be passed to the245        `open` function.246 247    """248    filelock = _soft_import(249        "filelock", purpose="parallel config set and get", strict=False250    )251 252    lock_context = contextlib.nullcontext()  # default to no lock253 254    if filelock:255        lock_path = f"{path}.lock"256        try:257            lock_context = filelock.FileLock(lock_path, timeout=5)258            lock_context.acquire()259        except TimeoutError:260            warn(261                "Could not acquire lock file after 5 seconds, consider deleting it "262                f"if you know the corresponding file is usable:\n{lock_path}"263            )264            lock_context = contextlib.nullcontext()265 266    with lock_context, open(path, *args, **kwargs) as fid:267        yield fid268 269 270def _load_config(config_path, raise_error=False):271    """Safely load a config file."""272    with _open_lock(config_path, "r+") as fid:273        try:274            config = json.load(fid)275        except ValueError:276            # No JSON object could be decoded --> corrupt file?277            msg = (278                f"The MNE-Python config file ({config_path}) is not a valid JSON "279                "file and might be corrupted"280            )281            if raise_error:282                raise RuntimeError(msg)283            warn(msg)284            config = dict()285    return config286 287 288def get_config_path(home_dir=None):289    r"""Get path to standard mne-python config file.290 291    Parameters292    ----------293    home_dir : str | None294        The folder that contains the .mne config folder.295        If None, it is found automatically.296 297    Returns298    -------299    config_path : str300        The path to the mne-python configuration file. On windows, this301        will be '%USERPROFILE%\.mne\mne-python.json'. On every other302        system, this will be ~/.mne/mne-python.json.303    """304    val = op.join(_get_extra_data_path(home_dir=home_dir), "mne-python.json")305    return val306 307 308def get_config(key=None, default=None, raise_error=False, home_dir=None, use_env=True):309    """Read MNE-Python preferences from environment or config file.310 311    Parameters312    ----------313    key : None | str314        The preference key to look for. The os environment is searched first,315        then the mne-python config file is parsed.316        If None, all the config parameters present in environment variables or317        the path are returned. If key is an empty string, a list of all valid318        keys (but not values) is returned.319    default : str | None320        Value to return if the key is not found.321    raise_error : bool322        If True, raise an error if the key is not found (instead of returning323        default).324    home_dir : str | None325        The folder that contains the .mne config folder.326        If None, it is found automatically.327    use_env : bool328        If True, consider env vars, if available.329        If False, only use MNE-Python configuration file values.330 331        .. versionadded:: 0.18332 333    Returns334    -------335    value : dict | str | None336        The preference key value.337 338    See Also339    --------340    set_config341    """342    _validate_type(key, (str, type(None)), "key", "string or None")343 344    if key == "":345        # These are str->str (immutable) so we should just copy the dict346        # itself, no need for deepcopy347        return _known_config_types.copy()348 349    # first, check to see if key is in env350    if use_env and key is not None and key in os.environ:351        return os.environ[key]352 353    # second, look for it in mne-python config file354    config_path = get_config_path(home_dir=home_dir)355    if not op.isfile(config_path):356        config = {}357    else:358        config = _load_config(config_path)359 360    if key is None:361        # update config with environment variables362        if use_env:363            env_keys = set(config).union(_known_config_types).intersection(os.environ)364            config.update({key: os.environ[key] for key in env_keys})365        return config366    elif raise_error is True and key not in config:367        loc_env = "the environment or in the " if use_env else ""368        meth_env = (369            (f'either os.environ["{key}"] = VALUE for a temporary solution, or ')370            if use_env371            else ""372        )373        extra_env = (374            " You can also set the environment variable before running python."375            if use_env376            else ""377        )378        meth_file = (379            f'mne.utils.set_config("{key}", VALUE, set_env=True) for a permanent one'380        )381        raise KeyError(382            f'Key "{key}" not found in {loc_env}'383            f"the mne-python config file ({config_path}). "384            f"Try {meth_env}{meth_file}.{extra_env}"385        )386    else:387        return config.get(key, default)388 389 390def set_config(key, value, home_dir=None, set_env=True):391    """Set a MNE-Python preference key in the config file and environment.392 393    Parameters394    ----------395    key : str396        The preference key to set.397    value : str |  None398        The value to assign to the preference key. If None, the key is399        deleted.400    home_dir : str | None401        The folder that contains the .mne config folder.402        If None, it is found automatically.403    set_env : bool404        If True (default), update :data:`os.environ` in addition to405        updating the MNE-Python config file.406 407    See Also408    --------409    get_config410    """411    _validate_type(key, "str", "key")412    # While JSON allow non-string types, we allow users to override config413    # settings using env, which are strings, so we enforce that here414    _validate_type(value, (str, "path-like", type(None)), "value")415    if value is not None:416        value = str(value)417 418    if key not in _known_config_types and not any(419        key.startswith(k) for k in _known_config_wildcards420    ):421        warn(f'Setting non-standard config type: "{key}"')422 423    # Read all previous values424    config_path = get_config_path(home_dir=home_dir)425    if op.isfile(config_path):426        config = _load_config(config_path, raise_error=True)427    else:428        config = dict()429        logger.info(430            f"Attempting to create new mne-python configuration file:\n{config_path}"431        )432    if value is None:433        config.pop(key, None)434        if set_env and key in os.environ:435            del os.environ[key]436    else:437        config[key] = value438        if set_env:439            os.environ[key] = value440        if key == "MNE_BROWSER_BACKEND":441            from ..viz._figure import set_browser_backend442 443            set_browser_backend(value)444 445    # Write all values. This may fail if the default directory is not446    # writeable.447    directory = op.dirname(config_path)448    if not op.isdir(directory):449        os.mkdir(directory)450 451    # Adapting the mode depend if you are create the file452    # or no.453    mode = "r+" if op.isfile(config_path) else "w+"454 455    with _open_lock(config_path, mode) as fid:456        try:457            data = json.load(fid)458        except (ValueError, json.JSONDecodeError) as exc:459            logger.info(460                f"Could not read the {config_path} json file during the writing."461                f" Assuming it is empty. Got: {exc}"462            )463            data = {}464 465        if value is None:466            data.pop(key, None)467        else:468            data[key] = value469 470        fid.seek(0)471        fid.truncate()472        json.dump(data, fid, sort_keys=True, indent=0)473 474 475def _get_extra_data_path(home_dir=None):476    """Get path to extra data (config, tables, etc.)."""477    global _temp_home_dir478    if home_dir is None:479        home_dir = os.environ.get("_MNE_FAKE_HOME_DIR")480    if home_dir is None:481        # this has been checked on OSX64, Linux64, and Win32482        if "nt" == os.name.lower():483            APPDATA_DIR = os.getenv("APPDATA")484            USERPROFILE_DIR = os.getenv("USERPROFILE")485            if APPDATA_DIR is not None and op.isdir(486                op.join(APPDATA_DIR, ".mne")487            ):  # backward-compat488                home_dir = APPDATA_DIR489            elif USERPROFILE_DIR is not None:490                home_dir = USERPROFILE_DIR491            else:492                raise FileNotFoundError(493                    "The USERPROFILE environment variable is not set, cannot "494                    "determine the location of the MNE-Python configuration "495                    "folder"496                )497            del APPDATA_DIR, USERPROFILE_DIR498        else:499            # This is a more robust way of getting the user's home folder on500            # Linux platforms (not sure about OSX, Unix or BSD) than checking501            # the HOME environment variable. If the user is running some sort502            # of script that isn't launched via the command line (e.g. a script503            # launched via Upstart) then the HOME environment variable will504            # not be set.505            if os.getenv("MNE_DONTWRITE_HOME", "") == "true":506                if _temp_home_dir is None:507                    _temp_home_dir = tempfile.mkdtemp()508                    atexit.register(509                        partial(shutil.rmtree, _temp_home_dir, ignore_errors=True)510                    )511                home_dir = _temp_home_dir512            else:513                home_dir = os.path.expanduser("~")514 515        if home_dir is None:516            raise ValueError(517                "mne-python config file path could "518                "not be determined, please report this "519                "error to mne-python developers"520            )521 522    return op.join(home_dir, ".mne")523 524 525def get_subjects_dir(subjects_dir=None, raise_error=False):526    """Safely use subjects_dir input to return SUBJECTS_DIR.527 528    Parameters529    ----------530    subjects_dir : path-like | None531        If a value is provided, return subjects_dir. Otherwise, look for532        SUBJECTS_DIR config and return the result.533    raise_error : bool534        If True, raise a KeyError if no value for SUBJECTS_DIR can be found535        (instead of returning None).536 537    Returns538    -------539    value : Path | None540        The SUBJECTS_DIR value.541    """542    from_config = False543    if subjects_dir is None:544        subjects_dir = get_config("SUBJECTS_DIR", raise_error=raise_error)545        from_config = True546        if subjects_dir is not None:547            subjects_dir = Path(subjects_dir)548    if subjects_dir is not None:549        # Emit a nice error or warning if their config is bad550        try:551            subjects_dir = _check_fname(552                fname=subjects_dir,553                overwrite="read",554                must_exist=True,555                need_dir=True,556                name="subjects_dir",557            )558        except FileNotFoundError:559            if from_config:560                msg = (561                    "SUBJECTS_DIR in your MNE-Python configuration or environment "562                    "does not exist, consider using mne.set_config to fix it: "563                    f"{subjects_dir}"564                )565                if raise_error:566                    raise FileNotFoundError(msg) from None567                else:568                    warn(msg)569            elif raise_error:570                raise571 572    return subjects_dir573 574 575@fill_doc576def _get_stim_channel(stim_channel, info, raise_error=True):577    """Determine the appropriate stim_channel.578 579    First, 'MNE_STIM_CHANNEL', 'MNE_STIM_CHANNEL_1', 'MNE_STIM_CHANNEL_2', etc.580    are read. If these are not found, it will fall back to 'STI 014' if581    present, then fall back to the first channel of type 'stim', if present.582 583    Parameters584    ----------585    stim_channel : str | list of str | None586        The stim channel selected by the user.587    %(info_not_none)s588 589    Returns590    -------591    stim_channel : list of str592        The name of the stim channel(s) to use593    """594    from .._fiff.pick import pick_types595 596    if stim_channel is not None:597        if not isinstance(stim_channel, list):598            _validate_type(stim_channel, "str", "Stim channel")599            stim_channel = [stim_channel]600        for channel in stim_channel:601            _validate_type(channel, "str", "Each provided stim channel")602        return stim_channel603 604    stim_channel = list()605    ch_count = 0606    ch = get_config("MNE_STIM_CHANNEL")607    while ch is not None and ch in info["ch_names"]:608        stim_channel.append(ch)609        ch_count += 1610        ch = get_config(f"MNE_STIM_CHANNEL_{ch_count}")611    if ch_count > 0:612        return stim_channel613 614    if "STI101" in info["ch_names"]:  # combination channel for newer systems615        return ["STI101"]616    if "STI 014" in info["ch_names"]:  # for older systems617        return ["STI 014"]618 619    stim_channel = pick_types(info, meg=False, ref_meg=False, stim=True)620    if len(stim_channel) == 0 and raise_error:621        raise ValueError(622            "No stim channels found. Consider specifying them "623            "manually using the 'stim_channel' parameter."624        )625    stim_channel = [info["ch_names"][ch_] for ch_ in stim_channel]626    return stim_channel627 628 629def _get_root_dir():630    """Get as close to the repo root as possible."""631    root_dir = Path(__file__).parents[1]632    up_dir = root_dir.parent633    if (up_dir / "setup.py").is_file() and all(634        (up_dir / x).is_dir() for x in ("mne", "examples", "doc")635    ):636        root_dir = up_dir637    return root_dir638 639 640def _get_numpy_libs():641    bad_lib = "unknown linalg bindings"642    try:643        from threadpoolctl import threadpool_info644    except Exception as exc:645        return bad_lib + f" (threadpoolctl module not found: {exc})"646    pools = threadpool_info()647    rename = dict(648        openblas="OpenBLAS",649        mkl="MKL",650    )651    for pool in pools:652        if pool["internal_api"] in ("openblas", "mkl"):653            return (654                f"{rename[pool['internal_api']]} "655                f"{pool['version']} with "656                f"{pool['num_threads']} thread{_pl(pool['num_threads'])}"657            )658    return bad_lib659 660 661_gpu_cmd = """\662from pyvista import GPUInfo; \663gi = GPUInfo(); \664print(gi.version); \665print(gi.renderer)"""666 667 668@lru_cache(maxsize=1)669def _get_gpu_info():670    # Once https://github.com/pyvista/pyvista/pull/2250 is merged and PyVista671    # does a release, we can triage based on version > 0.33.2672    proc = subprocess.run(673        [sys.executable, "-c", _gpu_cmd], check=False, capture_output=True674    )675    out = proc.stdout.decode().strip().replace("\r", "").split("\n")676    if proc.returncode or len(out) != 2:677        return None, None678    return out679 680 681def _get_total_memory():682    """Return the total memory of the system in bytes."""683    if platform.system() == "Windows":684        o = subprocess.check_output(685            [686                "powershell.exe",687                "(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory",688            ]689        ).decode()690        # Can get for example a "running scripts is disabled on this system"691        # error where "o" will be a long string rather than an int692        try:693            total_memory = int(o)694        except Exception:  # pragma: no cover695            total_memory = 0696    elif platform.system() == "Linux":697        o = subprocess.check_output(["free", "-b"]).decode()698        total_memory = int(o.splitlines()[1].split()[1])699    elif platform.system() == "Darwin":700        o = subprocess.check_output(["sysctl", "hw.memsize"]).decode()701        total_memory = int(o.split(":")[1].strip())702    else:703        raise UnknownPlatformError("Could not determine total memory")704 705    return total_memory706 707 708def _get_cpu_brand():709    """Return the CPU brand string."""710    if platform.system() == "Windows":711        o = subprocess.check_output(712            ["powershell.exe", "(Get-CimInstance Win32_Processor).Name"]713        ).decode()714        cpu_brand = o.strip().splitlines()[-1]715    elif platform.system() == "Linux":716        o = subprocess.check_output(["grep", "model name", "/proc/cpuinfo"]).decode()717        cpu_brand = o.splitlines()[0].split(": ")[1]718    elif platform.system() == "Darwin":719        o = subprocess.check_output(["sysctl", "machdep.cpu"]).decode()720        cpu_brand = o.split("brand_string: ")[1].strip()721    else:722        cpu_brand = "?"723 724    return cpu_brand725 726 727def sys_info(728    fid=None,729    show_paths=False,730    *,731    dependencies="user",732    unicode="auto",733    check_version=True,734):735    """Print system information.736 737    This function prints system information useful when triaging bugs.738 739    Parameters740    ----------741    fid : file-like | None742        The file to write to. Will be passed to :func:`print()`. Can be None to743        use :data:`sys.stdout`.744    show_paths : bool745        If True, print paths for each module.746    dependencies : 'user' | 'developer'747        Show dependencies relevant for users (default) or for developers748        (i.e., output includes additional dependencies).749    unicode : bool | "auto"750        Include Unicode symbols in output. If "auto", corresponds to True on Linux and751        macOS, and False on Windows.752 753        .. versionadded:: 0.24754    check_version : bool | float755        If True (default), attempt to check that the version of MNE-Python is up to date756        with the latest release on GitHub. Can be a float to give a different timeout757        (in sec) from the default (2 sec).758 759        .. versionadded:: 1.6760    """761    _validate_type(dependencies, str)762    _check_option("dependencies", dependencies, ("user", "developer"))763    _validate_type(check_version, (bool, "numeric"), "check_version")764    _validate_type(unicode, (bool, str), "unicode")765    _check_option("unicode", unicode, ("auto", True, False))766    if unicode == "auto":767        if platform.system() in ("Darwin", "Linux"):768            unicode = True769        else:  # Windows770            unicode = False771    ljust = 24 if dependencies == "developer" else 21772    platform_str = platform.platform()773 774    out = partial(print, end="", file=fid)775    out("Platform".ljust(ljust) + platform_str + "\n")776    out("Python".ljust(ljust) + str(sys.version).replace("\n", " ") + "\n")777    out("Executable".ljust(ljust) + sys.executable + "\n")778    try:779        cpu_brand = _get_cpu_brand()780    except Exception:781        cpu_brand = "?"782    out("CPU".ljust(ljust) + f"{cpu_brand} ")783    out(f"({multiprocessing.cpu_count()} cores)\n")784    out("Memory".ljust(ljust))785    try:786        total_memory = _get_total_memory()787    except UnknownPlatformError:788        total_memory = "?"789    else:790        total_memory = f"{total_memory / 1024**3:.1f}"  # convert to GiB791    out(f"{total_memory} GiB\n")792    out("\n")793    ljust -= 3  # account for +/- symbols794    libs = _get_numpy_libs()795    unavailable = []796    use_mod_names = (797        "# Core",798        "mne",799        "numpy",800        "scipy",801        "matplotlib",802        "",803        "# Numerical (optional)",804        "sklearn",805        "numba",806        "nibabel",807        "nilearn",808        "dipy",809        "openmeeg",810        "cupy",811        "pandas",812        "h5io",813        "h5py",814        "",815        "# Visualization (optional)",816        "pyvista",817        "pyvistaqt",818        "vtk",819        "qtpy",820        "ipympl",821        "pyqtgraph",822        "mne-qt-browser",823        "ipywidgets",824        # "trame",  # no version, see https://github.com/Kitware/trame/issues/183825        "trame_client",826        "trame_server",827        "trame_vtk",828        "trame_vuetify",829        "",830        "# Ecosystem (optional)",831        "mne-bids",832        "mne-nirs",833        "mne-features",834        "mne-connectivity",835        "mne-icalabel",836        "mne-bids-pipeline",837        "neo",838        "eeglabio",839        "edfio",840        "curryreader",841        "mffpy",842        "pybv",843        "antio",844        "defusedxml",845        "",846    )847    if dependencies == "developer":848        use_mod_names += (849            "# Testing",850            "pytest",851            "statsmodels",852            "numpydoc",853            "jupyter_client",854            "nbclient",855            "nbformat",856            "nitime",857            "imageio",858            "imageio-ffmpeg",859            "snirf",860            "",861            "# Documentation",862            "sphinx",863            "sphinx-gallery",864            "pydata-sphinx-theme",865            "",866            "# Infrastructure",867            "decorator",868            "jinja2",869            # "lazy-loader",870            "packaging",871            "pooch",872            "tqdm",873            "",874        )875    try:876        unicode = unicode and (sys.stdout.encoding.lower().startswith("utf"))877    except Exception:  # in case someone overrides sys.stdout in an unsafe way878        unicode = False879    mne_version_good = True880    for mi, mod_name in enumerate(use_mod_names):881        # upcoming break882        if mod_name == "":  # break883            if unavailable:884                out("└☐ " if unicode else " - ")885                out("unavailable".ljust(ljust))886                out(f"{', '.join(unavailable)}\n")887                unavailable = []888            if mi != len(use_mod_names) - 1:889                out("\n")890            continue891        elif mod_name.startswith("# "):  # header892            mod_name = mod_name.replace("# ", "")893            out(f"{mod_name}\n")894            continue895        pre = "├"896        last = use_mod_names[mi + 1] == "" and not unavailable897        if last:898            pre = "└"899        try:900            mod = import_module(mod_name.replace("-", "_"))901        except Exception:902            unavailable.append(mod_name)903        else:904            mark = "☑" if unicode else "+"905            mne_extra = ""906            if mod_name == "mne" and check_version:907                timeout = 2.0 if check_version is True else float(check_version)908                mne_version_good, mne_extra = _check_mne_version(timeout)909                if mne_version_good is None:910                    mne_version_good = True911                elif not mne_version_good:912                    mark = "☒" if unicode else "X"913            out(f"{pre}{mark} " if unicode else f" {mark} ")914            out(f"{mod_name}".ljust(ljust))915            if mod_name == "vtk":916                vtk_version = mod.vtkVersion()917                # 9.0 dev has VersionFull but 9.0 doesn't918                for attr in ("GetVTKVersionFull", "GetVTKVersion"):919                    if hasattr(vtk_version, attr):920                        version = getattr(vtk_version, attr)()921                        if version != "":922                            out(version)923                            break924                else:925                    out("unknown")926            else:927                out(mod.__version__.lstrip("v"))928            if mod_name == "numpy":929                out(f" ({libs})")930            elif mod_name == "qtpy":931                version, api = _check_qt_version(return_api=True)932                out(f" ({api}={version})")933            elif mod_name == "matplotlib":934                out(f" (backend={mod.get_backend()})")935            elif mod_name == "pyvista":936                version, renderer = _get_gpu_info()937                if version is None:938                    out(" (OpenGL unavailable)")939                else:940                    out(f" (OpenGL {version} via {renderer})")941            elif mod_name == "mne":942                out(f" ({mne_extra})")943            # Now comes stuff after the version944            if show_paths:945                if last:946                    pre = "   "947                elif unicode:948                    pre = "│  "949                else:950                    pre = " | "951                out(f"\n{pre}{' ' * ljust}{op.dirname(mod.__file__)}")952            out("\n")953 954    if not mne_version_good:955        out(956            "\nTo update to the latest supported release version to get bugfixes and "957            "improvements, visit "958            "https://mne.tools/stable/install/updating.html\n"959        )960 961 962def _get_latest_version(timeout):963    # Bandit complains about urlopen, but we know the URL here964    url = "https://api.github.com/repos/mne-tools/mne-python/releases/latest"965    try:966        with urlopen(url, timeout=timeout) as f:  # nosec967            response = json.load(f)968    except (URLError, TimeoutError) as err:969        # Triage error type970        if "SSL" in str(err):971            return "SSL error"972        elif "timed out" in str(err):973            return f"timeout after {timeout} sec"974        else:975            return f"unknown error: {err}"976    else:977        return response["tag_name"].lstrip("v") or "version unknown"978 979 980def _check_mne_version(timeout):981    rel_ver = _get_latest_version(timeout)982    if not rel_ver[0].isnumeric():983        return None, (f"unable to check for latest version on GitHub, {rel_ver}")984    rel_ver = parse(rel_ver)985    this_ver = parse(import_module("mne").__version__)986    if this_ver > rel_ver:987        return True, f"development, latest release is {rel_ver}"988    if this_ver == rel_ver:989        return True, "latest release"990    else:991        return False, f"outdated, release {rel_ver} is available!"992 
Aluode/PerceptionLabPortable · CoolFace