From 839215209ae88917dd6e757f9e7fbbea70188986 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 3 Jul 2020 22:40:50 +0100 Subject: [PATCH] [FB] [PI-3478] Lenient metadata (#3739) * add lenient infra-structure * add metadata lenient __eq__ support * complete __eq__, combine and difference support * explicit inherited lenient_service + support equal convenience * fix attributes difference + lenient kwargs * make lenient public + minor tidy * rename MetadataManagerFactory to metadata_manager_factory * extend lenient_client decorator to support services registration * add lenient test coverage * purge qualname usage in metadata.py * support global enable for lenient services * support partial mapping metadata assignment * purge Lenient.__setattr__ from api * add BaseMetadata compare test coverage * metadata rationalisation * add BaseMetadata difference test coverage * added context manager ephemeral comment clarification * add BaseMetadata __ne__ test coverage * standardise lenient decorator closure names * add BaseMetadata equal test coverage * half dunder context * add AncillaryVariableMetadata test coverage * add additional AncillaryVariableMetadata test coverage * add CellMeasureMetadata test coverage * Clarify lenient_service operation + simplify code. * add CoordMetadata test coverage * add CubeMetadata test coverage * metadata tests use self.cls * fix typo * fix context manager ephemeral services * add logging * Pin pillow to make graphics tests work again. (#3630) * Fixed tests since Numpy 1.18 deprecation of non-int num arguments for linspace. (#3655) * Switched use of datetime.weekday() to datetime.dayofwk. (#3687) * New image hashes for mpl 3x2 (#3682) * New image hash for iris.test.test_plot.TestSymbols.test_cloud_cover with matplotlib 3.2.0. * Further images changes for mpl3x2. * Yet more updated image results. * fix sentinel uniqueness test failure * remove redundant cdm mapping test * difference returns None for no difference * protect Lenient and LENIENT private * privitise lenient framework and add API veneer * add explicit maths feature default * review actions * review actions * trexfeathers review actions * stephenworsley review actions Co-authored-by: Patrick Peglar Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> --- lib/iris/aux_factory.py | 22 +- lib/iris/common/__init__.py | 1 + lib/iris/common/lenient.py | 661 ++++++++ lib/iris/common/metadata.py | 880 ++++++++++- lib/iris/common/mixin.py | 16 +- lib/iris/config.py | 11 + lib/iris/coords.py | 10 +- lib/iris/cube.py | 4 +- lib/iris/etc/logging.yaml | 29 + lib/iris/tests/test_cdm.py | 8 - .../tests/unit/common/lenient/__init__.py | 6 + .../tests/unit/common/lenient/test_Lenient.py | 182 +++ .../unit/common/lenient/test__Lenient.py | 835 ++++++++++ .../common/lenient/test__lenient_client.py | 182 +++ .../common/lenient/test__lenient_service.py | 116 ++ .../unit/common/lenient/test__qualname.py | 66 + .../test_AncillaryVariableMetadata.py | 405 ++++- .../unit/common/metadata/test_BaseMetadata.py | 1389 ++++++++++++++++- .../metadata/test_CellMeasureMetadata.py | 570 ++++++- .../common/metadata/test_CoordMetadata.py | 626 +++++++- .../unit/common/metadata/test_CubeMetadata.py | 580 ++++++- ...ry.py => test_metadata_manager_factory.py} | 34 +- .../unit/common/mixin/test_CFVariableMixin.py | 86 +- 23 files changed, 6534 insertions(+), 185 deletions(-) create mode 100644 lib/iris/common/lenient.py create mode 100644 lib/iris/etc/logging.yaml create mode 100644 lib/iris/tests/unit/common/lenient/__init__.py create mode 100644 lib/iris/tests/unit/common/lenient/test_Lenient.py create mode 100644 lib/iris/tests/unit/common/lenient/test__Lenient.py create mode 100644 lib/iris/tests/unit/common/lenient/test__lenient_client.py create mode 100644 lib/iris/tests/unit/common/lenient/test__lenient_service.py create mode 100644 lib/iris/tests/unit/common/lenient/test__qualname.py rename lib/iris/tests/unit/common/metadata/{test_MetadataManagerFactory.py => test_metadata_manager_factory.py} (85%) diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py index 1f134f65c0..0cc6bf068f 100644 --- a/lib/iris/aux_factory.py +++ b/lib/iris/aux_factory.py @@ -14,7 +14,11 @@ import dask.array as da import numpy as np -from iris.common import CFVariableMixin, CoordMetadata, MetadataManagerFactory +from iris.common import ( + CFVariableMixin, + CoordMetadata, + metadata_manager_factory, +) import iris.coords @@ -35,7 +39,7 @@ class AuxCoordFactory(CFVariableMixin, metaclass=ABCMeta): def __init__(self): # Configure the metadata manager. if not hasattr(self, "_metadata_manager"): - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) #: Descriptive name of the coordinate made by the factory self.long_name = None @@ -385,7 +389,7 @@ def __init__(self, delta=None, sigma=None, orography=None): """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() if delta and delta.nbounds not in (0, 2): @@ -574,7 +578,7 @@ def __init__(self, delta=None, sigma=None, surface_air_pressure=None): """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coords meet necessary conditions. @@ -779,7 +783,7 @@ def __init__( """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. @@ -1080,7 +1084,7 @@ def __init__(self, sigma=None, eta=None, depth=None): """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. @@ -1263,7 +1267,7 @@ def __init__(self, s=None, c=None, eta=None, depth=None, depth_c=None): """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. @@ -1486,7 +1490,7 @@ def __init__( """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. @@ -1704,7 +1708,7 @@ def __init__(self, s=None, c=None, eta=None, depth=None, depth_c=None): """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. diff --git a/lib/iris/common/__init__.py b/lib/iris/common/__init__.py index 3f25865a01..52759dac8e 100644 --- a/lib/iris/common/__init__.py +++ b/lib/iris/common/__init__.py @@ -5,5 +5,6 @@ # licensing details. +from .lenient import * from .metadata import * from .mixin import * diff --git a/lib/iris/common/lenient.py b/lib/iris/common/lenient.py new file mode 100644 index 0000000000..802d854554 --- /dev/null +++ b/lib/iris/common/lenient.py @@ -0,0 +1,661 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. + +from collections.abc import Iterable +from contextlib import contextmanager +from copy import deepcopy +from functools import wraps +from inspect import getmodule +import threading + + +__all__ = [ + "LENIENT", + "Lenient", +] + + +#: Default _Lenient services global activation state. +_LENIENT_ENABLE_DEFAULT = True + +#: Default Lenient maths feature state. +_LENIENT_MATHS_DEFAULT = True + +#: Protected _Lenient internal non-client, non-service keys. +_LENIENT_PROTECTED = ("active", "enable") + + +def _lenient_client(*dargs, services=None): + """ + Decorator that allows a client function/method to declare at runtime that + it is executing and requires lenient behaviour from a prior registered + lenient service function/method. + + This decorator supports being called with no arguments e.g., + + @_lenient_client() + def func(): + pass + + This is equivalent to using it as a simple naked decorator e.g., + + @_lenient_client + def func() + pass + + Alternatively, this decorator supports the lenient client explicitly + declaring the lenient services that it wishes to use e.g., + + @_lenient_client(services=(service1, service2, ...) + def func(): + pass + + Args: + + * dargs (tuple of callable): + A tuple containing the callable lenient client function/method to be + wrapped by the decorator. This is automatically populated by Python + through the decorator interface. No argument requires to be manually + provided. + + Kwargs: + + * services (callable or str or iterable of callable/str) + Zero or more function/methods, or equivalent fully qualified string names, of + lenient service function/methods. + + Returns: + Closure wrapped function/method. + + """ + ndargs = len(dargs) + + if ndargs: + assert ( + ndargs == 1 + ), f"Invalid lenient client arguments, expecting 1 got {ndargs}." + assert callable( + dargs[0] + ), "Invalid lenient client argument, expecting a callable." + + assert not ( + ndargs and services + ), "Invalid lenient client, got both arguments and keyword arguments." + + if ndargs: + # The decorator has been used as a simple naked decorator. + (func,) = dargs + + @wraps(func) + def lenient_client_inner_naked(*args, **kwargs): + """ + Closure wrapper function to register the wrapped function/method + as active at runtime before executing it. + + """ + with _LENIENT.context(active=_qualname(func)): + result = func(*args, **kwargs) + return result + + result = lenient_client_inner_naked + else: + # The decorator has been called with None, zero or more explicit lenient services. + if services is None: + services = () + + if isinstance(services, str) or not isinstance(services, Iterable): + services = (services,) + + def lenient_client_outer(func): + @wraps(func) + def lenient_client_inner(*args, **kwargs): + """ + Closure wrapper function to register the wrapped function/method + as active at runtime before executing it. + + """ + with _LENIENT.context(*services, active=_qualname(func)): + result = func(*args, **kwargs) + return result + + return lenient_client_inner + + result = lenient_client_outer + + return result + + +def _lenient_service(*dargs): + """ + Decorator that allows a function/method to declare that it supports lenient + behaviour as a service. + + Registration is at Python interpreter parse time. + + The decorator supports being called with no arguments e.g., + + @_lenient_service() + def func(): + pass + + This is equivalent to using it as a simple naked decorator e.g., + + @_lenient_service + def func(): + pass + + Args: + + * dargs (tuple of callable): + A tuple containing the callable lenient service function/method to be + wrapped by the decorator. This is automatically populated by Python + through the decorator interface. No argument requires to be manually + provided. + + Returns: + Closure wrapped function/method. + + """ + ndargs = len(dargs) + + if ndargs: + assert ( + ndargs == 1 + ), f"Invalid lenient service arguments, expecting 1 got {ndargs}." + assert callable( + dargs[0] + ), "Invalid lenient service argument, expecting a callable." + + if ndargs: + # The decorator has been used as a simple naked decorator. + # Thus the (single) argument is a function to be wrapped. + # We just register the argument function as a lenient service, and + # return it unchanged + (func,) = dargs + + _LENIENT.register_service(func) + + # This decorator registers 'func': the func itself is unchanged. + result = func + + else: + # The decorator has been called with no arguments. + # Return a decorator, to apply to 'func' immediately following. + def lenient_service_outer(func): + _LENIENT.register_service(func) + + # Decorator registers 'func', but func itself is unchanged. + return func + + result = lenient_service_outer + + return result + + +def _qualname(func): + """ + Return the fully qualified function/method string name. + + Args: + + * func (callable): + Callable function/method. Non-callable arguments are simply + passed through. + + .. note:: + Inherited methods will be qualified with the base class that + defines the method. + + """ + result = func + if callable(func): + module = getmodule(func) + result = f"{module.__name__}.{func.__qualname__}" + + return result + + +class Lenient(threading.local): + def __init__(self, **kwargs): + """ + A container for managing the run-time lenient features and options. + + Kwargs: + + * kwargs (dict) + Mapping of lenient key/value options to enable/disable. Note that, + only the lenient "maths" options is available, which controls + lenient/strict cube arithmetic. + + For example:: + + Lenient(maths=False) + + Note that, the values of these options are thread-specific. + + """ + # Configure the initial default lenient state. + self._init() + + if not kwargs: + # If not specified, set the default behaviour of the maths lenient feature. + kwargs = dict(maths=_LENIENT_MATHS_DEFAULT) + + # Configure the provided (or default) lenient features. + for feature, state in kwargs.items(): + self[feature] = state + + def __contains__(self, key): + return key in self.__dict__ + + def __getitem__(self, key): + if key not in self.__dict__: + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option, got {key!r}." + raise KeyError(emsg) + return self.__dict__[key] + + def __repr__(self): + cls = self.__class__.__name__ + msg = f"{cls}(maths={self.__dict__['maths']!r})" + return msg + + def __setitem__(self, key, value): + cls = self.__class__.__name__ + + if key not in self.__dict__: + emsg = f"Invalid {cls!r} option, got {key!r}." + raise KeyError(emsg) + + if not isinstance(value, bool): + emsg = f"Invalid {cls!r} option {key!r} value, got {value!r}." + raise ValueError(emsg) + + self.__dict__[key] = value + # Toggle the (private) lenient behaviour. + _LENIENT.enable = value + + def _init(self): + """Configure the initial default lenient state.""" + # This is the only public supported lenient feature i.e., cube arithmetic + self.__dict__["maths"] = None + + @contextmanager + def context(self, **kwargs): + """ + Return a context manager which allows temporary modification of the + lenient option state within the scope of the context manager. + + On entry to the context manager, all provided keyword arguments are + applied. On exit from the context manager, the previous lenient + option state is restored. + + For example:: + with iris.common.Lenient.context(maths=False): + pass + + """ + + def configure_state(state): + for feature, value in state.items(): + self[feature] = value + + # Save the original state. + original_state = deepcopy(self.__dict__) + + # Configure the provided lenient features. + configure_state(kwargs) + + try: + yield + finally: + # Restore the original state. + self.__dict__.clear() + self._init() + configure_state(original_state) + + +############################################################################### + + +class _Lenient(threading.local): + def __init__(self, *args, **kwargs): + """ + A container for managing the run-time lenient services and client + options for pre-defined functions/methods. + + Args: + + * args (callable or str or iterable of callable/str) + A function/method or fully qualified string name of the function/method + acting as a lenient service. + + Kwargs: + + * kwargs (dict of callable/str or iterable of callable/str) + Mapping of lenient client function/method, or fully qualified string name + of the function/method, to one or more lenient service + function/methods or fully qualified string name of function/methods. + + For example:: + + _Lenient(service1, service2, client1=service1, client2=(service1, service2)) + + Note that, the values of these options are thread-specific. + + """ + # The executing lenient client at runtime. + self.__dict__["active"] = None + # The global lenient services state activation switch. + self.__dict__["enable"] = _LENIENT_ENABLE_DEFAULT + + for service in args: + self.register_service(service) + + for client, services in kwargs.items(): + self.register_client(client, services) + + def __call__(self, func): + """ + Determine whether it is valid for the function/method to provide a + lenient service at runtime to the actively executing lenient client. + + Args: + + * func (callable or str): + A function/method or fully qualified string name of the function/method. + + Returns: + Boolean. + + """ + result = False + if self.__dict__["enable"]: + service = _qualname(func) + if service in self and self.__dict__[service]: + active = self.__dict__["active"] + if active is not None and active in self: + services = self.__dict__[active] + if isinstance(services, str) or not isinstance( + services, Iterable + ): + services = (services,) + result = service in services + return result + + def __contains__(self, name): + name = _qualname(name) + return name in self.__dict__ + + def __getattr__(self, name): + if name not in self.__dict__: + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option, got {name!r}." + raise AttributeError(emsg) + return self.__dict__[name] + + def __getitem__(self, name): + name = _qualname(name) + if name not in self.__dict__: + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option, got {name!r}." + raise KeyError(emsg) + return self.__dict__[name] + + def __repr__(self): + cls = self.__class__.__name__ + width = len(cls) + 1 + kwargs = [ + "{}={!r}".format(name, self.__dict__[name]) + for name in sorted(self.__dict__.keys()) + ] + joiner = ",\n{}".format(" " * width) + return "{}({})".format(cls, joiner.join(kwargs)) + + def __setitem__(self, name, value): + name = _qualname(name) + cls = self.__class__.__name__ + + if name not in self.__dict__: + emsg = f"Invalid {cls!r} option, got {name!r}." + raise KeyError(emsg) + + if name == "active": + value = _qualname(value) + if not isinstance(value, str) and value is not None: + emsg = f"Invalid {cls!r} option {name!r}, expected a registered {cls!r} client, got {value!r}." + raise ValueError(emsg) + self.__dict__[name] = value + elif name == "enable": + self.enable = value + else: + if isinstance(value, str) or callable(value): + value = (value,) + if isinstance(value, Iterable): + value = tuple([_qualname(item) for item in value]) + self.__dict__[name] = value + + @contextmanager + def context(self, *args, **kwargs): + """ + Return a context manager which allows temporary modification of + the lenient option state for the active thread. + + On entry to the context manager, all provided keyword arguments are + applied. On exit from the context manager, the previous lenient option + state is restored. + + For example:: + with iris._LENIENT.context(example_lenient_flag=False): + # ... code that expects some non-lenient behaviour + + .. note:: + iris._LENIENT.example_lenient_flag does not exist and is + provided only as an example. + + """ + + def update_client(client, services): + if client in self.__dict__: + existing_services = self.__dict__[client] + else: + existing_services = () + + self.__dict__[client] = tuple(set(existing_services + services)) + + # Save the original state. + original_state = deepcopy(self.__dict__) + + # Temporarily update the state with the kwargs first. + for name, value in kwargs.items(): + self[name] = value + + # Get the active client. + active = self.__dict__["active"] + + if args: + # Update the client with the provided services. + new_services = tuple([_qualname(arg) for arg in args]) + + if active is None: + # Ensure not to use "context" as the ephemeral name + # of the context manager runtime "active" lenient client, + # as this causes a namespace clash with this method + # i.e., _Lenient.context, via _Lenient.__getattr__ + active = "__context" + self.__dict__["active"] = active + self.__dict__[active] = new_services + else: + # Append provided services to any pre-existing services of the active client. + update_client(active, new_services) + else: + # Append previous ephemeral services (for non-specific client) to the active client. + if ( + active is not None + and active != "__context" + and "__context" in self.__dict__ + ): + new_services = self.__dict__["__context"] + update_client(active, new_services) + + try: + yield + finally: + # Restore the original state. + self.__dict__.clear() + self.__dict__.update(original_state) + + @property + def enable(self): + """Return the activation state of the lenient services.""" + return self.__dict__["enable"] + + @enable.setter + def enable(self, state): + """ + Set the activate state of the lenient services. + + Setting the state to `False` disables all lenient services, and + setting the state to `True` enables all lenient services. + + Args: + + * state (bool): + Activate state for lenient services. + + """ + if not isinstance(state, bool): + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option 'enable', expected a {type(True)!r}, got {state!r}." + raise ValueError(emsg) + self.__dict__["enable"] = state + + def register_client(self, func, services, append=False): + """ + Add the provided mapping of lenient client function/method to + required lenient service function/methods. + + Args: + + * func (callable or str): + A client function/method or fully qualified string name of the + client function/method. + + * services (callable or str or iterable of callable/str): + One or more service function/methods or fully qualified string names + of the required service function/method. + + Kwargs: + + * append (bool): + If True, append the lenient services to any pre-registered lenient + services for the provided lenient client. Default is False. + + """ + func = _qualname(func) + cls = self.__class__.__name__ + + if func in _LENIENT_PROTECTED: + emsg = ( + f"Cannot register {cls!r} client. " + f"Please rename your client to be something other than {func!r}." + ) + raise ValueError(emsg) + if isinstance(services, str) or not isinstance(services, Iterable): + services = (services,) + if not len(services): + emsg = f"Require at least one {cls!r} client service." + raise ValueError(emsg) + services = tuple([_qualname(service) for service in services]) + if append: + # The original provided service order is not significant. There is + # no requirement to preserve it, so it's safe to sort. + existing = self.__dict__[func] if func in self else () + services = tuple(sorted(set(existing) | set(services))) + self.__dict__[func] = services + + def register_service(self, func): + """ + Add the provided function/method as providing a lenient service and + activate it. + + Args: + + * func (callable or str): + A service function/method or fully qualified string name of the + service function/method. + + """ + func = _qualname(func) + if func in _LENIENT_PROTECTED: + cls = self.__class__.__name__ + emsg = ( + f"Cannot register {cls!r} service. " + f"Please rename your service to be something other than {func!r}." + ) + raise ValueError(emsg) + self.__dict__[func] = True + + def unregister_client(self, func): + """ + Remove the provided function/method as a lenient client using lenient services. + + Args: + + * func (callable or str): + A function/method of fully qualified string name of the function/method. + + """ + func = _qualname(func) + cls = self.__class__.__name__ + + if func in _LENIENT_PROTECTED: + emsg = f"Cannot unregister {cls!r} client, as {func!r} is a protected {cls!r} option." + raise ValueError(emsg) + + if func in self.__dict__: + value = self.__dict__[func] + if isinstance(value, bool): + emsg = f"Cannot unregister {cls!r} client, as {func!r} is not a valid {cls!r} client." + raise ValueError(emsg) + del self.__dict__[func] + else: + emsg = f"Cannot unregister unknown {cls!r} client {func!r}." + raise ValueError(emsg) + + def unregister_service(self, func): + """ + Remove the provided function/method as providing a lenient service. + + Args: + + * func (callable or str): + A function/method or fully qualified string name of the function/method. + + """ + func = _qualname(func) + cls = self.__class__.__name__ + + if func in _LENIENT_PROTECTED: + emsg = f"Cannot unregister {cls!r} service, as {func!r} is a protected {cls!r} option." + raise ValueError(emsg) + + if func in self.__dict__: + value = self.__dict__[func] + if not isinstance(value, bool): + emsg = f"Cannot unregister {cls!r} service, as {func!r} is not a valid {cls!r} service." + raise ValueError(emsg) + del self.__dict__[func] + else: + emsg = f"Cannot unregister unknown {cls!r} service {func!r}." + raise ValueError(emsg) + + +#: (Private) Instance that manages all Iris run-time lenient client and service options. +_LENIENT = _Lenient() + +#: (Public) Instance that manages all Iris run-time lenient features. +LENIENT = Lenient() diff --git a/lib/iris/common/metadata.py b/lib/iris/common/metadata.py index e51e34e8f8..eba3ffc6e8 100644 --- a/lib/iris/common/metadata.py +++ b/lib/iris/common/metadata.py @@ -8,22 +8,34 @@ from collections import namedtuple from collections.abc import Iterable, Mapping from functools import wraps +import logging import re +from .lenient import _LENIENT +from .lenient import _lenient_service as lenient_service +from .lenient import _qualname as qualname + __all__ = [ + "SERVICES_COMBINE", + "SERVICES_DIFFERENCE", + "SERVICES_EQUAL", + "SERVICES", "AncillaryVariableMetadata", "BaseMetadata", "CellMeasureMetadata", "CoordMetadata", "CubeMetadata", - "MetadataManagerFactory", + "metadata_manager_factory", ] # https://www.unidata.ucar.edu/software/netcdf/docs/netcdf_data_set_components.html#object_name _TOKEN_PARSE = re.compile(r"""^[a-zA-Z0-9][\w\.\+\-@]*$""") +# Configure the logger. +logger = logging.getLogger(__name__) + class _NamedTupleMeta(ABCMeta): """ @@ -33,12 +45,11 @@ class _NamedTupleMeta(ABCMeta): """ def __new__(mcs, name, bases, namespace): - token = "_members" names = [] for base in bases: - if hasattr(base, token): - base_names = getattr(base, token) + if hasattr(base, "_members"): + base_names = getattr(base, "_members") is_abstract = getattr( base_names, "__isabstractmethod__", False ) @@ -49,10 +60,10 @@ def __new__(mcs, name, bases, namespace): base_names = (base_names,) names.extend(base_names) - if token in namespace and not getattr( - namespace[token], "__isabstractmethod__", False + if "_members" in namespace and not getattr( + namespace["_members"], "__isabstractmethod__", False ): - namespace_names = namespace[token] + namespace_names = namespace["_members"] if (not isinstance(namespace_names, Iterable)) or isinstance( namespace_names, str @@ -89,25 +100,433 @@ class BaseMetadata(metaclass=_NamedTupleMeta): __slots__ = () - @classmethod - def token(cls, name): + @lenient_service + def __eq__(self, other): """ - Determine whether the provided name is a valid NetCDF name and thus - safe to represent a single parsable token. + Determine whether the associated metadata members are equivalent. Args: - * name: - The string name to verify + * other (metadata): + A metadata instance of the same type. Returns: - The provided name if valid, otherwise None. + Boolean. """ - if name is not None: - result = _TOKEN_PARSE.match(name) - name = result if result is None else name - return name + result = NotImplemented + if hasattr(other, "__class__") and other.__class__ is self.__class__: + if _LENIENT(self.__eq__) or _LENIENT(self.equal): + # Perform "lenient" equality. + logger.debug( + "lenient", extra=dict(cls=self.__class__.__name__) + ) + result = self._compare_lenient(other) + else: + # Perform "strict" equality. + logger.debug("strict", extra=dict(cls=self.__class__.__name__)) + result = super().__eq__(other) + + return result + + def __lt__(self, other): + # + # Support Python2 behaviour for a "<" operation involving a + # "NoneType" operand. + # + if not isinstance(other, self.__class__): + return NotImplemented + + def _sort_key(item): + keys = [] + for field in item._fields: + value = getattr(item, field) + keys.extend((value is not None, value)) + return tuple(keys) + + return _sort_key(self) < _sort_key(other) + + def __ne__(self, other): + result = self.__eq__(other) + if result is not NotImplemented: + result = not result + + return result + + def _api_common( + self, other, func_service, func_operation, action, lenient=None + ): + """ + Common entry-point for lenient metadata API methods. + + Args: + + * other (metadata): + A metadata instance of the same type. + + * func_service (callable): + The parent service method offering the API entry-point to the service. + + * func_operation (callable): + The parent service method that provides the actual service. + + * action (str): + The verb describing the service operation. + + Kwargs: + + * lenient (boolean): + Enable/disable the lenient service operation. The default is to automatically + detect whether this lenient service operation is enabled. + + Returns: + The result of the service operation to the parent service caller. + + """ + if ( + not hasattr(other, "__class__") + or other.__class__ is not self.__class__ + ): + emsg = "Cannot {} {!r} with {!r}." + raise TypeError( + emsg.format(action, self.__class__.__name__, type(other)) + ) + + if lenient is None: + result = func_operation(other) + else: + if lenient: + # Use qualname to disassociate from the instance bounded method. + args, kwargs = (qualname(func_service),), dict() + else: + # Use qualname to guarantee that the instance bounded method + # is a hashable key. + args, kwargs = (), {qualname(func_service): False} + + with _LENIENT.context(*args, **kwargs): + result = func_operation(other) + + return result + + def _combine(self, other): + """Perform associated metadata member combination.""" + if _LENIENT(self.combine): + # Perform "lenient" combine. + logger.debug("lenient", extra=dict(cls=self.__class__.__name__)) + values = self._combine_lenient(other) + else: + # Perform "strict" combine. + logger.debug("strict", extra=dict(cls=self.__class__.__name__)) + + def func(field): + value = getattr(self, field) + return value if value == getattr(other, field) else None + + # Note that, for strict we use "_fields" not "_members". + values = [func(field) for field in self._fields] + + return values + + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members. + + Args: + + * other (BaseMetadata): + The other metadata participating in the lenient combination. + + Returns: + A list of combined metadata member values. + + """ + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + result = None + if field == "units": + # Perform "strict" combination for "units". + result = left if left == right else None + elif self._is_attributes(field, left, right): + result = self._combine_lenient_attributes(left, right) + else: + if left == right: + result = left + elif left is None: + result = right + elif right is None: + result = left + return result + + # Note that, we use "_members" not "_fields". + return [func(field) for field in BaseMetadata._members] + + @staticmethod + def _combine_lenient_attributes(left, right): + """Leniently combine the dictionary members together.""" + sleft = set(left.items()) + sright = set(right.items()) + # Intersection of common items. + common = sleft & sright + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + # Intersection of common item keys with different values. + keys = set(dsleft.keys()) & set(dsright.keys()) + # Remove (in-place) common item keys with different values. + [dsleft.pop(key) for key in keys] + [dsright.pop(key) for key in keys] + # Now bring the result together. + result = dict(common) + result.update(dsleft) + result.update(dsright) + + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members. + + Args: + + * other (BaseMetadata): + The other metadata participating in the lenient comparison. + + Returns: + Boolean. + + """ + result = False + + # Use the "name" method to leniently compare "standard_name", + # "long_name", and "var_name" in a well defined way. + if self.name() == other.name(): + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if field == "units": + # Perform "strict" compare for "units". + result = left == right + elif self._is_attributes(field, left, right): + result = self._compare_lenient_attributes(left, right) + else: + # Perform "lenient" compare for members. + result = (left == right) or left is None or right is None + return result + + # Note that, we use "_members" not "_fields". + result = all([func(field) for field in BaseMetadata._members]) + + return result + + @staticmethod + def _compare_lenient_attributes(left, right): + """Perform lenient compare between the dictionary members.""" + sleft = set(left.items()) + sright = set(right.items()) + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + # Intersection of common item keys with different values. + keys = set(dsleft.keys()) & set(dsright.keys()) + + return not bool(keys) + + def _difference(self, other): + """Perform associated metadata member difference.""" + if _LENIENT(self.difference): + # Perform "lenient" difference. + logger.debug("lenient", extra=dict(cls=self.__class__.__name__)) + values = self._difference_lenient(other) + else: + # Perform "strict" difference. + logger.debug("strict", extra=dict(cls=self.__class__.__name__)) + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if self._is_attributes(field, left, right): + result = self._difference_strict_attributes(left, right) + else: + result = None if left == right else (left, right) + return result + + # Note that, for strict we use "_fields" not "_members". + values = [func(field) for field in self._fields] + + return values + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members. + + Args: + + * other (BaseMetadata): + The other metadata participating in the lenient difference. + + Returns: + A list of difference metadata member values. + + """ + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if field == "units": + # Perform "strict" difference for "units". + result = None if left == right else (left, right) + elif self._is_attributes(field, left, right): + result = self._difference_lenient_attributes(left, right) + else: + # Perform "lenient" difference for members. + result = ( + (left, right) + if left is not None and right is not None and left != right + else None + ) + return result + + # Note that, we use "_members" not "_fields". + return [func(field) for field in BaseMetadata._members] + + @staticmethod + def _difference_lenient_attributes(left, right): + """Perform lenient difference between the dictionary members.""" + sleft = set(left.items()) + sright = set(right.items()) + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + # Intersection of common item keys with different values. + keys = set(dsleft.keys()) & set(dsright.keys()) + # Keep (in-place) common item keys with different values. + [dsleft.pop(key) for key in list(dsleft.keys()) if key not in keys] + [dsright.pop(key) for key in list(dsright.keys()) if key not in keys] + + if not bool(dsleft) and not bool(dsright): + result = None + else: + result = (dsleft, dsright) + + return result + + @staticmethod + def _difference_strict_attributes(left, right): + """Perform strict difference between the dictionary members.""" + sleft = set(left.items()) + sright = set(right.items()) + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + + if not bool(dsleft) and not bool(dsright): + result = None + else: + result = (dsleft, dsright) + + return result + + @staticmethod + def _is_attributes(field, left, right): + """Determine whether we have two 'attributes' dictionaries.""" + return ( + field == "attributes" + and isinstance(left, Mapping) + and isinstance(right, Mapping) + ) + + @lenient_service + def combine(self, other, lenient=None): + """ + Return a new metadata instance created by combining each of the + associated metadata members. + + Args: + + * other (metadata): + A metadata instance of the same type. + + Kwargs: + + * lenient (boolean): + Enable/disable lenient combination. The default is to automatically + detect whether this lenient operation is enabled. + + Returns: + Metadata instance. + + """ + result = self._api_common( + other, self.combine, self._combine, "combine", lenient=lenient + ) + return self.__class__(*result) + + @lenient_service + def difference(self, other, lenient=None): + """ + Return a new metadata instance created by performing a difference + comparison between each of the associated metadata members. + + A metadata member returned with a value of "None" indicates that there + is no difference between the members being compared. Otherwise, a tuple + of the different values is returned. + + Args: + + * other (metadata): + A metadata instance of the same type. + + Kwargs: + + * lenient (boolean): + Enable/disable lenient difference. The default is to automatically + detect whether this lenient operation is enabled. + + Returns: + Metadata instance of member differences or None. + + """ + result = self._api_common( + other, self.difference, self._difference, "differ", lenient=lenient + ) + result = ( + None + if all([item is None for item in result]) + else self.__class__(*result) + ) + return result + + @lenient_service + def equal(self, other, lenient=None): + """ + Determine whether the associated metadata members are equivalent. + + Args: + + * other (metadata): + A metadata instance of the same type. + + Kwargs: + + * lenient (boolean): + Enable/disable lenient equivalence. The default is to automatically + detect whether this lenient operation is enabled. + + Returns: + Boolean. + + """ + result = self._api_common( + other, self.equal, self.__eq__, "compare", lenient=lenient + ) + return result def name(self, default=None, token=False): """ @@ -151,23 +570,26 @@ def _check(item): return result - def __lt__(self, other): - # - # Support Python2 behaviour for a "<" operation involving a - # "NoneType" operand. Require to at least implement this comparison - # operator to support sorting of instances. - # - if not isinstance(other, self.__class__): - return NotImplemented + @classmethod + def token(cls, name): + """ + Determine whether the provided name is a valid NetCDF name and thus + safe to represent a single parsable token. - def _sort_key(item): - keys = [] - for field in item._fields: - value = getattr(item, field) - keys.extend((value is not None, value)) - return tuple(keys) + Args: - return _sort_key(self) < _sort_key(other) + * name: + The string name to verify + + Returns: + The provided name if valid, otherwise None. + + """ + if name is not None: + result = _TOKEN_PARSE.match(name) + name = result if result is None else name + + return name class AncillaryVariableMetadata(BaseMetadata): @@ -178,6 +600,26 @@ class AncillaryVariableMetadata(BaseMetadata): __slots__ = () + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + class CellMeasureMetadata(BaseMetadata): """ @@ -189,6 +631,96 @@ class CellMeasureMetadata(BaseMetadata): __slots__ = () + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members for cell measures. + + Args: + + * other (CellMeasureMetadata): + The other cell measure metadata participating in the lenient + combination. + + Returns: + A list of combined metadata member values. + + """ + # Perform "strict" combination for "measure". + value = self.measure if self.measure == other.measure else None + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.append(value) + + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members for cell measures. + + Args: + + * other (CellMeasureMetadata): + The other cell measure metadata participating in the lenient + comparison. + + Returns: + Boolean. + + """ + # Perform "strict" comparison for "measure". + result = self.measure == other.measure + if result: + # Perform lenient comparison of the other parent members. + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members for cell measures. + + Args: + + * other (CellMeasureMetadata): + The other cell measure metadata participating in the lenient + difference. + + Returns: + A list of difference metadata member values. + + """ + # Perform "strict" difference for "measure". + value = ( + None + if self.measure == other.measure + else (self.measure, other.measure) + ) + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.append(value) + + return result + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + class CoordMetadata(BaseMetadata): """ @@ -200,6 +732,108 @@ class CoordMetadata(BaseMetadata): __slots__ = () + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members for coordinates. + + Args: + + * other (CoordMetadata): + The other coordinate metadata participating in the lenient + combination. + + Returns: + A list of combined metadata member values. + + """ + # Perform "strict" combination for "coord_system" and "climatological". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return left if left == right else None + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in self._members] + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.extend(values) + + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members for coordinates. + + Args: + + * other (CoordMetadata): + The other coordinate metadata participating in the lenient + comparison. + + Returns: + Boolean. + + """ + result = all( + [ + getattr(self, field) == getattr(other, field) + for field in self._members + ] + ) + if result: + # Perform lenient comparison of the other parent members. + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members for coordinates. + + Args: + + * other (CoordMetadata): + The other coordinate metadata participating in the lenient + difference. + + Returns: + A list of difference metadata member values. + + """ + # Perform "strict" difference for "coord_system" and "climatological". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return None if left == right else (left, right) + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in self._members] + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.extend(values) + + return result + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + class CubeMetadata(BaseMetadata): """ @@ -211,32 +845,78 @@ class CubeMetadata(BaseMetadata): __slots__ = () - @wraps(BaseMetadata.name) - def name(self, default=None, token=False): - def _check(item): - return self.token(item) if token else item + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) - default = self.DEFAULT_NAME if default is None else default + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members for cubes. - # Defensive enforcement of attributes being a dictionary. - if not isinstance(self.attributes, Mapping): - try: - self.attributes = dict() - except AttributeError: - emsg = "Invalid '{}.attributes' member, must be a mapping." - raise AttributeError(emsg.format(self.__class__.__name__)) + Args: - result = ( - _check(self.standard_name) - or _check(self.long_name) - or _check(self.var_name) - or _check(str(self.attributes.get("STASH", ""))) - or _check(default) + * other (CubeMetadata): + The other cube metadata participating in the lenient combination. + + Returns: + A list of combined metadata member values. + + """ + # Perform "strict" combination for "cell_methods". + value = ( + self.cell_methods + if self.cell_methods == other.cell_methods + else None ) + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.append(value) - if token and result is None: - emsg = "Cannot retrieve a valid name token from {!r}" - raise ValueError(emsg.format(self)) + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members for cubes. + + Args: + + * other (CubeMetadata): + The other cube metadata participating in the lenient comparison. + + Returns: + Boolean. + + """ + # Perform "strict" comparison for "cell_methods". + result = self.cell_methods == other.cell_methods + if result: + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members for cubes. + + Args: + + * other (CubeMetadata): + The other cube metadata participating in the lenient difference. + + Returns: + A list of difference metadata member values. + + """ + # Perform "strict" difference for "cell_methods". + value = ( + None + if self.cell_methods == other.cell_methods + else (self.cell_methods, other.cell_methods) + ) + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.append(value) return result @@ -265,10 +945,54 @@ def _names(self): if stash_name is not None: stash_name = str(stash_name) - return (standard_name, long_name, var_name, stash_name) + return standard_name, long_name, var_name, stash_name + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + + @wraps(BaseMetadata.name) + def name(self, default=None, token=False): + def _check(item): + return self.token(item) if token else item + default = self.DEFAULT_NAME if default is None else default -def MetadataManagerFactory(cls, **kwargs): + # Defensive enforcement of attributes being a dictionary. + if not isinstance(self.attributes, Mapping): + try: + self.attributes = dict() + except AttributeError: + emsg = "Invalid '{}.attributes' member, must be a mapping." + raise AttributeError(emsg.format(self.__class__.__name__)) + + result = ( + _check(self.standard_name) + or _check(self.long_name) + or _check(self.var_name) + or _check(str(self.attributes.get("STASH", ""))) + or _check(default) + ) + + if token and result is None: + emsg = "Cannot retrieve a valid name token from {!r}" + raise ValueError(emsg.format(self)) + + return result + + +def metadata_manager_factory(cls, **kwargs): """ A class instance factory function responsible for manufacturing metadata instances dynamically at runtime. @@ -314,6 +1038,7 @@ def __eq__(self, other): match = self.cls is other.cls if match: match = self.values == other.values + return match def __getstate__(self): @@ -324,6 +1049,7 @@ def __ne__(self, other): match = self.__eq__(other) if match is not NotImplemented: match = not match + return match def __reduce__(self): @@ -334,7 +1060,7 @@ def __reduce__(self): instance, and dump and load instance state successfully. """ - return (MetadataManagerFactory, (self.cls,), self.__getstate__()) + return metadata_manager_factory, (self.cls,), self.__getstate__() def __repr__(self): args = ", ".join( @@ -353,6 +1079,7 @@ def __setstate__(self, state): @property def fields(self): """Return the name of the metadata members.""" + # Proxy for built-in namedtuple._fields property. return self.cls._fields @property @@ -401,3 +1128,42 @@ def values(self): metadata = Metadata(cls, **kwargs) return metadata + + +#: Convenience collection of lenient metadata combine services. +SERVICES_COMBINE = ( + AncillaryVariableMetadata.combine, + BaseMetadata.combine, + CellMeasureMetadata.combine, + CoordMetadata.combine, + CubeMetadata.combine, +) + + +#: Convenience collection of lenient metadata difference services. +SERVICES_DIFFERENCE = ( + AncillaryVariableMetadata.difference, + BaseMetadata.difference, + CellMeasureMetadata.difference, + CoordMetadata.difference, + CubeMetadata.difference, +) + + +#: Convenience collection of lenient metadata equality services. +SERVICES_EQUAL = ( + AncillaryVariableMetadata.__eq__, + AncillaryVariableMetadata.equal, + BaseMetadata.__eq__, + BaseMetadata.equal, + CellMeasureMetadata.__eq__, + CellMeasureMetadata.equal, + CoordMetadata.__eq__, + CoordMetadata.equal, + CubeMetadata.__eq__, + CubeMetadata.equal, +) + + +#: Convenience collection of lenient metadata services. +SERVICES = SERVICES_COMBINE + SERVICES_DIFFERENCE + SERVICES_EQUAL diff --git a/lib/iris/common/mixin.py b/lib/iris/common/mixin.py index 5ef9d458e9..50ef561036 100644 --- a/lib/iris/common/mixin.py +++ b/lib/iris/common/mixin.py @@ -216,9 +216,7 @@ def metadata(self, metadata): metadata = metadata._asdict() if isinstance(metadata, Mapping): - missing = [ - field for field in fields if field not in metadata - ] + fields = [field for field in fields if field in metadata] else: # Generic iterable/container with no associated keys. missing = [ @@ -227,12 +225,12 @@ def metadata(self, metadata): if not hasattr(metadata, field) ] - if missing: - missing = ", ".join( - map(lambda i: "{!r}".format(i), missing) - ) - emsg = "Invalid {!r} metadata, require {} to be specified." - raise TypeError(emsg.format(type(arg), missing)) + if missing: + missing = ", ".join( + map(lambda i: "{!r}".format(i), missing) + ) + emsg = "Invalid {!r} metadata, require {} to be specified." + raise TypeError(emsg.format(type(arg), missing)) for field in fields: if hasattr(metadata, field): diff --git a/lib/iris/config.py b/lib/iris/config.py index e1d7dee29d..eeef1873f9 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -32,8 +32,11 @@ import configparser import contextlib +import logging.config import os.path +import pathlib import warnings +import yaml # Returns simple string options @@ -81,6 +84,14 @@ def get_dir_option(section, option, default=None): config = configparser.ConfigParser() config.read([os.path.join(CONFIG_PATH, "site.cfg")]) +# Configure logging. +fname_logging = pathlib.Path(CONFIG_PATH) / "logging.yaml" +if not fname_logging.exists(): + emsg = f"Logging configuration file '{fname_logging!s}' does not exist." + raise FileNotFoundError(emsg) +with open(fname_logging) as fi: + logging.config.dictConfig(yaml.safe_load(fi)) +del fname_logging ################## # Resource options diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 84a5a9bae6..d50234c1b6 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -31,7 +31,7 @@ CFVariableMixin, CellMeasureMetadata, CoordMetadata, - MetadataManagerFactory, + metadata_manager_factory, ) import iris.exceptions import iris.time @@ -99,7 +99,7 @@ def __init__( # Configure the metadata manager. if not hasattr(self, "_metadata_manager"): - self._metadata_manager = MetadataManagerFactory(BaseMetadata) + self._metadata_manager = metadata_manager_factory(BaseMetadata) #: CF standard name of the quantity that the metadata represents. self.standard_name = standard_name @@ -714,7 +714,7 @@ def __init__( """ # Configure the metadata manager. if not hasattr(self, "_metadata_manager"): - self._metadata_manager = MetadataManagerFactory( + self._metadata_manager = metadata_manager_factory( AncillaryVariableMetadata ) @@ -826,7 +826,7 @@ def __init__( """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CellMeasureMetadata) + self._metadata_manager = metadata_manager_factory(CellMeasureMetadata) super().__init__( data=data, @@ -1335,7 +1335,7 @@ def __init__( """ # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CoordMetadata) + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__( values=points, diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 064651c28e..a4b5997ec8 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -42,7 +42,7 @@ CFVariableMixin, CoordMetadata, CubeMetadata, - MetadataManagerFactory, + metadata_manager_factory, ) import iris.coord_systems import iris.coords @@ -837,7 +837,7 @@ def __init__( raise TypeError("Invalid data type: {!r}.".format(data)) # Configure the metadata manager. - self._metadata_manager = MetadataManagerFactory(CubeMetadata) + self._metadata_manager = metadata_manager_factory(CubeMetadata) # Initialise the cube data manager. self._data_manager = DataManager(data) diff --git a/lib/iris/etc/logging.yaml b/lib/iris/etc/logging.yaml new file mode 100644 index 0000000000..c651de95b8 --- /dev/null +++ b/lib/iris/etc/logging.yaml @@ -0,0 +1,29 @@ +version: 1 + +formatters: + basic: + format: "%(asctime)s %(name)s %(levelname)s - %(message)s" + datefmt: "%d-%m-%Y %H:%M:%S" + basic-func: + format: "%(asctime)s %(name)s %(levelname)s - %(message)s [%(cls)s.%(funcName)s]" + datefmt: "%d-%m-%Y %H:%M:%S" + +handlers: + console: + class: logging.StreamHandler + formatter: basic + stream: ext://sys.stdout + console-func: + class: logging.StreamHandler + formatter: basic-func + stream: ext://sys.stdout + +loggers: + iris.common.metadata: + level: INFO + handlers: [console-func] + propagate: no + +root: + level: INFO + handlers: [console] diff --git a/lib/iris/tests/test_cdm.py b/lib/iris/tests/test_cdm.py index ab27ad6040..bbaae1a8de 100644 --- a/lib/iris/tests/test_cdm.py +++ b/lib/iris/tests/test_cdm.py @@ -1022,14 +1022,6 @@ def test_metadata_fail(self): (), ) with self.assertRaises(TypeError): - self.t.metadata = { - "standard_name": "air_pressure", - "long_name": "foo", - "var_name": "bar", - "units": "", - "attributes": {"random": "12"}, - } - with self.assertRaises(TypeError): class Metadata: pass diff --git a/lib/iris/tests/unit/common/lenient/__init__.py b/lib/iris/tests/unit/common/lenient/__init__.py new file mode 100644 index 0000000000..2a99e7a4c2 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/__init__.py @@ -0,0 +1,6 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Unit tests for the :mod:`iris.common.lenient` package.""" diff --git a/lib/iris/tests/unit/common/lenient/test_Lenient.py b/lib/iris/tests/unit/common/lenient/test_Lenient.py new file mode 100644 index 0000000000..8ca98342ca --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test_Lenient.py @@ -0,0 +1,182 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the :class:`iris.common.lenient.Lenient`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from unittest.mock import sentinel + +from iris.common.lenient import Lenient, _LENIENT + + +class Test___init__(tests.IrisTest): + def test_default(self): + lenient = Lenient() + expected = dict(maths=True) + self.assertEqual(expected, lenient.__dict__) + + def test_kwargs(self): + lenient = Lenient(maths=False) + expected = dict(maths=False) + self.assertEqual(expected, lenient.__dict__) + + def test_kwargs_invalid(self): + emsg = "Invalid .* option, got 'merge'." + with self.assertRaisesRegex(KeyError, emsg): + _ = Lenient(merge=True) + + +class Test___contains__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_in(self): + self.assertTrue("maths", self.lenient) + + def test_not_in(self): + self.assertTrue(("concatenate", self.lenient)) + + +class Test___getitem__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_in(self): + self.assertTrue(self.lenient["maths"]) + + def test_not_in(self): + emsg = "Invalid .* option, got 'MATHS'." + with self.assertRaisesRegex(KeyError, emsg): + _ = self.lenient["MATHS"] + + +class Test___repr__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test(self): + expected = "Lenient(maths=True)" + self.assertEqual(expected, repr(self.lenient)) + + +class Test___setitem__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_key_invalid(self): + emsg = "Invalid .* option, got 'MATHS." + with self.assertRaisesRegex(KeyError, emsg): + self.lenient["MATHS"] = False + + def test_maths_value_invalid(self): + value = sentinel.value + emsg = f"Invalid .* option 'maths' value, got {value!r}." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient["maths"] = value + + def test_maths_disable__lenient_enable_true(self): + self.assertTrue(_LENIENT.enable) + self.lenient["maths"] = False + self.assertFalse(self.lenient.__dict__["maths"]) + self.assertFalse(_LENIENT.enable) + + def test_maths_disable__lenient_enable_false(self): + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.lenient["maths"] = False + self.assertFalse(self.lenient.__dict__["maths"]) + self.assertFalse(_LENIENT.enable) + + def test_maths_enable__lenient_enable_true(self): + self.assertTrue(_LENIENT.enable) + self.lenient["maths"] = True + self.assertTrue(self.lenient.__dict__["maths"]) + self.assertTrue(_LENIENT.enable) + + def test_maths_enable__lenient_enable_false(self): + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.lenient["maths"] = True + self.assertTrue(self.lenient.__dict__["maths"]) + self.assertTrue(_LENIENT.enable) + + +class Test_context(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_nop(self): + self.assertTrue(self.lenient["maths"]) + + with self.lenient.context(): + self.assertTrue(self.lenient["maths"]) + + self.assertTrue(self.lenient["maths"]) + + def test_maths_disable__lenient_true(self): + # synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + with self.lenient.context(maths=False): + # still synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) + + # still synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + def test_maths_disable__lenient_false(self): + # not synchronised + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + with self.lenient.context(maths=False): + # now synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) + + # still synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + def test_maths_enable__lenient_true(self): + # not synchronised + self.assertTrue(_LENIENT.enable) + self.lenient.__dict__["maths"] = False + self.assertFalse(self.lenient["maths"]) + + with self.lenient.context(maths=True): + # now synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + # still synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) + + def test_maths_enable__lenient_false(self): + # synchronised + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.lenient.__dict__["maths"] = False + self.assertFalse(self.lenient["maths"]) + + with self.lenient.context(maths=True): + # still synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + # still synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) diff --git a/lib/iris/tests/unit/common/lenient/test__Lenient.py b/lib/iris/tests/unit/common/lenient/test__Lenient.py new file mode 100644 index 0000000000..cdcf3df9a3 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__Lenient.py @@ -0,0 +1,835 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the :class:`iris.common.lenient._Lenient`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections import Iterable + +from iris.common.lenient import ( + _LENIENT_ENABLE_DEFAULT, + _LENIENT_PROTECTED, + _Lenient, + _qualname, +) + + +class Test___init__(tests.IrisTest): + def setUp(self): + self.expected = dict(active=None, enable=_LENIENT_ENABLE_DEFAULT) + + def test_default(self): + lenient = _Lenient() + self.assertEqual(self.expected, lenient.__dict__) + + def test_args_service_str(self): + service = "service1" + lenient = _Lenient(service) + self.expected.update(dict(service1=True)) + self.assertEqual(self.expected, lenient.__dict__) + + def test_args_services_str(self): + services = ("service1", "service2") + lenient = _Lenient(*services) + self.expected.update(dict(service1=True, service2=True)) + self.assertEqual(self.expected, lenient.__dict__) + + def test_args_services_callable(self): + def service1(): + pass + + def service2(): + pass + + services = (service1, service2) + lenient = _Lenient(*services) + self.expected.update( + {_qualname(service1): True, _qualname(service2): True,} + ) + self.assertEqual(self.expected, lenient.__dict__) + + def test_kwargs_client_str(self): + client = dict(client1="service1") + lenient = _Lenient(**client) + self.expected.update(dict(client1=("service1",))) + self.assertEqual(self.expected, lenient.__dict__) + + def test_kwargs_clients_str(self): + clients = dict(client1="service1", client2="service2") + lenient = _Lenient(**clients) + self.expected.update( + dict(client1=("service1",), client2=("service2",)) + ) + self.assertEqual(self.expected, lenient.__dict__) + + def test_kwargs_clients_callable(self): + def client1(): + pass + + def client2(): + pass + + def service1(): + pass + + def service2(): + pass + + qualname_client1 = _qualname(client1) + qualname_client2 = _qualname(client2) + clients = { + qualname_client1: service1, + qualname_client2: (service1, service2), + } + lenient = _Lenient(**clients) + self.expected.update( + { + _qualname(client1): (_qualname(service1),), + _qualname(client2): (_qualname(service1), _qualname(service2)), + } + ) + self.assertEqual(self.expected, lenient.__dict__) + + +class Test___call__(tests.IrisTest): + def setUp(self): + self.client = "myclient" + self.lenient = _Lenient() + + def test_missing_service_str(self): + self.assertFalse(self.lenient("myservice")) + + def test_missing_service_callable(self): + def myservice(): + pass + + self.assertFalse(self.lenient(myservice)) + + def test_disabled_service_str(self): + service = "myservice" + self.lenient.__dict__[service] = False + self.assertFalse(self.lenient(service)) + + def test_disable_service_callable(self): + def myservice(): + pass + + qualname_service = _qualname(myservice) + self.lenient.__dict__[qualname_service] = False + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_no_active_client(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_no_active_client(self): + def myservice(): + pass + + qualname_service = _qualname(myservice) + self.lenient.__dict__[qualname_service] = True + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_no_registered_services(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_active_client_with_no_registered_services( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = _qualname(myclient) + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_unmatched_registered_services( + self, + ): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = ("service1", "service2") + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_active_client_with_unmatched_registered_services( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = ("service1", "service2") + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_registered_services(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = ("service1", "service2", service) + self.assertTrue(self.lenient(service)) + + def test_service_callable_with_active_client_with_registered_services( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = ( + "service1", + "service2", + qualname_service, + ) + self.assertTrue(self.lenient(myservice)) + + def test_service_str_with_active_client_with_unmatched_registered_service_str( + self, + ): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = "serviceXXX" + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_active_client_with_unmatched_registered_service_str( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = f"{qualname_service}XXX" + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_registered_service_str(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = service + self.assertTrue(self.lenient(service)) + + def test_service_callable_with_active_client_with_registered_service_str( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = qualname_service + self.assertTrue(self.lenient(myservice)) + + def test_enable(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = service + self.assertTrue(self.lenient(service)) + self.lenient.__dict__["enable"] = False + self.assertFalse(self.lenient(service)) + + +class Test___contains__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_in(self): + self.assertIn("active", self.lenient) + + def test_not_in(self): + self.assertNotIn("ACTIVATE", self.lenient) + + def test_in_qualname(self): + def func(): + pass + + qualname_func = _qualname(func) + lenient = _Lenient() + lenient.__dict__[qualname_func] = None + self.assertIn(func, lenient) + self.assertIn(qualname_func, lenient) + + +class Test___getattr__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_in(self): + self.assertIsNone(self.lenient.active) + + def test_not_in(self): + emsg = "Invalid .* option, got 'wibble'." + with self.assertRaisesRegex(AttributeError, emsg): + _ = self.lenient.wibble + + +class Test__getitem__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_in(self): + self.assertIsNone(self.lenient["active"]) + + def test_in_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_service] = True + self.assertTrue(self.lenient[service]) + + def test_not_in(self): + emsg = "Invalid .* option, got 'wibble'." + with self.assertRaisesRegex(KeyError, emsg): + _ = self.lenient["wibble"] + + def test_not_in_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + emsg = f"Invalid .* option, got '{qualname_service}'." + with self.assertRaisesRegex(KeyError, emsg): + _ = self.lenient[service] + + +class Test___setitem__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_in(self): + emsg = "Invalid .* option, got 'wibble'." + with self.assertRaisesRegex(KeyError, emsg): + self.lenient["wibble"] = None + + def test_in_value_str(self): + client = "client" + service = "service" + self.lenient.__dict__[client] = None + self.lenient[client] = service + self.assertEqual(self.lenient.__dict__[client], (service,)) + + def test_callable_in_value_str(self): + def client(): + pass + + service = "service" + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = service + self.assertEqual(self.lenient.__dict__[qualname_client], (service,)) + + def test_in_value_callable(self): + def service(): + pass + + client = "client" + qualname_service = _qualname(service) + self.lenient.__dict__[client] = None + self.lenient[client] = service + self.assertEqual(self.lenient.__dict__[client], (qualname_service,)) + + def test_callable_in_value_callable(self): + def client(): + pass + + def service(): + pass + + qualname_client = _qualname(client) + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = service + self.assertEqual( + self.lenient.__dict__[qualname_client], (qualname_service,) + ) + + def test_in_value_bool(self): + client = "client" + self.lenient.__dict__[client] = None + self.lenient[client] = True + self.assertTrue(self.lenient.__dict__[client]) + self.assertFalse(isinstance(self.lenient.__dict__[client], Iterable)) + + def test_callable_in_value_bool(self): + def client(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = True + self.assertTrue(self.lenient.__dict__[qualname_client]) + self.assertFalse( + isinstance(self.lenient.__dict__[qualname_client], Iterable) + ) + + def test_in_value_iterable(self): + client = "client" + services = ("service1", "service2") + self.lenient.__dict__[client] = None + self.lenient[client] = services + self.assertEqual(self.lenient.__dict__[client], services) + + def test_callable_in_value_iterable(self): + def client(): + pass + + qualname_client = _qualname(client) + services = ("service1", "service2") + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = services + self.assertEqual(self.lenient.__dict__[qualname_client], services) + + def test_in_value_iterable_callable(self): + def service1(): + pass + + def service2(): + pass + + client = "client" + self.lenient.__dict__[client] = None + qualname_services = (_qualname(service1), _qualname(service2)) + self.lenient[client] = (service1, service2) + self.assertEqual(self.lenient.__dict__[client], qualname_services) + + def test_callable_in_value_iterable_callable(self): + def client(): + pass + + def service1(): + pass + + def service2(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = None + qualname_services = (_qualname(service1), _qualname(service2)) + self.lenient[client] = (service1, service2) + self.assertEqual( + self.lenient.__dict__[qualname_client], qualname_services + ) + + def test_active_iterable(self): + active = "active" + self.assertIsNone(self.lenient.__dict__[active]) + emsg = "Invalid .* option 'active'" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient[active] = (None,) + + def test_active_str(self): + active = "active" + client = "client1" + self.assertIsNone(self.lenient.__dict__[active]) + self.lenient[active] = client + self.assertEqual(self.lenient.__dict__[active], client) + + def test_active_callable(self): + def client(): + pass + + active = "active" + qualname_client = _qualname(client) + self.assertIsNone(self.lenient.__dict__[active]) + self.lenient[active] = client + self.assertEqual(self.lenient.__dict__[active], qualname_client) + + def test_enable(self): + enable = "enable" + self.assertEqual( + self.lenient.__dict__[enable], _LENIENT_ENABLE_DEFAULT + ) + self.lenient[enable] = True + self.assertTrue(self.lenient.__dict__[enable]) + self.lenient[enable] = False + self.assertFalse(self.lenient.__dict__[enable]) + + def test_enable_invalid(self): + emsg = "Invalid .* option 'enable'" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient["enable"] = None + + +class Test_context(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + self.default = dict(active=None, enable=_LENIENT_ENABLE_DEFAULT) + + def copy(self): + return self.lenient.__dict__.copy() + + def test_nop(self): + pre = self.copy() + with self.lenient.context(): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + self.assertEqual(context, self.default) + self.assertEqual(post, self.default) + + def test_active_str(self): + client = "client" + pre = self.copy() + with self.lenient.context(active=client): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + def test_active_callable(self): + def client(): + pass + + pre = self.copy() + with self.lenient.context(active=client): + context = self.copy() + post = self.copy() + qualname_client = _qualname(client) + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=qualname_client)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + def test_kwargs(self): + client = "client" + self.lenient.__dict__["service1"] = False + self.lenient.__dict__["service2"] = False + pre = self.copy() + with self.lenient.context(active=client, service1=True, service2=True): + context = self.copy() + post = self.copy() + self.default.update(dict(service1=False, service2=False)) + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client, service1=True, service2=True)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + def test_args_str(self): + client = "client" + services = ("service1", "service2") + pre = self.copy() + with self.lenient.context(*services, active=client): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client, client=services)) + self.assertEqual(context["active"], expected["active"]) + self.assertEqual(set(context["client"]), set(expected["client"])) + self.assertEqual(post, self.default) + + def test_args_callable(self): + def service1(): + pass + + def service2(): + pass + + client = "client" + services = (service1, service2) + pre = self.copy() + with self.lenient.context(*services, active=client): + context = self.copy() + post = self.copy() + qualname_services = tuple([_qualname(service) for service in services]) + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client, client=qualname_services)) + self.assertEqual(context["active"], expected["active"]) + self.assertEqual(set(context["client"]), set(expected["client"])) + self.assertEqual(post, self.default) + + def test_context_runtime(self): + services = ("service1", "service2") + pre = self.copy() + with self.lenient.context(*services): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active="__context", __context=services)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + +class Test_enable(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_getter(self): + self.assertEqual(self.lenient.enable, _LENIENT_ENABLE_DEFAULT) + + def test_setter_invalid(self): + emsg = "Invalid .* option 'enable'" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.enable = 0 + + def test_setter(self): + self.assertEqual(self.lenient.enable, _LENIENT_ENABLE_DEFAULT) + self.lenient.enable = False + self.assertFalse(self.lenient.enable) + + +class Test_register_client(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_protected(self): + emsg = "Cannot register .* client" + for protected in _LENIENT_PROTECTED: + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.register_client(protected, "service") + + def test_str_service_str(self): + client = "client" + services = "service" + self.lenient.register_client(client, services) + self.assertIn(client, self.lenient.__dict__) + self.assertEqual(self.lenient.__dict__[client], (services,)) + + def test_str_services_str(self): + client = "client" + services = ("service1", "service2") + self.lenient.register_client(client, services) + self.assertIn(client, self.lenient.__dict__) + self.assertEqual(self.lenient.__dict__[client], services) + + def test_callable_service_callable(self): + def client(): + pass + + def service(): + pass + + qualname_client = _qualname(client) + qualname_service = _qualname(service) + self.lenient.register_client(client, service) + self.assertIn(qualname_client, self.lenient.__dict__) + self.assertEqual( + self.lenient.__dict__[qualname_client], (qualname_service,) + ) + + def test_callable_services_callable(self): + def client(): + pass + + def service1(): + pass + + def service2(): + pass + + qualname_client = _qualname(client) + qualname_services = (_qualname(service1), _qualname(service2)) + self.lenient.register_client(client, (service1, service2)) + self.assertIn(qualname_client, self.lenient.__dict__) + self.assertEqual( + self.lenient.__dict__[qualname_client], qualname_services + ) + + def test_services_empty(self): + emsg = "Require at least one .* client service." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.register_client("client", ()) + + def test_services_overwrite(self): + client = "client" + services = ("service1", "service2") + self.lenient.__dict__[client] = services + self.assertEqual(self.lenient[client], services) + new_services = ("service3", "service4") + self.lenient.register_client(client, services=new_services) + self.assertEqual(self.lenient[client], new_services) + + def test_services_append(self): + client = "client" + services = ("service1", "service2") + self.lenient.__dict__[client] = services + self.assertEqual(self.lenient[client], services) + new_services = ("service3", "service4") + self.lenient.register_client( + client, services=new_services, append=True + ) + expected = set(services + new_services) + self.assertEqual(set(self.lenient[client]), expected) + + +class Test_register_service(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_str(self): + service = "service" + self.assertNotIn(service, self.lenient.__dict__) + self.lenient.register_service(service) + self.assertIn(service, self.lenient.__dict__) + self.assertFalse(isinstance(self.lenient.__dict__[service], Iterable)) + self.assertTrue(self.lenient.__dict__[service]) + + def test_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.assertNotIn(qualname_service, self.lenient.__dict__) + self.lenient.register_service(service) + self.assertIn(qualname_service, self.lenient.__dict__) + self.assertFalse( + isinstance(self.lenient.__dict__[qualname_service], Iterable) + ) + self.assertTrue(self.lenient.__dict__[qualname_service]) + + def test_not_protected(self): + emsg = "Cannot register .* service" + for protected in _LENIENT_PROTECTED: + self.lenient.__dict__[protected] = None + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.register_service("active") + + +class Test_unregister_client(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_protected(self): + emsg = "Cannot unregister .* client, as .* is a protected .* option." + for protected in _LENIENT_PROTECTED: + self.lenient.__dict__[protected] = None + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client(protected) + + def test_not_in(self): + emsg = "Cannot unregister unknown .* client" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client("client") + + def test_not_client(self): + client = "client" + self.lenient.__dict__[client] = True + emsg = "Cannot unregister .* client, as .* is not a valid .* client." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client(client) + + def test_not_client_callable(self): + def client(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = True + emsg = "Cannot unregister .* client, as .* is not a valid .* client." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client(client) + + def test_str(self): + client = "client" + self.lenient.__dict__[client] = (None,) + self.lenient.unregister_client(client) + self.assertNotIn(client, self.lenient.__dict__) + + def test_callable(self): + def client(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = (None,) + self.lenient.unregister_client(client) + self.assertNotIn(qualname_client, self.lenient.__dict__) + + +class Test_unregister_service(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_protected(self): + emsg = "Cannot unregister .* service, as .* is a protected .* option." + for protected in _LENIENT_PROTECTED: + self.lenient.__dict__[protected] = None + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service(protected) + + def test_not_in(self): + emsg = "Cannot unregister unknown .* service" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service("service") + + def test_not_service(self): + service = "service" + self.lenient.__dict__[service] = (None,) + emsg = "Cannot unregister .* service, as .* is not a valid .* service." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service(service) + + def test_not_service_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_service] = (None,) + emsg = "Cannot unregister .* service, as .* is not a valid .* service." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service(service) + + def test_str(self): + service = "service" + self.lenient.__dict__[service] = True + self.lenient.unregister_service(service) + self.assertNotIn(service, self.lenient.__dict__) + + def test_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_service] = True + self.lenient.unregister_service(service) + self.assertNotIn(qualname_service, self.lenient.__dict__) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/lenient/test__lenient_client.py b/lib/iris/tests/unit/common/lenient/test__lenient_client.py new file mode 100644 index 0000000000..29cf5e7f82 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__lenient_client.py @@ -0,0 +1,182 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the :func:`iris.common.lenient._lenient_client`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from inspect import getmodule +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _lenient_client + + +class Test(tests.IrisTest): + def setUp(self): + module_name = getmodule(self).__name__ + self.client = f"{module_name}" + ".Test.{}..myclient" + self.service = f"{module_name}" + ".Test.{}..myservice" + self.active = "active" + self.args_in = sentinel.arg1, sentinel.arg2 + self.kwargs_in = dict(kwarg1=sentinel.kwarg1, kwarg2=sentinel.kwarg2) + + def test_args_too_many(self): + emsg = "Invalid lenient client arguments, expecting 1" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_client(None, None) + + def test_args_not_callable(self): + emsg = "Invalid lenient client argument, expecting a callable" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_client(None) + + def test_args_and_kwargs(self): + def func(): + pass + + emsg = ( + "Invalid lenient client, got both arguments and keyword arguments" + ) + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_client(func, services=func) + + def test_call_naked(self): + @_lenient_client + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_naked") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_naked_alternative(self): + def myclient(): + return _LENIENT.__dict__.copy() + + result = _lenient_client(myclient)() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_naked_alternative") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_naked_client_args_kwargs(self): + @_lenient_client + def myclient(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myclient(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_naked_doc(self): + @_lenient_client + def myclient(): + """myclient doc-string""" + + self.assertEqual(myclient.__doc__, "myclient doc-string") + + def test_call_no_kwargs(self): + @_lenient_client() + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_no_kwargs") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_no_kwargs_alternative(self): + def myclient(): + return _LENIENT.__dict__.copy() + + result = (_lenient_client())(myclient)() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_no_kwargs_alternative") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_kwargs_none(self): + @_lenient_client(services=None) + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_kwargs_none") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_kwargs_single(self): + service = sentinel.service + + @_lenient_client(services=service) + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_kwargs_single") + self.assertEqual(result[self.active], qualname_client) + self.assertIn(qualname_client, result) + self.assertEqual(result[qualname_client], (service,)) + + def test_call_kwargs_single_callable(self): + def myservice(): + pass + + @_lenient_client(services=myservice) + def myclient(): + return _LENIENT.__dict__.copy() + + test_name = "test_call_kwargs_single_callable" + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format(test_name) + self.assertEqual(result[self.active], qualname_client) + self.assertIn(qualname_client, result) + qualname_services = (self.service.format(test_name),) + self.assertEqual(result[qualname_client], qualname_services) + + def test_call_kwargs_iterable(self): + services = (sentinel.service1, sentinel.service2) + + @_lenient_client(services=services) + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_kwargs_iterable") + self.assertEqual(result[self.active], qualname_client) + self.assertIn(qualname_client, result) + self.assertEqual(set(result[qualname_client]), set(services)) + + def test_call_client_args_kwargs(self): + @_lenient_client() + def myclient(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myclient(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_doc(self): + @_lenient_client() + def myclient(): + """myclient doc-string""" + + self.assertEqual(myclient.__doc__, "myclient doc-string") + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/lenient/test__lenient_service.py b/lib/iris/tests/unit/common/lenient/test__lenient_service.py new file mode 100644 index 0000000000..3b019c9de5 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__lenient_service.py @@ -0,0 +1,116 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the :func:`iris.common.lenient._lenient_service`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from inspect import getmodule +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _lenient_service + + +class Test(tests.IrisTest): + def setUp(self): + module_name = getmodule(self).__name__ + self.service = f"{module_name}" + ".Test.{}..myservice" + self.args_in = sentinel.arg1, sentinel.arg2 + self.kwargs_in = dict(kwarg1=sentinel.kwarg1, kwarg2=sentinel.kwarg2) + + def test_args_too_many(self): + emsg = "Invalid lenient service arguments, expecting 1" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_service(None, None) + + def test_args_not_callable(self): + emsg = "Invalid lenient service argument, expecting a callable" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_service(None) + + def test_call_naked(self): + @_lenient_service + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call_naked") + state = _LENIENT.__dict__ + self.assertIn(qualname_service, state) + self.assertTrue(state[qualname_service]) + result = myservice() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_naked_alternative(self): + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call_naked_alternative") + result = _lenient_service(myservice)() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_naked_service_args_kwargs(self): + @_lenient_service + def myservice(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myservice(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_naked_doc(self): + @_lenient_service + def myservice(): + """myservice doc-string""" + + self.assertEqual(myservice.__doc__, "myservice doc-string") + + def test_call(self): + @_lenient_service() + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call") + state = _LENIENT.__dict__ + self.assertIn(qualname_service, state) + self.assertTrue(state[qualname_service]) + result = myservice() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_alternative(self): + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call_alternative") + result = (_lenient_service())(myservice)() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_service_args_kwargs(self): + @_lenient_service() + def myservice(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myservice(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_doc(self): + @_lenient_service() + def myservice(): + """myservice doc-string""" + + self.assertEqual(myservice.__doc__, "myservice doc-string") + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/lenient/test__qualname.py b/lib/iris/tests/unit/common/lenient/test__qualname.py new file mode 100644 index 0000000000..e233b2ac78 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__qualname.py @@ -0,0 +1,66 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the :func:`iris.common.lenient._qualname`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from inspect import getmodule +from unittest.mock import sentinel + +from iris.common.lenient import _qualname + + +class Test(tests.IrisTest): + def setUp(self): + module_name = getmodule(self).__name__ + self.locals = f"{module_name}" + ".Test.{}..{}" + + def test_pass_thru_non_callable(self): + func = sentinel.func + result = _qualname(func) + self.assertEqual(result, func) + + def test_callable_function_local(self): + def myfunc(): + pass + + qualname_func = self.locals.format( + "test_callable_function_local", "myfunc" + ) + result = _qualname(myfunc) + self.assertEqual(result, qualname_func) + + def test_callable_function(self): + import iris + + result = _qualname(iris.load) + self.assertEqual(result, "iris.load") + + def test_callable_method_local(self): + class MyClass: + def mymethod(self): + pass + + qualname_method = self.locals.format( + "test_callable_method_local", "MyClass.mymethod" + ) + result = _qualname(MyClass.mymethod) + self.assertEqual(result, qualname_method) + + def test_callable_method(self): + import iris + + result = _qualname(iris.cube.Cube.add_ancillary_variable) + self.assertEqual(result, "iris.cube.Cube.add_ancillary_variable") + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py b/lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py index 0fa0cd56bf..53e16948fd 100644 --- a/lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py @@ -12,8 +12,11 @@ # importing anything else. import iris.tests as tests +from copy import deepcopy import unittest.mock as mock +from unittest.mock import sentinel +from iris.common.lenient import _LENIENT, _qualname from iris.common.metadata import BaseMetadata, AncillaryVariableMetadata @@ -24,9 +27,10 @@ def setUp(self): self.var_name = mock.sentinel.var_name self.units = mock.sentinel.units self.attributes = mock.sentinel.attributes + self.cls = AncillaryVariableMetadata def test_repr(self): - metadata = AncillaryVariableMetadata( + metadata = self.cls( standard_name=self.standard_name, long_name=self.long_name, var_name=self.var_name, @@ -54,10 +58,405 @@ def test__fields(self): "units", "attributes", ) - self.assertEqual(AncillaryVariableMetadata._fields, expected) + self.assertEqual(self.cls._fields, expected) def test_bases(self): - self.assertTrue(issubclass(AncillaryVariableMetadata, BaseMetadata)) + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.dummy = sentinel.dummy + self.cls = AncillaryVariableMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.dummy = sentinel.dummy + self.cls = AncillaryVariableMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.dummy = sentinel.dummy + self.cls = AncillaryVariableMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = AncillaryVariableMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/common/metadata/test_BaseMetadata.py b/lib/iris/tests/unit/common/metadata/test_BaseMetadata.py index 104a220370..600a26fb16 100644 --- a/lib/iris/tests/unit/common/metadata/test_BaseMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_BaseMetadata.py @@ -12,9 +12,12 @@ # importing anything else. import iris.tests as tests +from collections import OrderedDict import unittest.mock as mock +from unittest.mock import sentinel -from iris.common.metadata import BaseMetadata +from iris.common.lenient import _LENIENT, _qualname +from iris.common.metadata import BaseMetadata, CubeMetadata class Test(tests.IrisTest): @@ -24,9 +27,10 @@ def setUp(self): self.var_name = mock.sentinel.var_name self.units = mock.sentinel.units self.attributes = mock.sentinel.attributes + self.cls = BaseMetadata def test_repr(self): - metadata = BaseMetadata( + metadata = self.cls( standard_name=self.standard_name, long_name=self.long_name, var_name=self.var_name, @@ -54,14 +58,76 @@ def test__fields(self): "units", "attributes", ) - self.assertEqual(BaseMetadata._fields, expected) + self.assertEqual(expected, self.cls._fields) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.kwargs = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.cls = BaseMetadata + self.metadata = self.cls(**self.kwargs) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_cannot_compare_non_class(self): + result = self.metadata.__eq__(None) + self.assertIs(NotImplemented, result) + + def test_cannot_compare_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + result = self.metadata.__eq__(other) + self.assertIs(NotImplemented, result) + + def test_lenient(self): + return_value = sentinel.return_value + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ) as mlenient: + with mock.patch.object( + self.cls, "_compare_lenient", return_value=return_value + ) as mcompare: + result = self.metadata.__eq__(self.metadata) + + self.assertEqual(return_value, result) + self.assertEqual(1, mcompare.call_count) + (arg,), kwargs = mcompare.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + self.assertEqual(1, mlenient.call_count) + (arg,), kwargs = mlenient.call_args + self.assertEqual(_qualname(self.cls.__eq__), _qualname(arg)) + self.assertEqual(dict(), kwargs) + + def test_strict_same(self): + self.assertTrue(self.metadata.__eq__(self.metadata)) + other = self.cls(**self.kwargs) + self.assertTrue(self.metadata.__eq__(other)) + self.assertTrue(other.__eq__(self.metadata)) + + def test_strict_different(self): + self.kwargs["var_name"] = None + other = self.cls(**self.kwargs) + self.assertFalse(self.metadata.__eq__(other)) + self.assertFalse(other.__eq__(self.metadata)) class Test___lt__(tests.IrisTest): def setUp(self): - self.one = BaseMetadata(1, 1, 1, 1, 1) - self.two = BaseMetadata(1, 1, 1, 1, 2) - self.none = BaseMetadata(1, 1, 1, 1, None) + self.cls = BaseMetadata + self.one = self.cls(1, 1, 1, 1, 1) + self.two = self.cls(1, 1, 1, 1, 2) + self.none = self.cls(1, 1, 1, 1, None) def test__ascending_lt(self): result = self.one < self.two @@ -80,54 +146,1218 @@ def test__none_lhs_operand(self): self.assertTrue(result) -class Test_token(tests.IrisTest): - def test_passthru_None(self): - result = BaseMetadata.token(None) - self.assertIsNone(result) +class Test___ne__(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.other = sentinel.other - def test_fail_leading_underscore(self): - result = BaseMetadata.token("_nope") - self.assertIsNone(result) + def test_notimplemented(self): + return_value = NotImplemented + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.__ne__(self.other) - def test_fail_leading_dot(self): - result = BaseMetadata.token(".nope") + self.assertIs(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(self.other, arg) + self.assertEqual(dict(), kwargs) + + def test_negate_true(self): + return_value = True + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.__ne__(self.other) + + self.assertFalse(result) + (arg,), kwargs = mocker.call_args + self.assertEqual(self.other, arg) + self.assertEqual(dict(), kwargs) + + def test_negate_false(self): + return_value = False + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.__ne__(self.other) + + self.assertTrue(result) + (arg,), kwargs = mocker.call_args + self.assertEqual(self.other, arg) + self.assertEqual(dict(), kwargs) + + +class Test__combine(tests.IrisTest): + def setUp(self): + self.kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes=dict(one=sentinel.one, two=sentinel.two), + ) + self.cls = BaseMetadata + self.metadata = self.cls(**self.kwargs) + + def test_lenient(self): + return_value = sentinel._combine_lenient + other = sentinel.other + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ) as mlenient: + with mock.patch.object( + self.cls, "_combine_lenient", return_value=return_value + ) as mcombine: + result = self.metadata._combine(other) + + self.assertEqual(1, mlenient.call_count) + (arg,), kwargs = mlenient.call_args + self.assertEqual(self.metadata.combine, arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(return_value, result) + self.assertEqual(1, mcombine.call_count) + (arg,), kwargs = mcombine.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_strict(self): + dummy = sentinel.dummy + values = self.kwargs.copy() + values["standard_name"] = dummy + values["var_name"] = dummy + values["attributes"] = dummy + other = self.cls(**values) + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + result = self.metadata._combine(other) + + expected = [ + None if values[field] == dummy else values[field] + for field in self.cls._fields + ] + self.assertEqual(expected, result) + + +class Test__combine_lenient(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.none = self.cls(*(None,) * len(self.cls._fields))._asdict() + self.names = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + ) + + def test_strict_units(self): + left = self.none.copy() + left["units"] = "K" + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(left.values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_strict_units_different(self): + left = self.none.copy() + right = self.none.copy() + left["units"] = "K" + right["units"] = "km" + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = list(self.none.values()) + self.assertEqual(expected, result) + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_strict_units_different_none(self): + left = self.none.copy() + right = self.none.copy() + left["units"] = "K" + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = list(self.none.values()) + self.assertEqual(expected, result) + + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_attributes(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = dict(item=sentinel.right) + left["attributes"] = ldict + right["attributes"] = rdict + rmetadata = self.cls(**right) + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "_combine_lenient_attributes", return_value=return_value, + ) as mocker: + lmetadata = self.cls(**left) + result = lmetadata._combine_lenient(rmetadata) + + expected = self.none.copy() + expected["attributes"] = return_value + expected = list(expected.values()) + self.assertEqual(expected, result) + + self.assertEqual(1, mocker.call_count) + args, kwargs = mocker.call_args + expected = (ldict, rdict) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + def test_attributes_non_mapping_different(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = sentinel.right + left["attributes"] = ldict + right["attributes"] = rdict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(self.none.copy().values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_attributes_non_mapping_different_none(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + left["attributes"] = ldict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = self.none.copy() + expected["attributes"] = ldict + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_names(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(left.values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_names_different(self): + dummy = sentinel.dummy + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + right["standard_name"] = dummy + right["long_name"] = dummy + right["var_name"] = dummy + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(self.none.copy().values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_names_different_none(self): + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = list(left.values()) + self.assertEqual(expected, result) + + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + +class Test__combine_lenient_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=sentinel.three, + four=sentinel.four, + five=sentinel.five, + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + result = self.metadata._combine_lenient_attributes(left, right) + expected = dict(**left) + self.assertEqual(expected, result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertEqual(expected, result) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["two"] = left["four"] = self.dummy + + result = self.metadata._combine_lenient_attributes(left, right) + expected = self.values.copy() + for key in ["two", "four"]: + del expected[key] + self.assertEqual(dict(expected), result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertEqual(dict(expected), result) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + result = self.metadata._combine_lenient_attributes(left, right) + expected = self.values.copy() + for key in ["one", "three", "five"]: + del expected[key] + self.assertEqual(dict(expected), result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertEqual(dict(expected), result) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + + result = self.metadata._combine_lenient_attributes(left, right) + expected = self.values.copy() + expected["extra_left"] = left["extra_left"] + expected["extra_right"] = right["extra_right"] + self.assertEqual(dict(expected), result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertEqual(dict(expected), result) + + +class Test__compare_lenient(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.none = self.cls(*(None,) * len(self.cls._fields))._asdict() + self.names = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + ) + + def test_name_same(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + expected = (len(self.cls._fields) - 1) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_name_same_lenient_false(self): + left = self.none.copy() + left.update(self.names) + right = self.none.copy() + right["long_name"] = sentinel.standard_name + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + expected = (len(self.cls._fields) - 1) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_name_different(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + right["standard_name"] = None + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object(self.cls, "_is_attributes") as mocker: + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + self.assertEqual(0, mocker.call_count) + + def test_strict_units(self): + left = self.none.copy() + left.update(self.names) + left["units"] = "K" + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + expected = (len(self.cls._fields) - 1) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_strict_units_different(self): + left = self.none.copy() + left.update(self.names) + left["units"] = "K" + right = left.copy() + right["units"] = "m" + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + expected = (len(self.cls._fields) - 1) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_attributes(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + ldict = dict(item=sentinel.left) + rdict = dict(item=sentinel.right) + left["attributes"] = ldict + right["attributes"] = rdict + rmetadata = self.cls(**right) + with mock.patch.object( + self.cls, "_compare_lenient_attributes", return_value=True, + ) as mocker: + lmetadata = self.cls(**left) + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + self.assertEqual(2, mocker.call_count) + expected = [((ldict, rdict),), ((rdict, ldict),)] + self.assertEqual(expected, mocker.call_args_list) + + def test_attributes_non_mapping_different(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + ldict = dict(item=sentinel.left) + rdict = sentinel.right + left["attributes"] = ldict + right["attributes"] = rdict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + def test_attributes_non_mapping_different_none(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + ldict = dict(item=sentinel.left) + left["attributes"] = ldict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + def test_names(self): + left = self.none.copy() + left.update(self.names) + left["long_name"] = None + right = self.none.copy() + right["long_name"] = left["standard_name"] + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._combine_lenient(lmetadata)) + + +class Test__compare_lenient_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=sentinel.three, + four=sentinel.four, + five=sentinel.five, + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + self.assertTrue(self.metadata._compare_lenient_attributes(left, right)) + self.assertTrue(self.metadata._compare_lenient_attributes(right, left)) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["two"] = left["four"] = self.dummy + + self.assertFalse( + self.metadata._compare_lenient_attributes(left, right) + ) + self.assertFalse( + self.metadata._compare_lenient_attributes(right, left) + ) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + self.assertFalse( + self.metadata._compare_lenient_attributes(left, right) + ) + self.assertFalse( + self.metadata._compare_lenient_attributes(right, left) + ) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + + self.assertTrue(self.metadata._compare_lenient_attributes(left, right)) + self.assertTrue(self.metadata._compare_lenient_attributes(right, left)) + + +class Test__difference(tests.IrisTest): + def setUp(self): + self.kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes=dict(one=sentinel.one, two=sentinel.two), + ) + self.cls = BaseMetadata + self.metadata = self.cls(**self.kwargs) + + def test_lenient(self): + return_value = sentinel._difference_lenient + other = sentinel.other + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ) as mlenient: + with mock.patch.object( + self.cls, "_difference_lenient", return_value=return_value + ) as mdifference: + result = self.metadata._difference(other) + + self.assertEqual(1, mlenient.call_count) + (arg,), kwargs = mlenient.call_args + self.assertEqual(self.metadata.difference, arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(return_value, result) + self.assertEqual(1, mdifference.call_count) + (arg,), kwargs = mdifference.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_strict(self): + dummy = sentinel.dummy + values = self.kwargs.copy() + values["long_name"] = dummy + values["units"] = dummy + other = self.cls(**values) + method = "_difference_strict_attributes" + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + with mock.patch.object( + self.cls, method, return_value=None + ) as mdifference: + result = self.metadata._difference(other) + + expected = [ + (self.kwargs[field], dummy) if values[field] == dummy else None + for field in self.cls._fields + ] + self.assertEqual(expected, result) + self.assertEqual(1, mdifference.call_count) + args, kwargs = mdifference.call_args + expected = (self.kwargs["attributes"], values["attributes"]) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + with mock.patch.object( + self.cls, method, return_value=None + ) as mdifference: + result = other._difference(self.metadata) + + expected = [ + (dummy, self.kwargs[field]) if values[field] == dummy else None + for field in self.cls._fields + ] + self.assertEqual(expected, result) + self.assertEqual(1, mdifference.call_count) + args, kwargs = mdifference.call_args + expected = (self.kwargs["attributes"], values["attributes"]) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + +class Test__difference_lenient(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.none = self.cls(*(None,) * len(self.cls._fields))._asdict() + self.names = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + ) + + def test_strict_units(self): + left = self.none.copy() + left["units"] = "km" + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + expected = list(self.none.values()) + self.assertEqual(expected, lmetadata._difference_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._difference_lenient(lmetadata)) + + def test_strict_units_different(self): + left = self.none.copy() + right = self.none.copy() + lunits, runits = "m", "km" + left["units"] = lunits + right["units"] = runits + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["units"] = (lunits, runits) + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["units"] = (runits, lunits) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_strict_units_different_none(self): + left = self.none.copy() + right = self.none.copy() + lunits, runits = "m", None + left["units"] = lunits + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["units"] = (lunits, runits) + expected = list(expected.values()) + + self.assertEqual(expected, result) + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["units"] = (runits, lunits) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_attributes(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = dict(item=sentinel.right) + left["attributes"] = ldict + right["attributes"] = rdict + rmetadata = self.cls(**right) + return_value = sentinel.return_value + with mock.patch.object( + self.cls, + "_difference_lenient_attributes", + return_value=return_value, + ) as mocker: + lmetadata = self.cls(**left) + result = lmetadata._difference_lenient(rmetadata) + + expected = self.none.copy() + expected["attributes"] = return_value + expected = list(expected.values()) + self.assertEqual(expected, result) + + self.assertEqual(1, mocker.call_count) + args, kwargs = mocker.call_args + expected = (ldict, rdict) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + def test_attributes_non_mapping_different(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = sentinel.right + left["attributes"] = ldict + right["attributes"] = rdict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["attributes"] = (ldict, rdict) + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["attributes"] = (rdict, ldict) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_attributes_non_mapping_different_none(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + left["attributes"] = ldict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = list(self.none.copy().values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_names(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(self.none.values()) + self.assertEqual(expected, lmetadata._difference_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._difference_lenient(lmetadata)) + + def test_names_different(self): + dummy = sentinel.dummy + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + right["standard_name"] = dummy + right["long_name"] = dummy + right["var_name"] = dummy + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["standard_name"] = ( + left["standard_name"], + right["standard_name"], + ) + expected["long_name"] = (left["long_name"], right["long_name"]) + expected["var_name"] = (left["var_name"], right["var_name"]) + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["standard_name"] = ( + right["standard_name"], + left["standard_name"], + ) + expected["long_name"] = (right["long_name"], left["long_name"]) + expected["var_name"] = (right["var_name"], left["var_name"]) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_names_different_none(self): + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = list(self.none.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + self.assertEqual(expected, result) + + +class Test__difference_lenient_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=sentinel.three, + four=sentinel.four, + five=sentinel.five, + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + result = self.metadata._difference_lenient_attributes(left, right) self.assertIsNone(result) - def test_fail_leading_plus(self): - result = BaseMetadata.token("+nope") + result = self.metadata._difference_lenient_attributes(right, left) self.assertIsNone(result) - def test_fail_leading_at(self): - result = BaseMetadata.token("@nope") + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["two"] = left["four"] = self.dummy + + result = self.metadata._difference_lenient_attributes(left, right) + for key in ["one", "three", "five"]: + del left[key] + del right[key] + expected = (dict(left), dict(right)) + self.assertEqual(expected, result) + + result = self.metadata._difference_lenient_attributes(right, left) + expected = (dict(right), dict(left)) + self.assertEqual(expected, result) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + result = self.metadata._difference_lenient_attributes(left, right) + for key in ["two", "four"]: + del left[key] + del right[key] + expected = (dict(left), dict(right)) + self.assertEqual(expected, result) + + result = self.metadata._difference_lenient_attributes(right, left) + expected = (dict(right), dict(left)) + self.assertEqual(expected, result) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + result = self.metadata._difference_lenient_attributes(left, right) + expected = self.values.copy() + expected["extra_left"] = left["extra_left"] + expected["extra_right"] = right["extra_right"] self.assertIsNone(result) - def test_fail_space(self): - result = BaseMetadata.token("nope nope") + result = self.metadata._difference_lenient_attributes(right, left) self.assertIsNone(result) - def test_fail_colon(self): - result = BaseMetadata.token("nope:") + +class Test__difference_strict_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=sentinel.three, + four=sentinel.four, + five=sentinel.five, + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + result = self.metadata._difference_strict_attributes(left, right) + self.assertIsNone(result) + result = self.metadata._difference_strict_attributes(right, left) self.assertIsNone(result) - def test_pass_simple(self): - token = "simple" - result = BaseMetadata.token(token) - self.assertEqual(result, token) + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = self.dummy - def test_pass_leading_digit(self): - token = "123simple" - result = BaseMetadata.token(token) - self.assertEqual(result, token) + result = self.metadata._difference_strict_attributes(left, right) + expected_left = left.copy() + expected_right = right.copy() + for key in ["two", "four"]: + del expected_left[key] + del expected_right[key] + expected = (expected_left, expected_right) + self.assertEqual(expected, result) - def test_pass_mixture(self): - token = "S.imple@one+two_3" - result = BaseMetadata.token(token) - self.assertEqual(result, token) + result = self.metadata._difference_strict_attributes(right, left) + expected_left = left.copy() + expected_right = right.copy() + for key in ["two", "four"]: + del expected_left[key] + del expected_right[key] + expected = (expected_right, expected_left) + self.assertEqual(expected, result) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + result = self.metadata._difference_strict_attributes(left, right) + expected_left = left.copy() + expected_right = right.copy() + for key in ["two", "four"]: + del expected_left[key] + del expected_right[key] + expected = (expected_left, expected_right) + self.assertEqual(expected, result) + + result = self.metadata._difference_strict_attributes(right, left) + expected_left = left.copy() + expected_right = right.copy() + for key in ["two", "four"]: + del expected_left[key] + del expected_right[key] + expected = (expected_right, expected_left) + self.assertEqual(expected, result) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + + result = self.metadata._difference_strict_attributes(left, right) + expected_left = dict(extra_left=left["extra_left"]) + expected_right = dict(extra_right=right["extra_right"]) + expected = (expected_left, expected_right) + self.assertEqual(expected, result) + + result = self.metadata._difference_strict_attributes(right, left) + expected_left = dict(extra_left=left["extra_left"]) + expected_right = dict(extra_right=right["extra_right"]) + expected = (expected_right, expected_left) + self.assertEqual(expected, result) + + +class Test__is_attributes(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.field = "attributes" + + def test_field(self): + self.assertTrue(self.metadata._is_attributes(self.field, {}, {})) + + def test_field_not_attributes(self): + self.assertFalse(self.metadata._is_attributes(None, {}, {})) + + def test_left_not_mapping(self): + self.assertFalse(self.metadata._is_attributes(self.field, None, {})) + + def test_right_not_mapping(self): + self.assertFalse(self.metadata._is_attributes(self.field, {}, None)) + + +class Test_combine(tests.IrisTest): + def setUp(self): + kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes="attributes", + ) + self.cls = BaseMetadata + self.metadata = self.cls(**kwargs) + self.mock_kwargs = OrderedDict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_cannot_combine_non_class(self): + emsg = "Cannot combine" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.combine(None) + + def test_cannot_combine_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + emsg = "Cannot combine" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.combine(other) + + def test_lenient_default(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_combine", return_value=return_value + ) as mocker: + result = self.metadata.combine(self.metadata) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_true(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_combine", return_value=return_value + ) as mcombine: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.combine(self.metadata, lenient=True) + + self.assertEqual(1, mcontext.call_count) + (arg,), kwargs = mcontext.call_args + self.assertEqual(_qualname(self.cls.combine), arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(result._asdict(), self.mock_kwargs) + self.assertEqual(1, mcombine.call_count) + (arg,), kwargs = mcombine.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_false(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_combine", return_value=return_value + ) as mcombine: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.combine(self.metadata, lenient=False) + + self.assertEqual(1, mcontext.call_count) + args, kwargs = mcontext.call_args + self.assertEqual((), args) + self.assertEqual({_qualname(self.cls.combine): False}, kwargs) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mcombine.call_count) + (arg,), kwargs = mcombine.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + +class Test_difference(tests.IrisTest): + def setUp(self): + kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes="attributes", + ) + self.cls = BaseMetadata + self.metadata = self.cls(**kwargs) + self.mock_kwargs = OrderedDict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_cannot_differ_non_class(self): + emsg = "Cannot differ" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.difference(None) + + def test_cannot_differ_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + emsg = "Cannot differ" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.difference(other) + + def test_lenient_default(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_difference", return_value=return_value + ) as mocker: + result = self.metadata.difference(self.metadata) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_true(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_difference", return_value=return_value + ) as mdifference: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.difference(self.metadata, lenient=True) + + self.assertEqual(1, mcontext.call_count) + (arg,), kwargs = mcontext.call_args + self.assertEqual(_qualname(self.cls.difference), arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mdifference.call_count) + (arg,), kwargs = mdifference.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_false(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_difference", return_value=return_value + ) as mdifference: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.difference(self.metadata, lenient=False) + + self.assertEqual(mcontext.call_count, 1) + args, kwargs = mcontext.call_args + self.assertEqual((), args) + self.assertEqual({_qualname(self.cls.difference): False}, kwargs) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mdifference.call_count) + (arg,), kwargs = mdifference.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + +class Test_equal(tests.IrisTest): + def setUp(self): + kwargs = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.cls = BaseMetadata + self.metadata = self.cls(**kwargs) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue((_LENIENT[self.cls.equal])) + + def test_cannot_compare_non_class(self): + emsg = "Cannot compare" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.equal(None) + + def test_cannot_compare_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + emsg = "Cannot compare" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.equal(other) + + def test_lenient_default(self): + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.equal(self.metadata) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_true(self): + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as m__eq__: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.equal(self.metadata, lenient=True) + + self.assertEqual(return_value, result) + self.assertEqual(1, mcontext.call_count) + (arg,), kwargs = mcontext.call_args + self.assertEqual(_qualname(self.cls.equal), arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(1, m__eq__.call_count) + (arg,), kwargs = m__eq__.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_false(self): + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as m__eq__: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.equal(self.metadata, lenient=False) + + self.assertEqual(1, mcontext.call_count) + args, kwargs = mcontext.call_args + self.assertEqual((), args) + self.assertEqual({_qualname(self.cls.equal): False}, kwargs) + + self.assertEqual(return_value, result) + self.assertEqual(1, m__eq__.call_count) + (arg,), kwargs = m__eq__.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) class Test_name(tests.IrisTest): def setUp(self): - self.default = BaseMetadata.DEFAULT_NAME + self.cls = BaseMetadata + self.default = self.cls.DEFAULT_NAME @staticmethod def _make(standard_name=None, long_name=None, var_name=None): @@ -142,67 +1372,124 @@ def _make(standard_name=None, long_name=None, var_name=None): def test_standard_name(self): token = "standard_name" metadata = self._make(standard_name=token) + result = metadata.name() - self.assertEqual(result, token) + self.assertEqual(token, result) result = metadata.name(token=True) - self.assertEqual(result, token) + self.assertEqual(token, result) def test_standard_name__invalid_token(self): token = "nope nope" metadata = self._make(standard_name=token) + result = metadata.name() - self.assertEqual(result, token) + self.assertEqual(token, result) result = metadata.name(token=True) - self.assertEqual(result, self.default) + self.assertEqual(self.default, result) def test_long_name(self): token = "long_name" metadata = self._make(long_name=token) + result = metadata.name() - self.assertEqual(result, token) + self.assertEqual(token, result) result = metadata.name(token=True) - self.assertEqual(result, token) + self.assertEqual(token, result) def test_long_name__invalid_token(self): token = "nope nope" metadata = self._make(long_name=token) + result = metadata.name() - self.assertEqual(result, token) + self.assertEqual(token, result) result = metadata.name(token=True) - self.assertEqual(result, self.default) + self.assertEqual(self.default, result) def test_var_name(self): token = "var_name" metadata = self._make(var_name=token) + result = metadata.name() - self.assertEqual(result, token) + self.assertEqual(token, result) result = metadata.name(token=True) - self.assertEqual(result, token) + self.assertEqual(token, result) def test_var_name__invalid_token(self): token = "nope nope" metadata = self._make(var_name=token) + result = metadata.name() - self.assertEqual(result, token) + self.assertEqual(token, result) result = metadata.name(token=True) - self.assertEqual(result, self.default) + self.assertEqual(self.default, result) def test_default(self): metadata = self._make() + result = metadata.name() - self.assertEqual(result, self.default) + self.assertEqual(self.default, result) result = metadata.name(token=True) - self.assertEqual(result, self.default) + self.assertEqual(self.default, result) def test_default__invalid_token(self): token = "nope nope" metadata = self._make() + result = metadata.name(default=token) - self.assertEqual(result, token) + self.assertEqual(token, result) + emsg = "Cannot retrieve a valid name token" with self.assertRaisesRegex(ValueError, emsg): metadata.name(default=token, token=True) +class Test_token(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + + def test_passthru_None(self): + result = self.cls.token(None) + self.assertIsNone(result) + + def test_fail_leading_underscore(self): + result = self.cls.token("_nope") + self.assertIsNone(result) + + def test_fail_leading_dot(self): + result = self.cls.token(".nope") + self.assertIsNone(result) + + def test_fail_leading_plus(self): + result = self.cls.token("+nope") + self.assertIsNone(result) + + def test_fail_leading_at(self): + result = self.cls.token("@nope") + self.assertIsNone(result) + + def test_fail_space(self): + result = self.cls.token("nope nope") + self.assertIsNone(result) + + def test_fail_colon(self): + result = self.cls.token("nope:") + self.assertIsNone(result) + + def test_pass_simple(self): + token = "simple" + result = self.cls.token(token) + self.assertEqual(token, result) + + def test_pass_leading_digit(self): + token = "123simple" + result = self.cls.token(token) + self.assertEqual(token, result) + + def test_pass_mixture(self): + token = "S.imple@one+two_3" + result = self.cls.token(token) + self.assertEqual(token, result) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py b/lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py index 6a2ffbd70c..e5ed287fea 100644 --- a/lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py @@ -12,8 +12,11 @@ # importing anything else. import iris.tests as tests +from copy import deepcopy import unittest.mock as mock +from unittest.mock import sentinel +from iris.common.lenient import _LENIENT, _qualname from iris.common.metadata import BaseMetadata, CellMeasureMetadata @@ -25,9 +28,10 @@ def setUp(self): self.units = mock.sentinel.units self.attributes = mock.sentinel.attributes self.measure = mock.sentinel.measure + self.cls = CellMeasureMetadata def test_repr(self): - metadata = CellMeasureMetadata( + metadata = self.cls( standard_name=self.standard_name, long_name=self.long_name, var_name=self.var_name, @@ -58,10 +62,570 @@ def test__fields(self): "attributes", "measure", ) - self.assertEqual(CellMeasureMetadata._fields, expected) + self.assertEqual(self.cls._fields, expected) def test_bases(self): - self.assertTrue(issubclass(CellMeasureMetadata, BaseMetadata)) + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + measure=sentinel.measure, + ) + self.dummy = sentinel.dummy + self.cls = CellMeasureMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + measure=sentinel.measure, + ) + self.dummy = sentinel.dummy + self.cls = CellMeasureMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + expected = right.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertTrue(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["measure"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["measure"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["measure"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + measure=sentinel.measure, + ) + self.dummy = sentinel.dummy + self.cls = CellMeasureMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (sentinel.measure, None) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = (None, sentinel.measure) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different_measure(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (left["measure"], right["measure"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = lexpected["measure"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_measure(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (left["measure"], right["measure"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = lexpected["measure"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_measure_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (left["measure"], right["measure"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = lexpected["measure"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = CellMeasureMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/common/metadata/test_CoordMetadata.py b/lib/iris/tests/unit/common/metadata/test_CoordMetadata.py index 07f350bed7..ee79201541 100644 --- a/lib/iris/tests/unit/common/metadata/test_CoordMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_CoordMetadata.py @@ -12,8 +12,11 @@ # importing anything else. import iris.tests as tests +from copy import deepcopy import unittest.mock as mock +from unittest.mock import sentinel +from iris.common.lenient import _LENIENT, _qualname from iris.common.metadata import BaseMetadata, CoordMetadata @@ -26,9 +29,10 @@ def setUp(self): self.attributes = mock.sentinel.attributes self.coord_system = mock.sentinel.coord_system self.climatological = mock.sentinel.climatological + self.cls = CoordMetadata def test_repr(self): - metadata = CoordMetadata( + metadata = self.cls( standard_name=self.standard_name, long_name=self.long_name, var_name=self.var_name, @@ -63,10 +67,626 @@ def test__fields(self): "coord_system", "climatological", ) - self.assertEqual(CoordMetadata._fields, expected) + self.assertEqual(self.cls._fields, expected) def test_bases(self): - self.assertTrue(issubclass(CoordMetadata, BaseMetadata)) + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + coord_system=sentinel.coord_system, + climatological=sentinel.climatological, + ) + self.dummy = sentinel.dummy + self.cls = CoordMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + coord_system=sentinel.coord_system, + climatological=sentinel.climatological, + ) + self.dummy = sentinel.dummy + self.cls = CoordMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + expected = right.copy() + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertTrue( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertTrue( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected[member] = None + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertEqual( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertEqual( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected[member] = None + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertEqual( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected[member] = None + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertEqual( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + coord_system=sentinel.coord_system, + climatological=sentinel.climatological, + ) + self.dummy = sentinel.dummy + self.cls = CoordMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + member_value = getattr(lmetadata, member) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (member_value, None) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = (None, member_value) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different_members(self): + for member in self.cls._members: + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (left[member], right[member]) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = lexpected[member][::-1] + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_members(self): + for member in self.cls._members: + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (left[member], right[member]) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = lexpected[member][::-1] + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_members_none(self): + for member in self.cls._members: + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (left[member], right[member]) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = lexpected[member][::-1] + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = CoordMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/common/metadata/test_CubeMetadata.py b/lib/iris/tests/unit/common/metadata/test_CubeMetadata.py index 5d3f39f570..b4f185d7bd 100644 --- a/lib/iris/tests/unit/common/metadata/test_CubeMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_CubeMetadata.py @@ -12,8 +12,11 @@ # importing anything else. import iris.tests as tests +from copy import deepcopy import unittest.mock as mock +from unittest.mock import sentinel +from iris.common.lenient import _LENIENT, _qualname from iris.common.metadata import BaseMetadata, CubeMetadata @@ -48,9 +51,10 @@ def setUp(self): self.units = mock.sentinel.units self.attributes = mock.sentinel.attributes self.cell_methods = mock.sentinel.cell_methods + self.cls = CubeMetadata def test_repr(self): - metadata = CubeMetadata( + metadata = self.cls( standard_name=self.standard_name, long_name=self.long_name, var_name=self.var_name, @@ -81,10 +85,580 @@ def test__fields(self): "attributes", "cell_methods", ) - self.assertEqual(CubeMetadata._fields, expected) + self.assertEqual(self.cls._fields, expected) def test_bases(self): - self.assertTrue(issubclass(CubeMetadata, BaseMetadata)) + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + # Must be a mapping. + attributes=dict(), + cell_methods=sentinel.cell_methods, + ) + self.dummy = sentinel.dummy + self.cls = CubeMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + cell_methods=sentinel.cell_methods, + ) + self.dummy = sentinel.dummy + self.cls = CubeMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + expected = right.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertTrue(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["cell_methods"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["cell_methods"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["cell_methods"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + cell_methods=sentinel.cell_methods, + ) + self.dummy = sentinel.dummy + self.cls = CubeMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = (sentinel.cell_methods, None) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = (None, sentinel.cell_methods) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different_cell_methods(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = ( + left["cell_methods"], + right["cell_methods"], + ) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = lexpected["cell_methods"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_cell_methods(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = ( + left["cell_methods"], + right["cell_methods"], + ) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = lexpected["cell_methods"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_measure_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = ( + left["cell_methods"], + right["cell_methods"], + ) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = lexpected["cell_methods"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = CubeMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) class Test_name(tests.IrisTest): diff --git a/lib/iris/tests/unit/common/metadata/test_MetadataManagerFactory.py b/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py similarity index 85% rename from lib/iris/tests/unit/common/metadata/test_MetadataManagerFactory.py rename to lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py index bfc777cb0c..6678aca446 100644 --- a/lib/iris/tests/unit/common/metadata/test_MetadataManagerFactory.py +++ b/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py @@ -4,7 +4,7 @@ # See COPYING and COPYING.LESSER in the root of the repository for full # licensing details. """ -Unit tests for the :class:`iris.common.metadata.MetadataManagerFactory`. +Unit tests for the :func:`iris.common.metadata.metadata_manager_factory`. """ @@ -23,7 +23,7 @@ CellMeasureMetadata, CoordMetadata, CubeMetadata, - MetadataManagerFactory, + metadata_manager_factory, ) @@ -43,12 +43,12 @@ class Other: emsg = "Require a subclass of 'BaseMetadata'" with self.assertRaisesRegex(TypeError, emsg): - _ = MetadataManagerFactory(Other) + _ = metadata_manager_factory(Other) def test__kwargs_invalid(self): emsg = "Invalid 'BaseMetadata' field parameters, got 'wibble'." with self.assertRaisesRegex(ValueError, emsg): - MetadataManagerFactory(BaseMetadata, wibble="nope") + metadata_manager_factory(BaseMetadata, wibble="nope") class Test_instance(tests.IrisTest): @@ -71,7 +71,7 @@ def test__namespace(self): "values", ] for base in self.bases: - metadata = MetadataManagerFactory(base) + metadata = metadata_manager_factory(base) for name in namespace: self.assertTrue(hasattr(metadata, name)) if base is CubeMetadata: @@ -81,33 +81,33 @@ def test__namespace(self): def test__kwargs_default(self): for base in self.bases: kwargs = dict(zip(base._fields, [None] * len(base._fields))) - metadata = MetadataManagerFactory(base) + metadata = metadata_manager_factory(base) self.assertEqual(metadata.values._asdict(), kwargs) def test__kwargs(self): for base in self.bases: kwargs = dict(zip(base._fields, range(len(base._fields)))) - metadata = MetadataManagerFactory(base, **kwargs) + metadata = metadata_manager_factory(base, **kwargs) self.assertEqual(metadata.values._asdict(), kwargs) class Test_instance___eq__(tests.IrisTest): def setUp(self): - self.metadata = MetadataManagerFactory(BaseMetadata) + self.metadata = metadata_manager_factory(BaseMetadata) def test__not_implemented(self): self.assertNotEqual(self.metadata, 1) def test__not_is_cls(self): base = BaseMetadata - other = MetadataManagerFactory(base) + other = metadata_manager_factory(base) self.assertIs(other.cls, base) other.cls = CoordMetadata self.assertNotEqual(self.metadata, other) def test__not_values(self): standard_name = mock.sentinel.standard_name - other = MetadataManagerFactory( + other = metadata_manager_factory( BaseMetadata, standard_name=standard_name ) self.assertEqual(other.standard_name, standard_name) @@ -118,22 +118,22 @@ def test__not_values(self): self.assertNotEqual(self.metadata, other) def test__same_default(self): - other = MetadataManagerFactory(BaseMetadata) + other = metadata_manager_factory(BaseMetadata) self.assertEqual(self.metadata, other) def test__same(self): kwargs = dict( standard_name=1, long_name=2, var_name=3, units=4, attributes=5 ) - metadata = MetadataManagerFactory(BaseMetadata, **kwargs) - other = MetadataManagerFactory(BaseMetadata, **kwargs) + metadata = metadata_manager_factory(BaseMetadata, **kwargs) + other = metadata_manager_factory(BaseMetadata, **kwargs) self.assertEqual(metadata.values._asdict(), kwargs) self.assertEqual(metadata, other) class Test_instance____repr__(tests.IrisTest): def setUp(self): - self.metadata = MetadataManagerFactory(BaseMetadata) + self.metadata = metadata_manager_factory(BaseMetadata) def test(self): standard_name = mock.sentinel.standard_name @@ -169,7 +169,7 @@ def setUp(self): self.attributes, ) self.kwargs = dict(zip(BaseMetadata._fields, values)) - self.metadata = MetadataManagerFactory(BaseMetadata, **self.kwargs) + self.metadata = metadata_manager_factory(BaseMetadata, **self.kwargs) def test_pickle(self): for protocol in range(pickle.HIGHEST_PROTOCOL + 1): @@ -188,7 +188,7 @@ def setUp(self): def test(self): for base in self.bases: fields = base._fields - metadata = MetadataManagerFactory(base) + metadata = metadata_manager_factory(base) self.assertEqual(metadata.fields, fields) for field in fields: hasattr(metadata, field) @@ -200,7 +200,7 @@ def setUp(self): def test(self): for base in self.bases: - metadata = MetadataManagerFactory(base) + metadata = metadata_manager_factory(base) result = metadata.values self.assertIsInstance(result, base) self.assertEqual(result._fields, base._fields) diff --git a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py index 334c908e20..5ac9361e4f 100644 --- a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py @@ -17,7 +17,13 @@ from cf_units import Unit -from iris.common.metadata import BaseMetadata +from iris.common.metadata import ( + AncillaryVariableMetadata, + BaseMetadata, + CellMeasureMetadata, + CoordMetadata, + CubeMetadata, +) from iris.common.mixin import CFVariableMixin, LimitedAttributeDict @@ -174,12 +180,15 @@ def test_dict(self): self.item._metadata_manager.attributes, self.attributes ) - def test_dict__missing(self): + def test_dict__partial(self): metadata = dict(**self.args) del metadata["standard_name"] - emsg = "Invalid .* metadata, require 'standard_name' to be specified." - with self.assertRaisesRegex(TypeError, emsg): - self.item.metadata = metadata + self.item.metadata = metadata + metadata["standard_name"] = mock.sentinel.standard_name + self.assertEqual(self.item._metadata_manager.values, metadata) + self.assertIsNot( + self.item._metadata_manager.attributes, self.attributes + ) def test_ordereddict(self): metadata = self.args @@ -189,13 +198,14 @@ def test_ordereddict(self): self.item._metadata_manager.attributes, self.attributes ) - def test_ordereddict__missing(self): + def test_ordereddict__partial(self): metadata = self.args del metadata["long_name"] del metadata["units"] - emsg = "Invalid .* metadata, require 'long_name', 'units' to be specified." - with self.assertRaisesRegex(TypeError, emsg): - self.item.metadata = metadata + self.item.metadata = metadata + metadata["long_name"] = mock.sentinel.long_name + metadata["units"] = mock.sentinel.units + self.assertEqual(self.item._metadata_manager.values, metadata) def test_tuple(self): metadata = tuple(self.args.values()) @@ -232,16 +242,28 @@ def test_namedtuple(self): self.item._metadata_manager.attributes, metadata.attributes ) - def test_namedtuple__missing(self): + def test_namedtuple__partial(self): Metadata = namedtuple( "Metadata", ("standard_name", "long_name", "var_name", "units") ) - metadata = Metadata(standard_name=1, long_name=2, var_name=3, units=4) - emsg = "Invalid .* metadata, require 'attributes' to be specified." - with self.assertRaisesRegex(TypeError, emsg): - self.item.metadata = metadata + del self.args["attributes"] + metadata = Metadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + expected.update(dict(attributes=mock.sentinel.attributes)) + self.assertEqual(self.item._metadata_manager.values, expected) - def test_class(self): + def test_class_ancillaryvariablemetadata(self): + metadata = AncillaryVariableMetadata(**self.args) + self.item.metadata = metadata + self.assertEqual( + self.item._metadata_manager.values, metadata._asdict() + ) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_class_basemetadata(self): metadata = BaseMetadata(**self.args) self.item.metadata = metadata self.assertEqual( @@ -251,6 +273,40 @@ def test_class(self): self.item._metadata_manager.attributes, metadata.attributes ) + def test_class_cellmeasuremetadata(self): + self.args["measure"] = None + metadata = CellMeasureMetadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + del expected["measure"] + self.assertEqual(self.item._metadata_manager.values, expected) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_class_coordmetadata(self): + self.args.update(dict(coord_system=None, climatological=False)) + metadata = CoordMetadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + del expected["coord_system"] + del expected["climatological"] + self.assertEqual(self.item._metadata_manager.values, expected) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_class_cubemetadata(self): + self.args["cell_methods"] = None + metadata = CubeMetadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + del expected["cell_methods"] + self.assertEqual(self.item._metadata_manager.values, expected) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + class Test_rename(tests.IrisTest): def setUp(self):