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.