Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

lru metadata manager factory #4227

Merged
merged 10 commits into from Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 13 additions & 3 deletions docs/src/whatsnew/latest.rst
Expand Up @@ -71,6 +71,19 @@ This document explains the changes made to Iris for this release
unexpected artifacts in some edge cases, as shown at :issue:`4086`. (:pull:`4150`)


馃殌 Performance Enhancements
===========================

#. `@bjlittle`_ added support for automated ``import`` linting with `isort`_, which
also includes significant speed-ups for Iris imports. (:pull:`4174`)

#. `@bjlittle`_ Optimised the creation of dynamic metadata manager classes within the
:func:`~iris.common.metadata.metadata_manager_factory`, resulting in a significant
speed-up in the creation of Iris :class:`~iris.coords.AncillaryVariable`,
:class:`~iris.coords.AuxCoord`, :class:`~iris.coords.CellMeasure`, and
:class:`~iris.cube.Cube` instances. (:pull:`4227`)


馃挘 Incompatible Changes
=======================

Expand Down Expand Up @@ -183,9 +196,6 @@ This document explains the changes made to Iris for this release
#. `@bjlittle`_ consolidated the ``.flake8`` configuration into ``setup.cfg``.
(:pull:`4200`)

#. `@bjlittle`_ added support for automated ``import`` linting with `isort`_.
(:pull:`4174`)

#. `@bjlittle`_ renamed ``iris/master`` branch to ``iris/main`` and migrated
references of ``master`` to ``main`` within codebase. (:pull:`4202`)

Expand Down
6 changes: 6 additions & 0 deletions docs/src/whatsnew/latest.rst.template
Expand Up @@ -58,6 +58,12 @@ This document explains the changes made to Iris for this release
#. N/A


馃殌 Performance Enhancements
===========================

#. N/A


馃敟 Deprecations
===============

Expand Down
111 changes: 59 additions & 52 deletions lib/iris/common/metadata.py
Expand Up @@ -13,7 +13,7 @@
from collections import namedtuple
from collections.abc import Iterable, Mapping
from copy import deepcopy
from functools import wraps
from functools import lru_cache, wraps
import re

import numpy as np
Expand Down Expand Up @@ -1338,39 +1338,19 @@ def equal(self, other, lenient=None):
return super().equal(other, lenient=lenient)


def metadata_manager_factory(cls, **kwargs):
"""
A class instance factory function responsible for manufacturing
metadata instances dynamically at runtime.

The factory instances returned by the factory are capable of managing
their metadata state, which can be proxied by the owning container.

Args:

* cls:
A subclass of :class:`~iris.common.metadata.BaseMetadata`, defining
the metadata to be managed.

Kwargs:

* kwargs:
Initial values for the manufactured metadata instance. Unspecified
fields will default to a value of 'None'.

"""

@lru_cache(maxsize=None)
bjlittle marked this conversation as resolved.
Show resolved Hide resolved
def _factory_cache(cls):
def __init__(self, cls, **kwargs):
# Restrict to only dealing with appropriate metadata classes.
if not issubclass(cls, BaseMetadata):
emsg = "Require a subclass of {!r}, got {!r}."
raise TypeError(emsg.format(BaseMetadata.__name__, cls))
bjlittle marked this conversation as resolved.
Show resolved Hide resolved

#: The metadata class to be manufactured by this factory.
self.cls = cls

# Proxy for self.cls._fields for later internal use, as this
# saves on indirect property lookup via self.cls
self._fields = cls._fields

bjlittle marked this conversation as resolved.
Show resolved Hide resolved
# Initialise the metadata class fields in the instance.
for field in self.fields:
# Use cls directly here since it's available.
for field in cls._fields:
bjlittle marked this conversation as resolved.
Show resolved Hide resolved
setattr(self, field, None)

# Populate with provided kwargs, which have already been verified
Expand All @@ -1389,7 +1369,7 @@ def __eq__(self, other):

def __getstate__(self):
"""Return the instance state to be pickled."""
return {field: getattr(self, field) for field in self.fields}
return {field: getattr(self, field) for field in self._fields}

