Code cleanup
authorThierry Florac <tflorac@ulthar.net>
Wed, 04 Dec 2019 11:05:25 +0100
changeset 204 c435de184bda
parent 203 ba372884335f
child 205 975adcb82113
Code cleanup
src/pyams_form/__init__.py
src/pyams_form/doctests/README.rst
src/pyams_form/doctests/README.txt
src/pyams_form/form.py
src/pyams_form/group.py
src/pyams_form/help.py
src/pyams_form/include.py
src/pyams_form/interfaces/__init__.py
src/pyams_form/interfaces/form.py
src/pyams_form/schema.py
src/pyams_form/search.py
src/pyams_form/security.py
src/pyams_form/terms.py
src/pyams_form/tests/__init__.py
src/pyams_form/tests/test_utilsdocs.py
src/pyams_form/tests/test_utilsdocstrings.py
src/pyams_form/viewlet.py
src/pyams_form/widget/__init__.py
--- a/src/pyams_form/__init__.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/__init__.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,17 +10,23 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form package
+
+This package is an extension to z3c.form.
+
+It allows to integrate z3c forms into Pyramid; it is adding some features like inner subforms
+(handled with adapters), form groups (which are ued to group form fields together inside a
+fieldset in a form), modal forms (which are displayed into a modal window), and is providing
+default templates for all these elements to be displayed correctly when using PyAMS_zmi package
+or any other Bootstrap-based skin.
+"""
+
+from pyramid.i18n import TranslationStringFactory
 
 
-# import standard library
-
-# import interfaces
+_ = TranslationStringFactory('pyams_form')
 
-# import packages
-
-from pyramid.i18n import TranslationStringFactory
-_ = TranslationStringFactory('pyams_form')
+__docformat__ = 'restructuredtext'
 
 
 def includeme(config):
@@ -28,5 +34,5 @@
 
     Split in another package to remove cyclic dependencies with TranslationStringFactory
     """
-    from .include import include_package
+    from .include import include_package  # pylint: disable=import-outside-toplevel
     include_package(config)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_form/doctests/README.rst	Wed Dec 04 11:05:25 2019 +0100
@@ -0,0 +1,11 @@
+==================
+pyams_form package
+==================
+
+    >>> from pyramid.testing import setUp, tearDown
+    >>> config = setUp()
+
+    >>> from pyams_form.interfaces.form import *
+    >>> from pyams_form.form import *
+
+    >>> tearDown()
--- a/src/pyams_form/doctests/README.txt	Tue Nov 19 16:30:58 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-==================
-pyams_ package
-==================
--- a/src/pyams_form/form.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/form.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,12 +10,15 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form.form module
 
-import logging
-logger = logging.getLogger('PyAMS (form)')
+This module is the core of PyAMS_form package; it provides additions to form management as
+provided by z3c.form package.
+"""
 
 import json
+import logging
+
 import transaction
 import venusian
 from persistent import IPersistent
@@ -25,11 +28,11 @@
 from pyramid_chameleon.interfaces import IChameleonTranslate
 from pyramid_zope_request import PyramidPublisherRequest, PyramidToPublisher
 from z3c.form.button import Buttons
-from z3c.form.form import AddForm as BaseAddForm, DisplayForm as BaseDisplayForm, EditForm as BaseEditForm, Form, \
-    applyChanges
+from z3c.form.form import AddForm as BaseAddForm, DisplayForm as BaseDisplayForm, \
+    EditForm as BaseEditForm, Form, applyChanges
 from z3c.form.interfaces import DISPLAY_MODE, IErrorViewSnippet, IMultipleErrors
 from zope.component import queryUtility
-from zope.interface import Interface, alsoProvides, classImplements, implementer, Invalid
+from zope.interface import Interface, Invalid, alsoProvides, classImplements, implementer
 from zope.lifecycleevent import Attributes, ObjectCreatedEvent
 from zope.location import locate
 from zope.publisher.interfaces.browser import IBrowserRequest
@@ -39,7 +42,8 @@
 from pyams_form.group import GroupsBasedForm
 from pyams_form.interfaces import get_form_weight
 from pyams_form.interfaces.form import FormCreatedEvent, IAJAXForm, ICustomUpdateSubForm, IForm, \
-    IFormContextPermissionChecker, IFormCreatedEvent, IFormLayer, IInnerForm, IInnerSubForm, IInnerTabForm
+    IFormContextPermissionChecker, IFormCreatedEvent, IFormLayer, IInnerForm, IInnerSubForm, \
+    IInnerTabForm
 from pyams_form.interfaces.form import FormObjectCreatedEvent, FormObjectModifiedEvent
 from pyams_form.interfaces.form import IAddFormButtons, IEditFormButtons, IModalAddFormButtons, \
     IModalDisplayFormButtons, IModalEditFormButtons
@@ -52,8 +56,13 @@
 from pyams_utils.interfaces import FORBIDDEN_PERMISSION, ICacheKeyValue
 from pyams_utils.url import absolute_url
 
-from pyams_form import _
+
+__docformat__ = 'restructuredtext'
 
+from pyams_form import _    # pylint: disable=ungrouped-imports
+
+
+LOGGER = logging.getLogger('PyAMS (form)')
 
 REDIRECT_STATUS_CODES = (300, 301, 302, 303, 304, 305, 307)
 
@@ -96,16 +105,17 @@
 
     @property
     def title(self):
+        """Get form's title"""
         registry = self.request.registry
         adapter = registry.queryMultiAdapter((self.context, self.request, self), IContentTitle)
         if adapter is None:
             adapter = registry.queryAdapter(self.context, IContentTitle)
         if adapter is not None:
             return adapter.title
-        else:
-            return II18n(self.context).query_attribute('title', request=self.request)
+        return II18n(self.context).query_attribute('title', request=self.request)
 
     def check_mode(self):
+        """Check form's mode according to context's permissions"""
         content = self.getContent()
         # check form permission to get form mode
         if self.edit_permission and not self.request.has_permission(self.edit_permission, content):
@@ -114,52 +124,65 @@
         # check form mode based on context checker
         registry = self.request.registry
         permission = None
-        checker = registry.queryMultiAdapter((content, self.request, self), IFormContextPermissionChecker)
+        checker = registry.queryMultiAdapter((content, self.request, self),
+                                             IFormContextPermissionChecker)
         if checker is None:
             checker = registry.queryAdapter(content, IFormContextPermissionChecker)
         if checker is not None:
             permission = checker.edit_permission
         if permission and (permission != self.edit_permission):
-            if (permission == FORBIDDEN_PERMISSION) or not self.request.has_permission(permission, content):
+            if (permission == FORBIDDEN_PERMISSION) or not self.request.has_permission(permission,
+                                                                                       content):
                 self.mode = DISPLAY_MODE
 
     def update(self):
+        """Update form and all it's subforms"""
         # check form mode
         self.check_mode()
         # update form and sub-forms
-        [subform.update() for subform in self.subforms]
-        [tabform.update() for tabform in self.tabforms]
+        [subform.update() for subform in self.subforms]  # pylint: disable=expression-not-assigned
+        [tabform.update() for tabform in self.tabforms]  # pylint: disable=expression-not-assigned
         Form.update(self)
         # savepoint is required for each inner component to be persisted!!
         transaction.savepoint()
 
     def updateWidgets(self, prefix=None):
+        """Update form's widgets"""
         super(BaseForm, self).updateWidgets(prefix)
         if not self._groups:
             self.updateGroups()
 
     def get_form_action(self):
+        """Get action associated with form"""
         return self.action
 
     @reify
     def subforms(self):
+        """Get subforms adapters associated with this form"""
         registry = self.request.registry
         return sorted((adapter[1]
-                       for adapter in registry.getAdapters((self.context, self.request, self), IInnerSubForm)),
+                       for adapter in
+                       registry.getAdapters((self.context, self.request, self), IInnerSubForm)),
                       key=get_form_weight)
 
     @reify
     def tabforms(self):
+        """Get tabforms adapters associated with this form"""
         registry = self.request.registry
         return sorted((adapter[1]
-                       for adapter in registry.getAdapters((self.context, self.request, self), IInnerTabForm)),
+                       for adapter in
+                       registry.getAdapters((self.context, self.request, self), IInnerTabForm)),
                       key=get_form_weight)
 
     @property
     def forms(self):
+        """Get all forms associated with this form, including the form itself"""
         return [self, ] + self.subforms + self.tabforms
 
     def get_forms(self, include_self=True):
+        """Get forms associated with this form; if *include_self* argument is True, self is also
+        included into results list
+        """
         if include_self:
             yield self
         for group in self.groups:
@@ -173,30 +196,34 @@
 
     @reify
     def warn_on_change(self):
+        """JSON boolean value specifying "warn on change" flag"""
         if self._warn_on_change is True:
             return 'true'
-        elif self._warn_on_change is False:
+        if self._warn_on_change is False:
             return 'false'
-        else:
-            return None
+        return None
 
     @property
     def is_dialog(self):
+        """Boolean flag specifying if current form should be displayed in a modal dialog box"""
         return IDialog.providedBy(self)
 
     def get_widget_callback(self, widget):
+        """Get callback associated with a given widget"""
         return (self.callbacks or {}).get(widget)
 
     # Default z3c.form methods
 
     @property
     def errors(self):
+        """Get form's errors"""
         result = []
         for form in self.forms:
             result.extend(form.widgets.errors)
         return result
 
     def add_error(self, error, widget, status=None):
+        """Add error to current list of form's errors"""
         if isinstance(error, str):
             error = Invalid(error)
         if isinstance(widget, str):
@@ -213,13 +240,16 @@
         self.status += '\n{0}'.format(translate(error.args[0]))
 
     def update_content(self, content, data):
+        """Update content properties with given data"""
         changes = applyChanges(self, content, data.get(self, data))
         for subform in self.get_forms(include_self=False):
             if subform.mode == DISPLAY_MODE:
                 continue
             subform_update = ICustomUpdateSubForm(subform, None)
             if subform_update is not None:
