Skip to content

Commit

Permalink
Merge pull request #112 from alfred82santa/feature/field-access-mode
Browse files Browse the repository at this point in the history
Added field access mode
  • Loading branch information
alfred82santa committed Apr 15, 2020
2 parents 38837cf + e4ba050 commit 8e78fc4
Show file tree
Hide file tree
Showing 14 changed files with 762 additions and 121 deletions.
File renamed without changes.
3 changes: 0 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
language: python
python:
- "3.3"
- "3.4"
- "3.5"
- "3.6"
# command to install dependencies
install:
- make requirements
- pip install -r requirements-test.txt
- if [[ $TRAVIS_PYTHON_VERSION == 3.3.* ]]; then pip install -r requirements-py33.txt --use-mirrors; fi
- pip install coveralls
# command to run tests
script:
- if [[ $TRAVIS_PYTHON_VERSION -ne 3.3.* ]]; then flake8 dirty_models; fi
- flake8 tests
- nosetests --with-coverage -d --cover-package=dirty_models

Expand Down
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,26 @@ Features
Changelog
---------

Version 0.12.0
--------------

* Added `access_mode` property to fields.
It could be :class:`~dirty_models.base.AccessMode.READ_AND_WRITE` in order to allow to read and write.
:class:`~dirty_models.base.AccessMode.WRITABLE_ONLY_ON_CREATION` in order to set value only on creation.
:class:`~dirty_models.base.AccessMode.READ_ONLY` in order to prevent writing.
And :class:`~dirty_models.base.AccessMode.HIDDEN` in order to hide field.

* Old field property `read_only` is deprecated in favor of `access_mode` but it can be used like until this version.

* Helper :class:`~dirty_models.base.Creating` to mark model as in creation mode.

* Added class method :meth:`~dirty_models.models.BaseModel.create_new_model` build a model and insert data
in creation mode.

* Allowed to override field access model on inherited fields using
:attr:`~dirty_models.models.BaseModel.__override_field_access_modes__` hashmap.


Version 0.11.3
--------------

Expand Down
3 changes: 2 additions & 1 deletion dirty_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
from .models import *
from .fields import *
from .utils import *
from .base import *

__version__ = '0.11.3'
__version__ = '0.12.0'
104 changes: 84 additions & 20 deletions dirty_models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,67 @@

import weakref

__all__ = ['Unlocker', 'Creating', 'AccessMode']

class BaseData:
from abc import abstractmethod

from enum import IntEnum


class AccessMode(IntEnum):
READ_AND_WRITE = 0
WRITABLE_ONLY_ON_CREATION = 1
READ_ONLY = 2
HIDDEN = 3

def __and__(self, other):
return max(self, other)

def __or__(self, other):
return min(self, other)


class BaseData:
"""
Base class for data inside dirty model.
"""

__locked__ = None
__read_only__ = None
__access_mode__ = None
__is_creating__ = None
__parent__ = None

def __init__(self, *args, **kwargs):
def __init__(self, *args, __is_creating=False, **kwargs):
self.__locked__ = True
self.__read_only__ = False
self.__access_mode__ = AccessMode.READ_AND_WRITE
self.__is_creating__ = __is_creating
self.__parent__ = None

def get_read_only(self):
def get_access_mode(self):
"""
Returns whether model could be modified or not
Returns how model could be acceded
"""
return self.__read_only__
am = self.__access_mode__
if not am \
or (self.is_creating() and am <= AccessMode.WRITABLE_ONLY_ON_CREATION) \
or not self.is_locked():
am = AccessMode.READ_AND_WRITE

def set_read_only(self, value):
if self.get_parent():
am &= self.get_parent().get_access_mode()
return am

def set_access_mode(self, value):
"""
Sets whether model could be modified or not
Sets whether model could be acceded
"""
if self.__read_only__ != value:
self.__read_only__ = value
self._update_read_only()
if self.__access_mode__ != value:
self.__access_mode__ = value
self._update_access_mode()

@abstractmethod
def _update_access_mode(self): # pragma: no cover
pass