def __ne__(self, other):
match = self.__eq__(other)
Expand All @@ -1412,7 +1392,7 @@ def __repr__(self):
args = ", ".join(
[
"{}={!r}".format(field, getattr(self, field))
for field in self.fields
for field in self._fields
]
)
return "{}({})".format(self.__class__.__name__, args)
Expand All @@ -1426,27 +1406,14 @@ def __setstate__(self, state):
def fields(self):
"""Return the name of the metadata members."""
# Proxy for built-in namedtuple._fields property.
return self.cls._fields
return self._fields

@property
def values(self):
fields = {field: getattr(self, field) for field in self.fields}
fields = {field: getattr(self, field) for field in self._fields}
return self.cls(**fields)

# Restrict factory to appropriate metadata classes only.
if not issubclass(cls, BaseMetadata):
emsg = "Require a subclass of {!r}, got {!r}."
raise TypeError(emsg.format(BaseMetadata.__name__, cls))

# Check whether kwargs have valid fields for the specified metadata.
if kwargs:
extra = [field for field in kwargs.keys() if field not in cls._fields]
if extra:
bad = ", ".join(map(lambda field: "{!r}".format(field), extra))
emsg = "Invalid {!r} field parameters, got {}."
raise ValueError(emsg.format(cls.__name__, bad))

bjlittle marked this conversation as resolved.
Show resolved Hide resolved
# Define the name, (inheritance) bases and namespace of the dynamic class.
# Define the name, (inheritance) bases, and namespace of the dynamic class.
name = "MetadataManager"
bases = ()
namespace = {
Expand All @@ -1468,12 +1435,52 @@ def values(self):
if cls is CubeMetadata:
namespace["_names"] = cls._names

# Dynamically create the class.
Metadata = type(name, bases, namespace)
# Now manufacture an instance of that class.
metadata = Metadata(cls, **kwargs)
# Dynamically create the metadata manager class.
MetadataManager = type(name, bases, namespace)

return MetadataManager


def metadata_manager_factory(cls, **kwargs):
"""
A class instance factory function responsible for manufacturing
metadata instances dynamically at runtime.

The factory instances returned by the factory are capable of managing
their metadata state, which can be proxied by the owning container.

Args:

* cls:
A subclass of :class:`~iris.common.metadata.BaseMetadata`, defining
the metadata to be managed.

Kwargs:

* kwargs:
Initial values for the manufactured metadata instance. Unspecified
fields will default to a value of 'None'.

Returns:
A manager instance for the provided metadata ``cls``.

"""
# Check whether kwargs have valid fields for the specified metadata.
if kwargs:
extra = [field for field in kwargs.keys() if field not in cls._fields]
if extra:
bad = ", ".join(map(lambda field: "{!r}".format(field), extra))
emsg = "Invalid {!r} field parameters, got {}."
raise ValueError(emsg.format(cls.__name__, bad))
bjlittle marked this conversation as resolved.
Show resolved Hide resolved

# Dynamically create the metadata manager class at runtime or get a cached
# version of it.
MetadataManager = _factory_cache(cls)

# Now manufacture an instance of the metadata manager class.
manager = MetadataManager(cls, **kwargs)

return metadata
return manager


#: Convenience collection of lenient metadata combine services.
Expand Down
Expand Up @@ -36,14 +36,6 @@


class Test_factory(tests.IrisTest):
def test__subclass_invalid(self):
class Other:
pass

emsg = "Require a subclass of 'BaseMetadata'"
with self.assertRaisesRegex(TypeError, emsg):
_ = metadata_manager_factory(Other)

def test__kwargs_invalid(self):
emsg = "Invalid 'BaseMetadata' field parameters, got 'wibble'."
with self.assertRaisesRegex(ValueError, emsg):
Expand Down Expand Up @@ -167,8 +159,8 @@ def setUp(self):
self.units,
self.attributes,
)
self.kwargs = dict(zip(BaseMetadata._fields, values))
self.metadata = metadata_manager_factory(BaseMetadata, **self.kwargs)
kwargs = dict(zip(BaseMetadata._fields, values))
self.metadata = metadata_manager_factory(BaseMetadata, **kwargs)

def test_pickle(self):
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
Expand Down