-                updates = subform_update.update_content(subform.getContent(), data.get(subform, data))
+                # pylint: disable=assignment-from-no-return
+                updates = subform_update.update_content(subform.getContent(),
+                                                        data.get(subform, data))
                 if isinstance(updates, dict):
                     changes.update(updates)
             else:
@@ -227,9 +257,13 @@
         return changes
 
     def render(self):
+        """Render form using content template
+
+        Several default templates are defined for all base forms interfaces.
+        """
         request = self.request
         if isinstance(request, PyramidPublisherRequest):
-            request = request._request
+            request = request._request  # pylint: disable=protected-access
         cdict = {
             'context': self.context,
             'request': request,
@@ -242,9 +276,9 @@
             if template is None:
                 template = registry.getMultiAdapter((self, request), IContentTemplate)
             return template(**cdict)
-        return self.template(**cdict)
+        return self.template(**cdict)  # pylint: disable=not-callable
 
-    def __call__(self, **kwargs):
+    def __call__(self, **kwargs):  # pylint: disable=arguments-differ
         self.update()
         if self.request.response.status_code in REDIRECT_STATUS_CODES:
             return Response('')
@@ -266,15 +300,23 @@
             if layout is None:
                 layout = registry.getMultiAdapter((self, request), ILayoutTemplate)
             return Response(layout(**cdict))
-        return Response(self.layout(**cdict))
+        return Response(self.layout(**cdict))  # pylint: disable=not-callable
 
     def get_skin(self, request=None):
+        """Get current skin applied to request"""
+        if request is None:
+            request = self.request
         return request.annotations.get('__skin__')
 
 
 @implementer(IAJAXForm)
 class AJAXForm(BaseForm):
-    """AJAX form base class"""
+    # pylint: disable=abstract-method
+    """AJAX form base class
+
+    An AJAX form is a form which is submitted through an AJAX call; submit response is
+    returned as a JSON object which is handled by MyAMS.
+    """
 
     ajax_handler = FieldProperty(IAJAXForm['ajax_handler'])
     form_options = FieldProperty(IAJAXForm['form_options'])
@@ -282,16 +324,19 @@
     ajax_callback = FieldProperty(IAJAXForm['ajax_callback'])
 
     def get_form_action(self):
+        """Get form's target action URL"""
         return absolute_url(self.context, self.request, self.request.view_name)
 
     def get_form_options(self):
+        """Get form's options"""
         return json.dumps(self.form_options) if self.form_options else None
 
     def get_ajax_handler(self):
+        """Get form's AJAX handler"""
         return absolute_url(self.context, self.request, self.ajax_handler)
 
     def get_ajax_errors(self, ajax_errors=None):
-        """Extract form errors in AJAX format"""
+        """Extract form errors in JSON format"""
         translate = self.request.localizer.translate
         errors = {
             'status': u'error',
@@ -300,8 +345,9 @@
         registry = self.request.registry
         for error in (ajax_errors or self.errors):
             if isinstance(error, Exception):
-                error = registry.getMultiAdapter((error, self.request, None, None, self, self.request),
-                                                 IErrorViewSnippet)
+                error = registry.getMultiAdapter(
+                    (error, self.request, None, None, self, self.request),
+                    IErrorViewSnippet)
             error.update()
             if IMultipleErrors.providedBy(error.error):
                 for inner_error in error.error.errors:
@@ -356,6 +402,7 @@
 #
 
 class AddForm(AJAXForm, BaseAddForm):
+    # pylint: disable=abstract-method
     """Add form base class"""
 
     prefix = 'add_form.'
@@ -373,16 +420,16 @@
     def createAndAdd(self, data):
         registry = self.request.registry
         # create object
-        object = self.create(data.get(self, data))
-        if IPersistent.providedBy(object):
-            registry.notify(ObjectCreatedEvent(object))
+        obj = self.create(data.get(self, data))
+        if IPersistent.providedBy(obj):
+            registry.notify(ObjectCreatedEvent(obj))
             # set parent temporarily to avoid NotYet exceptions
-            locate(object, self.context)
+            locate(obj, self.context)
             # update object properties before adding it
-            self.update_content(object, data)
-            self.add(object)
-            registry.notify(FormObjectCreatedEvent(object, self))
-        return object
+            self.update_content(obj, data)
+            self.add(obj)
+            registry.notify(FormObjectCreatedEvent(obj, self))
+        return obj
 
     def update_content(self, content, data):
         changes = applyChanges(self, content, data.get(self, data))
@@ -391,6 +438,7 @@
                 continue
             subform_update = ICustomUpdateSubForm(subform, None)
             if subform_update is not None:
+                # pylint: disable=assignment-from-no-return
                 updates = subform_update.update_content(content, data.get(subform, data))
                 if isinstance(updates, dict):
                     changes.update(updates)
@@ -405,13 +453,15 @@
 
     @property
     def edit_permission(self):
+        """Get permission associated with this form's context"""
         return self.view.edit_permission
 
 
 class AJAXAddForm(AddForm):
+    # pylint: disable=abstract-method
     """AJAX add form"""
 
-    def __call__(self):
+    def __call__(self):  # pylint: disable=arguments-differ
         self.request.registry.notify(PageletCreatedEvent(self))
         data, errors = {}, ()
         for form in self.get_forms():
@@ -435,8 +485,9 @@
                 except KeyError:
                     continue
                 else:
-                    view = registry.getMultiAdapter((error, self.request, widget, widget.field, self, self.context),
-                                                    IErrorViewSnippet)
+                    view = registry.getMultiAdapter(
+                        (error, self.request, widget, widget.field, self, self.context),
+                        IErrorViewSnippet)
                     view.update()
                     widget.error = view
                     errors = (view,)
@@ -457,22 +508,23 @@
                 form_output = form.get_ajax_output(changes)
                 if form_output:
                     for key, value in form_output.items():
-                        if isinstance(value, (list, tuple)) and (key in output):  # concatenate lists
+                        if isinstance(value, (list, tuple)) and (
+                                key in output):  # concatenate lists
                             form_output[key] += output[key]
                     output.update(form_output)
             except NotImplementedError:
                 pass
         if output:
             return output
-        else:
-            return {
-                'status': 'reload',
-                'location': self.nextURL()
-            }
+        return {
+            'status': 'reload',
+            'location': self.nextURL()
+        }
 
 
 @implementer(IDialog)
 class DialogAddForm(AddForm):
+    # pylint: disable=abstract-method
     """Modal dialog add form"""
 
     buttons = Buttons(IModalAddFormButtons)
@@ -481,6 +533,7 @@
 
 @implementer(IInnerForm)
 class InnerAddForm(AddForm):
+    # pylint: disable=abstract-method
     """Inner add form"""
 
     css_class = 'inner'
@@ -528,7 +581,7 @@
 class AJAXEditForm(EditForm):
     """AJAX edit form"""
 
-    def __call__(self):
+    def __call__(self):  # pylint: disable=arguments-differ,too-many-branches
         # call form elements
         self.request.registry.notify(PageletCreatedEvent(self))
         data, errors = {}, ()
@@ -555,8 +608,9 @@
                 except KeyError:
                     continue
                 else:
-                    view = registry.getMultiAdapter((error, self.request, widget, widget.field, self, self.context),
-                                                    IErrorViewSnippet)
+                    view = registry.getMultiAdapter(
+                        (error, self.request, widget, widget.field, self, self.context),
+                        IErrorViewSnippet)
                     view.update()
                     widget.error = view
                     errors = (view,)
@@ -593,7 +647,8 @@
                 form_output = form.get_ajax_output(changes)
                 if form_output:
                     for key, value in form_output.items():
-                        if isinstance(value, (list, tuple)) and (key in output):  # concatenate lists
+                        if isinstance(value, (list, tuple)) and (
+                                key in output):  # concatenate lists
                             form_output[key] += output[key]
                     output.update(form_output)
             except NotImplementedError:
@@ -667,26 +722,28 @@
 
 @subscriber(IFormCreatedEvent, context_selector=ISkinnable)
 def handle_form_skin(event):
+    """Apply skin on form's creation event"""
     request = _request = event.object.request
     if isinstance(request, PyramidPublisherRequest):
-        _request = request._request
-    skin = ISkinnable(event.object).get_skin(_request)
+        _request = request._request  # pylint: disable=protected-access
+    skin = ISkinnable(event.object).get_skin(_request)  # pylint: disable=assignment-from-no-return
     if skin is not None:
         apply_skin(request, skin)
 
 
-class FormSelector(object):
+class FormSelector:
     """Form event selector
 
     This selector can be used by subscriber to filter form events
     """
 
-    def __init__(self, ifaces, config):
+    def __init__(self, ifaces, config):  # pylint: disable=unused-argument
         if not isinstance(ifaces, (list, tuple)):
             ifaces = (ifaces,)
         self.interfaces = ifaces
 
     def text(self):
+        """Form's selector text"""
         return 'form_selector = %s' % str(self.interfaces)
 
     phash = text
@@ -702,18 +759,19 @@
         return False
 
 
-class WidgetSelector(object):
+class WidgetSelector:
     """Widget event selector
 
     This selector can be used by subscribers to filter widgets events
     """
 
-    def __init__(self, ifaces, config):
+    def __init__(self, ifaces, config):  # pylint: disable=unused-argument
         if not isinstance(ifaces, (list, tuple)):
             ifaces = (ifaces,)
         self.interfaces = ifaces
 
     def text(self):
+        """Widget's selector text"""
         return 'widget_selector = %s' % str(self.interfaces)
 
     phash = text
@@ -729,24 +787,26 @@
         return False
 
 
-class ajax_config(object):
+class ajax_config:  # pylint: disable=invalid-name
     """Class decorator used to declare AJAX settings for a form.
 
-    When decorating a form class, this decorator create a new subclass which will handle AJAX queries
-    executed when submitting the form, and register this class as Pyramid's view.
+    When decorating a form class, this decorator create a new subclass which will handle AJAX
+    queries executed when submitting the form, and register this class as Pyramid's view.
 
     Decorator arguments (all optional) are:
 
-    - **name**: AJAX view name
-    - **context** (or **for_**): view context type
-    - **layer** (or **request_type**): request type for which view is registered
-    - **permission**: permission required to call the view; if not set, permission is extracted from form's
-      "edit_permission" attribute
-    - **base**: base class for newly created AJAX form; if not set, inherits from :py:class:`AJAXEditForm`
-    - **implementer**: list of interfaces implemented by the new class
-    - **method** (or **request_method**): HTTP method name; if not defined, view is restricted to "POST" requests
-    - **renderer**: name of Pyramid renderer used to return view output; defaults to 'json'
-    - **xhr**: Pyramid's view's "xhr" predicate; **True** by default
+    - :param name: AJAX view name
+    - :param context: (or **for_**): view context type
+    - :param layer: (or **request_type**): request type for which view is registered
+    - :param permission: permission required to call the view; if not set, permission is
+          extracted from form's "edit_permission" attribute
+    - :param base: base class for newly created AJAX form; if not set, inherits from
+          :py:class:`AJAXEditForm`
+    - :param implementer: list of interfaces implemented by the new class
+    - :param method: (or **request_method**): HTTP method name; if not defined, view is
+          restricted to "POST" requests
+    - :param renderer: name of Pyramid renderer used to return view output; defaults to 'json'
+    - :param xhr: Pyramid's view's "xhr" predicate; **True** by default
     """
 
     venusian = venusian  # for testing injection
@@ -773,58 +833,57 @@
         settings = self.__dict__.copy()
         depth = settings.pop('_depth', 0)
 
-        def callback(context, name, ob):
+        def callback(context, name, obj):  # pylint: disable=unused-argument
             cdict = {
                 '__name__': settings.get('name'),
-                '__module__': ob.__module__,
-                'permission': settings.get('permission') or ob.edit_permission
+                '__module__': obj.__module__,
+                'permission': settings.get('permission') or obj.edit_permission
             }
 
             # Set form's AJAX handler
-            ob.ajax_handler = settings.get('name')
+            obj.ajax_handler = settings.get('name')
 
             # Create new AJAX form and register view
             base = settings.pop('base')
-            new_class = type('AJAX' + ob.__name__, (base, ob), cdict)
+            new_class = type('AJAX' + obj.__name__, (base, obj), cdict)
             try:
                 # check if current form is overriding "get_ajax_output" method
                 if base not in (AJAXAddForm, AJAXEditForm):  # custom base
-                    ob_ajax_parent, _ = ob.get_ajax_output.__qualname__.split('.')
+                    ob_ajax_parent, _ = obj.get_ajax_output.__qualname__.split('.')
                     base_ajax_parent, _ = base.get_ajax_output.__qualname__.split('.')
                     if (ob_ajax_parent != base_ajax_parent) and \
-                       (ob_ajax_parent in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm')):
+                            (ob_ajax_parent in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm')):
                         new_class.get_ajax_output = base.get_ajax_output
                     else:
-                        new_class.get_ajax_output = ob.get_ajax_output
+                        new_class.get_ajax_output = obj.get_ajax_output
                 else:
-                    base_ajax_parent, _ = ob.get_ajax_output.__qualname__.split('.')
-                    if base_ajax_parent not in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm'):  # overriden method
-                        new_class.get_ajax_output = ob.get_ajax_output
+                    base_ajax_parent, _ = obj.get_ajax_output.__qualname__.split('.')
+                    if base_ajax_parent not in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm'):
+                        # overriden method
+                        new_class.get_ajax_output = obj.get_ajax_output
             except AttributeError:
                 pass
 
             if 'implementer' in settings:
-                implementer = settings.pop('implementer')
-                if not isinstance(implementer, (list, tuple, set)):
-                    implementer = (implementer,)
-                classImplements(new_class, *implementer)
+                impl = settings.pop('implementer')
+                if not isinstance(impl, (list, tuple, set)):
+                    impl = (impl,)
+                classImplements(new_class, *impl)
 
-            logger.debug('Registering AJAX view "{0}" for {1} ({2})'.format(settings.get('name'),
-                                                                            str(settings.get('context', Interface)),
-                                                                            str(new_class)))
+            LOGGER.debug('Registering AJAX view "{0}" for {1} ({2})'.format(
+                settings.get('name'), str(settings.get('context', Interface)), str(new_class)))
 
-            config = context.config.with_package(info.module)
+            config = context.config.with_package(info.module)  # pylint: disable=no-member
             config.add_view(view=new_class, **settings)
 
-        info = self.venusian.attach(wrapped, callback, category='pyams_form',
-                                    depth=depth + 1)
+        info = self.venusian.attach(wrapped, callback, category='pyams_form', depth=depth + 1)
 
-        if info.scope == 'class':
+        if info.scope == 'class':  # pylint: disable=no-member
             # if the decorator was attached to a method in a class, or
             # otherwise executed at class scope, we need to set an
             # 'attr' into the settings if one isn't already in there
             if settings.get('attr') is None:
                 settings['attr'] = wrapped.__name__
 
-        settings['_info'] = info.codeinfo  # fbo "action_method"
+        settings['_info'] = info.codeinfo  # pylint: disable=no-member
         return wrapped
--- a/src/pyams_form/group.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/group.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,28 +10,32 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
+"""PyAMS_form.group module
 
-# import standard library
+This module provides groups management to forms. A group is a set of widgets which are displayed
+together inside a fieldset; widgets which are not explicitly associated with a group are affected
+to a "default" group.
+"""
 
-# import interfaces
+from pyramid.decorator import reify
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
 from pyams_form.interfaces import get_form_weight
 from pyams_form.interfaces.form import IFormWidgetsGroup, IGroupsBasedForm, IInnerSubForm
 from pyams_i18n.interfaces.schema import II18nField
 
-# import packages
-from pyramid.decorator import reify
-from zope.interface import implementer
-from zope.schema.fieldproperty import FieldProperty
+
+__docformat__ = 'restructuredtext'
 
 
 @implementer(IFormWidgetsGroup)
-class FormWidgetsGroup(object):
+class FormWidgetsGroup:
+    # pylint: disable=too-many-instance-attributes
     """Form widgets group"""
 
     form = None
-    id = FieldProperty(IFormWidgetsGroup['id'])
+    id = FieldProperty(IFormWidgetsGroup['id'])  # pylint: disable=invalid-name
     widgets = FieldProperty(IFormWidgetsGroup['widgets'])
     bordered = FieldProperty(IFormWidgetsGroup['bordered'])
     fieldset_class = FieldProperty(IFormWidgetsGroup['fieldset_class'])
@@ -45,10 +49,32 @@
     display_mode = FieldProperty(IFormWidgetsGroup['display_mode'])
     subforms_legend = FieldProperty(IFormWidgetsGroup['subforms_legend'])
 
-    def __init__(self, form, id, widgets=None, bordered=True, fieldset_class=None, legend=None, help=None,
-                 css_class='', switch=False, checkbox_switch=False, checkbox_field=None, checkbox_mode='hide',
-                 display_mode='never'):
-        assert (not checkbox_switch) or checkbox_field, "You must define checkbox field when using checkbox switch"
+    def __init__(self, form, id, widgets=None, bordered=True, fieldset_class=None, legend=None,
+                 help=None, css_class='', switch=False, checkbox_switch=False, checkbox_field=None,
+                 checkbox_mode='hide', display_mode='never'):
+        # pylint: disable=too-many-arguments,redefined-builtin,invalid-name
+        """Form widgets group initialization
+
+        :param form: the form containing the widgets and the group
+        :param id: the unique ID (inside the form) of the group
+        :param widgets: the widgets names list associated with the group
+        :param bordered: if True, the fieldset will have a border
+        :param fieldset_class: custom fieldset CSS class
+        :param legend: fieldset legend label
+        :param help: help text associated with the group
+        :param css_class: custom group CSS class
+        :param switch: if True, the fieldset will be switchable
+        :param checkbox_switch: if True, the switch will be using a checkbox
+        :param checkbox_field: if *checkbox_switch* is True, you must define the name of the
+            field which will store the checkbox value
+        :param checkbox_mode: if set to 'hide', the fieldset content is hidden when the checkbox
+            is switched; if set to 'disable', the fieldset contents are only disabled
+        :param display_mode: if set to 'never', fieldset content is never displayed; if set to
+            'always', fieldset content is always displayed; if set to 'auto', fieldset content is
+            displayed when at least one widget value is not default
+        """
+        assert (not checkbox_switch) or checkbox_field, \
+            "You must define checkbox field when using checkbox switch"
         self.form = form
         self.id = id
         self.widgets = widgets or []
@@ -65,6 +91,7 @@
 
     @property
     def css_class(self):
+        """Group's CSS class"""
         css_class = self._css_class
         if self.switch:
             if self.checkbox_switch:
@@ -75,14 +102,18 @@
 
     @reify
     def checkbox_widget(self):
+        """Get widget associated with checkbox"""
         if self.checkbox_field is None:
             return None
         for widget in self.widgets:
             if widget.field is self.checkbox_field:
                 return widget
+        return None
 
     @reify
     def visible(self):
+        # pylint: disable=too-many-nested-blocks,too-many-branches
+        """Get visible status of group"""
         if self.checkbox_switch:
             widget = self.checkbox_widget
             if self.form.ignoreContext:
@@ -92,81 +123,113 @@
                 name = widget.field.getName()
                 value = getattr(widget.field.interface(context), name, None)
             return bool(value)
-        else:
-            if self.display_mode == 'never':
-                return False
-            elif (not self.switch) or (self.display_mode == 'always'):
-                return True
-            else:  # no switch or auto mode
-                for widget in self.widgets:
-                    if not widget.ignoreContext:
-                        field = widget.field
-                        if self.form.ignoreContext:
-                            value = field.default
-                        else:
-                            context = widget.context
-                            name = field.getName()
-                            value = getattr(field.interface(context), name, None)
-                        if value and (value != field.default):
-                            if II18nField.providedBy(field):
-                                for i18n_value in value.values():
-                                    if i18n_value:
-                                        return True
-                            else:
+        # else: no checkbox switch
+        if self.display_mode == 'never':
+            return False
+        if (not self.switch) or (self.display_mode == 'always'):
+            return True
+        # else: no switch or auto mode
+        for widget in self.widgets:
+            if not widget.ignoreContext:
+                field = widget.field
+                if self.form.ignoreContext:
+                    value = field.default
+                else:
+                    context = widget.context
+                    name = field.getName()
+                    value = getattr(field.interface(context), name, None)
+                if value and (value != field.default):
+                    if II18nField.providedBy(field):
+                        for i18n_value in value.values():
+                            if i18n_value:
                                 return True
-                return False
+                    return True
+        return False
 
     @property
     def visible_widgets(self):
+        """Get list of group's visible widgets"""
         for widget in self.widgets:
             if (self.checkbox_field is None) or (widget.field is not self.checkbox_field):
                 yield widget
 
     @property
     def switchable(self):
+        """Can fieldset be switched?"""
         return self.switch or self.checkbox_switch
 
     @property
     def switcher_state(self):
+        """Get switcher state"""
         return 'open' if self.visible else 'off'
 
     @property
     def checker_state(self):
+        """Get checker state"""
         return 'on' if self.visible else 'off'
 
     @reify
     def subforms(self):
+        """Get list of subforms associated with this group"""
         registry = self.form.request.registry
-        return sorted((adapter[1]
-                       for adapter in registry.getAdapters((self.form.context, self.form.request, self),
-                                                           IInnerSubForm)),
+        return sorted((adapter[1] for adapter in
+                       registry.getAdapters((self.form.context, self.form.request, self),
+                                            IInnerSubForm)),
                       key=get_form_weight)
 
     def get_forms(self):
+        """Get list of subforms (including all sub-sub-forms) associated with this group"""
         for form in self.subforms:
             for subform in form.get_forms():
                 yield subform
 
     def update(self):
+        """Update all subforms on group update"""
+        # pylint: disable=expression-not-assigned
         [subform.update() for subform in self.subforms]
 
     def update_content(self, content, data):
+        """Update content in all subforms"""
+        # pylint: disable=expression-not-assigned
         [subform.update_content(content, data) for subform in self.subforms]
 
 
-def NamedWidgetsGroup(form, id, widgets, names=(), bordered=True, fieldset_class=None, legend=None, help=None,
-                      css_class='', switch=False, checkbox_switch=False, checkbox_field=None, checkbox_mode='hide',
-                      display_mode='never', factory=FormWidgetsGroup):
-    """Create a widgets group based on widgets names"""
+def NamedWidgetsGroup(form, id, widgets, names=(), bordered=True, fieldset_class=None,
+                      legend=None, help=None, css_class='', switch=False, checkbox_switch=False,
+                      checkbox_field=None, checkbox_mode='hide', display_mode='never',
+                      factory=FormWidgetsGroup):
+    # pylint: disable=too-many-arguments,redefined-builtin,invalid-name
+    """Create a widgets group based on widgets names
+
+    :param form: the form containing the widgets and the group
+    :param id: the unique ID (inside the form) of the group
+    :param widgets: the widgets names list associated with the group
+    :param bordered: if True, the fieldset will have a border
+    :param fieldset_class: custom fieldset CSS class
+    :param legend: fieldset legend label
+    :param help: help text associated with the group
+    :param css_class: custom group CSS class
+    :param switch: if True, the fieldset will be switchable
+    :param checkbox_switch: if True, the switch will be using a checkbox
+    :param checkbox_field: if *checkbox_switch* is True, you must define the name of the
+        field which will store the checkbox value
+    :param checkbox_mode: if set to 'hide', the fieldset content is hidden when the checkbox
+        is switched; if set to 'disable', the fieldset contents are only disabled
+    :param display_mode: if set to 'never', fieldset content is never displayed; if set to
+        'always', fieldset content is always displayed; if set to 'auto', fieldset content is
+        displayed when at least one widget value is not default
+    :param factory: widgets group factory
+    """
     if widgets is None:
         form.updateWidgets()
         widgets = form.widgets
-    return factory(form, id, [widgets.get(name) for name in names], bordered, fieldset_class, legend, help,
-                   css_class, switch, checkbox_switch, checkbox_field, checkbox_mode, display_mode)
+    return factory(form, id, [widgets.get(name) for name in names], bordered, fieldset_class,
+                   legend, help, css_class, switch, checkbox_switch, checkbox_field,
+                   checkbox_mode, display_mode)
 
 
 @implementer(IGroupsBasedForm)
-class GroupsBasedForm(object):
+class GroupsBasedForm:
     """Groups based form
 
     Should be used as a base class for forms also implementing IForm
@@ -179,14 +242,20 @@
         self._groups = []
 
     def add_group(self, group):
+        """Add a group to current form's groups"""
         self._groups.append(group)
 
     @reify
     def groups(self):
+        """Get list of groups associated with this form
+
+        If some widgets are associated with any group, a default group is created and located
+        at the end of groups list.
+        """
         result = self._groups[:]
         others = []
-        if self.widgets:
-            for widget in self.widgets.values():
+        if self.widgets:  # pylint: disable=no-member
+            for widget in self.widgets.values():  # pylint: disable=no-member
                 found = False
                 for group in result:
                     if widget in group.widgets:
@@ -200,5 +269,7 @@
                                                   css_class=self.main_group_class))
         return result
 
-    def updateGroups(self):
+    def updateGroups(self):  # pylint: disable=invalid-name
+        """Update form groups"""
+        # pylint: disable=expression-not-assigned
         [group.update() for group in self.groups]
--- a/src/pyams_form/help.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/help.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,37 +10,43 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
+"""PyAMS_form.help module
 
-# import standard library
+This module provides a 'form_help' content provider, which can be used to insert help text
+inside any form using an :py:class:`IFormHelp <pyams_form.interfaces.form.IFormHlep>` adapter.
+"""
 
-# import interfaces
+from zope.interface import Interface, implementer
+from zope.schema.fieldproperty import FieldProperty
+
 from pyams_form.interfaces.form import IFormHelp
 from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces.text import IHTMLRenderer
-
-# import packages
 from pyams_template.template import template_config
 from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
+from pyams_utils.interfaces.text import IHTMLRenderer
 from pyams_utils.text import text_to_html
-from pyams_viewlet.viewlet import contentprovider_config
-from zope.interface import implementer, Interface
-from zope.schema.fieldproperty import FieldProperty
+from pyams_viewlet.viewlet import ViewContentProvider, contentprovider_config
+
+
+__docformat__ = 'restructuredtext'
 
 
 @contentprovider_config(name='form_help', view=Interface, layer=IPyAMSLayer)
 @template_config(template='templates/help.pt', layer=IPyAMSLayer)
-class HelpContentProvider(object):
-    """Form help provider"""
+class HelpContentProvider(ViewContentProvider):
+    """Form help content provider"""
 
     help = None
 
     def update(self):
+        """Update content provider state"""
         registry = self.request.registry
-        help = self.help = registry.queryMultiAdapter((self.context, self.request, self.view), IFormHelp)
+        # pylint: disable=redefined-builtin
+        help = self.help = registry.queryMultiAdapter((self.context, self.request, self.view),
+                                                      IFormHelp)
         if help is not None:
-            if help.permission and not self.request.has_permission(help.permission, context=self.context):
+            if help.permission and not self.request.has_permission(help.permission,
+                                                                   context=self.context):
                 self.help = None
             elif help.mode and (self.view.mode != help.mode):
                 self.help = None
@@ -48,7 +54,7 @@
 
 @implementer(IFormHelp)
 class FormHelp(ContextRequestViewAdapter):
-    """Form help"""
+    """Default form help base implementation"""
 
     permission = FieldProperty(IFormHelp['permission'])
     mode = FieldProperty(IFormHelp['mode'])
@@ -62,8 +68,9 @@
 
 @adapter_config(context=(IFormHelp, IPyAMSLayer, Interface), provides=IHTMLRenderer)
 class HelpRenderer(ContextRequestViewAdapter):
-    """Help renderer"""
+    """Default form help renderer"""
 
-    def render(self, **kwargs):
+    def render(self, **kwargs):  # pylint: disable=unused-argument
+        """Render help text to HTML"""
         message = self.request.localizer.translate(self.context.message)
         return text_to_html(message, self.context.message_format)
--- a/src/pyams_form/include.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/include.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,15 +10,15 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
-# import standard library
+"""PyAMS_form.include package
 
-# import interfaces
+This package if used for Pyramid integration.
+"""
 
-# import packages
 from pyams_form.form import FormSelector, WidgetSelector
 
+__docformat__ = 'restructuredtext'
+
 
 def include_package(config):
     """Pyramid include"""
--- a/src/pyams_form/interfaces/__init__.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/interfaces/__init__.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,15 +10,15 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form.interfaces module
 
-# import standard library
+This namespace module is the base of PyAMS form interfaces.
+"""
 
-# import interfaces
 from .form import IForm, IFormLayer
 
-# import packages
 
+__docformat__ = 'restructuredtext'
 
 def get_form_weight(form):
     """Try to get form weight attribute"""
--- a/src/pyams_form/interfaces/form.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/interfaces/form.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,6 +10,11 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
+"""PyAMS_form.interfaces.form module
+
+This module provides all custom form-related interfaces.
+"""
+
 from pyramid.interfaces import IView
 from z3c.form import button
 from z3c.form.interfaces import IButtonWidget, IFormLayer as IBaseFormLayer, INPUT_MODE, \
@@ -28,7 +33,7 @@
 
 __docformat__ = 'restructuredtext'
 
-from pyams_form import _
+from pyams_form import _  # pylint: disable=ungrouped-imports
 
 
 #
@@ -147,11 +152,8 @@
     def get_widget_callback(self, widget):
         """Get submit callback associated with a given widget"""
 
-    def update_content(self, object, data):
-        """Update given object with form data"""
-
-    def get_submit_output(self, writer, changes):
-        """Get submit output"""
+    def update_content(self, content, data):
+        """Update given content with form data"""
 
 
 class IAJAXForm(Interface):
@@ -192,6 +194,7 @@
     form = Object(title="Parent form",
                   schema=IForm)
 
+    # pylint: disable=invalid-name
     id = TextLine(title="Group ID",
                   required=False)
 
@@ -232,16 +235,16 @@
                              readonly=True)
 
     checkbox_mode = Choice(title="Checkbox mode",
-                           description="""To indicate if fieldset content should be hidden or disabled """
-                                       """when the checkbox if not checked""",
+                           description="To indicate if fieldset content should be hidden or "
+                                       "disabled when the checkbox if not checked",
                            required=True,
                            values=('disable', 'hide'),
                            default='hide')
 
     display_mode = Choice(title="Group display mode",
-                          description="""If 'auto', a switchable group containing only """
-                                      """widgets with default values is hidden; if 'always', group is always 
-                                      visible; if 'never', group is always hidden""",
+                          description="If 'auto', a switchable group containing only "
+                                      "widgets with default values is hidden; if 'always', group "
+                                      "is always visible; if 'never', group is always hidden",
                           values=('auto', 'never', 'always'),
                           required=True,
                           default='never')
@@ -283,7 +286,7 @@
     def add_group(self, group):
         """Add given group to form groups"""
 
-    def updateGroups(self):
+    def updateGroups(self):  # pylint: disable=invalid-name
         """Update inner groups state"""
 
 
@@ -358,7 +361,7 @@
 class ICustomUpdateSubForm(ISubForm):
     """SubForm interface with custom update method"""
 
-    def update_content(self, object, data):
+    def update_content(self, object, data):  # pylint: disable=redefined-builtin
         """Custom content update method"""
 
 
@@ -431,19 +434,19 @@
     in some custom contexts to handle form custom settings.
     """
 
-    def getFields(self):
+    def getFields(self):  # pylint: disable=invalid-name
         """Get form fields"""
 
-    def update(self):
+    def update(self):  # pylint: disable=invalid-name
         """Update form"""
 
-    def updateWidgets(self, prefix=None):
+    def updateWidgets(self, prefix=None):  # pylint: disable=invalid-name
         """Update form widgets"""
 
-    def updateActions(self):
+    def updateActions(self):  # pylint: disable=invalid-name
         """Update form actions"""
 
-    def updateGroups(self):
+    def updateGroups(self):  # pylint: disable=invalid-name
         """Update form groups"""
 
 
@@ -459,7 +462,7 @@
         if widget.mode == INPUT_MODE:
             return True
     if IForm.providedBy(form):
-        for subform in (form.subforms + form.tabforms):
+        for subform in form.subforms + form.tabforms:
             for widget in (subform.widgets or {}).values():
                 if widget.mode == INPUT_MODE:
                     return True
@@ -630,7 +633,7 @@
 class FormObjectCreatedEvent(ObjectCreatedEvent):
     """Form object created event"""
 
-    def __init__(self, object, form):
+    def __init__(self, object, form):  # pylint: disable=redefined-builtin
         super(FormObjectCreatedEvent, self).__init__(object)
         self.form = form
 
@@ -643,6 +646,6 @@
 class FormObjectModifiedEvent(ObjectModifiedEvent):
     """Form object modified event"""
 
-    def __init__(self, object, form, *descriptions):
+    def __init__(self, object, form, *descriptions):  # pylint: disable=redefined-builtin
         ObjectModifiedEvent.__init__(self, object, *descriptions)
         self.form = form
--- a/src/pyams_form/schema.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/schema.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,19 +10,19 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form.schema module
+
+This module provides an interface and a schema field for form buttons.
+"""
+
+from z3c.form.button import Button
+from z3c.form.interfaces import IButton
+from zope.interface import implementer
+from zope.schema import Bool, TextLine
+from zope.schema.fieldproperty import FieldProperty
 
 
-# import standard library
-
-# import interfaces
-from z3c.form.interfaces import IButton
-
-# import packages
-from z3c.form.button import Button
-from zope.interface import implementer
-from zope.schema import TextLine, Bool
-from zope.schema.fieldproperty import FieldProperty
+__docformat__ = 'restructuredtext'
 
 
 class IResetButton(IButton):
@@ -44,7 +44,12 @@
 
 
 class IActionButton(IButton):
-    """Action button interface"""
+    """Action button interface
+
+    An action button is a form button which can be used to redirect the user to a "target" URL,
+    eventually opened in a modal window, or which can call a "click" handler which is the name of
+    a javascript function.
+    """
 
     label = TextLine(title="Button label",
                      description="Button label displayed as hint",
--- a/src/pyams_form/search.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/search.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,30 +10,31 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form.search module
 
+This module provides a few helpers which can be used in search forms.
+"""
 
-# import standard library
+from pyramid.response import Response
+from z3c.form import button, field
+from z3c.table.interfaces import IValues
+from zope.interface import Interface, implementer
+from zope.schema import TextLine
 
-# import interfaces
-from pyams_form.interfaces.form import IWidgetForm, ISearchForm
+from pyams_form.form import AddForm
+from pyams_form.interfaces.form import ISearchForm, IWidgetForm
+from pyams_form.schema import ResetButton
 from pyams_skin.interfaces import IContentSearch, IInnerPage, ISearchPage
 from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from z3c.table.interfaces import IValues
-
-# import packages
-from pyams_form.form import AddForm
-from pyams_form.schema import ResetButton
 from pyams_skin.table import BaseTable
 from pyams_template.template import template_config
 from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
-from pyramid.response import Response
-from z3c.form import field, button
-from zope.interface import implementer, Interface
-from zope.schema import TextLine
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+
 
-from pyams_form import _
+__docformat__ = 'restructuredtext'
+
+from pyams_form import _  # pylint: disable=ungrouped-imports
 
 
 class ISearchFields(Interface):
@@ -52,6 +53,7 @@
 
 @implementer(IWidgetForm, ISearchForm)
 class SearchForm(AddForm):
+    # pylint: disable=abstract-method
     """Base search form"""
 
     legend = _("Search")
@@ -70,6 +72,9 @@
             self.actions['search'].addClass('btn-primary')
 
     def get_search_results(self):
+        """Get search results via an
+        :py:class:`IContentSearch <pyams_skin.interfaces.IContentSearch>` adapter
+        """
         registry = self.request.registry
         search = registry.queryMultiAdapter((self.context, self.request, self), IContentSearch)
         if search is None:
@@ -78,18 +83,22 @@
             search = IContentSearch(self.context, None)
         if search is not None:
             self.updateWidgets()
-            data, errors = self.extractData()
+            data, errors = self.extractData()  # pylint: disable=unused-variable
             return search.get_search_results(data)
+        return None
 
 
 @template_config(template='templates/search.pt', layer=IPyAMSLayer)
 @implementer(IInnerPage, ISearchPage)
-class SearchView(object):
+class SearchView:
     """Base search view"""
 
     search_form_factory = SearchForm
+    search_form = None
 
     def update(self):
+        """Update search view"""
+        # pylint: disable=no-member
         self.search_form = self.search_form_factory(self.context, self.request)
         self.search_form.update()
 
@@ -110,5 +119,6 @@
 
     @property
     def values(self):
+        """Get view search results"""
         form = self.view.search_form_factory(self.context, self.request)
         return form.get_search_results()
--- a/src/pyams_form/security.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/security.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,16 +10,15 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form.security module
 
-
-# import standard library
+"""
 
-# import interfaces
-from pyams_form.interfaces.form import IFormSecurityContext, IFormContextPermissionChecker
+from pyramid.decorator import reify
 
-# import packages
-from pyramid.decorator import reify
+from pyams_form.interfaces.form import IFormContextPermissionChecker, IFormSecurityContext
+
+__docformat__ = 'restructuredtext'
 
 
 def get_edit_permission(request, context=None):
@@ -32,22 +31,42 @@
         checker = registry.queryAdapter(context, IFormContextPermissionChecker)
     if checker is not None:
         return checker.edit_permission
+    return None
 
 
-class ProtectedFormObjectMixin(object):
-    """Form object protected by a permission"""
+class ProtectedFormObjectMixin:
+    """Form object protected by a permission
+
+    A "protected" form is a form on which you apply a permission; the context on which the security
+    applies can be provided by an :py:class:`IFormSecurityContext
+    <pyams_form.interfaces.IFormSecurityContext>` adapter, or will be extracted for the form
+    context itself.
+
+    The permission itself will be provided by an adapter to :py:class:`IFormContextPermissionChecker
+    <pyams_form.interfaces.IFormContextPermissionChecker>`
+    :
+
+    This class is a form mixin class which should be used for forms protected by a
+    security context.
+    """
 
     @reify
     def permission(self):
-        registry = self.request.registry
+        """This permission is required to be able to edit the form context"""
+        request = self.request  # pylint: disable=no-member
+        registry = request.registry
         checker = None
         context = IFormSecurityContext(self, None)
         if context is None:
-            context = self.context
-        view = getattr(self, '__parent__', None) or getattr(self, 'view', None) or getattr(self, 'table', None)
+            context = self.context  # pylint: disable=no-member
+        view = getattr(self, '__parent__', None) or \
+            getattr(self, 'view', None) or \
+            getattr(self, 'table', None)
         if view is not None:
-            checker = registry.queryMultiAdapter((context, self.request, view), IFormContextPermissionChecker)
+            checker = registry.queryMultiAdapter((context, request, view),
+                                                 IFormContextPermissionChecker)
         if checker is None:
             checker = registry.queryAdapter(context, IFormContextPermissionChecker)
         if checker is not None:
             return checker.edit_permission
+        return None
--- a/src/pyams_form/terms.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/terms.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,7 +10,11 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form.terms module
+
+This module is used only to provide a custom ITerms adapter providing translations for
+boolean terms.
+"""
 
 from z3c.form.interfaces import IBoolTerms, IFormLayer, ITerms, IWidget
 from z3c.form.term import BoolTerms as BaseBoolTerms
@@ -19,6 +23,9 @@
 
 from pyams_utils.adapter import adapter_config
 
+
+__docformat__ = 'restructuredtext'
+
 from pyams_form import _
 
 
--- a/src/pyams_form/tests/__init__.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/tests/__init__.py	Wed Dec 04 11:05:25 2019 +0100
@@ -1,1 +1,31 @@
+# -*- coding: utf-8 -*- ######################################################
+##############################################################################
+#
+# Copyright (c) 2008-2010 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
 
+"""
+Generic Test case for pyams_form doctest
+"""
+__docformat__ = 'restructuredtext'
+
+import os
+import sys
+
+
+def get_package_dir(value):
+    """Get package directory"""
+
+    package_dir = os.path.split(value)[0]
+    if package_dir not in sys.path:
+        sys.path.append(package_dir)
+    return package_dir
--- a/src/pyams_form/tests/test_utilsdocs.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/tests/test_utilsdocs.py	Wed Dec 04 11:05:25 2019 +0100
@@ -1,5 +1,7 @@
+# -*- coding: utf-8 -*- ######################################################
+##############################################################################
 #
-# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# Copyright (c) 2008-2010 Thierry Florac <tflorac AT ulthar.net>
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -9,22 +11,26 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
+##############################################################################
 
 """
-Generic Test case for pyams_form doctest
+Generic test case for pyams_form doctests
 """
+
 __docformat__ = 'restructuredtext'
 
+import doctest
+import os
 import unittest
-import doctest
-import sys
-import os
+
+from pyams_utils.tests import get_package_dir
 
 
-current_dir = os.path.dirname(__file__)
+CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
+
 
-def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):
-    """Returns a test suite, based on doctests found in /doctest."""
+def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):  # pylint: disable=invalid-name
+    """Returns a test suite, based on doctests found in /doctests"""
     suite = []
     if globs is None:
         globs = globals()
@@ -32,15 +38,12 @@
     flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
              doctest.REPORT_ONLY_FIRST_FAILURE)
 