def get_parent(self):
"""
Expand Down Expand Up @@ -69,21 +101,37 @@ def is_locked(self):

return True

def start_creation(self):
"""
Mark model to be able to set creation-only fields.
"""
self.__is_creating__ = True

def end_creation(self):
"""
Unmark model to be able to set creation-only fields.
"""
self.__is_creating__ = False

def is_creating(self):
"""
Returns whether model is marked on creation mode.
"""
if self.__is_creating__:
return True
elif self.get_parent():
return self.get_parent().is_creating()

return False

def _prepare_child(self, value):
try:
value.set_parent(self)
except AttributeError:
pass

if self.get_read_only():
try:
value.set_read_only(True)
except AttributeError:
pass


class InnerFieldTypeMixin:

__field_type__ = None

def __init__(self, *args, **kwargs):
Expand All @@ -96,7 +144,6 @@ def get_field_type(self):


class Unlocker():

"""
Unlocker instances helps to lock and unlock models easily
"""
Expand All @@ -106,6 +153,23 @@ def __init__(self, item):

def __enter__(self):
self.item.unlock()
return self.item

def __exit__(self, exc_type, exc_value, traceback):
self.item.lock()


class Creating():
"""
Creating instances helps to mark and unmark models as creating easily
"""

def __init__(self, item):
self.item = item

def __enter__(self):
self.item.start_creation()
return self.item

def __exit__(self, exc_type, exc_value, traceback):
self.item.end_creation()
70 changes: 52 additions & 18 deletions dirty_models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from dateutil.parser import parse as dateutil_parse

from .base import AccessMode, Creating
from .model_types import ListModel

__all__ = ['IntegerField', 'FloatField', 'BooleanField', 'StringField', 'StringIdField',
Expand All @@ -19,29 +20,46 @@
class BaseField:
"""Base field descriptor."""

def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=False,
default=None, title=None, doc=None, metadata=None):
def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=None,
default=None, title=None, doc=None, metadata=None, access_mode=AccessMode.READ_AND_WRITE,
json_schema=None):
if read_only is not None:
if read_only:
access_mode = AccessMode.READ_ONLY & access_mode
else:
access_mode = AccessMode.READ_AND_WRITE & access_mode

self._name = None
self.name = name
self.alias = alias
self.read_only = read_only
self.access_mode = access_mode
self.default = default
self.title = title
self.metadata = metadata
self.json_schema = json_schema
self._getter = getter
self._setter = setter
self.__doc__ = doc or self.get_field_docstring()

def get_field_docstring(self):
dcstr = '{0} field'.format(self.__class__.__name__)
if self.read_only:
dcstr += ' [READ ONLY]'
if self.access_mode:
if self.access_mode == AccessMode.WRITABLE_ONLY_ON_CREATION:
dcstr += ' [WRITABLE ONLY ON CREATION]'
elif self.access_mode == AccessMode.READ_ONLY:
dcstr += ' [READ ONLY]'
elif self.access_mode == AccessMode.HIDDEN:
dcstr += ' [HIDDEN]'
return dcstr

def export_definition(self):
return {'name': self.name,
'alias': self.alias,
'read_only': self.read_only,
'access_mode': self.access_mode,
'title': self.title,
'default': self.default,
'json_schema': self.json_schema,
'metadata': self.metadata,
'doc': self.__doc__}

@property
Expand All @@ -54,16 +72,21 @@ def name(self, name):
"""Name setter: Field name or field alias that it will be set."""
self._name = name

def use_value(self, value):
def use_value(self, value, creating=False):
"""Converts value to field type or use original"""
if self.check_value(value):
return value
if creating:
return self.convert_value_creating(value)
return self.convert_value(value)

def convert_value(self, value):
"""Converts value to field type"""
return value

def convert_value_creating(self, value):
return self.convert_value(value)

def check_value(self, value):
"""Checks whether value is field's type"""
return False
Expand Down Expand Up @@ -112,7 +135,7 @@ def set_value(v):
if value is None:
self.delete_value(obj)
elif self.check_value(v) or self.can_use_value(v):
self.set_value(obj, self.use_value(v))
self.set_value(obj, self.use_value(v, creating=obj.is_creating()))
elif isinstance(value, Factory):
set_value(v())

Expand Down Expand Up @@ -682,6 +705,9 @@ def model_class(self, model_class):
def convert_value(self, value):
return self._model_class(value)

def convert_value_creating(self, value):
return self._model_class.create_new_model(value)

def check_value(self, value):
return isinstance(value, self._model_class)

Expand Down Expand Up @@ -751,19 +777,27 @@ def get_field_docstring(self):
if self.field_type:
return 'Array of {0}'.format(self.field_type.get_field_docstring())

def convert_value(self, value):
def convert_element(element):
"""
Helper to convert a single item
"""
if not self.field_type.check_value(element) and self._field_type.can_use_value(element):
return self.field_type.convert_value(element)
return element
def _convert_element(self, element):
"""
Helper to convert a single item
"""
if not self.field_type.check_value(element) and self._field_type.can_use_value(element):
return self.field_type.convert_value(element)
return element

def convert_value(self, value):
if isinstance(value, (set, list, tuple, ListModel)):
return ListModel([convert_element(element) for element in value], field_type=self.field_type)
return ListModel([self._convert_element(element) for element in value], field_type=self.field_type)
elif self.autolist:
return ListModel([convert_element(value)], field_type=self.field_type)
return ListModel([self._convert_element(value)], field_type=self.field_type)

def convert_value_creating(self, value):
lst = ListModel(field_type=self.field_type)

with Creating(lst):
lst.extend(value)

return lst

def check_value(self, value):
if not isinstance(value, ListModel) or not isinstance(value.get_field_type(), type(self.field_type)):
Expand Down

0 comments on commit 8e78fc4

Please sign in to comment.