Group.__str__()   F
last analyzed

Complexity

Conditions 10

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
dl 0
loc 26
rs 3.1304
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like Group.__str__() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# Copyright (c) 2015,2016 MetPy Developers.
2
# Distributed under the terms of the BSD 3-Clause License.
3
# SPDX-License-Identifier: BSD-3-Clause
4
r"""Tools for mimicing the API of the Common Data Model (CDM).
5
6
The CDM is a data model for representing a wide array of data. The
7
goal is to be a simple, universal interface to different datasets. This API is a Python
8
implementation in the spirit of the original Java interface in netCDF-Java.
9
"""
10
11
from collections import OrderedDict
12
13
import numpy as np
14
15
16
class AttributeContainer(object):
17
    r"""Handle maintaining a list of netCDF attributes.
18
19
    Implements the attribute handling for other CDM classes.
20
21
    """
22
23
    def __init__(self):
24
        r"""Initialize an :class:`AttributeContainer`."""
25
        self._attrs = []
26
27
    def ncattrs(self):
28
        r"""Get a list of the names of the netCDF attributes.
29
30
        Returns
31
        -------
32
        List[str]
33
34
        """
35
        return self._attrs
36
37
    def __setattr__(self, key, value):
38
        """Handle setting attributes."""
39
        if hasattr(self, '_attrs'):
40
            self._attrs.append(key)
41
        self.__dict__[key] = value
42
43
    def __delattr__(self, item):
44
        """Handle attribute deletion."""
45
        self.__dict__.pop(item)
46
        if hasattr(self, '_attrs'):
47
            self._attrs.remove(item)
48
49
50
class Group(AttributeContainer):
51
    r"""Holds dimensions and variables.
52
53
    Every CDM dataset has at least a root group.
54
55
    """
56
57
    def __init__(self, parent, name):
58
        r"""Initialize this :class:`Group`.
59
60
        Instead of constructing a :class:`Group` directly, you should use
61
        :meth:`~Group.createGroup`.
62
63
        Parameters
64
        ----------
65
        parent : Group or None
66
            The parent Group for this one. Passing in :data:`None` implies that this is
67
            the root :class:`Group`.
68
        name : str
69
            The name of this group
70
71
        See Also
72
        --------
73
        Group.createGroup
74
75
        """
76
        self.parent = parent
77
        if parent:
78
            self.parent.groups[name] = self
79
80
        #: :desc: The name of the :class:`Group`
81
        #: :type: str
82
        self.name = name
83
84
        #: :desc: Any Groups nested within this one
85
        #: :type: dict[str, Group]
86
        self.groups = OrderedDict()
87
88
        #: :desc: Variables contained within this group
89
        #: :type: dict[str, Variable]
90
        self.variables = OrderedDict()
91
92
        #: :desc: Dimensions contained within this group
93
        #: :type: dict[str, Dimension]
94
        self.dimensions = OrderedDict()
95
96
        # Do this last so earlier attributes aren't captured
97
        super(Group, self).__init__()
98
99
    # CamelCase API names for netcdf4-python compatibility
100
    def createGroup(self, name):  # noqa: N802
101
        """Create a new Group as a descendant of this one.
102
103
        Parameters
104
        ----------
105
        name : str
106
            The name of the new Group.
107
108
        Returns
109
        -------
110
        Group
111
            The newly created :class:`Group`
112
113
        """
114
        grp = Group(self, name)
115
        self.groups[name] = grp
116
        return grp
117
118
    def createDimension(self, name, size):  # noqa: N802
119
        """Create a new :class:`Dimension` in this :class:`Group`.
120
121
        Parameters
122
        ----------
123
        name : str
124
            The name of the new Dimension.
125
        size : int
126
            The size of the Dimension
127
128
        Returns
129
        -------
130
        Dimension
131
            The newly created :class:`Dimension`
132
133
        """
134
        dim = Dimension(self, name, size)
135
        self.dimensions[name] = dim
136
        return dim
137
138
    def createVariable(self, name, datatype, dimensions=(), fill_value=None,  # noqa: N802
139
                       wrap_array=None):
140
        """Create a new Variable in this Group.
141
142
        Parameters
143
        ----------
144
        name : str
145
            The name of the new Variable.
146
        datatype : str or numpy.dtype
147
            A valid Numpy dtype that describes the layout of the data within the Variable.
148
        dimensions : tuple[str], optional
149
            The dimensions of this Variable. Defaults to empty, which implies a scalar
150
            variable.
151
        fill_value : number, optional
152
            A scalar value that is used to fill the created storage. Defaults to None, which
153
            performs no filling, leaving the storage uninitialized.
154
        wrap_array : numpy.ndarray, optional
155
            Instead of creating an array, the Variable instance will assume ownership of the
156
            passed in array as its data storage. This is a performance optimization to avoid
157
            copying large data blocks. Defaults to None, which means a new array will be
158
            created.
159
160
        Returns
161
        -------
162
        Variable
163
            The newly created :class:`Variable`
164
165
        """