-    package_dir = os.path.split(test_dir)[0]
-    if package_dir not in sys.path:
-        sys.path.append(package_dir)
-
+    package_dir = get_package_dir(test_dir)
     doctest_dir = os.path.join(package_dir, 'doctests')
 
     # filtering files on extension
     docs = [os.path.join(doctest_dir, doc) for doc in
-            os.listdir(doctest_dir) if doc.endswith('.txt')]
+            os.listdir(doctest_dir) if doc.endswith('.txt') or doc.endswith('.rst')]
 
     for test in docs:
         suite.append(doctest.DocFileSuite(test, optionflags=flags,
@@ -50,10 +53,11 @@
 
     return unittest.TestSuite(suite)
 
+
 def test_suite():
     """returns the test suite"""
-    return doc_suite(current_dir)
+    return doc_suite(CURRENT_DIR)
+
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-
--- a/src/pyams_form/tests/test_utilsdocstrings.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/tests/test_utilsdocstrings.py	Wed Dec 04 11:05:25 2019 +0100
@@ -11,17 +11,20 @@
 #
 
 """
-Generic Test case for pyams_form doc strings
+Generic test case for pyams_form docstrings
 """
+
 __docformat__ = 'restructuredtext'
 
+import doctest
+import os
 import unittest
-import doctest
-import sys
-import os
+
+from pyams_utils.tests import get_package_dir
 
 
-current_dir = os.path.abspath(os.path.dirname(__file__))
+CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
+
 
 def doc_suite(test_dir, globs=None):
     """Returns a test suite, based on doc tests strings found in /*.py"""
@@ -32,9 +35,7 @@
     flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
              doctest.REPORT_ONLY_FIRST_FAILURE)
 
-    package_dir = os.path.split(test_dir)[0]
-    if package_dir not in sys.path:
-        sys.path.append(package_dir)
+    package_dir = get_package_dir(test_dir)
 
     # filtering files on extension
     docs = [doc for doc in
@@ -42,7 +43,7 @@
     docs = [doc for doc in docs if not doc.startswith('__')]
 
     for test in docs:
-        fd = open(os.path.join(package_dir, test))
+        fd = open(os.path.join(package_dir, test))  # pylint: disable=invalid-name
         content = fd.read()
         fd.close()
         if '>>> ' not in content:
@@ -54,9 +55,11 @@
 
     return unittest.TestSuite(suite)
 
+
 def test_suite():
     """returns the test suite"""
-    return doc_suite(current_dir)
+    return doc_suite(CURRENT_DIR)
+
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
--- a/src/pyams_form/viewlet.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/viewlet.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,20 +10,21 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
+"""PyAMS_form.viewlet module
+
+This module provides several viewlet managers which are used into default forms templates.
+"""
+
+from zope.interface import implementer
+
+from pyams_form.interfaces.form import IFormHeaderViewletsManager, IFormLayer, \
+    IFormPrefixViewletsManager, IFormSuffixViewletsManager, IFormToolbarViewletsManager, \
+    IFormViewletsManager, IWidgetsPrefixViewletsManager, IWidgetsSuffixViewletsManager
+from pyams_template.template import get_view_template, template_config
+from pyams_viewlet.manager import WeightOrderedViewletManager, viewletmanager_config
 
 
-# import standard library
-
-# import interfaces
-from pyams_form.interfaces.form import IFormViewletsManager, IFormPrefixViewletsManager, IWidgetsPrefixViewletsManager, \
-    IWidgetsSuffixViewletsManager, IFormSuffixViewletsManager, IFormLayer, IFormHeaderViewletsManager, \
-    IFormToolbarViewletsManager
-
-# import packages
-from pyams_template.template import template_config, get_view_template
-from pyams_viewlet.manager import WeightOrderedViewletManager, viewletmanager_config
-from zope.interface import implementer
+__docformat__ = 'restructuredtext'
 
 
 @implementer(IFormViewletsManager)
--- a/src/pyams_form/widget/__init__.py	Tue Nov 19 16:30:58 2019 +0100
+++ b/src/pyams_form/widget/__init__.py	Wed Dec 04 11:05:25 2019 +0100
@@ -10,6 +10,17 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
+"""PyAMS_form.widget module
+
+This module provides several custom widgets used inside PyAMS, but also default templates
+customized for MyAMS.
+
+This module also provides several decorators, which can be used to define widgets templates
+in place of ZCML declarations.
+
+**WARNING**: please note that widgets templates used via z3c.form package use default
+"""
+
 import inspect
 import json
 import locale
@@ -54,14 +65,14 @@
 
 __docformat__ = 'restructuredtext'
 
-from pyams_form import _
+from pyams_form import _  # pylint: disable=ungrouped-imports
 
 
 #
 # Widget template configuration
 #
 
-class widgettemplate_config(object):
+class widgettemplate_config:  # pylint: disable=invalid-name
     """Class decorator used to declare a widget template"""
 
     venusian = venusian  # for testing injection
@@ -75,37 +86,40 @@
     def __call__(self, wrapped):
         settings = self.__dict__.copy()
 
-        def callback(context, name, ob):
-            template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(ob))),
+        def callback(context, name, obj):  # pylint: disable=unused-argument
+            template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(obj))),
                                     settings.get('template'))
             if not os.path.isfile(template):
                 raise ConfigurationError("No such file", template)
 
