#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Helper classes to use content fragments in :mod:`zope.interface`
or :mod:`zope.schema` declarations.
"""
from __future__ import print_function, absolute_import, division
__docformat__ = "restructuredtext en"
# pylint: disable=too-many-ancestors
# pylint:disable=useless-object-inheritance
import sys
import unicodedata
import six
from zope.interface import implementer
from zope.schema.interfaces import InvalidValue
from nti.schema.field import Object
from nti.schema.field import ValidText as Text
from nti.schema.field import ValidTextLine as TextLine
from .interfaces import HTMLContentFragment as HTMLContentFragmentType
from .interfaces import IHTMLContentFragment
from .interfaces import LatexContentFragment
from .interfaces import ILatexContentFragment
from .interfaces import UnicodeContentFragment
from .interfaces import IUnicodeContentFragment
from .interfaces import PlainTextContentFragment
from .interfaces import IPlainTextContentFragment
from .interfaces import SanitizedHTMLContentFragment as SanitizedHTMLContentFragmentType
from .interfaces import ISanitizedHTMLContentFragment
from .interfaces import RstContentFragment as RstContentFragmentType
from .interfaces import IRstContentFragment
from .interfaces import ITextUnicodeContentFragmentField
from .interfaces import ITextLineUnicodeContentFragmentField
from .interfaces import ILatexFragmentTextLineField
from .interfaces import IPlainTextLineField
from .interfaces import IPlainTextField
from .interfaces import IHTMLContentFragmentField
from .interfaces import ISanitizedHTMLContentFragmentField
from .interfaces import ITagField
from .interfaces import IRstContentFragmentField
from .rst import RstParseError
class _FromUnicodeMixin(object):
# Set the interface to use as self.schema. This will be implemented by
# objects returned from ``fromUnicode``. However...
_iface = None
# If the adapter registered to produce _iface may produce some
# interface less restrictive than that (e.g., _iface is HTML, but
# we can produce plain text)
# set this to become self.schema.
_iface_upper_bound = None
# This is the class used to copy defaults.
_impl = lambda *args: None
def __init__(self, *args, **kwargs):
super(_FromUnicodeMixin, self).__init__(
self._iface_upper_bound or self._iface, # Becomes self.schema.
*args,
**self.__massage_kwargs(kwargs))
self._fromUnicodeMixin_adapt_to_schema = self.schema
def __massage_kwargs(self, kwargs):
assert self._iface.isOrExtends(IUnicodeContentFragment), self._iface
assert self._iface.implementedBy(self._impl), self._impl
# We're imported too early for ZCA to be configured and we can't automatically
# adapt.
if 'default' in kwargs and not self._iface.providedBy(kwargs['default']):
kwargs['default'] = self._impl(kwargs['default'])
if 'default' not in kwargs and 'defaultFactory' not in kwargs and not kwargs.get('min_length'): # 0/None
kwargs['defaultFactory'] = self._impl
# Disable unicode normalization at this level; we need to handle it
# to properly deal with our content fragment subclasses.
assert 'unicode_normalization' not in kwargs
kwargs['unicode_normalization'] = None
return kwargs
def fromUnicode(self, value):
"""
We implement :class:`.IFromUnicode` by adapting the given object
to our text schema.
This happens *after* unicode normalization.
"""
# unicodedate.normalize does not preserve the class of the
# object it's given (it goes back to text_type; always under PyPy, only if
# changes are needed under CPython). So we must handle normalization ourself
# before converting to the schema.
value = unicodedata.normalize(self.__class__.unicode_normalization, value)
value = self._fromUnicodeMixin_adapt_to_schema(value)
result = super(_FromUnicodeMixin, self).fromUnicode(value)
return result
[docs]@implementer(ITextUnicodeContentFragmentField)
class TextUnicodeContentFragment(_FromUnicodeMixin, Object, Text):
"""
A :class:`zope.schema.Text` type that also requires the object
implement an interface descending from
:class:`~.IUnicodeContentFragment`.
Pass the keyword arguments for :class:`zope.schema.Text` to the
constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
.. caution::
This will perform conversions on the input data, stripping or adjusting
things that "look like" HTML, if it does not already implement the required interface;
the actual value is likely to be a :class:`~.ISanitizedHTMLContentFragment`
or a :class:`~.IPlainTextContentFragment`.
"""
_iface = IUnicodeContentFragment
_impl = UnicodeContentFragment
[docs]@implementer(ITextLineUnicodeContentFragmentField)
class TextLineUnicodeContentFragment(_FromUnicodeMixin, Object, TextLine):
"""
A :class:`zope.schema.TextLine` type that also requires the object
implement an interface descending from
:class:`~.IUnicodeContentFragment`.
Pass the keyword arguments for :class:`zope.schema.TextLine` to
the constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
If you pass neither a *default* nor *defaultFactory* argument, a
*defaultFactory* argument will be provided to construct an empty
content fragment.
.. caution::
This will perform conversions on the input data, stripping or adjusting
things that "look like" HTML, if it does not already implement the required interface;
the actual value is
likely to be a :class:`~.ISanitizedHTMLContentFragment`
or a :class:`~.IPlainTextContentFragment`.
"""
_iface = IUnicodeContentFragment
_impl = UnicodeContentFragment
[docs]@implementer(ILatexFragmentTextLineField)
class LatexFragmentTextLine(TextLineUnicodeContentFragment):
"""
A :class:`~zope.schema.TextLine` that requires content to be in
LaTeX format.
Pass the keyword arguments for :class:`~zope.schema.TextLine` to
the constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
.. note::
If you provide a *default* string that does not
already provide :class:`.ILatexContentFragment`,
one will be created simply by copying; no validation or transformation will occur.
"""
_iface = ILatexContentFragment
_impl = LatexContentFragment
[docs]@implementer(IPlainTextLineField)
class PlainTextLine(TextLineUnicodeContentFragment):
"""
A :class:`~zope.schema.TextLine` that requires content to be plain
text.
Pass the keyword arguments for :class:`~zope.schema.TextLine` to
the constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
.. note::
If you provide a *default* string that does
not already provide :class:`.ILatexContentFragment`,
one will be created simply by copying; no validation or transformation will occur.
.. caution::
This will perform conversions on the input data, stripping
things that "look like" HTML, if it does not already implement the required interface.
"""
_iface = IPlainTextContentFragment
_impl = PlainTextContentFragment
[docs]class VerbatimPlainTextLine(PlainTextLine):
"""
Like `PlainTextLine`, except instead of running a conversion on the
input data, stripping HTML, will simply assume that the input data
is already meant to be plain text and will preserve markup as-is.
"""
def __init__(self, *args, **kwargs):
super(VerbatimPlainTextLine, self).__init__(*args, **kwargs)
# We adapt by just copying
self._fromUnicodeMixin_adapt_to_schema = self._impl
[docs]@implementer(IHTMLContentFragmentField)
class HTMLContentFragment(TextUnicodeContentFragment):
"""
A :class:`~zope.schema.Text` type that also requires the object
implement an interface descending from
:class:`.IHTMLContentFragment`.
Pass the keyword arguments for :class:`zope.schema.Text` to the
constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
.. note::
If you provide a *default* string that does not already provide :class:`.IHTMLContentFragment`,
one will be created simply by copying; no validation or transformation will occur.
"""
_iface = IHTMLContentFragment
_impl = HTMLContentFragmentType
[docs]@implementer(ISanitizedHTMLContentFragmentField)
class SanitizedHTMLContentFragment(HTMLContentFragment):
"""
A :class:`Text` type that also requires the object implement an
interface descending from :class:`.ISanitizedHTMLContentFragment`.
Note that the default adapter for this can actually produce
``IPlainTextContentFragment`` if there is no HTML present in the
input.
Pass the keyword arguments for :class:`zope.schema.Text` to the
constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
.. note::
If you provide a ``default`` string that does not already provide :class:`.ISanitizedHTMLContentFragment`,
one will be created simply by copying; no validation or transformation will occur.
"""
_iface = ISanitizedHTMLContentFragment
_impl = SanitizedHTMLContentFragmentType
[docs]@implementer(IRstContentFragmentField)
class RstContentFragment(TextUnicodeContentFragment):
"""
A :class:`zope.schema.Text` type that also requires the object implement an
interface descending from :class:`~.IRstContentFragment`. Note that
currently this does no validation of the content to ensure it is
valid reStructuredText.
Pass the keyword arguments for :class:`zope.schema.Text` to the
constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
.. note::
If you provide a *default* string that does not already provide :class:`.IRstContentFragment`,
one will be created simply by copying; no validation or transformation will occur.
"""
_iface = IRstContentFragment
_impl = RstContentFragmentType
[docs] def fromUnicode(self, value):
try:
return _FromUnicodeMixin.fromUnicode(self, value)
except RstParseError as e:
ex = InvalidValue("Error parsing reStructuredText: %s" % (e,))
ex = ex.with_field_and_value(self, value)
six.reraise(InvalidValue, ex, sys.exc_info()[2])
[docs]@implementer(IPlainTextField)
class PlainText(TextUnicodeContentFragment):
"""
A :class:`zope.schema.Text` that requires content to be plain
text.
Pass the keyword arguments for :class:`~zope.schema.Text` to the
constructor; the ``schema`` argument for
:class:`~zope.schema.Object` is already handled.
.. note::
If you provide a *default* string that does not already provide :class:`.IPlainTextContentFragment`,
one will be created simply by copying; no validation or transformation will occur.
.. caution::
This will perform conversions on the input data, stripping
things that "look like" HTML, if it does not already implement the required interface.
"""
_iface = IPlainTextContentFragment
_impl = PlainTextContentFragment
[docs]class VerbatimPlainText(PlainText):
"""
Like `PlainText`, except instead of running a conversion on the
input data, stripping HTML, will simply assume that the input data
is already meant to be plain text and will preserve markup as-is.
"""
def __init__(self, *args, **kwargs):
super(VerbatimPlainText, self).__init__(*args, **kwargs)
# We adapt by just copying
self._fromUnicodeMixin_adapt_to_schema = self._impl
[docs]@implementer(ITagField)
class Tag(PlainTextLine):
"""
Requires its content to be only one plain text word that is lowercased.
"""
[docs] def fromUnicode(self, value):
return super(Tag, self).fromUnicode(value.lower())
def constraint(self, value): # pylint:disable=method-hidden
return super(Tag, self).constraint(value) and ' ' not in value
[docs]def Title():
"""
Return a :class:`zope.schema.interfaces.IField` representing
the standard title of some object. This should be stored in the `title`
field.
"""
return PlainTextLine(
max_length=140, # twitter
required=False,
title=u"The human-readable title of this object",
__name__='title')