Skip to content

Commit

Permalink
Merge 4fe5cfc into e5d9619
Browse files Browse the repository at this point in the history
  • Loading branch information
lnielsen committed Sep 21, 2020
2 parents e5d9619 + 4fe5cfc commit 50ed002
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 30 deletions.
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
Changes
=======

Version 1.4.0a2 (released 2020-09-21)

- Adds a SystemFieldContext which allows knowing the record class when
accessing the attribute through the class instead of object instance.

- Adds helpers for caching related objects on the record.

Version 1.4.0a1 (released 2020-09-16)

- Adds support for JSON encoding/decoding to/from the database. This allows
Expand Down
4 changes: 3 additions & 1 deletion invenio_records/systemfields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ class MyRecord(Record, SystemFieldsMixin):
records and related objects.
"""

from .base import SystemField, SystemFieldsMeta, SystemFieldsMixin
from .base import SystemField, SystemFieldContext, SystemFieldsMeta, \
SystemFieldsMixin
from .constant import ConstantField
from .dict import DictField
from .model import ModelField
Expand All @@ -79,6 +80,7 @@ class MyRecord(Record, SystemFieldsMixin):
'DictField',
'ModelField',
'SystemField',
'SystemFieldContext',
'SystemFieldsMeta',
'SystemFieldsMixin',
)
71 changes: 55 additions & 16 deletions invenio_records/systemfields/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ def _get_inherited_fields(class_, field_class):
return fields


class SystemFieldContext:
"""Base class for a system field context
A system field context is created once you access a field's attribute on
a class. As the system field may be defined on a super class, this context
allows us to know from which class the field was accessed.
Normally you should subclass this class, and implement methods the methods
on it that requires you to know the record class.
"""

def __init__(self, field, record_cls):
"""Initialise the field context."""
self._field = field
self._record_cls = record_cls

@property
def field(self):
"""Access the field to prevent it from being overwritten."""
return self._field

@property
def record_cls(self):
"""Record class to prevent it from being overwritten."""
return self._record_cls


class SystemField(ExtensionMixin):
"""Base class for all system fields.
Expand Down Expand Up @@ -88,45 +115,47 @@ def key(self):
#
# Data descriptor definition
#
def __get__(self, instance, class_):
def __get__(self, record, owner=None):
r"""Accessing the object attribute.
A subclass that overwrites this method, should handle two cases:
1. Class access - If ``instance`` is None, the field is accessed
through the class (e.g. Record.myfield). In this case the field
itself should be returned.
through the class (e.g. Record.myfield). In this case a field or
context should be returned. The purpose of the field context, is
to allow a field to know from which class it was accessed (as the
field may be created on a super class).
2. Instance access - If ``instance`` is not None, the field is
accessed through an instance of the class (e.g. record``.myfield``).
A simple example is provided below:
.. code-block:: python
def __get__(self, instance, class_):
if instance is None:
return self # returns the field itself.
if 'mykey' in instance:
return instance['mykey']
def __get__(self, record, owner=None):
if record is None:
return self
return SystemFieldContext(self, owner)
if 'mykey' in record:
return record['mykey']
return None
:param instance: The instance through which the field is being accessed
or ``None`` if the field is accessed through the
class.
:param class_: The ``class_`` which owns the field. In most cases you
should use this variable.
:param record: The instance through which the field is being accessed
or ``None`` if the field is accessed through the
class.
:param owner: The class which owns the field.
"""
# Class access
# - by default a system field accessed through a class (e.g.
# Record.myattr will return the field itself).
if instance is None:
# Record.myattr will return a field or field context).
if record is None:
return self
# Instance access
# - by default a system field accessed through an object instance (e.g.
# record.myattr will raise an Attribute error)
raise AttributeError

def __set__(self, instance, value):
def __set__(self, record, value):
"""Setting the attribute (instance access only).
This method only handles set operations from an instance (e.g.
Expand Down Expand Up @@ -195,6 +224,16 @@ def set_dictkey(self, instance, value, create_if_missing=False):

parent[keys[-1]] = value

def _set_cache(self, instance, obj):
"""Set an object on the instance's cache."""
if not hasattr(instance, '_obj_cache'):
instance._obj_cache = {}
instance._obj_cache[self.attr_name] = obj

def _get_cache(self, instance):
"""Get the object from the instance's cache."""
return getattr(instance, '_obj_cache', {}).get(self.attr_name)


class SystemFieldsExt(RecordExtension):
"""Record extension for system fields.
Expand Down
6 changes: 3 additions & 3 deletions invenio_records/systemfields/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ def pre_init(self, record, data, model=None, **kwargs):
# Key is not present, so add it.
data[self.key] = self.value

def __get__(self, instance, class_):
def __get__(self, record, owner=None):
"""Accessing the attribute."""
# Class access
if instance is None:
if record is None:
return self
# Instance access
try:
return dict_lookup(instance, self.key)
return dict_lookup(record, self.key)
except KeyError:
return None
12 changes: 6 additions & 6 deletions invenio_records/systemfields/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""Persistent identifier system field for record."""

from invenio_records.dictutils import clear_none, dict_lookup
from invenio_records.systemfields import SystemField
from .base import SystemField


class DictField(SystemField):
Expand All @@ -32,15 +32,15 @@ def __init__(self, key=None, clear_none=False, create_if_missing=True):
self.create_if_missing = create_if_missing
super().__init__(key=key)

def __get__(self, instance, class_):
def __get__(self, record, owner=None):
"""Getting the attribute value."""
if instance is None:
if record is None:
return self
return self.get_dictkey(instance)
return self.get_dictkey(record)

def __set__(self, instance, value):
def __set__(self, record, value):
"""Setting a new value."""
if self.clear_none:
clear_none(value)
self.set_dictkey(
instance, value, create_if_missing=self.create_if_missing)
record, value, create_if_missing=self.create_if_missing)
6 changes: 3 additions & 3 deletions invenio_records/systemfields/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ def _set(self, model, value):
#
# Data descriptor
#
def __get__(self, instance, class_):
def __get__(self, record, owner=None):
"""Accessing the attribute."""
# Class access
if instance is None:
if record is None:
return self
# Instance access
try:
return getattr(instance.model, self.model_field_name)
return getattr(record.model, self.model_field_name)
except AttributeError:
return None

Expand Down
2 changes: 1 addition & 1 deletion invenio_records/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
"""


__version__ = '1.4.0a1'
__version__ = '1.4.0a2'

0 comments on commit 50ed002

Please sign in to comment.