-            contentType = settings.get('contentType', 'text/html')
-            factory = WidgetTemplateFactory(template, contentType)
+            content_type = settings.get('contentType', 'text/html')
+            factory = WidgetTemplateFactory(template, content_type)
             provides = settings.get('provides', IPageTemplate)
             directlyProvides(factory, provides)
 
-            config = context.config.with_package(info.module)
-            config.registry.registerAdapter(factory,
-                                            (settings.get('context', Interface),
-                                             settings.get('layer', IRequest),
-                                             settings.get('view', None),
-                                             settings.get('field', None),
-                                             settings.get('widget', ob)),
-                                            provides,
-                                            settings.get('mode', INPUT_MODE))
+            registry = settings.get('registry')
+            if registry is None:
+                config = context.config.with_package(info.module)  # pylint: disable=no-member
+                registry = config.registry
+            registry.registerAdapter(factory,
+                                     (settings.get('context', Interface),
+                                      settings.get('layer', IRequest),
+                                      settings.get('view', None),
+                                      settings.get('field', None),
+                                      settings.get('widget', obj)),
+                                     provides,
+                                     settings.get('mode', INPUT_MODE))
 
         info = self.venusian.attach(wrapped, callback, category='pyams_form')
 
-        if info.scope == 'class':
+        if info.scope == 'class':  # pylint: disable=no-member
             # if the decorator was attached to a method in a class, or
             # otherwise executed at class scope, we need to set an
             # 'attr' into the settings if one isn't already in there
             if settings.get('attr') is None:
                 settings['attr'] = wrapped.__name__
 