166
        var = Variable(self, name, datatype, dimensions, fill_value, wrap_array)
167
        self.variables[name] = var
168
        return var
169
170
    def __str__(self):
171
        """Return a string representation of the Group."""
172
        print_groups = []
173
        if self.name:
174
            print_groups.append(self.name)
175
176
        if self.groups:
177
            print_groups.append('Groups:')
178
            for group in self.groups.values():
179
                print_groups.append(str(group))
180
181
        if self.dimensions:
182
            print_groups.append('\nDimensions:')
183
            for dim in self.dimensions.values():
184
                print_groups.append(str(dim))
185
186
        if self.variables:
187
            print_groups.append('\nVariables:')
188
            for var in self.variables.values():
189
                print_groups.append(str(var))
190
191
        if self.ncattrs():
192
            print_groups.append('\nAttributes:')
193
            for att in self.ncattrs():
194
                print_groups.append('\t{0}: {1}'.format(att, getattr(self, att)))
195
        return '\n'.join(print_groups)
196
197
198
class Dataset(Group):
199
    r"""Represents a set of data using the Common Data Model (CDM).
200
201
    This is currently only a wrapper around the root Group.
202
203
    """
204
205
    def __init__(self):
206
        """Initialize a Dataset."""
207
        super(Dataset, self).__init__(None, 'root')
208
209
210
class Variable(AttributeContainer):
211
    r"""Holds typed data (using a :class:`numpy.ndarray`), as well as attributes (e.g. units).
212
213
    In addition to its various attributes, the Variable supports getting *and* setting data
214
    using the ``[]`` operator and indices or slices. Getting data returns
215
    :class:`numpy.ndarray` instances.
216
217
    """
218
219
    def __init__(self, group, name, datatype, dimensions, fill_value, wrap_array):
220
        """Initialize a Variable.
221
222
        Instead of constructing a Variable directly, you should use
223
        :meth:`Group.createVariable`.
224
225
        Parameters
226
        ----------
227
        group : Group
228
            The parent :class:`Group` that owns this Variable.
229
        name : str
230
            The name of this Variable.
231
        datatype : str or numpy.dtype
232
            A valid Numpy dtype that describes the layout of each element of the data
233
        dimensions : tuple[str], optional
234
            The dimensions of this Variable. Defaults to empty, which implies a scalar
235
            variable.
236
        fill_value : scalar, optional
237
            A scalar value that is used to fill the created storage. Defaults to None, which
238
            performs no filling, leaving the storage uninitialized.
239
        wrap_array : numpy.ndarray, optional
240
            Instead of creating an array, the Variable instance will assume ownership of the
241
            passed in array as its data storage. This is a performance optimization to avoid
242
            copying large data blocks. Defaults to None, which means a new array will be
243
            created.
244
245
        See Also
246
        --------
247
        Group.createVariable
248
249
        """
250
        # Initialize internal vars
251
        self._group = group
252
        self._name = name
253
        self._dimensions = tuple(dimensions)
254
255
        # Set the storage--create/wrap as necessary
256
        shape = tuple(len(group.dimensions.get(d)) for d in dimensions)
257
        if wrap_array is not None:
258
            if shape != wrap_array.shape:
259
                raise ValueError('Array to wrap does not match dimensions.')
260
            self._data = wrap_array
261
        else:
262
            self._data = np.empty(shape, dtype=datatype)
263
            if fill_value is not None:
264
                self._data.fill(fill_value)
265
266
        # Do this last so earlier attributes aren't captured
267
        super(Variable, self).__init__()
268
269
    # Not a property to maintain compatibility with NetCDF4 python
270
    def group(self):
271
        """Get the Group that owns this Variable.
272
273
        Returns
274
        -------
275
        Group
276
            The parent Group.
277
278
        """
279
        return self._group
280
281
    @property
282
    def name(self):
283
        """str: the name of the variable."""
284
        return self._name
285
286
    @property
287
    def size(self):
288
        """int: the total number of elements."""
289
        return self._data.size
290
291
    @property
292
    def shape(self):
293
        """tuple[int]: Describes the size of the Variable along each of its dimensions."""
294
        return self._data.shape
295
296
    @property
297
    def ndim(self):
298
        """int: the number of dimensions used by this variable."""
299
        return self._data.ndim
300
301
    @property
302
    def dtype(self):
303
        """numpy.dtype: Describes the layout of each element of the data."""
