From 378bc9e871066506daad4401befee701df99e75c Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Fri, 14 Jan 2022 21:47:56 -0700 Subject: [PATCH 1/5] allow createDimension to accept Dimension instances (issue #1145) --- Changelog | 2 ++ src/netCDF4/_netCDF4.pyx | 21 ++++++++++++++------- test/tst_dims.py | 21 +++++++++++++++------ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Changelog b/Changelog index bb5cb18ce..a28e52dd2 100644 --- a/Changelog +++ b/Changelog @@ -4,6 +4,8 @@ if one does not already exist (similar to python open builtin). Issue #1144. Added a mode='x' option (as in python open) which is the same as mode='w' with clobber=False. + * allow createVariable to accept either Dimension instances or Dimension + names in dimensions tuple kwarg (issue #1145). version 1.5.8 (tag v1.5.8rel) ============================== diff --git a/src/netCDF4/_netCDF4.pyx b/src/netCDF4/_netCDF4.pyx index d84f74e4f..9b100f7a6 100644 --- a/src/netCDF4/_netCDF4.pyx +++ b/src/netCDF4/_netCDF4.pyx @@ -2653,8 +2653,9 @@ length greater than one are aliases for `str`. Data from netCDF variables is presented to python as numpy arrays with the corresponding data type. -`dimensions` must be a tuple containing dimension names (strings) that -have been defined previously using `Dataset.createDimension`. The default value +`dimensions` must be a tuple containing `Dimension` instances or +dimension names (strings) that have been defined +previously using `Dataset.createDimension`. The default value is an empty tuple, which means the variable is a scalar. If the optional keyword `zlib` is `True`, the data will be compressed in @@ -2757,6 +2758,17 @@ is the number of variable dimensions.""" group = self else: group = self.createGroup(dirname) + # if dimensions is a single string or Dimension instance, + # convert to a tuple. + # This prevents a common error that occurs when + # dimensions = 'lat' instead of ('lat',) + if type(dimensions) == str or type(dimensions) == bytes or\ + type(dimensions) == unicode or type(dimensions) == Dimension: + dimensions = dimensions, + # convert elements of dimensions tuple to names if they are + # Dimension instances. + dimensions =\ + tuple(d.name if isinstance(d,Dimension) else d for d in dimensions) # create variable. group.variables[varname] = Variable(group, varname, datatype, dimensions=dimensions, zlib=zlib, complevel=complevel, shuffle=shuffle, @@ -3696,11 +3708,6 @@ behavior is similar to Fortran or Matlab, but different than numpy. # if complevel is set to zero, set zlib to False. if not complevel: zlib = False - # if dimensions is a string, convert to a tuple - # this prevents a common error that occurs when - # dimensions = 'lat' instead of ('lat',) - if type(dimensions) == str or type(dimensions) == bytes or type(dimensions) == unicode: - dimensions = dimensions, self._grpid = grp._grpid # make a weakref to group to avoid circular ref (issue 218) # keep strong reference the default behaviour (issue 251) diff --git a/test/tst_dims.py b/test/tst_dims.py index 026361f48..5da7c3f89 100644 --- a/test/tst_dims.py +++ b/test/tst_dims.py @@ -20,6 +20,7 @@ TIME_LENG = None GROUP_NAME='forecasts' VAR_NAME='temp' +VAR_NAME2='wind' VAR_TYPE='f8' @@ -28,11 +29,14 @@ class DimensionsTestCase(unittest.TestCase): def setUp(self): self.file = FILE_NAME f = netCDF4.Dataset(self.file, 'w') - f.createDimension(LAT_NAME,LAT_LEN) - f.createDimension(LON_NAME,LON_LEN) - f.createDimension(LEVEL_NAME,LEVEL_LEN) - f.createDimension(TIME_NAME,TIME_LEN) - f.createVariable(VAR_NAME,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) + lat_dim=f.createDimension(LAT_NAME,LAT_LEN) + lon_dim=f.createDimension(LON_NAME,LON_LEN) + lev_dim=f.createDimension(LEVEL_NAME,LEVEL_LEN) + time_dim=f.createDimension(TIME_NAME,TIME_LEN) + # specify dimensions with names + fv1 = f.createVariable(VAR_NAME,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) + # specify dimensions using a mix of names and instances + fv2 = f.createVariable(VAR_NAME2,VAR_TYPE,(lev_dim, LAT_NAME, lon_dim, TIME_NAME)) g = f.createGroup(GROUP_NAME) g.createDimension(LAT_NAME,LAT_LENG) g.createDimension(LON_NAME,LON_LENG) @@ -40,7 +44,7 @@ def setUp(self): # (did not work prior to alpha 18) #g.createDimension(LEVEL_NAME,LEVEL_LENG) #g.createDimension(TIME_NAME,TIME_LENG) - g.createVariable(VAR_NAME,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) + gv = g.createVariable(VAR_NAME,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) f.close() def tearDown(self): @@ -52,6 +56,7 @@ def runTest(self): # check dimensions in root group. f = netCDF4.Dataset(self.file, 'r+') v = f.variables[VAR_NAME] + v2 = f.variables[VAR_NAME2] isunlim = [dim.isunlimited() for dim in f.dimensions.values()] dimlens = [len(dim) for dim in f.dimensions.values()] names_check = [LAT_NAME, LON_NAME, LEVEL_NAME, TIME_NAME] @@ -65,6 +70,10 @@ def runTest(self): # check that dimension names are correct. for name in f.dimensions.keys(): self.assertTrue(name in names_check) + for name in v.dimensions: + self.assertTrue(name in names_check) + for name in v2.dimensions: + self.assertTrue(name in names_check) # check that dimension lengths are correct. for name,dim in f.dimensions.items(): self.assertTrue(len(dim) == lensdict[name]) From 5e0894b76cef248fa37a750a345e6242acc2478d Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Sat, 15 Jan 2022 19:09:35 -0700 Subject: [PATCH 2/5] remove vestiges of python 2 and set language_level=3 --- Changelog | 4 +++- setup.py | 3 +++ src/netCDF4/_netCDF4.pyx | 26 +++++++++++++------------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Changelog b/Changelog index a28e52dd2..f8804a3c1 100644 --- a/Changelog +++ b/Changelog @@ -5,7 +5,9 @@ Added a mode='x' option (as in python open) which is the same as mode='w' with clobber=False. * allow createVariable to accept either Dimension instances or Dimension - names in dimensions tuple kwarg (issue #1145). + names in "dimensions" tuple kwarg (issue #1145). + * remove all vestiges of python 2 in _netCDF4.pyx and set cython language_level + directive to 3 in setup.py. version 1.5.8 (tag v1.5.8rel) ============================== diff --git a/setup.py b/setup.py index 17e7afc46..b4cd2d350 100644 --- a/setup.py +++ b/setup.py @@ -616,6 +616,9 @@ def _populate_hdf5_info(dirstosearch, inc_dirs, libs, lib_dirs): library_dirs=lib_dirs, include_dirs=inc_dirs + ['include'], runtime_library_dirs=runtime_lib_dirs)] + # set language_level directive to 3 + for e in ext_modules: + e.cython_directives = {'language_level': "3"} # else: ext_modules = None diff --git a/src/netCDF4/_netCDF4.pyx b/src/netCDF4/_netCDF4.pyx index 9b100f7a6..30f9a3ec4 100644 --- a/src/netCDF4/_netCDF4.pyx +++ b/src/netCDF4/_netCDF4.pyx @@ -2408,9 +2408,9 @@ version 4.1.2 or higher of the netcdf C lib, and rebuild netcdf4-python.""" raise ValueError(msg) def __repr__(self): - return self.__unicode__() + return self.__str__() - def __unicode__(self): + def __str__(self): ncdump = [repr(type(self))] dimnames = tuple(_tostr(dimname)+'(%s)'%len(self.dimensions[dimname])\ for dimname in self.dimensions.keys()) @@ -2763,7 +2763,7 @@ is the number of variable dimensions.""" # This prevents a common error that occurs when # dimensions = 'lat' instead of ('lat',) if type(dimensions) == str or type(dimensions) == bytes or\ - type(dimensions) == unicode or type(dimensions) == Dimension: + type(dimensions) == Dimension: dimensions = dimensions, # convert elements of dimensions tuple to names if they are # Dimension instances. @@ -3466,9 +3466,9 @@ Read-only class variables: raise AttributeError("size cannot be altered") def __repr__(self): - return self.__unicode__() + return self.__str__() - def __unicode__(self): + def __str__(self): if not dir(self._grp): return 'Dimension object no longer valid' if self.isunlimited(): @@ -3971,9 +3971,9 @@ behavior is similar to Fortran or Matlab, but different than numpy. return self[...] def __repr__(self): - return self.__unicode__() + return self.__str__() - def __unicode__(self): + def __str__(self): cdef int ierr, no_fill if not dir(self._grp): return 'Variable object no longer valid' @@ -5566,9 +5566,9 @@ the user. self.name = dtype_name def __repr__(self): - return self.__unicode__() + return self.__str__() - def __unicode__(self): + def __str__(self): return "%r: name = '%s', numpy dtype = %s" %\ (type(self), self.name, self.dtype) @@ -5848,9 +5848,9 @@ the user. self.name = dtype_name def __repr__(self): - return self.__unicode__() + return self.__str__() - def __unicode__(self): + def __str__(self): if self.dtype == str: return '%r: string type' % (type(self),) else: @@ -5958,9 +5958,9 @@ the user. self.enum_dict = enum_dict def __repr__(self): - return self.__unicode__() + return self.__str__() - def __unicode__(self): + def __str__(self): return "%r: name = '%s', numpy dtype = %s, fields/values =%s" %\ (type(self), self.name, self.dtype, self.enum_dict) From 3fdeda75fc6fff61aa2cbb50559c4d9bdef19bd3 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Mon, 17 Jan 2022 09:19:16 -0700 Subject: [PATCH 3/5] convert dim names to dim instances (instead of reverse) --- src/netCDF4/_netCDF4.pyx | 29 ++++++++++++----------------- src/netCDF4/utils.py | 5 ++++- test/tst_dims.py | 34 +++++++++++++++++++++++++--------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/netCDF4/_netCDF4.pyx b/src/netCDF4/_netCDF4.pyx index 30f9a3ec4..70cf56c9d 100644 --- a/src/netCDF4/_netCDF4.pyx +++ b/src/netCDF4/_netCDF4.pyx @@ -1909,6 +1909,7 @@ cdef _get_vars(group): grp = grp.parent free(dimids) # create new variable instance. + dimensions = tuple(_find_dim(group,d) for d in dimensions) if endianness == '>': variables[name] = Variable(group, name, datatype, dimensions, id=varid, endian='big') elif endianness == '<': @@ -2653,7 +2654,7 @@ length greater than one are aliases for `str`. Data from netCDF variables is presented to python as numpy arrays with the corresponding data type. -`dimensions` must be a tuple containing `Dimension` instances or +`dimensions` must be a tuple containing `Dimension` instances and/or dimension names (strings) that have been defined previously using `Dataset.createDimension`. The default value is an empty tuple, which means the variable is a scalar. @@ -2758,17 +2759,18 @@ is the number of variable dimensions.""" group = self else: group = self.createGroup(dirname) - # if dimensions is a single string or Dimension instance, + # if dimensions is a single string or Dimension instance, # convert to a tuple. # This prevents a common error that occurs when # dimensions = 'lat' instead of ('lat',) - if type(dimensions) == str or type(dimensions) == bytes or\ - type(dimensions) == Dimension: + if isinstance(dimensions, (str, bytes, Dimension)): dimensions = dimensions, - # convert elements of dimensions tuple to names if they are - # Dimension instances. + # convert elements of dimensions tuple to Dimension + # instances if they are strings. + # _find_dim looks for dimension in this group, and if not + # found there, looks in parent (and it's parent, etc, back to root). dimensions =\ - tuple(d.name if isinstance(d,Dimension) else d for d in dimensions) + tuple(_find_dim(group,d) if isinstance(d,(str,bytes)) else d for d in dimensions) # create variable. group.variables[varname] = Variable(group, varname, datatype, dimensions=dimensions, zlib=zlib, complevel=complevel, shuffle=shuffle, @@ -3628,7 +3630,7 @@ behavior is similar to Fortran or Matlab, but different than numpy. (for a variable-length string array). Numpy string and unicode datatypes with length greater than one are aliases for `str`. - **`dimensions`**: a tuple containing the variable's dimension names + **`dimensions`**: a tuple containing the variable's Dimension instances (defined previously with `createDimension`). Default is an empty tuple which means the variable is a scalar (and therefore has no dimensions). @@ -3794,12 +3796,7 @@ behavior is similar to Fortran or Matlab, but different than numpy. dims = [] dimids = malloc(sizeof(int) * ndims) for n from 0 <= n < ndims: - dimname = dimensions[n] - # look for dimension in this group, and if not - # found there, look in parent (and it's parent, etc, back to root). - dim = _find_dim(grp, dimname) - if dim is None: - raise KeyError("dimension %s not defined in group %s or any group in it's family tree" % (dimname, grp.path)) + dim = dimensions[n] dimids[n] = dim._dimid dims.append(dim) # go into define mode if it's a netCDF 3 compatible @@ -3930,9 +3927,7 @@ behavior is similar to Fortran or Matlab, but different than numpy. if grp.data_model != 'NETCDF4': grp._enddef() # count how many unlimited dimensions there are. self._nunlimdim = 0 - for dimname in dimensions: - # look in current group, and parents for dim. - dim = _find_dim(self._grp, dimname) + for dim in dimensions: if dim.isunlimited(): self._nunlimdim = self._nunlimdim + 1 # set ndim attribute (number of dimensions). with nogil: diff --git a/src/netCDF4/utils.py b/src/netCDF4/utils.py index 7a858ef42..c96cc7575 100644 --- a/src/netCDF4/utils.py +++ b/src/netCDF4/utils.py @@ -45,7 +45,10 @@ def _find_dim(grp, dimname): group = group.parent except: raise ValueError("cannot find dimension %s in this group or parent groups" % dimname) - return dim + if dim is None: + raise KeyError("dimension %s not defined in group %s or any group in it's family tree" % (dimname, grp.path)) + else: + return dim def _walk_grps(topgrp): """Iterate through all (sub-) groups of topgrp, similar to os.walktree. diff --git a/test/tst_dims.py b/test/tst_dims.py index 5da7c3f89..7af5d2ad7 100644 --- a/test/tst_dims.py +++ b/test/tst_dims.py @@ -19,8 +19,11 @@ TIME_LEN = None TIME_LENG = None GROUP_NAME='forecasts' -VAR_NAME='temp' -VAR_NAME2='wind' +VAR_NAME1='temp1' +VAR_NAME2='temp2' +VAR_NAME3='temp3' +VAR_NAME4='temp4' +VAR_NAME5='temp5' VAR_TYPE='f8' @@ -34,9 +37,14 @@ def setUp(self): lev_dim=f.createDimension(LEVEL_NAME,LEVEL_LEN) time_dim=f.createDimension(TIME_NAME,TIME_LEN) # specify dimensions with names - fv1 = f.createVariable(VAR_NAME,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) + fv1 = f.createVariable(VAR_NAME1,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) + # specify dimensions with instances + fv2 = f.createVariable(VAR_NAME2,VAR_TYPE,(lev_dim,lat_dim,lon_dim,time_dim)) # specify dimensions using a mix of names and instances - fv2 = f.createVariable(VAR_NAME2,VAR_TYPE,(lev_dim, LAT_NAME, lon_dim, TIME_NAME)) + fv3 = f.createVariable(VAR_NAME3,VAR_TYPE,(lev_dim, LAT_NAME, lon_dim, TIME_NAME)) + # single dim instance for name (not in a tuple) + fv4 = f.createVariable(VAR_NAME4,VAR_TYPE,time_dim) + fv5 = f.createVariable(VAR_NAME5,VAR_TYPE,TIME_NAME) g = f.createGroup(GROUP_NAME) g.createDimension(LAT_NAME,LAT_LENG) g.createDimension(LON_NAME,LON_LENG) @@ -44,7 +52,7 @@ def setUp(self): # (did not work prior to alpha 18) #g.createDimension(LEVEL_NAME,LEVEL_LENG) #g.createDimension(TIME_NAME,TIME_LENG) - gv = g.createVariable(VAR_NAME,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) + gv = g.createVariable(VAR_NAME1,VAR_TYPE,(LEVEL_NAME, LAT_NAME, LON_NAME, TIME_NAME)) f.close() def tearDown(self): @@ -55,8 +63,11 @@ def runTest(self): """testing dimensions""" # check dimensions in root group. f = netCDF4.Dataset(self.file, 'r+') - v = f.variables[VAR_NAME] + v1 = f.variables[VAR_NAME1] v2 = f.variables[VAR_NAME2] + v3 = f.variables[VAR_NAME3] + v4 = f.variables[VAR_NAME4] + v5 = f.variables[VAR_NAME5] isunlim = [dim.isunlimited() for dim in f.dimensions.values()] dimlens = [len(dim) for dim in f.dimensions.values()] names_check = [LAT_NAME, LON_NAME, LEVEL_NAME, TIME_NAME] @@ -70,10 +81,15 @@ def runTest(self): # check that dimension names are correct. for name in f.dimensions.keys(): self.assertTrue(name in names_check) - for name in v.dimensions: + for name in v1.dimensions: self.assertTrue(name in names_check) for name in v2.dimensions: self.assertTrue(name in names_check) + for name in v3.dimensions: + self.assertTrue(name in names_check) + self.assertTrue(v4.dimensions[0] == TIME_NAME) + self.assertTrue(v5.dimensions[0] == TIME_NAME) + # check that dimension lengths are correct. # check that dimension lengths are correct. for name,dim in f.dimensions.items(): self.assertTrue(len(dim) == lensdict[name]) @@ -84,7 +100,7 @@ def runTest(self): # make sure length of dimensions change correctly. nadd1 = 2 nadd2 = 4 - v[0:nadd1,:,:,0:nadd2] = uniform(size=(nadd1,LAT_LEN,LON_LEN,nadd2)) + v1[0:nadd1,:,:,0:nadd2] = uniform(size=(nadd1,LAT_LEN,LON_LEN,nadd2)) lensdict[LEVEL_NAME]=nadd1 lensdict[TIME_NAME]=nadd2 # check that dimension lengths are correct. @@ -92,7 +108,7 @@ def runTest(self): self.assertTrue(len(dim) == lensdict[name]) # check dimensions in subgroup. g = f.groups[GROUP_NAME] - vg = g.variables[VAR_NAME] + vg = g.variables[VAR_NAME1] isunlim = [dim.isunlimited() for dim in g.dimensions.values()] dimlens = [len(dim) for dim in g.dimensions.values()] names_check = [LAT_NAME, LON_NAME, LEVEL_NAME, TIME_NAME] From 9a4a885095ab21c9e9f225382b7a41bed65eb859 Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Mon, 17 Jan 2022 10:05:11 -0700 Subject: [PATCH 4/5] cleanup --- src/netCDF4/_netCDF4.pyx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/netCDF4/_netCDF4.pyx b/src/netCDF4/_netCDF4.pyx index 70cf56c9d..0dcd64626 100644 --- a/src/netCDF4/_netCDF4.pyx +++ b/src/netCDF4/_netCDF4.pyx @@ -3688,7 +3688,7 @@ behavior is similar to Fortran or Matlab, but different than numpy. is replaced with this value. If fill_value is set to `False`, then the variable is not pre-filled. The default netCDF fill values can be found in the dictionary `netCDF4.default_fillvals`. - + **`chunk_cache`**: If specified, sets the chunk cache size for this variable. Persists as long as Dataset is open. Use `set_var_chunk_cache` to change it when Dataset is re-opened. @@ -3793,12 +3793,9 @@ behavior is similar to Fortran or Matlab, but different than numpy. ndims = len(dimensions) # find dimension ids. if ndims: - dims = [] dimids = malloc(sizeof(int) * ndims) for n from 0 <= n < ndims: - dim = dimensions[n] - dimids[n] = dim._dimid - dims.append(dim) + dimids[n] = dimensions[n]._dimid # go into define mode if it's a netCDF 3 compatible # file format. Be careful to exit define mode before # any exceptions are raised. @@ -3866,8 +3863,8 @@ behavior is similar to Fortran or Matlab, but different than numpy. raise ValueError('chunksizes must be a sequence with the same length as dimensions') chunksizesp = malloc(sizeof(size_t) * ndims) for n from 0 <= n < ndims: - if not dims[n].isunlimited() and \ - chunksizes[n] > dims[n].size: + if not dimensions[n].isunlimited() and \ + chunksizes[n] > dimensions[n].size: msg = 'chunksize cannot exceed dimension size' raise ValueError(msg) chunksizesp[n] = chunksizes[n] From 0703c6b19ec3c240c25785c8367b9341f96bb25f Mon Sep 17 00:00:00 2001 From: Jeff Whitaker Date: Mon, 17 Jan 2022 10:05:25 -0700 Subject: [PATCH 5/5] update docs --- docs/index.html | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/index.html b/docs/index.html index 5f7928274..a20d5c83d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -21,7 +21,7 @@

Contents

  • Introduction
      @@ -456,7 +456,7 @@

      API Documentation

      netCDF4

      -

      Version 1.5.8

      +

      Version 1.6.0

      Introduction

      @@ -1595,7 +1595,7 @@

      In-memory (diskless) Datasets

      the parallel IO example, which is in examples/mpi_example.py. Unit tests are in the test directory.

      -

      contact: Jeffrey Whitaker jeffrey.s.whitaker@noaa.gov

      +

      contact: Jeffrey Whitaker jeffrey.s.whitaker@noaa.gov

      copyright: 2008 by Jeffrey Whitaker.

      @@ -1725,8 +1725,10 @@

      In-memory (diskless) Datasets

      mode: access mode. r means read-only; no data can be modified. w means write; a new file is created, an existing file with -the same name is deleted. a and r+ mean append (in analogy with -serial files); an existing file is opened for reading and writing. +the same name is deleted. 'x' means write, but fail if an existing +file with the same name already exists. a and r+ mean append; +an existing file is opened for reading and writing, if +file does not exist already, one is created. Appending s to modes r, w, r+ or a will enable unbuffered shared access to NETCDF3_CLASSIC, NETCDF3_64BIT_OFFSET or NETCDF3_64BIT_DATA formatted files. @@ -1737,7 +1739,8 @@

      In-memory (diskless) Datasets

      clobber: if True (default), opening a file with mode='w' will clobber an existing file with the same name. if False, an -exception will be raised if a file with the same name already exists.

      +exception will be raised if a file with the same name already exists. +mode='x' is identical to mode='w' with clobber=False.

      format: underlying file format (one of 'NETCDF4', 'NETCDF4_CLASSIC', 'NETCDF3_CLASSIC', 'NETCDF3_64BIT_OFFSET' or @@ -2062,8 +2065,9 @@

      In-memory (diskless) Datasets

      Data from netCDF variables is presented to python as numpy arrays with the corresponding data type.

      -

      dimensions must be a tuple containing dimension names (strings) that -have been defined previously using Dataset.createDimension. The default value +

      dimensions must be a tuple containing Dimension instances and/or +dimension names (strings) that have been defined +previously using Dataset.createDimension. The default value is an empty tuple, which means the variable is a scalar.

      If the optional keyword zlib is True, the data will be compressed in @@ -2813,7 +2817,7 @@

      In-memory (diskless) Datasets

      (for a variable-length string array). Numpy string and unicode datatypes with length greater than one are aliases for str.

      -

      dimensions: a tuple containing the variable's dimension names +

      dimensions: a tuple containing the variable's Dimension instances (defined previously with createDimension). Default is an empty tuple which means the variable is a scalar (and therefore has no dimensions).