Source code for nti.property.urlproperty

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Support for URLs.
"""
import logging

__docformat__ = "restructuredtext en"

logger = logging.getLogger(__name__)

from urllib.parse import urlparse

from zope.contenttype.parse import parse as ct_parse

from zope.file import file as zfile

from zope.file.interfaces import IFile

from zope.schema.interfaces import InvalidURI
from zope.schema.interfaces import ConstraintNotSatisfied

from nti.property import dataurl


def _dict_setattr(instance, name, value):
    instance.__dict__[name] = value


def _dict_getattr(instance, name, default=_dict_setattr):
    if default is _dict_setattr:
        try:
            return instance.__dict__[name]
        except KeyError as ex:  # pragma: no cover
            raise AttributeError(name) from ex
    return instance.__dict__.get(name, default)


def _dict_delattr(instance, name):  # pragma: no cover
    instance.__dict__.pop(name, None)


[docs] class UrlProperty(object): """ A data descriptor like :func:`property` for storing a URL value efficiently. This is an interesting situation because of ``data:`` URLs, which can be quite large. (See :mod:`nti.property.dataurl`) To store them efficiently, we transform them into a Blob-based :class:`zope.file.interfaces.IFile`. This means that we need up to two dictionary entries/attributes on the instance (one to store a URL string, one to store a IFile), although they can be the same; we will take care of type checking as needed. Commonly, to be useful, the file will need to be reachable by traversal on the instance holding the property. This object will ensure that the file is located (in the :mod:`zope.location` sense) as a child of the instance, and that it has a name; it is the responsibility of the instance to arrange for it to be traversable (through implementing ``__getitem__`` or ITraversable or an adapter). """ max_file_size = None url_attr_name = '_url' file_attr_name = '_file' data_name = 'data' ignore_url_with_missing_host = False reject_url_with_missing_host = False _getattr = staticmethod(getattr) _setattr = staticmethod(setattr) _delattr = staticmethod(delattr) def __init__(self, data_name=None, url_attr_name=None, file_attr_name=None, use_dict=False): """ :keyword bool use_dict: If set to `True`, then the instance dictionary will be used explicitly for access to the url and file data. This is necessary if this property is being assigned to the same value as one of the attr names. """ if url_attr_name: self.url_attr_name = url_attr_name if file_attr_name: self.file_attr_name = file_attr_name if data_name: self.data_name = data_name if use_dict: self._getattr = _dict_getattr self._setattr = _dict_setattr self._delattr = _dict_delattr
[docs] def make_getitem(self): """ As a convenience and to help with traversability, the results of this method can be assigned to __getitem__ (if there is no other instance of this property, or no other getitem). .. caution:: The traversal key must be equal to the ``data_name``, but the returned dictionary key is the ``file_attr_name``. """ def __getitem__(s, key): if key == self.data_name: return self._getattr(s, self.file_attr_name, None) raise KeyError(key) return __getitem__
[docs] def get_file(self, instance): """ Return the :class:`zope.file.interfaces.IFile` for the instance if there is one, otherwise None. """ the_file = self._getattr(instance, self.file_attr_name, None) # pylint:disable-next=no-value-for-parameter return (the_file if IFile.providedBy(the_file) else None)
def __get__(self, instance, owner): if instance is None: return self the_file = self.get_file(instance) if the_file is not None: fp = the_file.open() raw_bytes = fp.read() fp.close() return dataurl.encode(raw_bytes, the_file.mimeType) return self._getattr(instance, self.url_attr_name) def __set__(self, instance, value): if instance is None: # pragma: no cover return if not value: self._setattr(instance, self.file_attr_name, None) self._setattr(instance, self.url_attr_name, value) return if value.startswith('data:'): raw_bytes, mime_type = dataurl.decode(value) if self.max_file_size and len(raw_bytes) > self.max_file_size: raise ConstraintNotSatisfied("The uploaded file is too large.") major, minor, parms = ct_parse(mime_type) the_file = zfile.File(mimeType=major + '/' + minor, parameters=parms) fp = the_file.open('w') fp.write(raw_bytes) fp.close() the_file.__parent__ = instance the_file.__name__ = self.data_name # By keeping url in __dict__, toExternalDictionary # still does the right thing self._setattr(instance, self.url_attr_name, None) self._setattr(instance, self.file_attr_name, the_file) else: # Be sure it at least parses parsed = urlparse(value) if not parsed.netloc: if self.reject_url_with_missing_host: raise InvalidURI(value) if self.ignore_url_with_missing_host: return self._setattr(instance, self.file_attr_name, None) self._setattr(instance, self.url_attr_name, value) def __delete__(self, instance): if instance is None: return for name in self.url_attr_name, self.file_attr_name: try: self._delattr(instance, name) except AttributeError: # pragma: no cover pass