diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 1db4e6c61e..36bd7212ea 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -14,7 +14,7 @@ """ -from abc import ABCMeta, abstractmethod +from abc import ABCMeta, abstractclassmethod from collections.abc import Iterable, MutableMapping import os @@ -70,23 +70,34 @@ def _is_str_dtype(var): ################################################################################ class CFVariable(metaclass=ABCMeta): - """Abstract base class wrapper for a CF-netCDF variable.""" + """ + Abstract base class wrapper for a CF-netCDF variable. + + Each object of this sort contains a netcdf variable object, and "wraps" it + so that its nc-related properties are accessible as properties of *this* + object, e.g. "cf_var.dimensions". + + This class is abstract : various concrete subclasses encode the different + cf roles that a file variable may have. + + """ #: Name of the netCDF variable attribute that identifies this #: CF-netCDF variable. cf_identity = None - def __init__(self, name, data): + def __init__(self, name, nc_variable): # 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() + # Get all names: see also __getitem__, which will cache their values. + self._nc_attrs = nc_variable.ncattrs() #: NetCDF variable name. self.cf_name = name #: NetCDF4 Variable data instance. - self.cf_data = data + self.nc_variable = nc_variable #: Collection of CF-netCDF variables associated with this variable. self.cf_group = None @@ -115,8 +126,8 @@ def _identify_common(variables, ignore, target): return (ignore, target) - @abstractmethod - def identify(self, variables, ignore=None, target=None, warn=True): + @abstractclassmethod + def identify(cls, variables, ignore=None, target=None, warn=True): """ Identify all variables that match the criterion for this CF-netCDF variable class. @@ -173,26 +184,33 @@ def __hash__(self): return hash(self.cf_name) def __getattr__(self, name): - # Accessing netCDF attributes is surprisingly slow. Since - # they're often read repeatedly, caching the values makes things - # quite a bit faster. + # Extend the "wrapper" object by adding all the additional properties + # of the "wrapped" nc_variable. + value = getattr(self.nc_variable, name) + + # Maintain the record of the "used" (cf-recognised) attributes. if name in self._nc_attrs: self._cf_attrs.add(name) - value = getattr(self.cf_data, name) + + # Also *cache* these properties, on the wrapper object. + # Accessing netCDF attributes is surprisingly slow, as they're often + # read repeatedly. This makes things quite a bit faster. setattr(self, name, value) return value + # NOTE: getitem and len need to be provided *explicitly*, i.e. not via + # wrapping, for "magic" syntaxes to work : "len(x)" and "x[k]". def __getitem__(self, key): - return self.cf_data.__getitem__(key) + return self.nc_variable.__getitem__(key) def __len__(self): - return self.cf_data.__len__() + return self.nc_variable.__len__() def __repr__(self): return "%s(%r, %r)" % ( self.__class__.__name__, self.cf_name, - self.cf_data, + self.nc_variable, ) def cf_attrs(self): @@ -557,8 +575,8 @@ class _CFFormulaTermsVariable(CFVariable): cf_identity = "formula_terms" - def __init__(self, name, data, formula_root, formula_term): - CFVariable.__init__(self, name, data) + def __init__(self, name, nc_variable, formula_root, formula_term): + CFVariable.__init__(self, name, nc_variable) # Register the formula root and term relationship. self.add_formula_term(formula_root, formula_term) @@ -607,7 +625,7 @@ def __repr__(self): return "%s(%r, %r, %r)" % ( self.__class__.__name__, self.cf_name, - self.cf_data, + self.nc_variable, self.cf_terms_by_root, ) @@ -832,8 +850,8 @@ class CFMeasureVariable(CFVariable): cf_identity = "cell_measures" - def __init__(self, name, data, measure): - CFVariable.__init__(self, name, data) + def __init__(self, name, nc_variable, measure): + CFVariable.__init__(self, name, nc_variable) #: Associated cell measure of the cell variable self.cf_measure = measure @@ -1092,7 +1110,7 @@ def _translate(self): cf_name = cf_var.cf_name if cf_var.cf_name not in self.cf_group: self.cf_group[cf_name] = CFAuxiliaryCoordinateVariable( - cf_name, cf_var.cf_data + cf_name, cf_var.nc_variable ) self.cf_group[cf_name].add_formula_term(cf_root, cf_term) @@ -1232,7 +1250,7 @@ def _build(cf_variable): cf_term in terms and cf_var_name not in self.cf_group.promoted ): - data_var = CFDataVariable(cf_var_name, cf_var.cf_data) + data_var = CFDataVariable(cf_var_name, cf_var.nc_variable) self.cf_group.promoted[cf_var_name] = data_var _build(data_var) break @@ -1246,7 +1264,7 @@ def _build(cf_variable): and cf_name not in self.cf_group.promoted ): data_var = CFDataVariable( - cf_name, self.cf_group[cf_name].cf_data + cf_name, self.cf_group[cf_name].nc_variable ) self.cf_group.promoted[cf_name] = data_var _build(data_var) diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 4d7ddedc61..c85dc987f6 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -559,7 +559,7 @@ def _get_cf_var_data(cf_var, filename): # Create cube with deferred data, but no metadata fill_value = getattr( - cf_var.cf_data, + cf_var.nc_variable, "_FillValue", netCDF4.default_fillvals[cf_var.dtype.str[1:]], ) @@ -568,7 +568,7 @@ def _get_cf_var_data(cf_var, filename): ) # Get the chunking specified for the variable : this is either a shape, or # maybe the string "contiguous". - chunks = cf_var.cf_data.chunking() + chunks = cf_var.nc_variable.chunking() # In the "contiguous" case, pass chunks=None to 'as_lazy_data'. if chunks == "contiguous": chunks = None diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py b/lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py index 8795cceb77..8376c2d661 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py @@ -26,12 +26,12 @@ def setUp(self): self.expected_chunks = _optimum_chunksize(self.shape, self.shape) def _make(self, chunksizes): - cf_data = mock.Mock(_FillValue=None) - cf_data.chunking = mock.MagicMock(return_value=chunksizes) + nc_variable = mock.Mock(_FillValue=None) + nc_variable.chunking = mock.MagicMock(return_value=chunksizes) cf_var = mock.MagicMock( spec=iris.fileformats.cf.CFVariable, dtype=np.dtype("i4"), - cf_data=cf_data, + nc_variable=nc_variable, cf_name="DUMMY_VAR", shape=self.shape, ) diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py b/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py index ffe48d437d..b0f6dd81a6 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py @@ -51,12 +51,12 @@ def _make(self, names, attrs): cf_group[name] = mock.Mock(cf_attrs_unused=cf_attrs_unused) cf = mock.Mock(cf_group=cf_group) - cf_data = mock.Mock(_FillValue=None) - cf_data.chunking = mock.MagicMock(return_value=shape) + nc_variable = mock.Mock(_FillValue=None) + nc_variable.chunking = mock.MagicMock(return_value=shape) cf_var = mock.MagicMock( spec=iris.fileformats.cf.CFVariable, dtype=np.dtype("i4"), - cf_data=cf_data, + nc_variable=nc_variable, cf_name="DUMMY_VAR", cf_group=coords, shape=shape, @@ -129,12 +129,12 @@ def setUp(self): def _make(self, attrs): shape = (1,) cf_attrs_unused = mock.Mock(return_value=attrs) - cf_data = mock.Mock(_FillValue=None) - cf_data.chunking = mock.MagicMock(return_value=shape) + nc_variable = mock.Mock(_FillValue=None) + nc_variable.chunking = mock.MagicMock(return_value=shape) cf_var = mock.MagicMock( spec=iris.fileformats.cf.CFVariable, dtype=np.dtype("i4"), - cf_data=cf_data, + nc_variable=nc_variable, cf_name="DUMMY_VAR", cf_group=mock.Mock(), cf_attrs_unused=cf_attrs_unused,