304
        return self._data.dtype
305
306
    @property
307
    def datatype(self):
308
        """numpy.dtype: Describes the layout of each element of the data."""
309
        return self._data.dtype
310
311
    @property
312
    def dimensions(self):
313
        """tuple[str]: all the names of :class:`Dimension` used by this :class:`Variable`."""
314
        return self._dimensions
315
316
    def __setitem__(self, ind, value):
317
        """Handle setting values on the Variable."""
318
        self._data[ind] = value
319
320
    def __getitem__(self, ind):
321
        """Handle getting values from the Variable."""
322
        return self._data[ind]
323
324
    def __str__(self):
325
        """Return a string representation of the Variable."""
326
        groups = [str(type(self)) +
327
                  ': {0.datatype} {0.name}({1})'.format(self, ', '.join(self.dimensions))]
328
        for att in self.ncattrs():
329
            groups.append('\t{0}: {1}'.format(att, getattr(self, att)))
330
        if self.ndim:
331
            # Ensures we get the same string output on windows where shape contains longs
332
            shape = tuple(int(s) for s in self.shape)
333
            if self.ndim > 1:
334
                shape_str = str(shape)
335
            else:
336
                shape_str = str(shape[0])
337
            groups.append('\tshape = ' + shape_str)
338
        return '\n'.join(groups)
339
340
341
# Punting on unlimited dimensions for now since we're relying upon numpy for storage
342
# We don't intend to be a full file API or anything, just need to be able to represent
343
# other files using a common API.
344
class Dimension(object):
345
    r"""Represent a shared dimension between different Variables.
346
347
    For instance, variables that are dependent upon a common set of times.
348
349
    """
350
351
    def __init__(self, group, name, size=None):
352
        """Initialize a Dimension.
353
354
        Instead of constructing a Dimension directly, you should use ``Group.createDimension``.
355
356
        Parameters
357
        ----------
358
        group : Group
359
            The parent Group that owns this Variable.
360
        name : str
361
            The name of this Variable.
362
        size : int or None, optional
363
            The size of the Dimension. Defaults to None, which implies an empty dimension.
364
365
        See Also
366
        --------
367
        Group.createDimension
368
369
        """
370
        self._group = group
371
372
        #: :desc: The name of the Dimension
373
        #: :type: str
374
        self.name = name
375
376
        #: :desc: The size of this Dimension
377
        #: :type: int
378
        self.size = size
379
380
    # Not a property to maintain compatibility with NetCDF4 python
381
    def group(self):
382
        """Get the Group that owns this Dimension.
383
384
        Returns
385
        -------
386
        Group
387
            The parent Group.
388
389
        """
390
        return self._group
391
392
    def __len__(self):
393
        """Return the length of this Dimension."""
394
        return self.size
395
396
    def __str__(self):
397
        """Return a string representation of this Dimension."""
398
        return '{0}: name = {1.name}, size = {1.size}'.format(type(self), self)
399
400
401
# Not sure if this lives long-term or not
402
def cf_to_proj(var):
403
    r"""Convert a Variable with projection information to a Proj.4 Projection instance.
404
405
    The attributes of this Variable must conform to the Climate and Forecasting (CF)
406
    netCDF conventions.
407
408
    Parameters
409
    ----------
410
    var : Variable
411
        The projection variable with appropriate attributes.
412
413
    """
414
    import pyproj
415
    kwargs = {'lat_0': var.latitude_of_projection_origin, 'a': var.earth_radius,
416
              'b': var.earth_radius}
417
    if var.grid_mapping_name == 'lambert_conformal_conic':
418
        kwargs['proj'] = 'lcc'
419
        kwargs['lon_0'] = var.longitude_of_central_meridian
420
        kwargs['lat_1'] = var.standard_parallel
421
        kwargs['lat_2'] = var.standard_parallel
422
    elif var.grid_mapping_name == 'polar_stereographic':
423
        kwargs['proj'] = 'stere'
424
        kwargs['lon_0'] = var.straight_vertical_longitude_from_pole
425
        kwargs['lat_0'] = var.latitude_of_projection_origin
426
        kwargs['lat_ts'] = var.standard_parallel
427
        kwargs['x_0'] = False  # Easting
428
        kwargs['y_0'] = False  # Northing
429
    elif var.grid_mapping_name == 'mercator':
430
        kwargs['proj'] = 'merc'
431
        kwargs['lon_0'] = var.longitude_of_projection_origin
432
        kwargs['lat_ts'] = var.standard_parallel
433
        kwargs['x_0'] = False  # Easting
434
        kwargs['y_0'] = False  # Northing
435
436
    return pyproj.Proj(**kwargs)
437