Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 107 additions & 2 deletions lib/iris/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2010 - 2016, Met Office
# (C) British Crown Copyright 2010 - 2017, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -66,8 +66,10 @@

from __future__ import (absolute_import, division, print_function)
from six.moves import (filter, input, map, range, zip) # noqa

import six
from six.moves import configparser

import contextlib
import os.path
import warnings

Expand Down Expand Up @@ -154,3 +156,106 @@ def get_dir_option(section, option, default=None):


IMPORT_LOGGER = get_option(_LOGGING_SECTION, 'import_logger')


#################
# Runtime options

class NetCDF(object):
"""Control Iris NetCDF options."""

def __init__(self, conventions_override=None):
"""
Set up NetCDF processing options for Iris.

Currently accepted kwargs:

* conventions_override (bool):
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.

Example usages:

* 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 six.iteritems(self.__dict__)]
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))
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]},
}
Copy link
Member

@bjlittle bjlittle Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkillick The _defaults_dict is private to the class, so is there a real need to make this a property? I can understand making this a read-only property if it was part of the public API, but it's not, so is there a need to be this restrictive or am I missing something? (most likely)

Why can't the self._defaults_dict be created and initialised just the once in the __init__ ?


@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 six.iteritems(kwargs):
setattr(self, name, value)
try:
yield
finally:
# Return the state to the starting state.
self.__dict__.clear()
self.__dict__.update(starting_state)


netcdf = NetCDF()
7 changes: 5 additions & 2 deletions lib/iris/fileformats/netcdf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2010 - 2016, Met Office
# (C) British Crown Copyright 2010 - 2017, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -2244,7 +2244,10 @@ def is_valid_packspec(p):
shuffle, fletcher32, contiguous, chunksizes, endian,
least_significant_digit, packing=packspec)

conventions = CF_CONVENTIONS_VERSION
if iris.config.netcdf.conventions_override:
conventions = cube.attributes['Conventions']
Copy link
Member

@bjlittle bjlittle Apr 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkillick The cube.attributes may not necessarily have any Conventions to use ...

I guess if that is the case, it seems reasonable to use the default CF_CONVENTIONS_VERSION instead rather than raise an exception ...

else:
conventions = CF_CONVENTIONS_VERSION

# Perform a CF patch of the conventions attribute.
cf_profile_available = (iris.site_configuration.get('cf_profile') not
Expand Down
20 changes: 20 additions & 0 deletions lib/iris/tests/unit/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# (C) British Crown Copyright 2017, Met Office
#
# This file is part of Iris.
#
# Iris is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Iris 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
"""Unit tests for the :mod:`iris.config` module."""

from __future__ import (absolute_import, division, print_function)
from six.moves import (filter, input, map, range, zip) # noqa
57 changes: 57 additions & 0 deletions lib/iris/tests/unit/config/test_NetCDF.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# (C) British Crown Copyright 2017, Met Office
#
# This file is part of Iris.
#
# Iris is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Iris 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
"""Unit tests for the `iris.options.Paralle` class."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkillick cut'n paste ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And a typo!


from __future__ import (absolute_import, division, print_function)
from six.moves import (filter, input, map, range, zip) # noqa
import six

# Import iris.tests first so that some things can be initialised before
# importing anything else.
import iris.tests as tests

import warnings

import iris.config


class Test(tests.IrisTest):
def test_basic(self):
self.assertFalse(iris.config.netcdf.conventions_override)

def test_enabled(self):
iris.config.netcdf.conventions_override = True
self.assertTrue(iris.config.netcdf.conventions_override)

def test_bad_value(self):
# A bad value should be ignored and replaced with the default value.
bad_value = 'wibble'
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
iris.config.netcdf.conventions_override = bad_value
self.assertFalse(iris.config.netcdf.conventions_override)
exp_wmsg = 'Attempting to set invalid value {!r}'.format(bad_value)
six.assertRegex(self, str(w[0].message), exp_wmsg)

def test__contextmgr(self):
with iris.config.netcdf.context(conventions_override=True):
self.assertTrue(iris.config.netcdf.conventions_override)
self.assertFalse(iris.config.netcdf.conventions_override)


if __name__ == '__main__':
tests.main()
29 changes: 22 additions & 7 deletions lib/iris/tests/unit/fileformats/netcdf/test_save.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2014 - 2016, Met Office
# (C) British Crown Copyright 2014 - 2017, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -32,20 +32,35 @@
from iris.tests.stock import lat_lon_cube


class Test_attributes(tests.IrisTest):
def test_custom_conventions(self):
class Test_conventions(tests.IrisTest):
def setUp(self):
self.cube = Cube([0])
self.custom_conventions = 'convention1 convention2'
self.cube.attributes['Conventions'] = self.custom_conventions

def test_custom_conventions__ignored(self):
# Ensure that we drop existing conventions attributes and replace with
# CF convention.
cube = Cube([0])
cube.attributes['Conventions'] = 'convention1 convention2'

with self.temp_filename('.nc') as nc_path:
save(cube, nc_path, 'NETCDF4')
save(self.cube, nc_path, 'NETCDF4')
ds = nc.Dataset(nc_path)
res = ds.getncattr('Conventions')
ds.close()
self.assertEqual(res, CF_CONVENTIONS_VERSION)

def test_custom_conventions__allowed(self):
# Ensure that existing conventions attributes are passed through if the
# relevant Iris option is set.
with iris.config.netcdf.context(conventions_override=True):
with self.temp_filename('.nc') as nc_path:
save(self.cube, nc_path, 'NETCDF4')
ds = nc.Dataset(nc_path)
res = ds.getncattr('Conventions')
ds.close()
self.assertEqual(res, self.custom_conventions)


class Test_attributes(tests.IrisTest):
def test_attributes_arrays(self):
# Ensure that attributes containing NumPy arrays can be equality
# checked and their cubes saved as appropriate.
Expand Down