Code cleanup
authorThierry Florac <tflorac@ulthar.net>
Wed, 27 Nov 2019 19:37:28 +0100
changeset 12 fc3542685741
parent 11 bd5143e87b1d
child 13 ccb2abb60b46
Code cleanup
src/pyams_pagelet/__init__.py
src/pyams_pagelet/doctests/README.rst
src/pyams_pagelet/doctests/README.txt
src/pyams_pagelet/interfaces.py
src/pyams_pagelet/interfaces/__init__.py
src/pyams_pagelet/metaconfigure.py
src/pyams_pagelet/metadirectives.py
src/pyams_pagelet/pagelet.py
src/pyams_pagelet/tests/__init__.py
src/pyams_pagelet/tests/test_utilsdocs.py
src/pyams_pagelet/tests/test_utilsdocstrings.py
--- a/src/pyams_pagelet/__init__.py	Mon Jun 11 17:30:43 2018 +0200
+++ b/src/pyams_pagelet/__init__.py	Wed Nov 27 19:37:28 2019 +0100
@@ -10,8 +10,19 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
+"""PyAMS_pagelet package
+
+This package is an update of z3c.pagelet package for use with Pyramid.
+
+Pagelets use :py:mod:`pyams_template` package, which participate in separation of Python view code
+and their template implementation, to split an HTML view template into two parts: a content
+template and a layout template. It also provides a "pagelet" ZCML directive which can be used to
+register pagelets, as well as a "pagelet_config" decorator which can be used alternatively.
+"""
 
 from pyramid.i18n import TranslationStringFactory
