From fa8843f0cd335f56cd267bbdb3446c64408d8c7c Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 27 Mar 2017 11:49:21 +0100 Subject: [PATCH 1/8] Add options.py (cherry picked from commit d910548) --- lib/iris/__init__.py | 1 + lib/iris/options.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 lib/iris/options.py diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 3019a0adcb..1b1b91dafc 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -113,6 +113,7 @@ def callback(cube, field, filename): from iris._deprecation import IrisDeprecation, warn_deprecated import iris.fileformats import iris.io +import iris.options try: diff --git a/lib/iris/options.py b/lib/iris/options.py new file mode 100644 index 0000000000..914aa2c767 --- /dev/null +++ b/lib/iris/options.py @@ -0,0 +1,23 @@ +# (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 . +""" +Control runtime options of Iris. + +""" +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + From d957f99d51dff6cb369d3d2f2eb87b6cdf133936 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 29 Mar 2017 15:53:52 +0100 Subject: [PATCH 2/8] Add NetCDF controlling Option --- lib/iris/fileformats/netcdf.py | 6 +++- lib/iris/options.py | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 527ebfabb5..42276e3636 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -55,6 +55,7 @@ import iris.fileformats.cf import iris.fileformats._pyke_rules import iris.io +import iris.options import iris.util @@ -2244,7 +2245,10 @@ def is_valid_packspec(p): shuffle, fletcher32, contiguous, chunksizes, endian, least_significant_digit, packing=packspec) - conventions = CF_CONVENTIONS_VERSION + if iris.options.netcdf.conventions_override: + conventions = cube.attributes['Conventions'] + else: + conventions = CF_CONVENTIONS_VERSION # Perform a CF patch of the conventions attribute. cf_profile_available = (iris.site_configuration.get('cf_profile') not diff --git a/lib/iris/options.py b/lib/iris/options.py index 914aa2c767..9e3da2016d 100644 --- a/lib/iris/options.py +++ b/lib/iris/options.py @@ -21,3 +21,54 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa + +class NetCDF(object): + """Control Iris NetCDF options.""" + + def __init__(self, conventions_override=False): + """ + 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.options.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.options.netcdf(conventions_override=True): + ... iris.save('my_cube', 'my_dataset.nc') + + """ + self._override_default = False + self.__dict__['conventions_override'] = conventions_override + + def __str__(self): + msg = 'NetCDF options: conventions_override={}.' + return msg.format(self.conventions_override) + + def __enter__(self): + return + + def __exit__(self, exception_type, exception_val, exception_traceback): + self.__dict__['conventions_override'] = self._override_default + + +netcdf = NetCDF() From 6c757146700f1eb843714b03ac3ccd2faf1ee231 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 29 Mar 2017 16:55:35 +0100 Subject: [PATCH 3/8] Add and modify tests --- lib/iris/options.py | 43 ++++++++++++-- .../unit/fileformats/netcdf/test_save.py | 27 +++++++-- lib/iris/tests/unit/options/__init__.py | 20 +++++++ lib/iris/tests/unit/options/test_NetCDF.py | 58 +++++++++++++++++++ 4 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 lib/iris/tests/unit/options/__init__.py create mode 100644 lib/iris/tests/unit/options/test_NetCDF.py diff --git a/lib/iris/options.py b/lib/iris/options.py index 9e3da2016d..4bf73a3ed0 100644 --- a/lib/iris/options.py +++ b/lib/iris/options.py @@ -20,6 +20,10 @@ """ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa +import six + +import contextlib +import warnings class NetCDF(object): @@ -57,18 +61,47 @@ def __init__(self, conventions_override=False): ... iris.save('my_cube', 'my_dataset.nc') """ - self._override_default = False self.__dict__['conventions_override'] = conventions_override def __str__(self): msg = 'NetCDF options: conventions_override={}.' return msg.format(self.conventions_override) - def __enter__(self): - return + def __setattr__(self, name, value): + if name not in self.__dict__: + msg = "'Future' object has no attribute {!r}".format(name) + raise AttributeError(msg) + if value not in [True, False]: + good_value = self._defaults_dict[name] + wmsg = ('Attempting to set bad 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): + return {'conventions_override': False} + + @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. - def __exit__(self, exception_type, exception_val, exception_traceback): - self.__dict__['conventions_override'] = self._override_default + """ + # 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() diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_save.py b/lib/iris/tests/unit/fileformats/netcdf/test_save.py index b1b76f56ce..e804e4c0b3 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_save.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_save.py @@ -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.options.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. diff --git a/lib/iris/tests/unit/options/__init__.py b/lib/iris/tests/unit/options/__init__.py new file mode 100644 index 0000000000..93397c38fa --- /dev/null +++ b/lib/iris/tests/unit/options/__init__.py @@ -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 . +"""Unit tests for the :mod:`iris.options` module.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa diff --git a/lib/iris/tests/unit/options/test_NetCDF.py b/lib/iris/tests/unit/options/test_NetCDF.py new file mode 100644 index 0000000000..d8a2cb8545 --- /dev/null +++ b/lib/iris/tests/unit/options/test_NetCDF.py @@ -0,0 +1,58 @@ +# (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 . +"""Unit tests for the `iris.options.Paralle` class.""" + +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.options +from iris.tests import mock + + +class Test(tests.IrisTest): + def test_basic(self): + self.assertFalse(iris.options.netcdf.conventions_override) + + def test_enabled(self): + iris.options.netcdf.conventions_override = True + self.assertTrue(iris.options.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.options.netcdf.conventions_override = bad_value + self.assertFalse(iris.options.netcdf.conventions_override) + exp_wmsg = 'Attempting to set bad value {!r}'.format(bad_value) + six.assertRegex(self, str(w[0].message), exp_wmsg) + + def test__contextmgr(self): + with iris.options.netcdf.context(conventions_override=True): + self.assertTrue(iris.options.netcdf.conventions_override) + self.assertFalse(iris.options.netcdf.conventions_override) + + +if __name__ == '__main__': + tests.main() From 016f8299635b0bf2baf50e61215ebaafd45faaba Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Thu, 30 Mar 2017 09:48:40 +0100 Subject: [PATCH 4/8] Corrections for non-functional tests --- lib/iris/fileformats/netcdf.py | 2 +- lib/iris/options.py | 10 +++++----- lib/iris/tests/unit/fileformats/netcdf/test_save.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 42276e3636..b380df6098 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -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. # diff --git a/lib/iris/options.py b/lib/iris/options.py index 4bf73a3ed0..40b355d3b2 100644 --- a/lib/iris/options.py +++ b/lib/iris/options.py @@ -50,15 +50,15 @@ def __init__(self, conventions_override=False): * Specify, for the lifetime of the session, that we want all cubes written to NetCDF to define their own CF Conventions versions:: - >>> iris.options.netcdf(conventions_override=True) - >>> iris.save('my_cube', 'my_dataset.nc') - >>> iris.save('my_second_cube', 'my_second_dataset.nc') + iris.options.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.options.netcdf(conventions_override=True): - ... iris.save('my_cube', 'my_dataset.nc') + with iris.options.netcdf.context(conventions_override=True): + iris.save('my_cube', 'my_dataset.nc') """ self.__dict__['conventions_override'] = conventions_override diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_save.py b/lib/iris/tests/unit/fileformats/netcdf/test_save.py index e804e4c0b3..efd640ef2d 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_save.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_save.py @@ -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. # From af96e746e5060f1e84b1ff540a6c8050d6645ad4 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Fri, 31 Mar 2017 16:47:26 +0100 Subject: [PATCH 5/8] Move to config.py --- lib/iris/config.py | 102 ++++++++++++++++- lib/iris/fileformats/netcdf.py | 3 +- lib/iris/options.py | 107 ------------------ .../unit/{options => config}/__init__.py | 2 +- .../unit/{options => config}/test_NetCDF.py | 19 ++-- .../unit/fileformats/netcdf/test_save.py | 2 +- 6 files changed, 113 insertions(+), 122 deletions(-) delete mode 100644 lib/iris/options.py rename lib/iris/tests/unit/{options => config}/__init__.py (94%) rename lib/iris/tests/unit/{options => config}/test_NetCDF.py (73%) diff --git a/lib/iris/config.py b/lib/iris/config.py index e14ac058a4..393d0aa856 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -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 @@ -154,3 +156,101 @@ 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.options.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.options.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 __str__(self): + msg = 'NetCDF options: conventions_override={}.' + return msg.format(self.conventions_override) + + def __setattr__(self, name, value): + if name not in self.__dict__: + # Can't add new names. + msg = "'Option' object has no attribute {!r}".format(name) + raise AttributeError(msg) + 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 bad 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): + return {'conventions_override': {'default': False, + 'options': [True, False]}, + } + + @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() diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index b380df6098..8cdae02e82 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -55,7 +55,6 @@ import iris.fileformats.cf import iris.fileformats._pyke_rules import iris.io -import iris.options import iris.util @@ -2245,7 +2244,7 @@ def is_valid_packspec(p): shuffle, fletcher32, contiguous, chunksizes, endian, least_significant_digit, packing=packspec) - if iris.options.netcdf.conventions_override: + if iris.config.netcdf.conventions_override: conventions = cube.attributes['Conventions'] else: conventions = CF_CONVENTIONS_VERSION diff --git a/lib/iris/options.py b/lib/iris/options.py deleted file mode 100644 index 40b355d3b2..0000000000 --- a/lib/iris/options.py +++ /dev/null @@ -1,107 +0,0 @@ -# (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 . -""" -Control runtime options of Iris. - -""" -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa -import six - -import contextlib -import warnings - - -class NetCDF(object): - """Control Iris NetCDF options.""" - - def __init__(self, conventions_override=False): - """ - 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.options.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.options.netcdf.context(conventions_override=True): - iris.save('my_cube', 'my_dataset.nc') - - """ - self.__dict__['conventions_override'] = conventions_override - - def __str__(self): - msg = 'NetCDF options: conventions_override={}.' - return msg.format(self.conventions_override) - - def __setattr__(self, name, value): - if name not in self.__dict__: - msg = "'Future' object has no attribute {!r}".format(name) - raise AttributeError(msg) - if value not in [True, False]: - good_value = self._defaults_dict[name] - wmsg = ('Attempting to set bad 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): - return {'conventions_override': False} - - @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() diff --git a/lib/iris/tests/unit/options/__init__.py b/lib/iris/tests/unit/config/__init__.py similarity index 94% rename from lib/iris/tests/unit/options/__init__.py rename to lib/iris/tests/unit/config/__init__.py index 93397c38fa..dd625e1e91 100644 --- a/lib/iris/tests/unit/options/__init__.py +++ b/lib/iris/tests/unit/config/__init__.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . -"""Unit tests for the :mod:`iris.options` module.""" +"""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 diff --git a/lib/iris/tests/unit/options/test_NetCDF.py b/lib/iris/tests/unit/config/test_NetCDF.py similarity index 73% rename from lib/iris/tests/unit/options/test_NetCDF.py rename to lib/iris/tests/unit/config/test_NetCDF.py index d8a2cb8545..2a487996f9 100644 --- a/lib/iris/tests/unit/options/test_NetCDF.py +++ b/lib/iris/tests/unit/config/test_NetCDF.py @@ -26,32 +26,31 @@ import warnings -import iris.options -from iris.tests import mock +import iris.config class Test(tests.IrisTest): def test_basic(self): - self.assertFalse(iris.options.netcdf.conventions_override) + self.assertFalse(iris.config.netcdf.conventions_override) def test_enabled(self): - iris.options.netcdf.conventions_override = True - self.assertTrue(iris.options.netcdf.conventions_override) + 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.options.netcdf.conventions_override = bad_value - self.assertFalse(iris.options.netcdf.conventions_override) + iris.config.netcdf.conventions_override = bad_value + self.assertFalse(iris.config.netcdf.conventions_override) exp_wmsg = 'Attempting to set bad value {!r}'.format(bad_value) six.assertRegex(self, str(w[0].message), exp_wmsg) def test__contextmgr(self): - with iris.options.netcdf.context(conventions_override=True): - self.assertTrue(iris.options.netcdf.conventions_override) - self.assertFalse(iris.options.netcdf.conventions_override) + 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__': diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_save.py b/lib/iris/tests/unit/fileformats/netcdf/test_save.py index efd640ef2d..bff8bf2c50 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_save.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_save.py @@ -51,7 +51,7 @@ def test_custom_conventions__ignored(self): def test_custom_conventions__allowed(self): # Ensure that existing conventions attributes are passed through if the # relevant Iris option is set. - with iris.options.netcdf.context(conventions_override=True): + 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) From 6f35125785fbb934007226492de7f22ac1f0c84c Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 10 Apr 2017 11:41:44 +0100 Subject: [PATCH 6/8] Remove old options import --- lib/iris/__init__.py | 1 - lib/iris/config.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 1b1b91dafc..3019a0adcb 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -113,7 +113,6 @@ def callback(cube, field, filename): from iris._deprecation import IrisDeprecation, warn_deprecated import iris.fileformats import iris.io -import iris.options try: diff --git a/lib/iris/config.py b/lib/iris/config.py index 393d0aa856..04ea8aedbf 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -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. # From 6d2b735ef4c687c0887dfab75d2bf916a37bca24 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 10 Apr 2017 12:35:26 +0100 Subject: [PATCH 7/8] Review actions --- lib/iris/config.py | 25 ++++++++++++++--------- lib/iris/tests/unit/config/test_NetCDF.py | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/iris/config.py b/lib/iris/config.py index 04ea8aedbf..2bb82924a2 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -178,21 +178,21 @@ def __init__(self, conventions_override=None): 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. + 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.options.netcdf.conventions_override = True + 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.options.netcdf.context(conventions_override=True): + with iris.config.netcdf.context(conventions_override=True): iris.save('my_cube', 'my_dataset.nc') """ @@ -202,15 +202,19 @@ def __init__(self, conventions_override=None): # Now set specific values. setattr(self, 'conventions_override', conventions_override) - def __str__(self): - msg = 'NetCDF options: conventions_override={}.' - return msg.format(self.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 = "'Option' object has no attribute {!r}".format(name) - raise AttributeError(msg) + 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'] @@ -220,14 +224,15 @@ def __setattr__(self, name, value): # anything goes. if value not in self._defaults_dict[name]['options']: good_value = self._defaults_dict[name]['default'] - wmsg = ('Attempting to set bad value {!r} for attribute {!r}. ' - 'Defaulting to {!r}.') + 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]}, } diff --git a/lib/iris/tests/unit/config/test_NetCDF.py b/lib/iris/tests/unit/config/test_NetCDF.py index 2a487996f9..3aec50bca1 100644 --- a/lib/iris/tests/unit/config/test_NetCDF.py +++ b/lib/iris/tests/unit/config/test_NetCDF.py @@ -44,7 +44,7 @@ def test_bad_value(self): warnings.simplefilter('always') iris.config.netcdf.conventions_override = bad_value self.assertFalse(iris.config.netcdf.conventions_override) - exp_wmsg = 'Attempting to set bad value {!r}'.format(bad_value) + exp_wmsg = 'Attempting to set invalid value {!r}'.format(bad_value) six.assertRegex(self, str(w[0].message), exp_wmsg) def test__contextmgr(self): From 03a8fb54a1bd77bbe9bd203a66b78dd2f804e187 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Tue, 11 Apr 2017 10:37:58 +0100 Subject: [PATCH 8/8] Use mock in tests --- lib/iris/fileformats/netcdf.py | 4 +++- lib/iris/tests/unit/config/test_NetCDF.py | 21 +++++++++++-------- .../unit/fileformats/netcdf/test_save.py | 16 +++++++++++++- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 8cdae02e82..11565299fb 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -2245,7 +2245,9 @@ def is_valid_packspec(p): least_significant_digit, packing=packspec) if iris.config.netcdf.conventions_override: - conventions = cube.attributes['Conventions'] + # Set to the default if custom conventions are not available. + conventions = cube.attributes.get('Conventions', + CF_CONVENTIONS_VERSION) else: conventions = CF_CONVENTIONS_VERSION diff --git a/lib/iris/tests/unit/config/test_NetCDF.py b/lib/iris/tests/unit/config/test_NetCDF.py index 3aec50bca1..3929319854 100644 --- a/lib/iris/tests/unit/config/test_NetCDF.py +++ b/lib/iris/tests/unit/config/test_NetCDF.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . -"""Unit tests for the `iris.options.Paralle` class.""" +"""Unit tests for the `iris.config.NetCDF` class.""" from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa @@ -30,27 +30,30 @@ class Test(tests.IrisTest): + def setUp(self): + self.options = iris.config.NetCDF() + def test_basic(self): - self.assertFalse(iris.config.netcdf.conventions_override) + self.assertFalse(self.options.conventions_override) def test_enabled(self): - iris.config.netcdf.conventions_override = True - self.assertTrue(iris.config.netcdf.conventions_override) + self.options.conventions_override = True + self.assertTrue(self.options.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) + self.options.conventions_override = bad_value + self.assertFalse(self.options.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) + with self.options.context(conventions_override=True): + self.assertTrue(self.options.conventions_override) + self.assertFalse(self.options.conventions_override) if __name__ == '__main__': diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_save.py b/lib/iris/tests/unit/fileformats/netcdf/test_save.py index bff8bf2c50..936de128a6 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_save.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_save.py @@ -23,6 +23,7 @@ # importing anything else. import iris.tests as tests +import mock import netCDF4 as nc import numpy as np @@ -37,6 +38,7 @@ def setUp(self): self.cube = Cube([0]) self.custom_conventions = 'convention1 convention2' self.cube.attributes['Conventions'] = self.custom_conventions + self.options = iris.config.netcdf def test_custom_conventions__ignored(self): # Ensure that we drop existing conventions attributes and replace with @@ -51,7 +53,7 @@ def test_custom_conventions__ignored(self): 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 mock.patch.object(self.options, 'conventions_override', True): with self.temp_filename('.nc') as nc_path: save(self.cube, nc_path, 'NETCDF4') ds = nc.Dataset(nc_path) @@ -59,6 +61,18 @@ def test_custom_conventions__allowed(self): ds.close() self.assertEqual(res, self.custom_conventions) + def test_custom_conventions__allowed__missing(self): + # Ensure the default conventions attribute is set if the relevant Iris + # option is set but there is no custom conventions attribute. + del self.cube.attributes['Conventions'] + with mock.patch.object(self.options, '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, CF_CONVENTIONS_VERSION) + class Test_attributes(tests.IrisTest): def test_attributes_arrays(self):