Skip to content

Commit

Permalink
StorageClass lazy loading now done in instance
Browse files Browse the repository at this point in the history
This allows us to remove the metaclass
  • Loading branch information
timj committed Feb 20, 2018
1 parent d0e689b commit 89e6cbb
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 62 deletions.
91 changes: 35 additions & 56 deletions python/lsst/daf/butler/core/storageClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,38 @@
from .mappingFactory import MappingFactory


class StorageClassMeta(type):

"""Metaclass used by `StorageClass`.
class StorageClass:
"""Class describing how a label maps to a particular Python type.
"""

Implements lazy loading of class attributes, allowing datastores to
delay loading of external code until it is needed.
# Components are fixed per class
_components = {}

Attributes
----------
pytype
assembler
# These are internal class attributes supporting lazy loading of concrete
# python types and functions from the string. The lazy loading is only done
# once the property is requested by an instance. The loading is fixed per
# class but class access to attribute is not supported.

"""
# The names are defined when the class is constructed
_pytypeName = None
_assemblerClassName = None

# Caches of imported code objects.
# The types are created on demand and cached
# We set a default assembler so that a class is guaranteed to support
# something.
_pytype = None
_assembler = CompositeAssembler

def __init__(self, name, bases, dct):
super().__init__(name, bases, dct)
if hasattr(self, "name"):
StorageClass.subclasses[self.name] = self
@property
def components(self):
"""Component names mapped to associated `StorageClass`
"""
return type(self)._components

@property
def pytype(cls): # noqa N805
def pytype(self):
"""Python type associated with this `StorageClass`."""
cls = type(self)
if cls._pytype is not None:
return cls._pytype
# Handle case where we did get a python type not string
Expand All @@ -71,54 +77,27 @@ def pytype(cls): # noqa N805
return cls._pytype

@property
def assembler(cls): # noqa N805
"""Function to use to assemble an object from components."""
def assemblerClass(self):
"""Class to use to (dis)assemble an object from components."""
cls = type(self)
if cls._assembler is not None:
return cls._assembler
if cls._assemblerClassName is None:
return None
cls._assembler = doImport(cls._assemblerClassName)
return cls._assembler


class StorageClass(metaclass=StorageClassMeta):
"""Class describing how a label maps to a particular Python type.
Attributes
----------
name : `str`
Name associated with the StorageClass.
pytype : `type`
Python type associated with this name.
components : `dict`
Dict mapping component names of a composite to an associated
`StorageClass`.
assembler : `function`, optional
Class to use to assemble and disassemble an object of type `pytype`
from components.
"""

subclasses = dict()
components = dict()

# These are internal class attributes supporting lazy loading of concrete
# python types and functions from the string. The lazy loading is handled
# in the metaclass.
_pytypeName = None
_assemblerClassName = None

@property
def pytype(self):
"""Python type associated with this StorageClass."""
return type(self).pytype

@property
def assembler(self):
"""Function object to use to create a type from components."""
return type(self).assembler
"""Return an instance of an assembler.
Returns
-------
assembler : `CompositeAssembler`
Instance of the assembler associated with this `StorageClass`
"""
return self.assemblerClass()

@classmethod
def validateInstance(cls, instance):
def validateInstance(self, instance):
"""Check that the supplied instance has the expected Python type
Parameters
Expand All @@ -132,7 +111,7 @@ def validateInstance(cls, instance):
True is the supplied instance object can be handled by this
`StorageClass`, False otherwise.
"""
return isinstance(instance, cls.pytype)
return isinstance(instance, self.pytype)


def makeNewStorageClass(name, pytype=None, components=None, assembler=None):
Expand Down
2 changes: 1 addition & 1 deletion python/lsst/daf/butler/datastores/posixDatastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def put(self, inMemoryDataset, storageClass, storageHint, typeName=None):

# Check to see if this storage class has a disassembler
# and also has components
if storageClass.assembler.disassemble is not None and storageClass.components:
if storageClass.assemblerClass.disassemble is not None and storageClass.components:
compUris = {}
components = storageClass.assembler().disassemble(inMemoryDataset, storageClass)
if components:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_storageClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,17 @@ def testCreation(self):
self.assertIsInstance(sc, storageClass.StorageClass)
self.assertEqual(sc.name, className)
self.assertIsNone(sc.components)
self.assertEqual(sc.pytype, dict)
self.assertEqual(newclass.pytype, dict)

# Check that this class is listed in the subclasses
self.assertIn(className, newclass.subclasses)
# Test the caching by using private class attribute
self.assertIsNone(newclass._pytype)
self.assertEqual(sc.pytype, dict)
self.assertEqual(newclass._pytype, dict)

# Check we can create a storageClass using the name of an importable
# type.
newclass = storageClass.makeNewStorageClass("TestImage2",
"lsst.daf.butler.core.storageClass.StorageClassFactory")
self.assertIsInstance(newclass.pytype(), storageClass.StorageClassFactory)
self.assertIsInstance(newclass().pytype(), storageClass.StorageClassFactory)

def testRegistry(self):
"""Check that storage classes can be created on the fly and stored
Expand Down

0 comments on commit 89e6cbb

Please sign in to comment.