Aluode/PerceptionLabPortable
0
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 