In the base Frame class the _fields attribute defines the known fields for documents within the related collection, trying to access a field that's not in the _fields set will raise an AttributeError.
Most of the time this is the desired behaviour but there are occasions where the fields for documents in the same collection may vary. One way to handle this is to define a bigger set that covers all possible fields and accept that not all of them will be set for each document, but if you don't know what all the possible fields are in advance or the list is huge then this approach isn't practical.
Introducing the Frameless class
This snippet introduces a Frame-like class with no defined set of fields that allows new fields to be set ad hoc:
from mongoframes import *
from mongoframes.frames import _FrameMeta
__all__ = [
'Frameless',
'SubFrameless'
]
class _FramelessMeta(_FrameMeta):
"""
Meta class for `Frameless` to set the `_collection` value if not set.
"""
def __new__(meta, name, bases, dct):
# If no collection name is set then use the class name
if dct.get('_collection') is None:
dct['_collection'] = name
return super(_FramelessMeta, meta).__new__(meta, name, bases, dct)
class Frameless(Frame, metaclass=_FramelessMeta):
"""
A Frame-like class with no defined set of fields.
"""
def __getattr__(self, name):
if '_document' in self.__dict__:
return self.__dict__['_document'].get(name, None)
raise AttributeError(
"'{0}' has no attribute '{1}'".format(self.__class__.__name__, name)
)
def __setattr__(self, name, value):
if '_document' in self.__dict__:
self.__dict__['_document'][name] = value
else:
super(Frameless, self).__setattr__(name, value)
@property
def fields(self):
"""Return a list of fields for this document"""
return self._document.keys()
@classmethod
def _flatten_projection(cls, projection):
"""
Flatten a structured projection (structure projections support for
projections of (to be) dereferenced fields.
"""
# If `projection` is empty return a full projection
if not projection:
return {'__': False}, {}, {}
# Flatten the projection
flat_projection = {}
references = {}
subs = {}
inclusive = True
for key, value in deepcopy(projection).items():
if isinstance(value, dict):
# Store a reference/SubFrame projection
if '$ref' in value:
references[key] = value
elif '$sub' in value or '$sub.' in value:
subs[key] = value
flat_projection[key] = True
elif key == '$ref':
# Strip any `$ref` key
continue
elif key == '$sub' or key == '$sub.':
# Strip any `$sub` key
continue
else:
# Store the root projection value
flat_projection[key] = value
inclusive = False
# If only references and `SubFrames` were specified in the projection
# then return a full projection.
if inclusive:
flat_projection = {'__': False}
return flat_projection, references, subs
class SubFrameless(SubFrame):
"""
A SubFrame-like class with no defined set of fields.
"""
def __getattr__(self, name):
if '_document' in self.__dict__:
return self.__dict__['_document'].get(name, None)
raise AttributeError(
"'{0}' has no attribute '{1}'".format(self.__class__.__name__, name)
)
def __setattr__(self, name, value):
if '_document' in self.__dict__:
self.__dict__['_document'][name] = value
else:
super(SubFrameless, self).__setattr__(name, value)
Working with Frameless classes
A Frameless class is defined just like a Frame with the exception that we don't define the _fields set. Let's define our Dragon class from the Getting started page as frameless:
class Dragon(Frameless):
pass
class Item(SubFrameless):
pass
# Create and insert some dragons
burt = Dragon(
name='Burt',
breed='Fire-drake',
stashed_items=[
Item(desc='Rotting lamb carcass', worth=1),
Item(desc='Montecristo No. 2', worth=30)
])
burt.insert()
edison = Dragon(
name='Edison',
breed='Ice-drake',
stashed_items=[
Item(desc='Mojito', worth=15),
])
edison.insert()
As Dragon and Item are now frameless classes we can set new attributes for individual instances ad hoc, in the example below we give Edison a nickname:
# Select dragons (Burt and Edison)
burt = Dragon.one(Q.name == 'Burt')
edison = Dragon.one(Q.name == 'Edison')
# Give Edison a nickname
edison.nickname = 'Eddy'
edison.update()
# Edison now has an additional field compared to burt
print(edison.fields)
print(burt.fields)
>> ['_id', 'name', 'breed', 'stashed_items', 'nickname']
>> ['_id', 'name', 'breed', 'stashed_items']
The code within this article is available in the MongoFrames repository within the snippets directory.