+
+
 _ = TranslationStringFactory('pyams_pagelet')
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_pagelet/doctests/README.rst	Wed Nov 27 19:37:28 2019 +0100
@@ -0,0 +1,121 @@
+
+=====================
+pyams_pagelet package
+=====================
+
+Let's start by creating a new template:
+
+    >>> from pyramid.testing import setUp, tearDown
+    >>> from pyams_utils.request import get_annotations
+    >>> config = setUp()
+    >>> config.add_request_method(get_annotations, 'annotations', reify=True)
+
+    >>> import os, tempfile
+    >>> temp_dir = tempfile.mkdtemp()
+
+    >>> content_template = os.path.join(temp_dir, 'content-template.pt')
+    >>> with open(content_template, 'w') as file:
+    ...     _ = file.write('<div>Base template content</div>')
+
+    >>> layout_template = os.path.join(temp_dir, 'layout-template.pt')
+    >>> with open(layout_template, 'w') as file:
+    ...     _ = file.write('''
+    ... <html>
+    ...   <body>
+    ...     <div class="layout">${structure:view.render()}</div>
+    ...   </body>
+    ... </html>
+    ... ''')
+
+The templates must now be registered for a view and a request. We use the TemplateFactory directly
+here from *pyams_template* package, while it may be done using a *template_config* decorator:
+
+    >>> from zope.interface import implementer, Interface
+    >>> from pyramid.interfaces import IRequest
+    >>> from pyams_template.interfaces import IContentTemplate, ILayoutTemplate
+
+    >>> from pyams_template.template import TemplateFactory
+    >>> factory = TemplateFactory(content_template, 'text/html')
+    >>> config.registry.registerAdapter(factory, (Interface, IRequest), IContentTemplate)
+
+    >>> factory = TemplateFactory(layout_template, 'text/html')
+    >>> config.registry.registerAdapter(factory, (Interface, IRequest), ILayoutTemplate)
+
+Let's now create a pagelet view:
+
+    >>> class IMyView(Interface):
+    ...     """View marker interface"""
+
+    >>> from pyams_pagelet.pagelet import Pagelet
+    >>> @implementer(IMyView)
+    ... class MyView(Pagelet):
+    ...     """View class"""
+
+    >>> from pyramid.testing import DummyRequest
+    >>> content = object()
+    >>> request = DummyRequest()
+    >>> view = MyView(content, request)
+    >>> print(view.render())
+    <div>Base template content</div>
+
+    >>> print(view())
+    200 OK
+    Content-Type: text/html; charset=UTF-8
+    Content-Length: 98
+    <BLANKLINE>
+    <html>
+      <body>
+        <div class="layout"><div>Base template content</div></div>
+      </body>
+    </html>
+    <BLANKLINE>
+
+But the standard way of using a pagelet is by using the "pagelet:" TALES expression:
+
+    >>> pagelet_template = os.path.join(temp_dir, 'pagelet-template.pt')
+    >>> with open(pagelet_template, 'w') as file:
+    ...     _ = file.write('''
+    ... <html>
+    ...   <body>
+    ...     <div class="pagelet">${structure:provider:pagelet}</div>
+    ...   </body>
+    ... </html>
+    ... ''')
+
+This template will be registered using the custom view interface:
+
+    >>> from chameleon import PageTemplateFile
+    >>> from pyams_viewlet.provider import ProviderExpr
+    >>> PageTemplateFile.expression_types['provider'] = ProviderExpr
+
+    >>> factory = TemplateFactory(pagelet_template, 'text/html')
+    >>> config.registry.registerAdapter(factory, (IMyView, IRequest), ILayoutTemplate)
+
+    >>> try:
+    ...     view()
+    ... except Exception as e:
+    ...     print(repr(e))
+    ContentProviderLookupError('pagelet...)
+
+This exception is raised because the pagelet is not yet registered; this should be done
+automatically when *pyams_pagelet* package is included into Pyramid configuration:
+
+    >>> from zope.contentprovider.interfaces import IContentProvider
+    >>> from pyams_pagelet.interfaces import IPagelet
+    >>> from pyams_pagelet.pagelet import PageletRenderer
+    >>> config.registry.registerAdapter(PageletRenderer,
+    ...                                 (Interface, IRequest, IPagelet),
+    ...                                 IContentProvider, name='pagelet')
+    >>> print(view())
+    200 OK
+    Content-Type: text/html; charset=UTF-8
+    Content-Length: 99
+    <BLANKLINE>
+    <html>
+      <body>
+        <div class="pagelet"><div>Base template content</div></div>
+      </body>
+    </html>
+    <BLANKLINE>
+
+    >>> tearDown()
--- a/src/pyams_pagelet/doctests/README.txt	Mon Jun 11 17:30:43 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-=====================
-pyams_pagelet package
-=====================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_pagelet/interfaces.py	Wed Nov 27 19:37:28 2019 +0100
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2008-2015 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.
+#
+
+"""PyAMS_pagelet.interfaces module
+
+"""
+
+from pyramid.interfaces import IView
+from zope.contentprovider.interfaces import IContentProvider
+from zope.interface import Attribute, implementer
+from zope.interface.interfaces import IObjectEvent, ObjectEvent
+
+
+__docformat__ = 'restructuredtext'
+
+
+class IPagelet(IView):
+    """Pagelet interface"""
+
+    def update(self):
+        """Update the pagelet data."""
+
+    def render(self):
+        """Render the pagelet content w/o o-wrap."""
+
+
+class IPageletRenderer(IContentProvider):
+    """Render a pagelet by calling it's 'render' method"""
+
+
+class IPageletCreatedEvent(IObjectEvent):
+    """Pagelet created event interface"""
+
+    request = Attribute('The request object')
+
+
+@implementer(IPageletCreatedEvent)
+class PageletCreatedEvent(ObjectEvent):
+    """Pagelet created event"""
+
+    def __init__(self, object):  # pylint: disable=redefined-builtin
+        super(PageletCreatedEvent, self).__init__(object)
+        self.request = object.request
--- a/src/pyams_pagelet/interfaces/__init__.py	Mon Jun 11 17:30:43 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-#
-# Copyright (c) 2008-2015 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.
-#
-
-__docformat__ = 'restructuredtext'
-
-# import standard packages
-
-# import interfaces
-from pyramid.interfaces import IView
-from zope.component.interfaces import IObjectEvent, ObjectEvent
-from zope.contentprovider.interfaces import IContentProvider
-
-# import packages
-from zope.interface import implementer, Attribute
-
-
-class IPagelet(IView):
-    """Pagelet interface"""
-
-    def update(self):
-        """Update the pagelet data."""
-
-    def render(self):
-        """Render the pagelet content w/o o-wrap."""
-
-
-class IPageletRenderer(IContentProvider):
-    """Render a pagelet by calling it's 'render' method"""
-
-
-class IPageletCreatedEvent(IObjectEvent):
-    """Pagelet created event interface"""
-
-    request = Attribute('The request object')
-
-
-@implementer(IPageletCreatedEvent)
-class PageletCreatedEvent(ObjectEvent):
-    """Pagelet created event"""
-
-    def __init__(self, object):
-        super(PageletCreatedEvent, self).__init__(object)
-        self.request = object.request
--- a/src/pyams_pagelet/metaconfigure.py	Mon Jun 11 17:30:43 2018 +0200
+++ b/src/pyams_pagelet/metaconfigure.py	Wed Nov 27 19:37:28 2019 +0100
@@ -10,32 +10,36 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-
-# import standard packages
+"""PyAMS_pagelet.metaconfigure module
 
-# import interfaces
-from pyams_pagelet.interfaces import IPagelet
-from pyramid.interfaces import IRequest
+This module provides handlers for ZCML directives.
+"""
 
-# import packages
-from pyams_pagelet.pagelet import Pagelet
 from pyramid.exceptions import ConfigurationError
+from pyramid.interfaces import IRequest
 from pyramid_zcml import with_context
 from zope.component import zcml
 from zope.component.interface import provideInterface
 from zope.interface import Interface, classImplements
 
+from pyams_pagelet.interfaces import IPagelet
+from pyams_pagelet.pagelet import Pagelet
+
 
 def PageletDirective(_context, name, view,
                      context=Interface,
                      permission=None,
                      layer=IRequest,
                      **kwargs):
+    # pylint: disable=invalid-name
+    """Pagelet ZCML directive"""
+
     if not view:
-        raise ConfigurationError("You must specify a view class.")
-    cdict = {}
-    cdict['__name__'] = name
-    cdict['permission'] = permission
+        raise ConfigurationError("You must specify a view class or interface")
+    cdict = {
+        '__name__': name,
+        'permission': permission
+    }
     cdict.update(kwargs)
     new_class = type(view.__name__, (view, Pagelet), cdict)
 
--- a/src/pyams_pagelet/metadirectives.py	Mon Jun 11 17:30:43 2018 +0200
+++ b/src/pyams_pagelet/metadirectives.py	Wed Nov 27 19:37:28 2019 +0100
@@ -10,19 +10,19 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
+"""PyAMS_paget.metadirectives module
 
-# import standard packages
+This module provides interface of ZCML directives.
+"""
 
-# import interfaces
-
-# import packages
 from zope.configuration.fields import GlobalObject
 from zope.interface import Interface
 from zope.schema import TextLine
 
 
+__docformat__ = 'restructuredtext'
+
+
 class IPageletDirective(Interface):
     """Pagelet ZCML directive interface"""
 
@@ -34,7 +34,7 @@
                            required=False)
 
     layer = GlobalObject(title="The request interface or class this pagelet is for",
-                         description="Defaults to zope.publisher.interfaces.browser.IDefaultBrowserLayer.",
+                         description="Defaults to pyramid.interfaces.IRequest",
                          required=False)
 
     view = GlobalObject(title='View class',
@@ -46,4 +46,5 @@
                           required=False)
 
 