-        settings['_info'] = info.codeinfo  # fbo "action_method"
+        settings['_info'] = info.codeinfo  # pylint: disable=no-member
         return wrapped
 
 
@@ -120,8 +134,8 @@
                             settings.get('template'))
     if not os.path.isfile(template):
         raise ConfigurationError("No such file", template)
-    contentType = settings.get('contentType', 'text/html')
-    factory = WidgetTemplateFactory(template, contentType)
+    content_type = settings.get('contentType', 'text/html')
+    factory = WidgetTemplateFactory(template, content_type)
     provides = settings.get('provides', IPageTemplate)
     directlyProvides(factory, provides)
     registry = getGlobalSiteManager()
@@ -135,7 +149,7 @@
                              settings.get('mode', INPUT_MODE))
 
 
-class widgetlayout_config(object):
+class widgetlayout_config:  # pylint: disable=invalid-name
     """Class decorator used to declare a widget layout"""
 
     venusian = venusian  # for testing injection
@@ -149,37 +163,40 @@
     def __call__(self, wrapped):
         settings = self.__dict__.copy()
 
-        def callback(context, name, ob):
-            template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(ob))),
+        def callback(context, name, obj):  # pylint: disable=unused-argument
+            template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(obj))),
                                     settings.get('template'))
             if not os.path.isfile(template):
                 raise ConfigurationError("No such file", template)
 
