Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

del_properties methods, and ignore_properties keyword to cf.unique_constructs #241

Merged
merged 15 commits into from
Feb 27, 2023
12 changes: 12 additions & 0 deletions Changelog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
Version 1.10.0.3
----------------

**2023-??-??**

* New method: `cfdm.Field.del_properties`
(https://github.com/NCAS-CMS/cfdm/issues/241)
* New keyword parameter to `cfdm.unique_constructs`:
``ignore_properties`` (https://github.com/NCAS-CMS/cfdm/issues/240)

----

Version 1.10.0.2
----------------

Expand Down
26 changes: 13 additions & 13 deletions cfdm/cellmeasure.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,32 +224,33 @@ def equals(
verbose=None,
ignore_data_type=False,
ignore_fill_value=False,
ignore_properties=(),
ignore_properties=None,
ignore_compression=True,
ignore_type=False,
):
"""Whether two cell measure constructs are the same.

Equality is strict by default. This means that:

* the same descriptive properties must be present, with the same
values and data types, and vector-valued properties must also have
same the size and be element-wise equal (see the *ignore_properties*
and *ignore_data_type* parameters), and
* the same descriptive properties must be present, with the
same values and data types, and vector-valued properties
must also have same the size and be element-wise equal (see
the *ignore_properties* and *ignore_data_type* parameters),
and

..

* if there are data arrays then they must have same shape and data
type, the same missing data mask, and be element-wise equal (see the
*ignore_data_type* parameter).
* if there are data arrays then they must have same shape and
data type, the same missing data mask, and be element-wise
equal (see the *ignore_data_type* parameter).

{{equals tolerance}}

{{equals compression}}

Any type of object may be tested but, in general, equality is only
possible with another cell measure construct, or a subclass of
one. See the *ignore_type* parameter.
Any type of object may be tested but, in general, equality is
only possible with another cell measure construct, or a
subclass of one. See the *ignore_type* parameter.

{{equals netCDF}}

Expand All @@ -268,8 +269,7 @@ def equals(

{{verbose: `int` or `str` or `None`, optional}}

ignore_properties: sequence of `str`, optional
The names of properties to omit from the comparison.
{{ignore_properties: (sequence of `str`), optional}}

{{ignore_data_type: `bool`, optional}}

Expand Down
58 changes: 57 additions & 1 deletion cfdm/core/abstract/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def clear_properties(self):

.. versionadded:: (cfdm) 1.7.0

.. seealso:: `del_property` `properties`, `set_properties`
.. seealso:: `del_properties`, `del_property`, `properties`,
`set_properties`

:Returns:

Expand Down Expand Up @@ -134,6 +135,61 @@ def del_property(self, prop, default=ValueError()):
f"{self.__class__.__name__!r} has no {prop!r} property",
)

def del_properties(self, properties):
"""Remove properties.

.. versionadded:: (cfdm) 1.10.0.3

.. seealso:: `clear_properties`, `del_property`, `properties`,
`set_properties`

:Parameters:

properties: (sequence of) `str`
The names of the properties to be removed. If the
`{{class}}` does not have a particular property then
that property is ignored. No properties are removed if
*properties* is an empty sequence.

*Parameter example:*
``'long_name'``

*Parameter example:*
``['long_name', 'comment']``

:Returns:

`dict`
The removed property values, keyed by property name.

**Examples**

>>> f = {{package}}.{{class}}()
>>> f.set_properties({'project': 'CMIP7', 'comment': 'model'})
>>> removed_properties = f.del_properties('project')
>>> removed_properties
{'project': 'CMIP7'}
>>> f.properties()
{'comment': 'model'}
>>> f.set_properties(removed_properties)
>>> f.properties()
{'comment': 'model', 'project': 'CMIP7'}
>>> f.del_properties('foo')
{}

"""
if isinstance(properties, str):
properties = (properties,)

out = {}
for prop in properties:
try:
out[prop] = self.del_property(prop)
except ValueError:
pass

return out

def get_property(self, prop, default=ValueError()):
"""Return a property.

Expand Down
2 changes: 1 addition & 1 deletion cfdm/docstring/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
If True then all ``_FillValue`` and ``missing_value``
properties are omitted from the comparison.""",
# ignore_properties
"{{ignore_properties: sequence of `str`, optional}}": """ignore_properties: sequence of `str`, optional
"{{ignore_properties: (sequence of) `str`, optional}}": """ignore_properties: (sequence of) `str`, optional
The names of properties to omit from the
comparison.""",
# inplace
Expand Down
81 changes: 59 additions & 22 deletions cfdm/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def abspath(filename):
return os.path.abspath(filename)


def unique_constructs(constructs, copy=True):
def unique_constructs(constructs, ignore_properties=None, copy=True):
"""Return the unique constructs from a sequence.

.. versionadded:: (cfdm) 1.9.0.0
Expand All @@ -443,14 +443,29 @@ def unique_constructs(constructs, copy=True):
The constructs to be compared. The constructs may comprise
a mixture of types. The sequence can be empty.

ignore_properties: (sequence of) `str`, optional
The names of construct properties to be ignored when
testing for uniqueness. Any of these properties which have
unequal values on otherwise equal input constructs are
removed from the returned unique construct.

.. versionadded:: (cfdm) 1.10.0.3

copy: `bool`, optional
If True (the default) then deep copy returned constructs,
else they are not (deep) copied.
If True (the default) then each returned construct is a
deep copy of an input construct, otherwise they are not
copies.

If *ignore_properties* has been set then *copy* is ignored
and deep copies are always returned, even if
*ignore_properties* is an empty sequence.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 Very clear and concise.


:Returns:

`list`
The unique constructs. May be an empty list.
Sequence of constructs
The unique constructs in a sequence of the same type as
the input *constructs*. If *constructs* was a generator
then a generator is returned.

**Examples**

Expand Down Expand Up @@ -482,53 +497,75 @@ def unique_constructs(constructs, copy=True):
<Field: air_temperature(atmosphere_hybrid_height_coordinate(1), grid_latitude(10), grid_longitude(9)) K>]

"""
out_type = type(constructs)

if not constructs:
# constructs is an empty sequence
return []
# 'constructs' is an empty sequence
return out_type([])

# ----------------------------------------------------------------
# Find the first construct in the sequence and create an iterator
# for the rest
# ----------------------------------------------------------------
try:
# constructs is a sequence?
# 'constructs' is a sequence?
construct0 = constructs[0]
constructs = (c for c in constructs[1:])
except TypeError:
try:
# constructs is a generator?
# 'constructs' is a generator?
construct0 = next(constructs)
except StopIteration:
# constructs is an empty generator
return []
# --- End: try
# 'constructs' is an empty generator
return (c for c in ())
else:
generator_out = True
else:
generator_out = False

if ignore_properties is not None:
copy = True

if isinstance(ignore_properties, str):
ignore_properties = (ignore_properties,)

if copy:
construct0 = construct0.copy()

# Initialise the output list
# Initialise the list of unique constructs
out = [construct0]

# ----------------------------------------------------------------
# Loop round the iterator, adding any "new" constructs to the
# output list
# Loop round the iterator, adding any new unique constructs to the
# list
# ----------------------------------------------------------------
for construct in constructs:
is_equal = False
equal = False

for c in out:
if construct.equals(c, verbose="DISABLE"):
is_equal = True
if construct.equals(
c, ignore_properties=ignore_properties, verbose="DISABLE"
):
equal = True
if ignore_properties:
for prop in ignore_properties:
if construct.get_property(
prop, None
) != c.get_property(prop, None):
c.del_property(prop, None)

break
# --- End: for

if not is_equal:
if not equal:
if copy:
construct = construct.copy()

out.append(construct)
# --- End: for

return out
if generator_out:
return (c for c in out)

return out_type(out)


@total_ordering
Expand Down
11 changes: 8 additions & 3 deletions cfdm/mixin/fielddomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1768,7 +1768,7 @@ def equals(
verbose=None,
ignore_data_type=False,
ignore_fill_value=False,
ignore_properties=(),
ignore_properties=None,
ignore_compression=True,
ignore_type=False,
):
Expand Down Expand Up @@ -1813,7 +1813,7 @@ def equals(

{{ignore_fill_value: `bool`, optional}}

ignore_properties: sequence of `str`, optional
ignore_properties: (sequence of) `str`, optional
The names of properties of the construct (not the
metadata constructs) to omit from the comparison. Note
that the ``Conventions`` property is always omitted.
Expand Down Expand Up @@ -1853,7 +1853,12 @@ def equals(

"""
# Check the properties and data
ignore_properties = tuple(ignore_properties) + ("Conventions",)
if not ignore_properties:
ignore_properties = ("Conventions",)
elif isinstance(ignore_properties, str):
ignore_properties = (ignore_properties, "Conventions")
else:
ignore_properties = tuple(ignore_properties) + ("Conventions",)

if not super().equals(
other,
Expand Down
7 changes: 5 additions & 2 deletions cfdm/mixin/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def equals(
verbose=None,
ignore_data_type=False,
ignore_fill_value=False,
ignore_properties=(),
ignore_properties=None,
ignore_type=False,
ignore_compression=True,
):
Expand Down Expand Up @@ -276,7 +276,7 @@ def equals(

{{verbose: `int` or `str` or `None`, optional}}

{{ignore_properties: sequence of `str`, optional}}
{{ignore_properties: (sequence of `str`), optional}}

{{ignore_data_type: `bool`, optional}}

Expand Down Expand Up @@ -327,6 +327,9 @@ def equals(
other_properties = other.properties()

if ignore_properties:
if isinstance(ignore_properties, str):
ignore_properties = (ignore_properties,)

for prop in ignore_properties:
self_properties.pop(prop, None)
other_properties.pop(prop, None)
Expand Down
4 changes: 2 additions & 2 deletions cfdm/mixin/propertiesdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ def equals(
verbose=None,
ignore_data_type=False,
ignore_fill_value=False,
ignore_properties=(),
ignore_properties=None,
ignore_compression=True,
ignore_type=False,
):
Expand Down Expand Up @@ -723,7 +723,7 @@ def equals(

{{verbose: `int` or `str` or `None`, optional}}

{{ignore_properties: sequence of `str`, optional}}
{{ignore_properties: (sequence of) `str`, optional}}

{{ignore_data_type: `bool`, optional}}

Expand Down
4 changes: 2 additions & 2 deletions cfdm/mixin/propertiesdatabounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,7 @@ def equals(
verbose=None,
ignore_data_type=False,
ignore_fill_value=False,
ignore_properties=(),
ignore_properties=None,
ignore_compression=True,
ignore_type=False,
):
Expand Down Expand Up @@ -950,7 +950,7 @@ def equals(

{{verbose: `int` or `str` or `None`, optional}}

{{ignore_properties: sequence of `str`, optional}}
{{ignore_properties: (sequence of) `str`, optional}}

{{ignore_data_type: `bool`, optional}}

Expand Down
Loading