+# pylint: disable=vo-value-for-parameter
 IPageletDirective.setTaggedValue('keyword_arguments', True)
--- a/src/pyams_pagelet/pagelet.py	Mon Jun 11 17:30:43 2018 +0200
+++ b/src/pyams_pagelet/pagelet.py	Wed Nov 27 19:37:28 2019 +0100
@@ -10,34 +10,36 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
+"""PyAMS_pagelet.pagelet module
 
-# import standard packages
+This module provides the core pagelet implementation, and a "pagelet_config" decorator which
+can be use to register pagelets instead of ZCML directives.
+"""
+
 import logging
-logger = logging.getLogger('PyAMS (pagelet)')
 
 import venusian
+from pyramid.httpexceptions import HTTPUnauthorized
+from pyramid.interfaces import IRequest
+from pyramid.response import Response
+from pyramid_chameleon.interfaces import IChameleonTranslate
+from zope.component import queryUtility
+from zope.interface import Interface, implementer
 
-# import interfaces
 from pyams_pagelet.interfaces import IPagelet, IPageletRenderer, PageletCreatedEvent
 from pyams_template.interfaces import IContentTemplate, ILayoutTemplate
-from pyramid.interfaces import IRequest
-from pyramid_chameleon.interfaces import IChameleonTranslate
+from pyams_utils.adapter import adapter_config
+
 
-# import packages
-from pyams_utils.adapter import adapter_config
-from pyramid.httpexceptions import HTTPUnauthorized
-from pyramid.response import Response
-from zope.component import queryUtility
-from zope.interface import implementer, Interface
+__docformat__ = 'restructuredtext'
 
+LOGGER = logging.getLogger('PyAMS (pagelet)')
 
 REDIRECT_STATUS_CODES = (301, 302, 303)
 
 
 @implementer(IPagelet)
-class Pagelet(object):
+class Pagelet:
     """Content provider with layout support"""
 
     template = None
@@ -53,14 +55,20 @@
         request.registry.notify(PageletCreatedEvent(self))
 
     def update(self):
-        self.request.annotations['view'] = self
+        """See `zope.contentprovider.interfaces.IContentProvider`"""
+        annotations = getattr(self.request, 'annotations', None)
+        if annotations is not None:
+            annotations['view'] = self
 
     def render(self):
+        """See `zope.contentprovider.interfaces.IContentProvider`"""
         request = self.request
-        cdict = {'context': self.context,
-                 'request': request,
-                 'view': self,
-                 'translate': queryUtility(IChameleonTranslate)}
+        cdict = {
+            'context': self.context,
+            'request': request,
+            'view': self,
+            'translate': queryUtility(IChameleonTranslate)
+        }
         if self.template is None:
             registry = request.registry
             template = registry.queryMultiAdapter((self, request, self.context),
@@ -68,7 +76,7 @@
             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):
         """Call update and return layout template"""