-            contentType = settings.get('contentType', 'text/html')
-            factory = WidgetLayoutFactory(template, contentType)
+            content_type = settings.get('contentType', 'text/html')
+            factory = WidgetLayoutFactory(template, content_type)
             provides = settings.get('provides', IWidgetLayoutTemplate)
             directlyProvides(factory, provides)
 
-            config = context.config.with_package(info.module)
-            config.registry.registerAdapter(factory,
-                                            (settings.get('context', Interface),
-                                             settings.get('layer', IRequest),
-                                             settings.get('view', None),
-                                             settings.get('field', None),
-                                             settings.get('widget', ob)),
-                                            provides,
-                                            settings.get('mode', INPUT_MODE))
+            registry = settings.get('registry')
+            if registry is None:
+                config = context.config.with_package(info.module)  # pylint: disable=no-member
+                registry = config.registry
+            registry.registerAdapter(factory,
+                                     (settings.get('context', Interface),
+                                      settings.get('layer', IRequest),
+                                      settings.get('view', None),
+                                      settings.get('field', None),
+                                      settings.get('widget', obj)),
+                                     provides,
+                                     settings.get('mode', INPUT_MODE))
 
         info = self.venusian.attach(wrapped, callback, category='pyams_form')
 
-        if info.scope == 'class':
+        if info.scope == 'class':  # pylint: disable=no-member
             # if the decorator was attached to a method in a class, or
             # otherwise executed at class scope, we need to set an
             # 'attr' into the settings if one isn't already in there
             if settings.get('attr') is None:
                 settings['attr'] = wrapped.__name__
 
