Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 24 additions & 8 deletions lib/iris/fileformats/cf.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,23 @@ class CFVariable(object):
'''Name of the netCDF variable attribute that identifies this CF-netCDF variable'''

def __init__(self, name, data):
if not isinstance(data, netCDF4.Variable):
raise TypeError('%s expects a netCDF4 Variable data instance' % self.__class__.__name__)
# Accessing the list of netCDF attributes is surprisingly slow.
# Since it's used repeatedly, caching the list makes things
# quite a bit faster.
self._nc_attrs = data.ncattrs()

self.cf_name = name
'''NetCDF variable name'''

self.cf_data = data
'''NetCDF4 Variable data instance'''

self.cf_group = None
'''Collection of CF-netCDF variables associated with this variable'''

self.cf_terms_by_root = {}
'''CF-netCDF formula terms that his variable participates in'''

self.cf_attrs_reset()

@staticmethod
Expand Down Expand Up @@ -125,9 +132,14 @@ def __ne__(self, other):
return self.cf_name != other.cf_name

def __getattr__(self, name):
if name in self.cf_data.ncattrs():
# Accessing netCDF attributes is surprisingly slow. Since
# they're often read repeatedly, caching the values makes things
# quite a bit faster.
if name in self._nc_attrs:
self._cf_attrs.add(name)
return getattr(self.cf_data, name)
value = getattr(self.cf_data, name)
setattr(self, name, value)
return value

def __getitem__(self, key):
return self.cf_data.__getitem__(key)
Expand All @@ -140,19 +152,23 @@ def __repr__(self):

def cf_attrs(self):
"""Return a list of all attribute name and value pairs of the CF-netCDF variable."""
return tuple([(attr, self.getncattr(attr)) for attr in sorted(self.ncattrs())])
return tuple((attr, self.getncattr(attr))
for attr in sorted(self._nc_attrs))

def cf_attrs_ignored(self):
"""Return a list of all ignored attribute name and value pairs of the CF-netCDF variable."""
return tuple([(attr, self.getncattr(attr)) for attr in sorted(set(self.ncattrs()) & _CF_ATTRS_IGNORE)])
return tuple((attr, self.getncattr(attr)) for attr in
sorted(set(self._nc_attrs) & _CF_ATTRS_IGNORE))

def cf_attrs_used(self):
"""Return a list of all accessed attribute name and value pairs of the CF-netCDF variable."""
return tuple([(attr, self.getncattr(attr)) for attr in sorted(self._cf_attrs)])
return tuple((attr, self.getncattr(attr)) for attr in
sorted(self._cf_attrs))

def cf_attrs_unused(self):
"""Return a list of all non-accessed attribute name and value pairs of the CF-netCDF variable."""
return tuple([(attr, self.getncattr(attr)) for attr in sorted(set(self.ncattrs()) - self._cf_attrs)])
return tuple((attr, self.getncattr(attr)) for attr in
sorted(set(self._nc_attrs) - self._cf_attrs))

def cf_attrs_reset(self):
"""Reset the history of accessed attribute names of the CF-netCDF variable."""
Expand Down
35 changes: 35 additions & 0 deletions lib/iris/tests/test_cf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,45 @@
# import iris tests first so that some things can be initialised before importing anything else
import iris.tests as tests

import unittest

import mock

import iris
import iris.fileformats.cf as cf


class TestCaching(unittest.TestCase):
def test_cached(self):
# Make sure attribute access to the underlying netCDF4.Variable
# is cached.
name = 'foo'
nc_var = mock.MagicMock()
cf_var = cf.CFAncillaryDataVariable(name, nc_var)
self.assertEqual(nc_var.ncattrs.call_count, 1)

# Accessing a netCDF attribute should result in no further calls
# to nc_var.ncattrs() and the creation of an attribute on the
# cf_var.
# NB. Can't use hasattr() because that triggers the attribute
# to be created!
self.assertTrue('coordinates' not in cf_var.__dict__)
_ = cf_var.coordinates
self.assertEqual(nc_var.ncattrs.call_count, 1)
self.assertTrue('coordinates' in cf_var.__dict__)

# Trying again results in no change.
_ = cf_var.coordinates
self.assertEqual(nc_var.ncattrs.call_count, 1)
self.assertTrue('coordinates' in cf_var.__dict__)

# Trying another attribute results in just a new attribute.
self.assertTrue('standard_name' not in cf_var.__dict__)
_ = cf_var.standard_name
self.assertEqual(nc_var.ncattrs.call_count, 1)
self.assertTrue('standard_name' in cf_var.__dict__)


@iris.tests.skip_data
class TestCFReader(tests.IrisTest):
def setUp(self):
Expand Down