@@ -77,10 +85,12 @@
             return ''
 
         request = self.request
-        cdict = {'context': self.context,
-                 'request': request,
-                 'view': self,
-                 'translate': queryUtility(IChameleonTranslate)}
+        cdict = {
+            'context': self.context,
+            'request': request,
+            'view': self,
+            'translate': queryUtility(IChameleonTranslate)
+        }
         cdict.update(kwargs)
         if self.layout is None:
             registry = request.registry
@@ -89,11 +99,11 @@
             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
 
 
 @adapter_config(name='pagelet', context=(Interface, IRequest, IPagelet), provides=IPageletRenderer)
-class PageletRenderer(object):
+class PageletRenderer:
     """Pagelet renderer"""
 
     def __init__(self, context, request, pagelet):
@@ -103,13 +113,14 @@
         self.request = request
 
     def update(self):
-        pass
+        """See `zope.contentprovider.interfaces.IContentProvider`"""
 
     def render(self):
+        """See `zope.contentprovider.interfaces.IContentProvider`"""
         return self.__parent__.render()
 
 
-class pagelet_config(object):
+class pagelet_config:  # pylint: disable=invalid-name
     """Function or class decorator used to declare a pagelet"""
 
     venusian = venusian  # for testing injection
@@ -126,33 +137,34 @@
         settings = self.__dict__.copy()
         depth = settings.pop('_depth', 0)
 
-        def callback(context, name, ob):
+        def callback(context, name, obj):  # pylint: disable=unused-argument
+            """Venusian decorator callback"""
             cdict = {
                 '__name__': settings.get('name'),
-                '__module__': ob.__module__,
+                '__module__': obj.__module__,
                 'permission': settings.get('permission')
             }
-            new_class = type(ob.__name__, (ob, Pagelet), cdict)
+            new_class = type(obj.__name__, (obj, Pagelet), cdict)
 
-            config = context.config.with_package(info.module)
-            logger.debug('Registering pagelet view "{0}" for {1} ({2})'.format(settings.get('name'),
-                                                                               str(settings.get('context', Interface)),
-                                                                               str(new_class)))
-            config.registry.registerAdapter(new_class,
-                                            (settings.get('context', Interface),
-                                             settings.get('request_type', IRequest)),
-                                            IPagelet, settings.get('name'))
+            LOGGER.debug('Registering pagelet view "{0}" for {1} ({2})'.format(
+                settings.get('name'), str(settings.get('context', Interface)), str(new_class)))
+            config = context.config.with_package(info.module)  # pylint: disable=no-member
+            registry = settings.get('registry') or config.registry
+            registry.registerAdapter(new_class,
+                                     (settings.get('context', Interface),
+                                      settings.get('request_type', IRequest)),
+                                     IPagelet, settings.get('name'))
             config.add_view(view=new_class, **settings)
 
         info = self.venusian.attach(wrapped, callback, category='pyams_pagelet',
                                     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_pagelet/tests/__init__.py	Mon Jun 11 17:30:43 2018 +0200
+++ b/src/pyams_pagelet/tests/__init__.py	Wed Nov 27 19:37:28 2019 +0100
@@ -1,1 +1,29 @@
+#
+# Copyright (c) 2008-2015 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_pagelet doctest
+"""
+
+import os
+import sys
+
+__docformat__ = 'restructuredtext'
+
+
+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_pagelet/tests/test_utilsdocs.py	Mon Jun 11 17:30:43 2018 +0200
+++ b/src/pyams_pagelet/tests/test_utilsdocs.py	Wed Nov 27 19:37:28 2019 +0100
@@ -14,18 +14,20 @@
 Generic Test case for pyams_pagelet doctest
 """
 
+import doctest
+import os
+import unittest
+
+from pyams_pagelet.tests import get_package_dir
+
+
 __docformat__ = 'restructuredtext'
 
-import unittest
-import doctest
-import sys
-import os
+CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
 
 
-current_dir = 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()
@@ -33,15 +35,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,
@@ -51,10 +50,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_pagelet/tests/test_utilsdocstrings.py	Mon Jun 11 17:30:43 2018 +0200
+++ b/src/pyams_pagelet/tests/test_utilsdocstrings.py	Wed Nov 27 19:37:28 2019 +0100
@@ -14,16 +14,17 @@
 Generic Test case for pyams_pagelet doc strings
 """
 
+import doctest
+import os
+import sys
+import unittest
+
+
 __docformat__ = 'restructuredtext'
 
-import unittest
-import doctest
-import sys
-import os
+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"""
     suite = []
@@ -43,7 +44,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:
@@ -55,9 +56,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')