-        settings['_info'] = info.codeinfo  # fbo "action_method"
+        settings['_info'] = info.codeinfo  # pylint: disable=no-member
         return wrapped
 
 
@@ -194,8 +211,8 @@
                             settings.get('template'))
     if not os.path.isfile(template):
         raise ConfigurationError("No such file", template)
-    contentType = settings.get('contentType', 'text/html')
-    factory = WidgetTemplateFactory(template, contentType)
+    content_type = settings.get('contentType', 'text/html')
+    factory = WidgetTemplateFactory(template, content_type)
     provides = settings.get('provides', IWidgetLayoutTemplate)
     directlyProvides(factory, provides)
     registry = getGlobalSiteManager()
@@ -224,7 +241,8 @@
 
 
 @adapter_config(context=(IResetButton, IFormLayer), provides=IFieldWidget)
-def ResetFieldWidget(field, request):
+def ResetFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Reset buton field widget factory"""
     reset = FieldWidget(field, ResetWidget(request))
     reset.value = field.title
     return reset
@@ -235,8 +253,9 @@
     """Reset button action"""
 
     def __init__(self, request, field):
-        Action.__init__(self, request, field.title)
-        ResetWidget.__init__(self, request)
+        # pylint: disable=super-init-not-called
+        Action.__init__(self, request, field.title)  # pylint: disable=non-parent-init-called
+        ResetWidget.__init__(self, request)  # pylint: disable=non-parent-init-called
         self.field = field
 
 
@@ -255,7 +274,8 @@
 
 
 @adapter_config(context=(ICloseButton, IFormLayer), provides=IFieldWidget)
-def CloseFieldWidget(field, request):
+def CloseFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Close button field widget factory"""
     close = FieldWidget(field, CloseWidget(request))
     close.value = field.title
     return close
@@ -266,8 +286,9 @@
     """Close button action"""
 
     def __init__(self, request, field):
-        Action.__init__(self, request, field.title)
-        CloseWidget.__init__(self, request)
+        # pylint: disable=super-init-not-called
+        Action.__init__(self, request, field.title)  # pylint: disable=non-parent-init-called
+        CloseWidget.__init__(self, request)  # pylint: disable=non-parent-init-called
         self.field = field
 
 
@@ -288,7 +309,8 @@
 
 
 @adapter_config(context=(IActionButton, IFormLayer), provides=IFieldWidget)
-def ActionFieldWidget(field, request):
+def ActionFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Action button field widget factory"""
     action = FieldWidget(field, ActionWidget(request))
     action.value = field.title
     return action
@@ -299,8 +321,9 @@
     """Action button action"""
 
     def __init__(self, request, field):
+        # pylint: disable=super-init-not-called
         ActionWidget.__init__(self, request)
-        Action.__init__(self, request, field.title)
+        Action.__init__(self, request, field.title)  # pylint: disable=non-parent-init-called
         self.field = field
         self.label_css_class = field.label_css_class
         self.click_handler = field.click_handler
@@ -319,9 +342,11 @@
     error_message = _("Invalid UTF-8 encoded data")
 
     def toWidgetValue(self, value):
+        """Convert bytes to string"""
         return value.decode('utf-8') if isinstance(value, bytes) else value
 
     def toFieldValue(self, value):
+        """Convert string to bytes"""
         return value.encode('utf-8') if isinstance(value, str) else value
 
 
@@ -340,11 +365,13 @@
     error_message = _("Invalid integer value")
 
     def toWidgetValue(self, value):
+        """Convert integer value to string"""
         if value is self.field.missing_value:
             return ''
         return locale.format_string('%d', value, grouping=True)
 
     def toFieldValue(self, value):
+        """Convert string to integer"""
         if not value:
             return self.field.missing_value
         try:
@@ -360,21 +387,25 @@
 
     @property
     def validate_rules(self):
+        """Add validation rules to JQuery-validate plug-in"""
         conv = locale.localeconv()
         allow_negative = True
         if (self.field.min is not None) and (self.field.min >= 0):
             allow_negative = False
-        rules = {'pattern': '{}[\d{}]+{}'.format(
-            '\{}?'.format(conv.get('negative_sign', '-'))
+        rules = {
+            'pattern': r'{}[\d{}]+{}'.format(
+                r'\{}?'.format(conv.get('negative_sign', '-'))
                 if (allow_negative and (conv.get('n_sign_posn') in SIGN_BEFORE_VALUE)) else '',
-            conv.get('thousands_sep', ''),
-            '\{}?'.format(conv.get('negative_sign', '-'))
-                if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')}
+                conv.get('thousands_sep', ''),
+                r'\{}?'.format(conv.get('negative_sign', '-'))
+                if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')
+        }
         return json.dumps(rules)
 
 
 @adapter_config(context=(IInt, IFormLayer), provides=IFieldWidget)
-def IntegerFieldWidget(field, request):
+def IntegerFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Integer field widget factory"""
     return FieldWidget(field, IntegerWidget(request))
 
 
@@ -389,11 +420,13 @@
     error_message = _("Invalid floating value")
 
     def toWidgetValue(self, value):
+        """Convert float to string"""
         if value is self.field.missing_value:
             return ''
         return locale.format_string('%.{0}f'.format(self.widget.precision), value, grouping=True)
 
     def toFieldValue(self, value):
+        """Convert string to float"""
         if not value:
             return self.field.missing_value
         try:
@@ -411,23 +444,27 @@
 
     @property
     def validate_rules(self):
+        """Add validation rules to JQuery-validate plug-in"""
         conv = locale.localeconv()
         allow_negative = True
         if (self.field.min is not None) and (self.field.min >= 0):
             allow_negative = False
