# Copyright Iris contributors
#
# This file is part of Iris and is released under the BSD license.
# See LICENSE in the root of the repository for full licensing details.
"""Provides access to Iris-specific configuration values.
The default configuration values can be overridden by creating the file
``iris/etc/site.cfg``. If it exists, this file must conform to the format
defined by :mod:`configparser`.
----------
.. py:data:: iris.config.TEST_DATA_DIR
Local directory where test data exists. Defaults to "test_data"
sub-directory of the Iris package install directory. The test data
directory supports the subset of Iris unit tests that require data.
Directory contents accessed via :func:`iris.tests.get_data_path`.
.. py:data:: iris.config.PALETTE_PATH
The full path to the Iris palette configuration directory
----------
"""
import configparser
import contextlib
import logging
import os.path
import warnings
import iris.warnings
[docs]
def get_logger(name, datefmt=None, fmt=None, level=None, propagate=None, handler=True):
"""Create a custom class for logging.
Create a :class:`logging.Logger` with a :class:`logging.StreamHandler`
and custom :class:`logging.Formatter`.
Parameters
----------
name :
The name of the logger. Typically this is the module filename that
owns the logger.
datefmt : optional
The date format string of the :class:`logging.Formatter`.
Defaults to ``%d-%m-%Y %H:%M:%S``.
fmt : optional
The additional format string of the :class:`logging.Formatter`.
This is appended to the default format string
``%(asctime)s %(name)s %(levelname)s - %(message)s``.
level : optional
The threshold level of the logger. Defaults to ``INFO``.
propagate : optional
Sets the ``propagate`` attribute of the :class:`logging.Logger`,
which determines whether events logged to this logger will be
passed to the handlers of higher level loggers. Defaults to
``False``.
handler : bool, default=True
Create and attach a :class:`logging.StreamHandler` to the
logger. Defaults to ``True``.
Returns
-------
:class:`logging.Logger`.
"""
if level is None:
# Default logging level.
level = "INFO"
if propagate is None:
# Default logging propagate behaviour.
propagate = False
# Create the named logger.
logger = logging.getLogger(name)
logger.setLevel(level)
logger.propagate = propagate
# Create and add the handler to the logger, if required.
if handler:
if datefmt is None:
# Default date format string.
datefmt = "%d-%m-%Y %H:%M:%S"
# Default format string.
_fmt = "%(asctime)s %(name)s %(levelname)s - %(message)s"
# Append additional format string, if appropriate.
fmt = _fmt if fmt is None else f"{_fmt} {fmt}"
# Create a formatter.
formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
# Create a logging handler.
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
# Returns simple string options
[docs]
def get_option(section, option, default=None):
"""Return the option value for the given section.
Returns the option value for the given section, or the default value
if the section/option is not present.
"""
value = default
if config.has_option(section, option):
value = config.get(section, option)
return value
# Returns directory path options
[docs]
def get_dir_option(section, option, default=None):
"""Return the directory path from the given option and section.
Returns the directory path from the given option and section, or
returns the given default value if the section/option is not present
or does not represent a valid directory.
"""
path = default
if config.has_option(section, option):
c_path = config.get(section, option)
if os.path.isdir(c_path):
path = c_path
else:
msg = (
"Ignoring config item {!r}:{!r} (section:option) as {!r}"
" is not a valid directory path."
)
warnings.warn(
msg.format(section, option, c_path),
category=iris.warnings.IrisIgnoringWarning,
)
return path
# Figure out the full path to the "iris" package.
ROOT_PATH = os.path.abspath(os.path.dirname(__file__))
# The full path to the configuration directory of the active Iris instance.
CONFIG_PATH = os.path.join(ROOT_PATH, "etc")
# Load the optional "site.cfg" file if it exists.
config = configparser.ConfigParser()
config.read([os.path.join(CONFIG_PATH, "site.cfg")])
##################
# Resource options
_RESOURCE_SECTION = "Resources"
TEST_DATA_DIR = get_dir_option(
_RESOURCE_SECTION,
"test_data_dir",
default=os.path.join(os.path.dirname(__file__), "test_data"),
)
# Override the data repository if the appropriate environment variable
# has been set.
override = os.environ.get("OVERRIDE_TEST_DATA_REPOSITORY")
if override:
TEST_DATA_DIR = None
if os.path.isdir(os.path.expanduser(override)):
TEST_DATA_DIR = os.path.abspath(override)
PALETTE_PATH = get_dir_option(
_RESOURCE_SECTION, "palette_path", os.path.join(CONFIG_PATH, "palette")
)
# Runtime options
[docs]
class NetCDF:
"""Control Iris NetCDF options."""
def __init__(self, conventions_override=None):
"""Set up NetCDF processing options for Iris.
Parameters
----------
conventions_override : bool, optional
Define whether the CF Conventions version (e.g. `CF-1.6`) set when
saving a cube to a NetCDF file should be defined by
Iris (the default) or the cube being saved. If `False`
(the default), specifies that Iris should set the
CF Conventions version when saving cubes as NetCDF files.
If `True`, specifies that the cubes being saved to NetCDF should
set the CF Conventions version for the saved NetCDF files.
Examples
--------
* Specify, for the lifetime of the session, that we want all cubes
written to NetCDF to define their own CF Conventions versions::
iris.config.netcdf.conventions_override = True
iris.save('my_cube', 'my_dataset.nc')
iris.save('my_second_cube', 'my_second_dataset.nc')
* Specify, with a context manager, that we want a cube written to
NetCDF to define its own CF Conventions version::
with iris.config.netcdf.context(conventions_override=True):
iris.save('my_cube', 'my_dataset.nc')
"""
# Define allowed `__dict__` keys first.
self.__dict__["conventions_override"] = None
# Now set specific values.
setattr(self, "conventions_override", conventions_override)
def __repr__(self):
msg = "NetCDF options: {}."
# Automatically populate with all currently accepted kwargs.
options = ["{}={}".format(k, v) for k, v in self.__dict__.items()]
joined = ", ".join(options)
return msg.format(joined)
def __setattr__(self, name, value):
if name not in self.__dict__:
# Can't add new names.
msg = "Cannot set option {!r} for {} configuration."
raise AttributeError(msg.format(name, self.__class__.__name__))
if value is None:
# Set an unset value to the name's default.
value = self._defaults_dict[name]["default"]
if self._defaults_dict[name]["options"] is not None:
# Replace a bad value with a good one if there is a defined set of
# specified good values. If there isn't, we can assume that
# anything goes.
if value not in self._defaults_dict[name]["options"]:
good_value = self._defaults_dict[name]["default"]
wmsg = (
"Attempting to set invalid value {!r} for "
"attribute {!r}. Defaulting to {!r}."
)
warnings.warn(
wmsg.format(value, name, good_value),
category=iris.warnings.IrisDefaultingWarning,
)
value = good_value
self.__dict__[name] = value
@property
def _defaults_dict(self):
# Set this as a property so that it isn't added to `self.__dict__`.
return {
"conventions_override": {
"default": False,
"options": [True, False],
},
}
[docs]
@contextlib.contextmanager
def context(self, **kwargs):
"""Allow temporary modification of the options via a context manager.
Accepted kwargs are the same as can be supplied to the Option.
"""
# Snapshot the starting state for restoration at the end of the
# contextmanager block.
starting_state = self.__dict__.copy()
# Update the state to reflect the requested changes.
for name, value in kwargs.items():
setattr(self, name, value)
try:
yield
finally:
# Return the state to the starting state.
self.__dict__.clear()
self.__dict__.update(starting_state)
netcdf = NetCDF()