Source code for satpy._config

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2016-2019 Satpy developers
#
# This file is part of satpy.
#
# satpy is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# satpy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# satpy.  If not, see <http://www.gnu.org/licenses/>.
"""Satpy Configuration directory and file handling."""
from __future__ import annotations

import ast
import glob
import logging
import os
import sys
import tempfile
from collections import OrderedDict
from importlib.metadata import EntryPoint, entry_points
from importlib.resources import files as impr_files
from typing import Iterable

from donfig import Config
from platformdirs import AppDirs

from satpy._compat import cache

LOG = logging.getLogger(__name__)

BASE_PATH = os.path.dirname(os.path.realpath(__file__))
# FIXME: Use package_resources?
PACKAGE_CONFIG_PATH = os.path.join(BASE_PATH, "etc")

_satpy_dirs = AppDirs(appname="satpy", appauthor="pytroll")
_CONFIG_DEFAULTS = {
    "tmp_dir": tempfile.gettempdir(),
    "cache_dir": _satpy_dirs.user_cache_dir,
    "cache_lonlats": False,
    "cache_sensor_angles": False,
    "config_path": [],
    "data_dir": _satpy_dirs.user_data_dir,
    "demo_data_dir": ".",
    "download_aux": True,
    "sensor_angles_position_preference": "actual",
    "readers": {
        "clip_negative_radiances": False,
    },
}

# Satpy main configuration object
# See https://donfig.readthedocs.io/en/latest/configuration.html
# for more information.
#
# Configuration values will be loaded from files at:
# 1. The builtin package satpy.yaml (not present currently)
# 2. $SATPY_ROOT_CONFIG (default: /etc/satpy/satpy.yaml)
# 3. <python-env-prefix>/etc/satpy/satpy.yaml
# 4. ~/.config/satpy/satpy.yaml
# 5. ~/.satpy/satpy.yaml
# 6. $SATPY_CONFIG_PATH/satpy.yaml if present (colon separated)
_CONFIG_PATHS = [
    os.path.join(PACKAGE_CONFIG_PATH, "satpy.yaml"),
    os.getenv("SATPY_ROOT_CONFIG", os.path.join("/etc", "satpy", "satpy.yaml")),
    os.path.join(sys.prefix, "etc", "satpy", "satpy.yaml"),
    os.path.join(_satpy_dirs.user_config_dir, "satpy.yaml"),
    os.path.join(os.path.expanduser("~"), ".satpy", "satpy.yaml"),
]
# The above files can also be directories. If directories all files
# with `.yaml`., `.yml`, or `.json` extensions will be used.

_ppp_config_dir = os.getenv("PPP_CONFIG_DIR", None)
_satpy_config_path = os.getenv("SATPY_CONFIG_PATH", None)

if _ppp_config_dir is not None and _satpy_config_path is None:
    LOG.warning("'PPP_CONFIG_DIR' is deprecated. Please use 'SATPY_CONFIG_PATH' instead.")
    _satpy_config_path = _ppp_config_dir

if _satpy_config_path is not None:
    if _satpy_config_path.startswith("["):
        # 'SATPY_CONFIG_PATH' is set by previous satpy config as a reprsentation of a 'list'
        # need to use 'ast.literal_eval' to parse the string back to a list
        _satpy_config_path_list = ast.literal_eval(_satpy_config_path)
    else:
        # colon-separated are ordered by custom -> builtins
        # i.e. last-applied/highest priority to first-applied/lowest priority
        _satpy_config_path_list = _satpy_config_path.split(os.pathsep)

    os.environ["SATPY_CONFIG_PATH"] = repr(_satpy_config_path_list)
    for config_dir in _satpy_config_path_list:
        _CONFIG_PATHS.append(os.path.join(config_dir, "satpy.yaml"))

_ancpath = os.getenv("SATPY_ANCPATH", None)
_data_dir = os.getenv("SATPY_DATA_DIR", None)
if _ancpath is not None and _data_dir is None:
    LOG.warning("'SATPY_ANCPATH' is deprecated. Please use 'SATPY_DATA_DIR' instead.")
    os.environ["SATPY_DATA_DIR"] = _ancpath

config = Config("satpy", defaults=[_CONFIG_DEFAULTS], paths=_CONFIG_PATHS)


[docs] def get_config_path_safe(): """Get 'config_path' and check for proper 'list' type.""" config_path = config.get("config_path") if not isinstance(config_path, list): raise ValueError("Satpy config option 'config_path' must be a " "list, not '{}'".format(type(config_path))) return config_path
[docs] def get_entry_points_config_dirs(group_name: str, include_config_path: bool = True) -> list[str]: """Get the config directories for all entry points of given name.""" dirs: list[str] = [] for entry_point in cached_entry_point(group_name): module = _entry_point_module(entry_point) new_dir = str(impr_files(module) / "etc") if not dirs or dirs[-1] != new_dir: dirs.append(new_dir) if include_config_path: dirs.extend(config.get("config_path")[::-1]) return dirs
[docs] @cache def cached_entry_point(group_name: str) -> Iterable[EntryPoint]: """Return entry_point for specified ``group``. This is a dummy proxy to allow caching and provide compatibility between versions of Python and importlib_metadata. """ try: # mypy in pre-commit currently checks for Python 3.8 compatibility # this line is for Python 3.10+ so it will fail checks return entry_points(group=group_name) # type: ignore except TypeError: # Python <3.10 entry_points_list = entry_points() return entry_points_list.get(group_name, [])
[docs] def _entry_point_module(entry_point): try: return entry_point.module except AttributeError: # Python 3.8 return entry_point.value.split(":")[0].strip()
[docs] def config_search_paths(filename, search_dirs=None, **kwargs): """Get series of configuration base paths where Satpy configs are located.""" if search_dirs is None: search_dirs = get_config_path_safe()[::-1] paths = [filename, os.path.basename(filename)] paths += [os.path.join(search_dir, filename) for search_dir in search_dirs] paths += [os.path.join(PACKAGE_CONFIG_PATH, filename)] paths = [os.path.abspath(path) for path in paths] if kwargs.get("check_exists", True): paths = [x for x in paths if os.path.isfile(x)] paths = list(OrderedDict.fromkeys(paths)) # flip the order of the list so builtins are loaded first return paths[::-1]
[docs] def glob_config(pattern, search_dirs=None): """Return glob results for all possible configuration locations. Note: This method does not check the configuration "base" directory if the pattern includes a subdirectory. This is done for performance since this is usually used to find *all* configs for a certain component. """ patterns = config_search_paths(pattern, search_dirs=search_dirs, check_exists=False) for pattern_fn in patterns: for path in glob.iglob(pattern_fn): yield path
[docs] def get_config_path(filename): """Get the path to the highest priority version of a config file.""" paths = config_search_paths(filename) for path in paths[::-1]: if os.path.exists(path): return path raise FileNotFoundError("Could not find file in configuration path: " "'{}'".format(filename))