-        rules = {'pattern': '{}[\d{}]+{}?\d*{}'.format(
-            '\{}?'.format(conv.get('negative_sign', '-'))
+        rules = {
+            'pattern': r'{}[\d{}]+{}?\d*{}'.format(
+                r'\{}?'.format(conv.get('negative_sign', '-'))
                 if (allow_negative and (conv.get('n_sign_posn') in SIGN_BEFORE_VALUE)) else '',
-            conv.get('thousands_sep', ''),
-            conv.get('decimal_point', '\.'),
-            '\{}?'.format(conv.get('negative_sign', '-'))
-                if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')}
+                conv.get('thousands_sep', ''),
+                conv.get('decimal_point', r'\.'),
+                r'\{}?'.format(conv.get('negative_sign', '-'))
+                if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')
+        }
         return json.dumps(rules)
 
 
 @adapter_config(context=(IFloat, IFormLayer), provides=IFieldWidget)
 @adapter_config(context=(IDecimal, IFormLayer), provides=IFieldWidget)
-def FloatFieldWidget(field, request):
+def FloatFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Float field widget factory"""
     return FieldWidget(field, FloatWidget(request))
 
 
@@ -438,28 +475,33 @@
 
     @property
     def validate_rules(self):
+        """Add validation rules to JQuery-validate plug-in"""
         conv = locale.localeconv()
         allow_negative = True
         if (self.field.min is not None) and (self.field.min >= 0):
             allow_negative = False
-        rules = {'pattern': '{}[\d{}]+{}?\d*{}'.format(
-            '\{}?'.format(conv.get('negative_sign', '-'))
+        rules = {
+            'pattern': r'{}[\d{}]+{}?\d*{}'.format(
+                r'\{}?'.format(conv.get('negative_sign', '-'))
                 if (allow_negative and (conv.get('n_sign_posn') in SIGN_BEFORE_VALUE)) else '',
-            '',    # no thousands separator
-            '\.',  # dot as decimal separator !!
-            '\{}?'.format(conv.get('negative_sign', '-'))
-                if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')}
+                '',  # no thousands separator
+                r'\.',  # dot as decimal separator !!
+                r'\{}?'.format(conv.get('negative_sign', '-'))
+                if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')
+        }
         return json.dumps(rules)
 
     @property
     def validate_messages(self):
+        """Get validation message for pattern rule"""
         return json.dumps({
             'pattern': self.request.localizer.translate(DottedDecimalDataConverter.errorMessage)
         })
 
 
 @adapter_config(context=(IDottedDecimalField, IFormLayer), provides=IFieldWidget)
-def DottedDecimalFieldWidget(field, request):
+def DottedDecimalFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Dotted decimal field widget factory"""
     return FieldWidget(field, DottedDecimalWidget(request))
 
 
@@ -474,7 +516,8 @@
 
 
 @adapter_config(context=(IDate, IFormLayer), provides=IFieldWidget)
-def DateFieldWidget(field, request):
+def DateFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Date field widget factory"""
     return FieldWidget(field, DateWidget(request))
 
 
@@ -487,10 +530,12 @@
     """Datetime field data converter"""
 
     def toWidgetValue(self, value):
+        """Convert datetime to string"""
         value = tztime(value)
         return super(DatetimeDataConverter, self).toWidgetValue(value)
 
     def toFieldValue(self, value):
+        """Convert string to local datetime"""
         value = super(DatetimeDataConverter, self).toFieldValue(value)
         return localgmtime(value)
 
@@ -502,7 +547,8 @@
 
 
 @adapter_config(context=(IDatetime, IFormLayer), provides=IFieldWidget)
-def DatetimeFieldWidget(field, request):
+def DatetimeFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Datetime field widget factory"""
     return FieldWidget(field, DatetimeWidget(request))
 
 
@@ -517,7 +563,8 @@
 
 
 @adapter_config(context=(ITime, IFormLayer), provides=IFieldWidget)
-def TimeFieldWidget(field, request):
+def TimeFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Time field widget factory"""
     return FieldWidget(field, TimeWidget(request))
 
 
@@ -530,12 +577,14 @@
     """Color field data converter"""
 
     def toWidgetValue(self, value):
+        """Convert color to string"""
         value = super(ColorDataConverter, self).toWidgetValue(value)
         if value:
             value = '#' + value
         return value
 
     def toFieldValue(self, value):
+        """Convert string to color value"""
         if value and value.startswith('#'):
             value = value[1:]
         return super(ColorDataConverter, self).toFieldValue(value)
@@ -548,7 +597,8 @@
 
 
 @adapter_config(context=(IColorField, IFormLayer), provides=IFieldWidget)
-def ColorFieldWidget(field, request):
+def ColorFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Color field widget factory"""
     return FieldWidget(field, ColorWidget(request))
 
 
@@ -564,22 +614,26 @@
 
     @property
     def editor_data(self):
+        """Get editor data"""
         registry = self.request.registry
-        config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration, name=self.name)
+        config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration,
+                                            name=self.name)
         if (config is None) and hasattr(self, 'basename'):  # I18n widget
-            config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration, name=self.basename)
+            config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration,
+                                                name=self.basename)  # pylint: disable=no-member
         if config is None:
             config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration)
         if config is None:
             config = getattr(self, 'editor_config_data', None)
             if config is not None:
                 return json.dumps(config)
-        else:
-            return json.dumps(config.configuration)
+            return None
+        return json.dumps(config.configuration)
 
 
 @adapter_config(context=(IHTMLField, IFormLayer), provides=IFieldWidget)
-def HTMLFieldWidget(field, request):
+def HTMLFieldWidget(field, request):  # pylint: disable=invalid-name
+    """HTML editor field widget factory"""
     return FieldWidget(field, HTMLWidget(request))
 
 
@@ -592,9 +646,11 @@
     """JSON dict data converter"""
 
     def toWidgetValue(self, value):
+        """Convert JSON dict to string"""
         return value
 
     def toFieldValue(self, value):
+        """Load JSON from string"""
         return json.loads(value)
 
 
@@ -604,6 +660,7 @@
     """JSON dict field widget"""
 
     def get_fields(self):
+        """Get fields from JSON mapping"""
         form = self.__parent__.form
         getter = self.request.registry.queryMultiAdapter((form.getContent(), self.request, form),
                                                          IJSONDictFieldsGetter)
@@ -614,7 +671,7 @@
 
 
 @adapter_config(context=(IJSONDictField, IFormLayer), provides=IFieldWidget)
-def JSONDictFieldWidget(field, request):
+def JSONDictFieldWidget(field, request):  # pylint: disable=invalid-name
     """JSON dict field widget factory"""
     return FieldWidget(field, JSONDictWidget(request))
 
@@ -623,8 +680,10 @@
 # Select2 widget
 #
 
-@widgettemplate_config(mode=INPUT_MODE, template='templates/select-input.pt', layer=IFormLayer)
-@widgettemplate_config(mode=DISPLAY_MODE, template='templates/select-display.pt', layer=IFormLayer)
+@widgettemplate_config(mode=INPUT_MODE, template='templates/select-input.pt',
+                       layer=IFormLayer)
+@widgettemplate_config(mode=DISPLAY_MODE, template='templates/select-display.pt',
+                       layer=IFormLayer)
 @implementer_only(ISelect2Widget)
 class Select2Widget(SelectWidget):
     """Select2 widget"""
@@ -632,17 +691,21 @@
     noValueMessage = _("(no selected value)")
 
     def get_content(self, entry):
+        """Get translated entry content"""
         translate = self.request.localizer.translate
         return translate(entry['content'])
 
 
 @adapter_config(context=(IChoice, IFormLayer), provides=IFieldWidget)
-def ChoiceFieldWidget(field, request):
+def ChoiceFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Choice field widget factory"""
     return FieldWidget(field, Select2Widget(request))
 
 
-@widgettemplate_config(mode=INPUT_MODE, template='templates/hidden-select-input.pt', layer=IFormLayer)
-@widgettemplate_config(mode=DISPLAY_MODE, template='templates/hidden-select-display.pt', layer=IFormLayer)
+@widgettemplate_config(mode=INPUT_MODE, template='templates/hidden-select-input.pt',
+                       layer=IFormLayer)
+@widgettemplate_config(mode=DISPLAY_MODE, template='templates/hidden-select-display.pt',
+                       layer=IFormLayer)
 @implementer(IObjectData)
 class HiddenSelect2Widget(Select2Widget):
     """Hidden select2 widget
@@ -651,6 +714,7 @@
     """
 
     def extract(self, default=NO_VALUE):
+        """Extract terms from input value"""
         if self.name not in self.request and self.name + '-empty-marker' in self.request:
             return []
         value = self.request.get(self.name, default)
@@ -669,17 +733,21 @@
 
     @property
     def values(self):
+        """Convert values list to string"""
         return '|'.join(self.value or '')
 
     @property
     def values_map(self):
+        """Create JSON mapping from selected values"""
         result = {}
         terms = self.terms
+        # pylint: disable=expression-not-assigned
         [result.update({value: terms.getTermByToken(value).title}) for value in self.value or ()]
         return json.dumps(result)
 
 
-def HiddenSelect2FieldWidget(field, request):
+def HiddenSelect2FieldWidget(field, request):  # pylint: disable=invalid-name
+    """Hidden Select2 field widget factory"""
     return FieldWidget(field, HiddenSelect2Widget(request))
 
 
@@ -690,9 +758,11 @@
     """Hidden select2 data converter"""
 
     def toWidgetValue(self, value):
+        """Convert selection to string"""
         return value or ()
 
     def toFieldValue(self, value):
+        """Create set from selected values"""
         return set(value or ())
 
 
@@ -705,25 +775,31 @@
     """Text line list field data converter"""
 
     def toWidgetValue(self, value):
+        """Convert values list to string"""
         return '|'.join(value or [])
 
     def toFieldValue(self, value):
+        """Create list from string"""
         return value.split('|') if value else None
 
 
-@widgettemplate_config(mode=INPUT_MODE, template='templates/textlinelist-input.pt', layer=IFormLayer)
-@widgettemplate_config(mode=DISPLAY_MODE, template='templates/textlinelist-display.pt', layer=IFormLayer)
+@widgettemplate_config(mode=INPUT_MODE, template='templates/textlinelist-input.pt',
+                       layer=IFormLayer)
+@widgettemplate_config(mode=DISPLAY_MODE, template='templates/textlinelist-display.pt',
+                       layer=IFormLayer)
 @implementer_only(ITextLineListWidget)
 class TextLineListWidget(TextWidget):
     """Text line list widget"""
 
     @property
     def tags(self):
+        """Create string from input value"""
         return json.dumps((self.value or '').split('|'))
 
 
 @adapter_config(context=(ITextLineListField, IFormLayer), provides=IFieldWidget)
-def TextLineListFieldWidget(field, request):
+def TextLineListFieldWidget(field, request):  # pylint: disable=invalid-name
+    """Textlines list field widget"""
     return FieldWidget(field, TextLineListWidget(request))
 
 
@@ -731,17 +807,22 @@
 # TextLine widget with SEO length status
 #
 
-@widgettemplate_config(mode=INPUT_MODE, template='templates/seo-textline-input.pt', layer=IFormLayer)
+@widgettemplate_config(mode=INPUT_MODE, template='templates/seo-textline-input.pt',
+                       layer=IFormLayer)
 @implementer_only(ISEOTextLineWidget)
 class SEOTextLineWidget(TextWidget):
     """SEO textline widget"""
 
     @property
     def length(self):
+        """Get current length of text"""
         return len(self.value or '')
 
     @property
     def status(self):
+        """Get widget status based on text length; a "good" length is between 40 and 66
+        characters
+        """
         status = 'success'
         length = self.length
         if length < 20 or length > 80:
@@ -751,5 +832,6 @@
         return status
 
 
-def SEOTextLineFieldWidget(field, request):
+def SEOTextLineFieldWidget(field, request):  # pylint: disable=invalid-name
+    """SEO textline field widget factory"""
     return FieldWidget(field, SEOTextLineWidget(request))