# HG changeset patch # User Thierry Florac # Date 1574445097 -3600 # Node ID cf2304af0fabbcfe9b55e79eda92f476fe2afa76 # Parent 0037199881fb69d5191402f5c42837e61a83be23 Gitlab CI integration and Pylint code cleanup diff -r 0037199881fb -r cf2304af0fab .gitlab-ci.yml --- a/.gitlab-ci.yml Wed Nov 20 19:26:23 2019 +0100 +++ b/.gitlab-ci.yml Fri Nov 22 18:51:37 2019 +0100 @@ -1,8 +1,9 @@ image: python:3.5 stages: - - build - test + - dist + - quality before_script: - export http_proxy=http://172.17.0.1:3128/ @@ -10,26 +11,44 @@ - export https_proxy=http://172.17.0.1:3128/ - export HTTPS_PROXY=http://172.17.0.1:3128/ -build: - stage: build +bootstrap: + stage: .pre script: - python3.5 bootstrap.py --buildout-version=2.12.0 - ./bin/buildout + +test: + stage: test + script: - ./bin/test +dist: + stage: dist + script: + - ./bin/buildout setup setup.py clean --all sdist bdist_egg bdist_wheel + artifacts: + paths: + - ./dist pylint: - stage: test + stage: quality allow_failure: true script: - - pip install pylint --quiet - - pylint src/pyams_utils/ + - pip install pylint-exit anybadge + - mkdir ./pylint + - ./bin/pylint src/pyams_utils/ | tee ./pylint/pylint.log || pylint-exit $? + - PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log) + - anybadge --label=Pylint --file=./pylint/pylint.svg --value=$PYLINT_SCORE 2=red 4=orange 8=yellow 10=green + - echo "Pylint score is $PYLINT_SCORE" + artifacts: + paths: + - ./pylint/ quality: - stage: test + stage: quality + allow_failure: true image: docker:stable variables: DOCKER_DRIVER: overlay2 - allow_failure: true services: - docker:stable-dind script: diff -r 0037199881fb -r cf2304af0fab .pylintrc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.pylintrc Fri Nov 22 18:51:37 2019 +0100 @@ -0,0 +1,407 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=2 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=import-star-module-level,old-octal-literal,oct-method,inherit-non-class,logging-format-interpolation,too-many-ancestors,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=10 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=yes + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff -r 0037199881fb -r cf2304af0fab buildout.cfg --- a/buildout.cfg Wed Nov 20 19:26:23 2019 +0100 +++ b/buildout.cfg Fri Nov 22 18:51:37 2019 +0100 @@ -27,43 +27,12 @@ package i18n pyflakes + pylint test [package] recipe = zc.recipe.egg -eggs = - babel - BTrees - chameleon - docutils - httplib2 - persistent - pyams_utils - pyramid - pyramid_zodbconn - pysocks - pytz - transaction - z3c.form - z3c.pt - z3c.ptcompat - ZEO - ZODB - zope.annotation - zope.component - zope.container - zope.contentprovider - zope.datetime - zope.dublincore - zope.interface - zope.intid - zope.keyreference - zope.lifecycleevent - zope.location - zope.publisher - zope.schema - zope.site - zope.traversing +eggs = pyams_utils interpreter = py [i18n] @@ -84,6 +53,12 @@ on_install = true cmds = ${buildout:develop}/bin/${pyflakes:scripts} +[pylint] +recipe = zc.recipe.egg +eggs = pylint +entry-points = pylint=pylint.lint:Run +arguments = sys.argv[1:] + [test] recipe = zc.recipe.testrunner eggs = pyams_utils [test] diff -r 0037199881fb -r cf2304af0fab docs/HISTORY.txt --- a/docs/HISTORY.txt Wed Nov 20 19:26:23 2019 +0100 +++ b/docs/HISTORY.txt Fri Nov 22 18:51:37 2019 +0100 @@ -1,6 +1,10 @@ Changelog ========= +0.1.35 +------ + - Pylint code cleanup and GitLab-CI integration updates + 0.1.34.2 -------- - correction in "generate_url" function diff -r 0037199881fb -r cf2304af0fab setup.py --- a/setup.py Wed Nov 20 19:26:23 2019 +0100 +++ b/setup.py Fri Nov 22 18:51:37 2019 +0100 @@ -65,28 +65,38 @@ # -*- Extra requirements: -*- 'babel', 'beaker', + 'BTrees', 'chameleon', 'docutils', + 'fanstatic', 'httplib2', 'markdown', 'persistent', + 'pygments', 'pyramid', 'pyramid_zodbconn', 'pyramid_zope_request', 'pysocks', 'pytz', 'transaction', + 'venusian', 'z3c.form', 'z3c.pt', 'z3c.ptcompat', + 'ZEO', 'ZODB', 'zope.annotation', 'zope.component', 'zope.container', - 'zope.dublincore', + 'zope.contentprovider', 'zope.datetime', + 'zope.dublincore', 'zope.interface', + 'zope.intid', + 'zope.keyreference', + 'zope.lifecycleevent', 'zope.location', + 'zope.publisher', 'zope.schema', 'zope.site', 'zope.traversing' diff -r 0037199881fb -r cf2304af0fab src/pyams_utils.egg-info/SOURCES.txt --- a/src/pyams_utils.egg-info/SOURCES.txt Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils.egg-info/SOURCES.txt Fri Nov 22 18:51:37 2019 +0100 @@ -51,6 +51,7 @@ src/pyams_utils.egg-info/top_level.txt src/pyams_utils/doctests/README.txt src/pyams_utils/doctests/dates.txt +src/pyams_utils/doctests/inherit.txt src/pyams_utils/doctests/request.txt src/pyams_utils/doctests/unicode.txt src/pyams_utils/interfaces/__init__.py diff -r 0037199881fb -r cf2304af0fab src/pyams_utils.egg-info/requires.txt --- a/src/pyams_utils.egg-info/requires.txt Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils.egg-info/requires.txt Fri Nov 22 18:51:37 2019 +0100 @@ -1,27 +1,38 @@ setuptools babel beaker +BTrees chameleon docutils +fanstatic httplib2 markdown persistent +pygments pyramid pyramid_zodbconn +pyramid_zope_request pysocks pytz transaction +venusian z3c.form z3c.pt z3c.ptcompat +ZEO ZODB zope.annotation zope.component zope.container -zope.dublincore +zope.contentprovider zope.datetime +zope.dublincore zope.interface +zope.intid +zope.keyreference +zope.lifecycleevent zope.location +zope.publisher zope.schema zope.site zope.traversing diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/__init__.py --- a/src/pyams_utils/__init__.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/__init__.py Fri Nov 22 18:51:37 2019 +0100 @@ -18,24 +18,23 @@ custom decorators to define several kinds of properties, and so on. """ +from pyramid.i18n import TranslationStringFactory +from zope.schema.fieldproperty import FieldProperty + + __docformat__ = 'restructuredtext' -from zope.schema.fieldproperty import FieldProperty - -from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory('pyams_utils') def get_field_doc(self): """Try to get FieldProperty field docstring from field interface""" - field = self._FieldProperty__field + field = self._FieldProperty__field # pylint: disable=protected-access if field.title: if field.description: return '{0}: {1}'.format(field.title, field.description) - else: - return field.title - else: - return super(self.__class__, self).__doc__ + return field.title + return super(self.__class__, self).__doc__ FieldProperty.__doc__ = property(get_field_doc) @@ -43,5 +42,5 @@ def includeme(config): """pyams_utils features include""" - from .include import include_package + from .include import include_package # pylint: disable=import-outside-toplevel include_package(config) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/adapter.py --- a/src/pyams_utils/adapter.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/adapter.py Fri Nov 22 18:51:37 2019 +0100 @@ -12,8 +12,8 @@ """Adapters management package -This package provides a small set of standard base adapters for *context*, *context* and *request*, and -*context* and *request* and *view*. +This package provides a small set of standard base adapters for *context*, *context* and *request*, +and *context* and *request* and *view*. See :ref:`zca` to see how PyAMS can help components management. """ @@ -27,22 +27,22 @@ from zope.location import locate as zope_locate from pyams_utils.factory import get_object_factory, is_interface -from pyams_utils.registry import get_current_registry +from pyams_utils.registry import get_current_registry, get_global_registry __docformat__ = 'restructuredtext' -logger = logging.getLogger('PyAMS (utils)') +LOGGER = logging.getLogger('PyAMS (utils)') -class ContextAdapter(object): +class ContextAdapter: """Context adapter""" def __init__(self, context): self.context = context -class ContextRequestAdapter(object): +class ContextRequestAdapter: """Context + request multi-adapter""" def __init__(self, context, request): @@ -50,7 +50,7 @@ self.request = request -class ContextRequestViewAdapter(object): +class ContextRequestViewAdapter: """Context + request + view multi-adapter""" def __init__(self, context, request, view): @@ -59,17 +59,17 @@ self.view = view -class NullAdapter(object): +class NullAdapter: """An adapter which always return None! Can be useful to override a default adapter... """ - def __new__(cls, *arsg, **kwargs): + def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument return None -class adapter_config(object): +class adapter_config: # pylint: disable=invalid-name """Function or class decorator to declare an adapter Annotation parameters can be: @@ -77,6 +77,7 @@ :param str='' name: name of the adapter :param [Interface...] context: an interface, or a tuple of interfaces, that the component adapts :param Interface provides: the interface that the adapter provides + :param registry: the registry into which adapter registration should be made """ venusian = venusian @@ -91,48 +92,49 @@ settings = self.__dict__.copy() depth = settings.pop('_depth', 0) - def callback(context, name, ob): + def callback(context, name, obj): adapts = settings.get('context') if adapts is None: - adapts = getattr(ob, '__component_adapts__', None) + adapts = getattr(obj, '__component_adapts__', None) if adapts is None: raise TypeError("No for argument was provided for %r and " - "can't determine what the factory adapts." % ob) + "can't determine what the factory adapts." % obj) if not isinstance(adapts, tuple): adapts = (adapts,) provides = settings.get('provides') if provides is None: - intfs = list(implementedBy(ob)) + intfs = list(implementedBy(obj)) if len(intfs) == 1: provides = intfs[0] if provides is None: raise TypeError("Missing 'provides' argument") - config = context.config.with_package(info.module) - logger.debug("Registering adapter {0} for {1} providing {2}".format(str(ob), - str(adapts), - str(provides))) - config.registry.registerAdapter(ob, adapts, provides, settings.get('name', '')) + config = context.config.with_package(info.module) # pylint: disable=no-member + LOGGER.debug("Registering adapter %s for %s providing %s", + str(obj), str(adapts), str(provides)) + registry = settings.get('registry', config.registry) + registry.registerAdapter(obj, adapts, provides, settings.get('name', '')) info = self.venusian.attach(wrapped, callback, category='pyams_adapter', 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 def get_annotation_adapter(context, key, factory=None, markers=None, notify=True, locate=True, parent=None, name=None, callback=None, **kwargs): + # pylint: disable=too-many-arguments """Get an adapter via object's annotations, creating it if not existent - + :param object context: context object which should be adapted :param str key: annotations key to look for :param factory: if annotations key is not found, this is the factory which will be used to @@ -151,28 +153,27 @@ annotations = IAnnotations(context, None) if annotations is None: return None - adapter = annotations.get(key) + adapter = annotations.get(key) # pylint: disable=assignment-from-no-return if adapter is None: if 'default' in kwargs: return kwargs['default'] - elif factory is None: + if factory is None: return None - else: - if is_interface(factory): - factory = get_object_factory(factory) - assert factory is not None, "Missing object factory" - adapter = annotations[key] = factory() - if markers: - if not isinstance(markers, (list, tuple, set)): - markers = {markers} - for marker in markers: - alsoProvides(adapter, marker) - if notify: - get_current_registry().notify(ObjectCreatedEvent(adapter)) - if locate: - zope_locate(adapter, context if parent is None else parent, name) - if callback: - callback(adapter) + if is_interface(factory): + factory = get_object_factory(factory) + assert factory is not None, "Missing object factory" + adapter = annotations[key] = factory() + if markers: + if not isinstance(markers, (list, tuple, set)): + markers = {markers} + for marker in markers: + alsoProvides(adapter, marker) + if notify: + get_current_registry().notify(ObjectCreatedEvent(adapter)) + if locate: + zope_locate(adapter, context if parent is None else parent, name) + if callback: + callback(adapter) return adapter diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/attr.py --- a/src/pyams_utils/attr.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/attr.py Fri Nov 22 18:51:37 2019 +0100 @@ -17,8 +17,6 @@ This adapter is actually used to get access to 'file' attributes in PyAMS_file package. """ -__docformat__ = 'restructuredtext' - from pyramid.exceptions import NotFound from zope.interface import Interface from zope.traversing.interfaces import ITraversable @@ -26,6 +24,9 @@ from pyams_utils.adapter import ContextAdapter, adapter_config +__docformat__ = 'restructuredtext' + + @adapter_config(name='attr', context=Interface, provides=ITraversable) class AttributeTraverser(ContextAdapter): """++attr++ namespace traverser @@ -38,7 +39,8 @@ Where *name* is the name of the requested attribute. """ - def traverse(self, name, furtherpath=None): + def traverse(self, name, furtherpath=None): # pylint: disable=unused-argument + """Traverse from current context to given attribute""" if '.' in name: name = name.split('.', 1) try: diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/cache.py --- a/src/pyams_utils/cache.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/cache.py Fri Nov 22 18:51:37 2019 +0100 @@ -21,8 +21,6 @@ A TALES helper extension is also provided to get an object's cache key from a Chameleon template. """ -__docformat__ = 'restructuredtext' - from persistent.interfaces import IPersistent from zope.interface import Interface @@ -31,25 +29,32 @@ from pyams_utils.interfaces.tales import ITALESExtension +__docformat__ = 'restructuredtext' + + @adapter_config(context=object, provides=ICacheKeyValue) def object_cache_key_adapter(obj): + """Cache key adapter for any object""" return str(id(obj)) @adapter_config(context=str, provides=ICacheKeyValue) def string_cache_key_adapter(obj): + """Cache key adapter for string value""" return obj @adapter_config(context=IPersistent, provides=ICacheKeyValue) def persistent_cache_key_adapter(obj): + """Cache key adapter for persistent object""" + # pylint: disable=protected-access if obj._p_oid: return str(int.from_bytes(obj._p_oid, byteorder='big')) - else: # unsaved object - return str(id(obj)) + return str(id(obj)) -@adapter_config(name='cache_key', context=(Interface, Interface, Interface), provides=ITALESExtension) +@adapter_config(name='cache_key', context=(Interface, Interface, Interface), + provides=ITALESExtension) class CacheKeyTalesExtension(ContextRequestViewAdapter): """extension:cache_key(context) TALES extension @@ -57,6 +62,7 @@ """ def render(self, context=None): + """Rendering of TALES extension""" if context is None: context = self.request.context return ICacheKeyValue(context) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/container.py --- a/src/pyams_utils/container.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/container.py Fri Nov 22 18:51:37 2019 +0100 @@ -9,14 +9,13 @@ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # +# pylint: disable=no-name-in-module """PyAMS_utils.container module This module provides several classes, adapters and functions about containers. """ -__docformat__ = 'restructuredtext' - from BTrees.OOBTree import OOBTree from persistent.list import PersistentList from pyramid.threadlocal import get_current_registry @@ -28,6 +27,9 @@ from pyams_utils.adapter import ContextAdapter, adapter_config +__docformat__ = 'restructuredtext' + + class BTreeOrderedContainer(OrderedContainer): """BTree based ordered container @@ -35,11 +37,12 @@ """ def __init__(self): + # pylint: disable=super-init-not-called self._data = OOBTree() self._order = PersistentList() -class ParentSelector(object): +class ParentSelector: """Interface based parent selector This selector can be used as a subscriber predicate on IObjectAddedEvent to define @@ -55,11 +58,13 @@ """ def __init__(self, ifaces, config): + # pylint: disable=unused-argument if not isinstance(ifaces, (list, tuple, set)): ifaces = (ifaces,) self.interfaces = ifaces def text(self): + """Predicate string output""" return 'parent_selector = %s' % str(self.interfaces) phash = text @@ -119,7 +124,7 @@ yield root locations = ISublocations(root, None) if locations is not None: - for location in locations.sublocations(): + for location in locations.sublocations(): # pylint: disable=too-many-function-args if condition(location): yield location yield from find_objects_matching(location, condition, ignore_root=True) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/context.py --- a/src/pyams_utils/context.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/context.py Fri Nov 22 18:51:37 2019 +0100 @@ -18,10 +18,39 @@ interface). """ +import sys +from io import StringIO + +from contextlib import contextmanager + __docformat__ = 'restructuredtext' -class ContextSelector(object): +@contextmanager +def capture(func, *args, **kwargs): + """Context manager used to capture standard output""" + out, sys.stdout = sys.stdout, StringIO() + try: + func(*args, **kwargs) + sys.stdout.seek(0) + yield sys.stdout.read() + finally: + sys.stdout = out + + +@contextmanager +def capture_stderr(func, *args, **kwargs): + """Context manager used to capture error output""" + err, sys.stderr = sys.stderr, StringIO() + try: + func(*args, **kwargs) + sys.stderr.seek(0) + yield sys.stderr.read() + finally: + sys.stderr = err + + +class ContextSelector(object): # pylint: disable=too-few-public-methods """Interface based context selector This selector can be used as a predicate to define a class or an interface that the context @@ -37,7 +66,7 @@ '''This is an event handler for an ISiteRoot object modification event''' """ - def __init__(self, ifaces, config): + def __init__(self, ifaces, config): # pylint: disable=unused-argument if not isinstance(ifaces, (list, tuple, set)): ifaces = (ifaces,) self.interfaces = ifaces diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/data.py --- a/src/pyams_utils/data.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/data.py Fri Nov 22 18:51:37 2019 +0100 @@ -12,10 +12,10 @@ """PyAMS_utils.data module -The *IObjectData* interface is a generic interface which can be used to assign custom data to any object. -This object data may be any object which can be serialized to JSON, and assigned to any HTML *data* attribute. -It can typically be used to set a *data-ams-data* attribute to objects, which is afterwards converted to -classic *data-* attributes by **MyAMS.js** framework. +The *IObjectData* interface is a generic interface which can be used to assign custom data to any +object. This object data may be any object which can be serialized to JSON, and assigned to any +HTML *data* attribute. It can typically be used to set a *data-ams-data* attribute to objects, +which is afterwards converted to classic *data-* attributes by **MyAMS.js** framework. For example, for a custom widget in a form:: @@ -34,8 +34,6 @@
...
""" -__docformat__ = 'restructuredtext' - import json from pyramid.interfaces import IRequest @@ -47,6 +45,9 @@ from pyams_utils.interfaces.tales import ITALESExtension +__docformat__ = 'restructuredtext' + + @adapter_config(context=IObjectData, provides=IObjectDataRenderer) class ObjectDataRenderer(ContextAdapter): """Object data JSON renderer""" @@ -57,12 +58,14 @@ return json.dumps(data.object_data) if data is not None else None -@adapter_config(name='object_data', context=(Interface, Interface, Interface), provides=ITALESExtension) +@adapter_config(name='object_data', context=(Interface, Interface, Interface), + provides=ITALESExtension) class ObjectDataExtension(ContextRequestViewAdapter): """extension:object_data TALES extension This TALES extension is to be used in Chameleon templates to define a custom data attribute - which stores all object data (see :py:class:`pyams_utils.interfaces.data.IObjectData` interface), like this:: + which stores all object data (see :py:class:`pyams_utils.interfaces.data.IObjectData` + interface), like this::
...
""" @@ -74,13 +77,16 @@ renderer = IObjectDataRenderer(context, None) if renderer is not None: return renderer.get_object_data() + return None -@adapter_config(name='request_data', context=(Interface, IRequest, Interface), provides=ITALESExtension) +@adapter_config(name='request_data', context=(Interface, IRequest, Interface), + provides=ITALESExtension) class PyramidRequestDataExtension(ContextRequestViewAdapter): """extension:request_data TALES extension for Pyramid request - This TALES extension can be used to get a request data, previously stored in the request via an annotation. + This TALES extension can be used to get a request data, previously stored in the request via + an annotation. For example::
...
@@ -91,11 +97,13 @@ return self.request.annotations.get(params) -@adapter_config(name='request_data', context=(Interface, IBrowserRequest, Interface), provides=ITALESExtension) +@adapter_config(name='request_data', context=(Interface, IBrowserRequest, Interface), + provides=ITALESExtension) class BrowserRequestDataExtension(ContextRequestViewAdapter): """extension:request_data TALES extension for Zope browser request - This TALES extension can be used to get a request data, previously stored in the request via an annotation. + This TALES extension can be used to get a request data, previously stored in the request via + an annotation. For example::
...
diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/date.py --- a/src/pyams_utils/date.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/date.py Fri Nov 22 18:51:37 2019 +0100 @@ -16,8 +16,6 @@ dates and datetimes. """ -__docformat__ = 'restructuredtext' - from datetime import datetime from zope.datetime import parseDatetimetz @@ -29,14 +27,17 @@ from pyams_utils.request import check_request from pyams_utils.timezone import gmtime, tztime + +__docformat__ = 'restructuredtext' + from pyams_utils import _ def unidate(value): """Get specified date converted to unicode ISO format - + Dates are always assumed to be stored in GMT timezone - + :param date value: input date to convert to unicode :return: unicode; input date converted to unicode @@ -54,9 +55,9 @@ def parse_date(value): """Get date specified in unicode ISO format to Python datetime object - + Dates are always assumed to be stored in GMT timezone - + :param str value: unicode date to be parsed :return: datetime; the specified value, converted to datetime @@ -71,7 +72,7 @@ def date_to_datetime(value): """Get datetime value converted from a date or datetime object - + :param date/datetime value: a date or datetime value to convert :return: datetime; input value converted to datetime @@ -88,7 +89,7 @@ """ if not value: return None - if type(value) is datetime: + if isinstance(value, datetime): return value return datetime(value.year, value.month, value.day) @@ -100,11 +101,11 @@ EXT_DATETIME_FORMAT = _("on %d/%m/%Y at %H:%M") -def format_date(value, format=EXT_DATE_FORMAT, request=None): +def format_date(value, format_string=EXT_DATE_FORMAT, request=None): """Format given date with the given format :param datetime value: the value to format - :param str format: a format string to use by `strftime` function + :param str format_string: a format string to use by `strftime` function :param request: the request from which to extract localization info for translation :return: str; input datetime converted to given format @@ -121,14 +122,14 @@ if request is None: request = check_request() localizer = request.localizer - return datetime.strftime(tztime(value), localizer.translate(format)) + return datetime.strftime(tztime(value), localizer.translate(format_string)) -def format_datetime(value, format=EXT_DATETIME_FORMAT, request=None): +def format_datetime(value, format_string=EXT_DATETIME_FORMAT, request=None): """Format given datetime with the given format including time :param datetime value: the value to format - :param str format: a format string to use by `strftime` function + :param str format_string: a format string to use by `strftime` function :param request: request; the request from which to extract localization info for translation :return: str; input datetime converted to given format @@ -140,11 +141,12 @@ >>> format_datetime(value, SH_DATETIME_FORMAT) '15/11/2016 - 10:13' """ - return format_date(value, format, request) + return format_date(value, format_string, request) def get_age(value, request=None): - """Get 'human' age of a given datetime (including timezone) compared to current datetime (in UTC) + """Get 'human' age of a given datetime (including timezone) compared to current datetime + (in UTC) :param datetime value: input datetime to be compared with current datetime :return: str; the delta value, converted to months, weeks, days, hours or minutes @@ -155,30 +157,31 @@ now = gmtime(datetime.utcnow()) delta = now - gmtime(value) if delta.days > 60: - return translate(_("%d months ago")) % int(round(delta.days * 1.0 / 30)) + result = translate(_("%d months ago")) % int(round(delta.days * 1.0 / 30)) elif delta.days > 10: - return translate(_("%d weeks ago")) % int(round(delta.days * 1.0 / 7)) + result = translate(_("%d weeks ago")) % int(round(delta.days * 1.0 / 7)) elif delta.days > 2: - return translate(_("%d days ago")) % delta.days + result = translate(_("%d days ago")) % delta.days elif delta.days == 2: - return translate(_("the day before yesterday")) + result = translate(_("the day before yesterday")) elif delta.days == 1: - return translate(_("yesterday")) - else: + result = translate(_("yesterday")) + else: # less than one day hours = int(round(delta.seconds * 1.0 / 3600)) if hours > 1: - return translate(_("%d hours ago")) % hours + result = translate(_("%d hours ago")) % hours elif delta.seconds > 300: - return translate(_("%d minutes ago")) % int(round(delta.seconds * 1.0 / 60)) + result = translate(_("%d minutes ago")) % int(round(delta.seconds * 1.0 / 60)) else: - return translate(_("less than 5 minutes ago")) + result = translate(_("less than 5 minutes ago")) + return result -def get_duration(v1, v2=None, request=None): +def get_duration(first, last=None, request=None): # pylint: disable=too-many-branches """Get 'human' delta as string between two dates - :param datetime v1: start date - :param datetime v2: end date, or current date (in UTC) if None + :param datetime first: start date + :param datetime last: end date, or current date (in UTC) if None :param request: the request from which to extract localization infos :return: str; approximate delta between the two input dates @@ -221,39 +224,41 @@ >>> get_duration(date1, date2, request) '15 seconds' """ - if v2 is None: - v2 = datetime.utcnow() - assert isinstance(v1, datetime) and isinstance(v2, datetime) + if last is None: + last = datetime.utcnow() + assert isinstance(first, datetime) and isinstance(last, datetime) if request is None: request = check_request() translate = request.localizer.translate - v1, v2 = min(v1, v2), max(v1, v2) - delta = v2 - v1 + first, last = min(first, last), max(first, last) + delta = last - first if delta.days > 60: - return translate(_("%d months")) % int(round(delta.days * 1.0 / 30)) + result = translate(_("%d months")) % int(round(delta.days * 1.0 / 30)) elif delta.days > 10: - return translate(_("%d weeks")) % int(round(delta.days * 1.0 / 7)) + result = translate(_("%d weeks")) % int(round(delta.days * 1.0 / 7)) elif delta.days >= 2: - return translate(_("%d days")) % delta.days + result = translate(_("%d days")) % delta.days else: hours = int(round(delta.seconds * 1.0 / 3600)) if delta.days == 1: if hours == 0: - return translate(_("24 hours")) + result = translate(_("24 hours")) else: - return translate(_("%d day and %d hours")) % (delta.days, hours) + result = translate(_("%d day and %d hours")) % (delta.days, hours) else: if hours > 2: - return translate(_("%d hours")) % hours + result = translate(_("%d hours")) % hours else: minutes = int(round(delta.seconds * 1.0 / 60)) if minutes > 2: - return translate(_("%d minutes")) % minutes + result = translate(_("%d minutes")) % minutes else: - return translate(_("%d seconds")) % delta.seconds + result = translate(_("%d seconds")) % delta.seconds + return result -@adapter_config(name='timestamp', context=(Interface, Interface, Interface), provides=ITALESExtension) +@adapter_config(name='timestamp', context=(Interface, Interface, Interface), + provides=ITALESExtension) class TimestampTalesAdapter(ContextRequestViewAdapter): """extension:timestamp(context) TALES adapter @@ -261,14 +266,14 @@ """ def render(self, context=None, formatting=None): + """Render TALES extension""" if context is None: context = self.request.context if formatting == 'iso': format_func = datetime.isoformat else: format_func = datetime.timestamp - dc = IZopeDublinCore(context, None) - if dc is None: - return format_func(tztime(datetime.utcnow())) - else: - return format_func(tztime(dc.modified)) + zdc = IZopeDublinCore(context, None) + if zdc is None: + return format_func(tztime(zdc.modified)) + return format_func(tztime(datetime.utcnow())) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/decorator.py --- a/src/pyams_utils/decorator.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/decorator.py Fri Nov 22 18:51:37 2019 +0100 @@ -16,22 +16,43 @@ deprecated. """ -__docformat__ = 'restructuredtext' - import functools import warnings +__docformat__ = 'restructuredtext' + def deprecated(*msg): """This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used. + + >>> from pyams_utils.context import capture_stderr + >>> from pyams_utils.decorator import deprecated + + >>> @deprecated + ... def my_function(value): + ... return value + + >>> with capture_stderr(my_function, 1) as err: + ... print(err.split('\\n')[0]) + >> @deprecated('Deprecation message') + ... def my_function_2(value): + ... return value + + >>> with capture_stderr(my_function_2, 2) as err: + ... print(err.split('\\n')[0]) + ' % (url, 'defer' if defer else '') @@ -59,10 +60,9 @@ """Render resource tag""" if self.resource_type == 'css': return render_css(self.relpath) - elif self.resource_type == 'js': + if self.resource_type == 'js': return render_js(self.relpath, self.defer) - else: - return '' + return '' def get_resource_path(resource, signature='--static--', versioning=True): diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/html.py --- a/src/pyams_utils/html.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/html.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,15 +10,17 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.html module + +This module provides functions which are used to convert HTML code to plain text, by extracting +useful text and removing all HTML tags. +""" + +from html.parser import HTMLParser +from warnings import warn -# import standard library -from html.parser import HTMLParser - -# import interfaces - -# import packages +__docformat__ = 'restructuredtext' class MyHTMLParser(HTMLParser): @@ -27,22 +29,26 @@ entitydefs = {'amp': '&', 'lt': '<', 'gt': '>', 'nbsp': ' ', 'apos': "'", 'quot': '"', - 'Agrave': 'À', 'Aacute': 'A', 'Acirc': 'Â', 'Atilde': 'A', 'Auml': 'Ä', 'Aring': 'A', + 'Agrave': 'À', 'Aacute': 'A', 'Acirc': 'Â', 'Atilde': 'A', + 'Auml': 'Ä', 'Aring': 'A', 'AElig': 'AE', 'Ccedil': 'Ç', 'Egrave': 'É', 'Eacute': 'È', 'Ecirc': 'Ê', 'Euml': 'Ë', 'Igrave': 'I', 'Iacute': 'I', 'Icirc': 'I', 'Iuml': 'I', 'Ntilde': 'N', - 'Ograve': 'O', 'Oacute': 'O', 'Ocirc': 'Ô', 'Otilde': 'O', 'Ouml': 'Ö', 'Oslash': 'O', + 'Ograve': 'O', 'Oacute': 'O', 'Ocirc': 'Ô', 'Otilde': 'O', + 'Ouml': 'Ö', 'Oslash': '0', 'Ugrave': 'Ù', 'Uacute': 'U', 'Ucirc': 'Û', 'Uuml': 'Ü', 'Yacute': 'Y', 'THORN': 'T', - 'agrave': 'à', 'aacute': 'a', 'acirc': 'â', 'atilde': 'a', 'auml': 'ä', 'aring': 'a', 'aelig': 'ae', + 'agrave': 'à', 'aacute': 'a', 'acirc': 'â', 'atilde': 'a', + 'auml': 'ä', 'aring': 'a', 'aelig': 'ae', 'ccedil': 'ç', 'egrave': 'è', 'eacute': 'é', 'ecirc': 'ê', 'euml': 'ë', 'igrave': 'i', 'iacute': 'i', 'icirc': 'î', 'iuml': 'ï', 'ntilde': 'n', - 'ograve': 'o', 'oacute': 'o', 'ocirc': 'ô', 'otilde': 'o', 'ouml': 'ö', 'oslash': 'o', + 'ograve': 'o', 'oacute': 'o', 'ocirc': 'ô', 'otilde': 'o', + 'ouml': 'ö', 'oslash': 'o', 'ugrave': 'ù', 'uacute': 'u', 'ucirc': 'û', 'uuml': 'ü', 'yacute': 'y', 'thorn': 't', @@ -84,12 +90,12 @@ def handle_charref(self, name): try: - n = int(name) + int_value = int(name) except ValueError: return - if not 0 <= n <= 255: + if not 0 <= int_value <= 255: return - self.handle_data(self.charrefs.get(n)) + self.handle_data(self.charrefs.get(int_value)) def handle_starttag(self, tag, attrs): if tag == 'td': @@ -101,6 +107,9 @@ if tag == 'p': self.data += '\n' + def error(self, message): + warn(message) + def html_to_text(value): """Utility function to extract text content from HTML @@ -120,7 +129,8 @@ >>> html_to_text(html) 'Header\\nThis is an < ò > entity.\\n\\n' - >>> html = '''

Header

This is an < ò > entity.

''' + >>> html = '''

Header

This is an < ò > ''' + \ + '''entity.

''' >>> html_to_text(html) 'Header\\nThis is an <\xa0ò\xa0> entity.\\n\\n' """ diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/i18n.py --- a/src/pyams_utils/i18n.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/i18n.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,25 +10,43 @@ # FOR A PARTICULAR PURPOSE. # -"""I18n module used to get browser language from request""" +"""PyAMS_utils.i18n module -__docformat__ = 'restructuredtext' +This module is used to get browser language from request +""" import locale +__docformat__ = 'restructuredtext' + + def normalize_lang(lang): - """Normalize input languages string""" - lang = lang.strip().lower() - lang = lang.replace('_', '-') - lang = lang.replace(' ', '') - return lang + """Normalize input languages string + + >>> from pyams_utils.i18n import normalize_lang + >>> lang = 'fr,en-US ; q=0.9, en-GB ; q=0.8, en ; q=0.7' + >>> normalize_lang(lang) + 'fr,en-us;q=0.9,en-gb;q=0.8,en;q=0.7' + """ + return lang.strip() \ + .lower() \ + .replace('_', '-') \ + .replace(' ', '') def get_browser_language(request): """Custom locale negotiator Copied from zope.publisher code + + >>> from pyramid.testing import DummyRequest + >>> from pyams_utils.i18n import get_browser_language + + >>> request = DummyRequest() + >>> request.headers['Accept-Language'] = 'fr, en-US ; q=0.9, en-GB ; q=0.8, en ; q=0.7' + >>> get_browser_language(request) + 'fr' """ accept_langs = request.headers.get('Accept-Language', '').split(',') @@ -75,7 +93,10 @@ def set_locales(config): - """Define locale environment variables""" + """Define locale environment variables + + :param config: Pyramid's settings object + """ for attr in ('LC_CTYPE', 'LC_COLLATE', 'LC_TIME', 'LC_MONETARY', 'LC_NUMERIC', 'LC_ALL'): value = config.get('pyams.locale.{0}'.format(attr.lower())) if value: diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/include.py --- a/src/pyams_utils/include.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/include.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,8 +10,6 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - from chameleon import PageTemplateFile from persistent.interfaces import IPersistent from z3c.pt.pagetemplate import PageTemplateFile as Z3cPageTemplateFile @@ -29,6 +27,8 @@ from pyams_utils.url import get_display_context from pyams_utils.traversing import NamespaceTraverser +__docformat__ = 'restructuredtext' + def include_package(config): """Pyramid package include""" diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/inherit.py --- a/src/pyams_utils/inherit.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/inherit.py Fri Nov 22 18:51:37 2019 +0100 @@ -15,9 +15,114 @@ This module is used to manage a generic inheritance between a content and it's parent container. It also defines a custom InheritedFieldProperty which allows to automatically manage inherited properties. -""" + +This PyAMS module is used to handle inheritance between a parent object and a child which can +"inherit" from some of it's properties, as long as they share the same "target" interface. + + >>> from zope.interface import implementer, Interface, Attribute + >>> from zope.schema import TextLine + >>> from zope.schema.fieldproperty import FieldProperty + + >>> from pyams_utils.adapter import adapter_config + >>> from pyams_utils.interfaces.inherit import IInheritInfo + >>> from pyams_utils.inherit import BaseInheritInfo, InheritedFieldProperty + >>> from pyams_utils.registry import get_global_registry + +Let's start by creating a "content" interface, and a marker interface for objects for which we +want to provide this interface: + + >>> class IMyInfoInterface(IInheritInfo): + ... '''Custom interface''' + ... value = TextLine(title="Custom attribute") + + >>> class IMyTargetInterface(Interface): + ... '''Target interface''' + + >>> @implementer(IMyInfoInterface) + ... class MyInfo(BaseInheritInfo): + ... target_interface = IMyTargetInterface + ... adapted_interface = IMyInfoInterface + ... + ... _value = FieldProperty(IMyInfoInterface['value']) + ... value = InheritedFieldProperty(IMyInfoInterface['value']) + +Please note that for each field of the interface which can be inherited, you must define to +properties: one using "InheritedFieldProperty" with the name of the field, and one using a classic +"FieldProperty" with the same name prefixed by "_"; this property is used to store the "local" +property value, when inheritance is unset. + +The adapter is created to adapt an object providing IMyTargetInterface to IMyInfoInterface; +please note that the adapter *must* attach the created object to it's parent by setting +__parent__ attribute: + + >>> @adapter_config(context=IMyTargetInterface, provides=IMyInfoInterface) + ... def my_info_factory(context): + ... info = getattr(context, '__info__', None) + ... if info is None: + ... info = context.__info__ = MyInfo() + ... info.__parent__ = context + ... return info + +Adapter registration is here only for testing; the "adapter_config" decorator may do the job in +a normal application context: + + >>> registry = get_global_registry() + >>> registry.registerAdapter(my_info_factory, (IMyTargetInterface, ), IMyInfoInterface) -__docformat__ = 'restructuredtext' +We can then create classes which will be adapted to support inheritance: + + >>> @implementer(IMyTargetInterface) + ... class MyTarget: + ... '''Target class''' + ... __parent__ = None + ... __info__ = None + + >>> parent = MyTarget() + >>> parent_info = IMyInfoInterface(parent) + >>> parent.__info__ + + >>> parent_info.value = 'parent' + >>> parent_info.value + 'parent' + >>> parent_info.can_inherit + False + +As soon as a parent is defined, the child object can inherit from it's parent: + + >>> child = MyTarget() + >>> child.__parent__ = parent + >>> child_info = IMyInfoInterface(child) + >>> child.__info__ + + + >>> child_info.can_inherit + True + >>> child_info.inherit + True + >>> child_info.value + 'parent' + +Setting child value while inheritance is enabled donesn't have any effect: + + >>> child_info.value = 'child' + >>> child_info.value + 'parent' + >>> child_info.inherit_from == parent + True + +You can disable inheritance and define your own value: + + >>> child_info.inherit = False + >>> child_info.value = 'child' + >>> child_info.value + 'child' + >>> child_info.inherit_from == child + True + +Please note that parent and child in this example share the same class, but this is not a +requirement; they just have to implement the same marker interface, to be adapted to the same +content interface. +""" from zope.interface import Interface, implementer from zope.location import Location @@ -28,9 +133,17 @@ from pyams_utils.zodb import volatile_property +__docformat__ = 'restructuredtext' + + @implementer(IInheritInfo) class BaseInheritInfo(Location): - """Base inherit class""" + """Base inherit class + + Subclasses may generaly override target_interface and adapted_interface to + correctly handle inheritance (see example in doctests). + Please note also that adapters to this interface must correctly 'locate' + """ target_interface = Interface adapted_interface = Interface @@ -73,14 +186,14 @@ def inherit_from(self): """Get current parent from which we inherit""" if not self.inherit: - return self + return self.__parent__ parent = self.parent while self.adapted_interface(parent).inherit: - parent = parent.parent + parent = parent.parent # pylint: disable=no-member return parent -class InheritedFieldProperty(object): +class InheritedFieldProperty: """Inherited field property""" def __init__(self, field, name=None): @@ -94,10 +207,10 @@ if inst is None: return self inherit_info = IInheritInfo(inst) - if inherit_info.inherit: + if inherit_info.inherit and (inherit_info.parent is not None): + # pylint: disable=not-callable return getattr(inherit_info.adapted_interface(inherit_info.parent), self.__name) - else: - return getattr(inst, '_{0}'.format(self.__name)) + return getattr(inst, '_{0}'.format(self.__name)) def __set__(self, inst, value): inherit_info = IInheritInfo(inst) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/interfaces/pygments.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/pygments.py Fri Nov 22 18:51:37 2019 +0100 @@ -0,0 +1,55 @@ +# +# Copyright (c) 2008-2018 Thierry Florac +# 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_utils.interfaces.pygments module + +This module is used to load Pygments CSS +""" + +from zope.interface import Interface +from zope.schema import Bool, Choice + + +__docformat__ = 'restructuredtext' + +from pyams_utils import _ + + +PYGMENTS_LEXERS_VOCABULARY = 'Pygments lexers vocabulary' +PYGMENTS_STYLES_VOCABULARY = 'Pygments styles vocabulary' + + +class IPygmentsCodeConfiguration(Interface): + """Pygments html formatter options""" + + lexer = Choice(title=_("Selected lexer"), + description=_("Lexer used to format source code"), + required=True, + vocabulary=PYGMENTS_LEXERS_VOCABULARY, + default='auto') + + display_linenos = Bool(title=_("Display line numbers?"), + description=_("If 'no', line numbers will be hidden"), + required=True, + default=True) + + disable_wrap = Bool(title=_("Lines wrap?"), + description=_("If 'yes', lines wraps will be enabled; line numbers will " + "not be displayed if lines wrap is enabled..."), + required=True, + default=False) + + style = Choice(title=_("Color style"), + description=_("Selected color style"), + required=True, + vocabulary=PYGMENTS_STYLES_VOCABULARY, + default='default') diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/interfaces/site.py --- a/src/pyams_utils/interfaces/site.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/interfaces/site.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,17 +10,12 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +from zope.annotation.interfaces import IAttributeAnnotatable +from zope.interface import Attribute, Interface +from zope.interface.interfaces import IObjectEvent -# import standard library - -# import interfaces -from zope.annotation.interfaces import IAttributeAnnotatable -from zope.component.interfaces import IObjectEvent - -# import packages -from zope.interface import Interface, Attribute +__docformat__ = 'restructuredtext' class ISiteRoot(IAttributeAnnotatable): diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/intids.py --- a/src/pyams_utils/intids.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/intids.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,26 +10,29 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - +"""PyAMS_utils.intids module -# import standard packages +This module provides utility functions and helpers to help usage of IIntIds utilities. +Pyramid events subscribers are also declared to match Zope events with Pyramid IntIds related +events +""" -# import interfaces from persistent.interfaces import IPersistent -from pyams_utils.interfaces.intids import IUniqueID -from zope.intid.interfaces import IIntIds, IIntIdEvent, IntIdAddedEvent, IntIdRemovedEvent -from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent -from zope.location.interfaces import ISublocations -from zope.keyreference.interfaces import IKeyReference, NotYet - -# import packages -from pyams_utils.adapter import adapter_config, ContextAdapter -from pyams_utils.registry import get_all_utilities_registered_for, query_utility from pyramid.events import subscriber from pyramid.threadlocal import get_current_registry from zope.intid import intIdEventNotify +from zope.intid.interfaces import IIntIdEvent, IIntIds, IntIdAddedEvent, IntIdRemovedEvent +from zope.keyreference.interfaces import IKeyReference, NotYet from zope.lifecycleevent import ObjectRemovedEvent +from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent +from zope.location.interfaces import ISublocations + +from pyams_utils.adapter import ContextAdapter, adapter_config +from pyams_utils.interfaces.intids import IUniqueID +from pyams_utils.registry import get_all_utilities_registered_for, query_utility + + +__docformat__ = 'restructuredtext' @adapter_config(context=IPersistent, provides=IUniqueID) @@ -46,6 +49,7 @@ intids = query_utility(IIntIds) if intids is not None: return hex(intids.queryId(self.context))[2:] + return None @subscriber(IObjectAddedEvent, context_selector=IPersistent) @@ -82,7 +86,7 @@ registry = get_current_registry() locations = ISublocations(event.object, None) if locations is not None: - for location in locations.sublocations(): + for location in locations.sublocations(): # pylint: disable=too-many-function-args registry.notify(ObjectRemovedEvent(location)) utilities = tuple(get_all_utilities_registered_for(IIntIds)) if utilities: @@ -100,7 +104,9 @@ @subscriber(IIntIdEvent) def handle_intid_event(event): - """Event subscriber used to dispatch all IIntIdEvent events using Pyramid events subscribers to matching - subscribers using Zope events + """IntId event subscriber + + This event is used to dispatch all IIntIdEvent events using Pyramid events subscribers + to matching subscribers using Zope events """ intIdEventNotify(event) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/json.py --- a/src/pyams_utils/json.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/json.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,26 +10,36 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.json package + +A small utility module which provides a default JSON encoder to automatically convert +dates and datetimes to ISO format + >>> import json as stock_json + >>> from datetime import datetime + >>> from pyams_utils import json + >>> from pyams_utils.timezone import GMT -# import standard library + >>> value = datetime.fromtimestamp(1205000000, GMT) + >>> stock_json.dumps(value) + '"2008-03-08T18:13:20+00:00"' +""" + import json from datetime import date, datetime -# import interfaces - -# import packages +__docformat__ = 'restructuredtext' def default_json_encoder(obj): + """Default JSON encoding of dates and datetimes""" if isinstance(obj, (date, datetime)): return obj.isoformat() - else: - return obj + return obj +# pylint: disable=protected-access json._default_encoder = json.JSONEncoder(skipkeys=False, ensure_ascii=True, check_circular=True, diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/list.py --- a/src/pyams_utils/list.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/list.py Fri Nov 22 18:51:37 2019 +0100 @@ -20,8 +20,6 @@ iterator). """ -__docformat__ = 'restructuredtext' - from itertools import filterfalse, tee from random import random, shuffle @@ -31,14 +29,17 @@ from pyams_utils.interfaces.tales import ITALESExtension +__docformat__ = 'restructuredtext' + + def unique(seq, key=None): """Extract unique values from list, preserving order :param iterator seq: input list :param callable key: an identity function which is used to get 'identity' value of each element in the list - :return: list; a new list containing only unique elements of the original list in their initial order. - Original list is not modified. + :return: list; a new list containing only unique elements of the original list in their initial + order. Original list is not modified. >>> from pyams_utils.list import unique >>> mylist = [1, 2, 3, 2, 1] @@ -173,7 +174,8 @@ return next(values), values -@adapter_config(name='boolean_iter', context=(Interface, Interface, Interface), provides=ITALESExtension) +@adapter_config(name='boolean_iter', context=(Interface, Interface, Interface), + provides=ITALESExtension) class IterValuesCheckerExpression(ContextRequestViewAdapter): """TALES expression used to handle iterators @@ -182,6 +184,7 @@ """ def render(self, context=None): + """Render TALES extension; see `ITALESExtension` interface""" if context is None: context = self.context return boolean_iter(context) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/lock.py --- a/src/pyams_utils/lock.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/lock.py Fri Nov 22 18:51:37 2019 +0100 @@ -16,24 +16,25 @@ processes; the lock relies on a shared value stored info Beaker's cache. """ -__docformat__ = 'restructuredtext' - import time from threading import local from beaker import cache -_local = local() +__docformat__ = 'restructuredtext' + + +_LOCAL = local() def get_locks_cache(): """Get locks shared cache""" try: - locks_cache = _local.locks_cache + locks_cache = _LOCAL.locks_cache except AttributeError: manager = cache.CacheManager(**cache.cache_regions['persistent']) - locks_cache = _local.locks_cache = manager.get_cache('PyAMS::locks') + locks_cache = _LOCAL.locks_cache = manager.get_cache('PyAMS::locks') return locks_cache @@ -41,7 +42,7 @@ """Cache lock exception""" -class CacheLock(object): +class CacheLock: """Beaker based lock This lock can be used when you need to get a lot across several processes or even computers. @@ -66,8 +67,7 @@ if test: if not self.wait: raise LockException() - else: - time.sleep(0.1) + time.sleep(0.1) else: locks_cache.set_value(self.key, 1) self.has_lock = True diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/progress.py --- a/src/pyams_utils/progress.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/progress.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,7 +10,28 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.progress module + +This module can be used to get progress status on a long running operation. + +The process is a s follow: + + - the client generate a "progress ID"; this ID can be any unique ID, and can be generated by + MyAMS client library + - the client browser send a POST request containg this progress ID to a view + - the view calls "init_progress_status(progress_id, request.principal.id, "Task label") when + starting it's long operation + - during the operation, a call is made regularly to "set_progress_status(progress_id)"; additional + arguments can contain a status (to indicate if operation is finished or not), and a simple + "message" or two "length" and "current" arguments which can specify the length of the operation + and it's current position + - at the end of the operation, the view calls "set_progress_status(progress_id, 'finished')" to + specify that the operation is finished. + +During the whole operation, while waiting for server response, the client browser can send +requests to "get_progress_status.json", providing the progress ID, to get the operation progress. +This last operation is done automatically in PyAMS forms. +""" from datetime import datetime from threading import local @@ -22,7 +43,10 @@ from pyams_utils.lock import locked -_local = local() +__docformat__ = 'restructuredtext' + + +_LOCAL = local() PROGRESS_CACHE_NAME = 'PyAMS::progress' @@ -34,20 +58,20 @@ def get_tasks_cache(): """Get cache storing tasks list""" try: - tasks_cache = _local.running_tasks_cache + tasks_cache = _LOCAL.running_tasks_cache except AttributeError: manager = cache.CacheManager(**cache.cache_regions['persistent']) - tasks_cache = _local.running_tasks_cache = manager.get_cache(PROGRESS_CACHE_NAME) + tasks_cache = _LOCAL.running_tasks_cache = manager.get_cache(PROGRESS_CACHE_NAME) return tasks_cache def get_progress_cache(): """Get cache storing tasks progress""" try: - local_cache = _local.progress_cache + local_cache = _LOCAL.progress_cache except AttributeError: manager = cache.CacheManager(**cache.cache_regions['default']) - local_cache = _local.progress_cache = manager.get_cache(PROGRESS_CACHE_NAME) + local_cache = _LOCAL.progress_cache = manager.get_cache(PROGRESS_CACHE_NAME) return local_cache @@ -65,6 +89,7 @@ @locked(name=PROGRESS_LOCK_NAME) def init_progress_status(progress_id, owner, label, tags=None, length=None, current=None): + # pylint: disable=too-many-arguments """Initialize progress status for given task :param str progress_id: task ID diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/property.py --- a/src/pyams_utils/property.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/property.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,38 +10,57 @@ # FOR A PARTICULAR PURPOSE. # +"""PyAMS_utils.property module + +This module is used to define: + - a cached property; this read-only property is evaluated only once; it's value is stored into + object's attributes, and so should be freed with the object (so it should behave like a + Pyramid's "reify" decorator, but we have kept it for compatibility of existing code) + - a class property; this decorator is working like a classic property, but can be assigned to a + class; to support class properties, this class also have to decorated with the + "classproperty_support" decorator + + >>> from pyams_utils.property import cached_property + + >>> class ClassWithCache: + ... '''Class with cache''' + ... @cached_property + ... def cached_value(self): + ... print("This is a cached value") + ... return 1 + + >>> obj = ClassWithCache() + >>> obj.cached_value + This is a cached value + 1 + +On following calls, cached property method shouldn't be called again: + + >>> obj.cached_value + 1 + +Class properties are used to define properties on class level: + + >>> from pyams_utils.property import classproperty, classproperty_support + + >>> @classproperty_support + ... class ClassWithProperties: + ... '''Class with class properties''' + ... + ... class_attribute = 1 + ... + ... @classproperty + ... def my_class_property(cls): + ... return cls.class_attribute + + >>> ClassWithProperties.my_class_property + 1 +""" + __docformat__ = 'restructuredtext' -# import standard library - -# import interfaces - -# import packages - - -class cached(object): - """Custom property decorator to define a property or function which is calculated only once - - When applied on a function, caching is based on input arguments - """ - - def __init__(self, function): - self._function = function - self._cache = {} - - def __call__(self, *args): - try: - return self._cache[args] - except KeyError: - self._cache[args] = self._function(*args) - return self._cache[args] - - def expire(self, *args): - del self._cache[args] - - -class cached_property(object): +class cached_property: # pylint: disable=invalid-name """A read-only property decorator that is only evaluated once. The value is cached on the object itself rather than the function or class; this should prevent @@ -60,9 +79,9 @@ return result -class classproperty: +class classproperty: # pylint: disable=invalid-name """Same decorator as property(), but passes obj.__class__ instead of obj to fget/fset/fdel. - + Original code for property emulation: https://docs.python.org/3.5/howto/descriptor.html#properties """ @@ -92,27 +111,30 @@ self.fdel(obj.__class__) def getter(self, fget): + """Property getter""" return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): + """Property setter""" return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): + """Property deleter""" return type(self)(self.fget, self.fset, fdel, self.__doc__) def classproperty_support(cls): """Class decorator to add metaclass to a class. - + Metaclass uses to add descriptors to class attributes """ class Meta(type): - pass + """Meta class""" for name, obj in vars(cls).items(): if isinstance(obj, classproperty): setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel)) class Wrapper(cls, metaclass=Meta): - pass + """Wrapper class""" return Wrapper diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/pygments.py --- a/src/pyams_utils/pygments.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/pygments.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,7 +10,11 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.pygments module + +This module is used to provide an URL which allows you to load Pygments CSS files. +It also provides two vocabularies of available lexers and styles. +""" from fanstatic import get_library_registry from persistent import Persistent @@ -21,16 +25,19 @@ from pyramid.response import Response from pyramid.view import view_config from zope.container.contained import Contained -from zope.interface import Interface, implementer -from zope.schema import Bool, Choice from zope.schema.fieldproperty import FieldProperty from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary from pyams_utils.factory import factory_config from pyams_utils.fanstatic import ExternalResource +from pyams_utils.interfaces.pygments import IPygmentsCodeConfiguration, \ + PYGMENTS_LEXERS_VOCABULARY, PYGMENTS_STYLES_VOCABULARY from pyams_utils.list import unique_iter from pyams_utils.vocabulary import vocabulary_config + +__docformat__ = 'restructuredtext' + from pyams_utils import _ @@ -47,11 +54,13 @@ from pyams_default_theme import library +# pylint: disable=invalid-name pygments_css = ExternalResource(library, 'get-pygments-style.css', resource_type='css') @view_config(name='get-pygments-style.css') def get_pygments_style_view(request): + """View used to download Pygments style""" style = request.params.get('style', 'default') styles = HtmlFormatter(linenos='inline', nowrap=False, @@ -64,32 +73,28 @@ # Pygments lexers # -PYGMENTS_LEXERS_VOCABULARY = 'Pygments lexers vocabulary' - - @vocabulary_config(name=PYGMENTS_LEXERS_VOCABULARY) class PygmentsLexersVocabulary(SimpleVocabulary): """Pygments lexers vocabulary""" - def __init__(self, context): + def __init__(self, context): # pylint: disable=unused-argument terms = [SimpleTerm('auto', title=_("Automatic detection"))] + # pylint: disable=unused-variable for name, aliases, filetypes, mimetypes in sorted(unique_iter(get_all_lexers(), key=lambda x: x[0].lower()), key=lambda x: x[0].lower()): terms.append(SimpleTerm(aliases[0] if len(aliases) > 0 else name, title='{0}{1}'.format(name, - ' ({})'.format(', '.join(filetypes)) if filetypes else ''))) + ' ({})'.format(', '.join(filetypes)) + if filetypes else ''))) super(PygmentsLexersVocabulary, self).__init__(terms) -PYGMENTS_STYLES_VOCABULARY = 'Pygments styles vocabulary' - - @vocabulary_config(name=PYGMENTS_STYLES_VOCABULARY) class PygmentsStylesVocabulary(SimpleVocabulary): """Pygments styles vocabulary""" - def __init__(self, context): + def __init__(self, context): # pylint: disable=unused-argument terms = [] for name in sorted(get_all_styles()): terms.append(SimpleTerm(name)) @@ -100,33 +105,6 @@ # Pygments configuration # -class IPygmentsCodeConfiguration(Interface): - """Pygments html formatter options""" - - lexer = Choice(title=_("Selected lexer"), - description=_("Lexer used to format source code"), - required=True, - vocabulary=PYGMENTS_LEXERS_VOCABULARY, - default='auto') - - display_linenos = Bool(title=_("Display line numbers?"), - description=_("If 'no', line numbers will be hidden"), - required=True, - default=True) - - disable_wrap = Bool(title=_("Lines wrap?"), - description=_("If 'yes', lines wraps will be enabled; line numbers will not be " - "displayed if lines wrap is enabled..."), - required=True, - default=False) - - style = Choice(title=_("Color style"), - description=_("Selected color style"), - required=True, - vocabulary=PYGMENTS_STYLES_VOCABULARY, - default='default') - - @factory_config(IPygmentsCodeConfiguration) class PygmentsCodeRendererSettings(Persistent, Contained): """Pygments code renderer settings""" @@ -149,3 +127,4 @@ cssclass='source', style=settings.style) return highlight(code, lexer, formatter) + return None diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/registry.py --- a/src/pyams_utils/registry.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/registry.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,13 +10,13 @@ # FOR A PARTICULAR PURPOSE. # -__doc__ = """Registry management package +"""PyAMS_utils.registry module This package is used to manage *local registry*. A local registry is a *site management* component -created automatically on application startup by PyAMS_utils package. It can be used to store and register -components, mainly utilities which are created and configured dynamically by a site administrator; this can include -SQLAlchemy engines, ZEO connections, and several PyAMS utilities like security manager, medias converter, -tasks scheduler and many other ones. +created automatically on application startup by PyAMS_utils package. It can be used to store and +register components, mainly utilities which are created and configured dynamically by a site +administrator; this can include SQLAlchemy engines, ZEO connections, and several PyAMS utilities +like security manager, medias converter, tasks scheduler and many other ones. See :ref:`zca` to get a brief introduction about using a local registry with PyAMS packages. """ @@ -39,7 +39,7 @@ __docformat__ = 'restructuredtext' -logger = logging.getLogger('PyAMS (utils)') +LOGGER = logging.getLogger('PyAMS (utils)') class LocalRegistry(threading.local): @@ -196,7 +196,7 @@ return result -class utility_config(object): +class utility_config(object): # pylint: disable=invalid-name """Function or class decorator to register a utility in the global registry :param str name: default=''; name under which the utility is registered @@ -237,23 +237,24 @@ else: raise TypeError("Missing 'provides' argument") - config = context.config.with_package(info.module) - logger.debug("Registering utility {0} named '{1}' providing {2}".format( + config = context.config.with_package(info.module) # pylint: disable=no-member + LOGGER.debug("Registering utility {0} named '{1}' providing {2}".format( str(component) if component else str(factory), settings.get('name', ''), str(provides))) - config.registry.registerUtility(component=component, factory=factory, - provided=provides, name=settings.get('name', '')) + registry = settings.get('registry', config.registry) + registry.registerUtility(component=component, factory=factory, + provided=provides, name=settings.get('name', '')) info = self.venusian.attach(wrapped, callback, category='pyams_utility', 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 diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/request.py --- a/src/pyams_utils/request.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/request.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,7 +10,10 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.request module + + +""" import logging @@ -24,13 +27,15 @@ from pyams_utils.interfaces import ICacheKeyValue, MissingRequestError from pyams_utils.registry import get_global_registry - -logger = logging.getLogger('PyAMS (utils)') - -_marker = object() +__docformat__ = 'restructuredtext' -class RequestSelector(object): +LOGGER = logging.getLogger('PyAMS (utils)') + +_MARKER = object() + + +class RequestSelector: """Interface based request selector This selector can be used as a subscriber predicate to define @@ -45,12 +50,13 @@ '''This is an event handler for an IPyAMSRequest modification event''' """ - def __init__(self, ifaces, config): + def __init__(self, ifaces, config): # pylint: disable=unused-argument if not isinstance(ifaces, (list, tuple, set)): ifaces = (ifaces,) self.interfaces = ifaces def text(self): + """Predicate label""" return 'request_selector = %s' % str(self.interfaces) phash = text @@ -72,9 +78,10 @@ If no request is currently running, a new one is created. `key` is a required argument; if None, the key will be the method's object - :param str key: annotations value key; if *None*, the key will be the method's object; if *key* is a callable - object, it will be called to get the actual session key - :param prefix: str; prefix to use for session key; if *None*, the prefix will be the property name + :param str key: annotations value key; if *None*, the key will be the method's object; if + *key* is a callable object, it will be called to get the actual session key + :param prefix: str; prefix to use for session key; if *None*, the prefix will be the property + name """ def request_decorator(func): @@ -93,18 +100,19 @@ key += '::' + '::'.join((ICacheKeyValue(arg) for arg in key_args)) if kwargs: key += '::' + \ - '::'.join(('{0}={1}'.format(key, ICacheKeyValue(val)) for key, val in kwargs.items())) - logger.debug(">>> Looking for request cache key {0}".format(key)) - data = get_request_data(request, key, _marker) - if data is _marker: - logger.debug("<<< no cached value!") + '::'.join(('{0}={1}'.format(key, ICacheKeyValue(val)) + for key, val in kwargs.items())) + LOGGER.debug(">>> Looking for request cache key {0}".format(key)) + data = get_request_data(request, key, _MARKER) + if data is _MARKER: + LOGGER.debug("<<< no cached value!") data = func if callable(data): data = data(obj, *args, **kwargs) set_request_data(request, key, data) else: - logger.debug("<<< cached value found!") - logger.debug(" < {0!r}".format(data)) + LOGGER.debug("<<< cached value found!") + LOGGER.debug(" < {0!r}".format(data)) else: data = func if callable(data): @@ -126,7 +134,7 @@ @request_property(key=None) def has_permission(self, permission, context=None): if context is None: - context = self.context + context = self.context # pylint: disable=no-member try: reg = self.registry except AttributeError: @@ -163,6 +171,7 @@ return None +# pylint: disable=invalid-name,too-many-arguments def check_request(path='/', environ=None, base_url=None, headers=None, POST=None, registry=None, principal_id=None, **kwargs): """Get current request, or create a new blank one if missing""" @@ -177,13 +186,15 @@ if factory is None: factory = PyAMSRequest request = factory.blank(path, environ, base_url, headers, POST, **kwargs) - request.registry = registry + request.registry = registry # pylint: disable=attribute-defined-outside-init if principal_id is not None: try: + # pylint: disable=import-outside-toplevel from pyams_security.utility import get_principal except ImportError: pass else: + # pylint: disable=attribute-defined-outside-init request.principal = get_principal(request, principal_id) return request @@ -210,12 +221,13 @@ return IAnnotations(request) -def get_debug(request): +def get_debug(request): # pylint: disable=unused-argument """Define 'debug' request property This function is automatically defined as a custom request method on package include. """ class Debug(): + """Request debug class""" def __init__(self): self.showTAL = False self.sourceAnnotations = False @@ -232,7 +244,7 @@ """ try: annotations = request.annotations - except (TypeError, AttributeError) as e: + except (TypeError, AttributeError): annotations = get_annotations(request) return annotations.get(key, default) @@ -246,6 +258,6 @@ """ try: annotations = request.annotations - except (TypeError, AttributeError) as e: + except (TypeError, AttributeError): annotations = get_annotations(request) annotations[key] = value diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/schema.py --- a/src/pyams_utils/schema.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/schema.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,6 +10,11 @@ # FOR A PARTICULAR PURPOSE. # +"""PyAMS_utils.schema module + +This module is used to define custom schema fields +""" + import re import string @@ -69,10 +74,10 @@ _type = None - def fromUnicode(self, str): + def fromUnicode(self, str): # pylint: disable=redefined-builtin return str - def constraint(self, value): + def constraint(self, value): # pylint: disable=method-hidden return True @@ -135,8 +140,8 @@ def _validate(self, value): if len(value) not in (3, 6): raise ValidationError(_("Color length must be 3 or 6 characters")) - for v in value: - if v not in string.hexdigits: + for val in value: + if val not in string.hexdigits: raise ValidationError(_("Color value must contain only valid hexadecimal color " "codes (numbers or letters between 'A' end 'F')")) super(ColorField, self)._validate(value) @@ -196,10 +201,12 @@ """Marker interface for mail address field""" -EMAIL_REGEX = re.compile("^[^ @]+@[^ @]+\.[^ @]+$") +EMAIL_REGEX = re.compile(r"^[^ @]+@[^ @]+\.[^ @]+$") class InvalidEmail(ValidationError): + """Invalid email validation error""" + __doc__ = _( "Email address must be entered as « name@domain.name », without '<' and '>' characters") diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/session.py --- a/src/pyams_utils/session.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/session.py Fri Nov 22 18:51:37 2019 +0100 @@ -18,9 +18,9 @@ It also adds to function to get and set session data. """ -__docformat__ = 'restructuredtext' +from pyams_utils.request import check_request -from pyams_utils.request import check_request +__docformat__ = 'restructuredtext' def get_session_data(request, app, key, default=None): @@ -66,7 +66,7 @@ session['{0}::{1}'.format(app, key)] = value -_marker = object() +_MARKER = object() def session_property(app, key=None, prefix=None): @@ -89,8 +89,8 @@ key = key(obj, *args, **kwargs) if not key: key = '{1}::{0!r}'.format(obj, prefix or func.__name__) - data = get_session_data(request, app, key, _marker) - if data is _marker: + data = get_session_data(request, app, key, _MARKER) + if data is _MARKER: data = func if callable(data): data = data(obj, *args, **kwargs) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/site.py --- a/src/pyams_utils/site.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/site.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,33 +10,33 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - - -# import standard library +"""PyAMS_utils.site module +""" -# import interfaces -from pyams_utils.interfaces import PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME, \ - PYAMS_APPLICATION_FACTORY_KEY, PUBLIC_PERMISSION -from pyams_utils.interfaces.site import ISiteRoot, ISiteRootFactory, INewLocalSiteCreatedEvent, ISiteUpgradeEvent, \ - ISiteGenerations, SITE_GENERATIONS_KEY, IConfigurationManager -from zope.component.interfaces import IPossibleSite, ObjectEvent +from persistent.dict import PersistentDict +from pyramid.exceptions import NotFound +from pyramid.path import DottedNameResolver +from pyramid.security import ALL_PERMISSIONS, Allow, Everyone +from pyramid.threadlocal import get_current_registry +from pyramid_zodbconn import get_connection +from zope.component import hooks +from zope.component.interfaces import IPossibleSite +from zope.container.folder import Folder +from zope.interface import implementer +from zope.interface.interfaces import ObjectEvent +from zope.lifecycleevent import ObjectCreatedEvent +from zope.site.site import LocalSiteManager, SiteManagerContainer from zope.traversing.interfaces import ITraversable -# import packages -from persistent.dict import PersistentDict -from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter +from pyams_utils.adapter import ContextAdapter, adapter_config, get_annotation_adapter +from pyams_utils.interfaces import PUBLIC_PERMISSION, PYAMS_APPLICATION_DEFAULT_NAME, \ + PYAMS_APPLICATION_FACTORY_KEY, PYAMS_APPLICATION_SETTINGS_KEY +from pyams_utils.interfaces.site import IConfigurationManager, INewLocalSiteCreatedEvent, \ + ISiteGenerations, ISiteRoot, ISiteRootFactory, ISiteUpgradeEvent, SITE_GENERATIONS_KEY from pyams_utils.registry import get_utilities_for, query_utility -from pyramid.exceptions import NotFound -from pyramid.path import DottedNameResolver -from pyramid.security import Allow, Everyone, ALL_PERMISSIONS -from pyramid.threadlocal import get_current_registry -from pyramid_zodbconn import get_connection -from zope.container.folder import Folder -from zope.interface import implementer -from zope.lifecycleevent import ObjectCreatedEvent -from zope.site import hooks -from zope.site.site import LocalSiteManager, SiteManagerContainer + + +__docformat__ = 'restructuredtext' @implementer(ISiteRoot, IConfigurationManager) @@ -139,7 +139,8 @@ if not current: print("Upgrading {0} to generation {1}...".format(name, utility.generation)) elif current < utility.generation: - print("Upgrading {0} from generation {1} to {2}...".format(name, current, utility.generation)) + print("Upgrading {0} from generation {1} to {2}...".format(name, current, + utility.generation)) utility.evolve(application, current) generations[name] = utility.generation finally: @@ -152,14 +153,16 @@ def check_required_utilities(site, utilities): """Utility function to check for required utilities - :param object site: the site manager into which configuration may be checked - :param tuple utilities: each element of the tuple is another tuple made of the utility interface, - the utility registration name, the utility factory and the object name when creating the utility, as in: + :param ISite site: the site manager into which configuration may be checked + :param tuple utilities: each element of the tuple is another tuple made of the utility + interface, the utility registration name, the utility factory and the object name when + creating the utility, as in: .. code-block:: python REQUIRED_UTILITIES = ((ISecurityManager, '', SecurityManager, 'Security manager'), - (IPrincipalAnnotationUtility, '', PrincipalAnnotationUtility, 'User profiles')) + (IPrincipalAnnotationUtility, '', PrincipalAnnotationUtility, + 'User profiles')) """ registry = get_current_registry() for interface, name, factory, default_id in utilities: diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/size.py --- a/src/pyams_utils/size.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/size.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,7 +10,11 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.size module + +This module provides a small function which can be used to convert +a "size" value, given in bytes, to it's "human" representation. +""" from babel import UnknownLocaleError from babel.core import Locale @@ -18,6 +22,9 @@ from pyams_utils.request import check_request + +__docformat__ = 'restructuredtext' + from pyams_utils import _ diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/timezone/__init__.py --- a/src/pyams_utils/timezone/__init__.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/timezone/__init__.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,28 +10,32 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.timezone package +All datetime values should be stored in UTC to avoid any problem. +Then values can be displayed to users using a specific timezone; by default, used timezone +is the one specified into server settings via an IServerTimezone utility which is created +automatically when initializing a new site. -# import standard library +There is no easy way to get user's timezone from it's browser settings; so the more common +choice is to let users define their timezone in their profile's settings. +""" + from datetime import datetime import pytz - - -# import interfaces -from pyams_utils.interfaces.timezone import IServerTimezone from pyramid.interfaces import IRequest from zope.interface.common.idatetime import ITZInfo -# import packages from pyams_utils.adapter import adapter_config +from pyams_utils.interfaces.timezone import IServerTimezone from pyams_utils.registry import query_utility +__docformat__ = 'restructuredtext' + + GMT = pytz.timezone('GMT') -_tz = pytz.timezone('Europe/Paris') -tz = _tz @adapter_config(context=IRequest, provides=ITZInfo) diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/unicode.py --- a/src/pyams_utils/unicode.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/unicode.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,16 +10,22 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_utils.unicode module + +This module provides a small set of functions which can be used to handle unicode data and +their bytes equivalent. +""" import codecs import string - -_unicodeTransTable = {} +__docformat__ = 'restructuredtext' -def _fillUnicodeTransTable(): +_UNICODE_TRANS_TABLE = {} + + +def _fill_unicode_trans_table(): _corresp = [ ("A", [0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x0100, 0x0102, 0x0104]), ("AE", [0x00C6]), @@ -69,20 +75,22 @@ ] for char, codes in _corresp: for code in codes: - _unicodeTransTable[code] = char + _UNICODE_TRANS_TABLE[code] = char -_fillUnicodeTransTable() +_fill_unicode_trans_table() -removed_chars = '®©™…' + +_REMOVED_CHARS = '®©™…' """List of custom characters to remove from input strings""" -def translate_string(s, escape_slashes=False, force_lower=True, +def translate_string(value, escape_slashes=False, force_lower=True, spaces=' ', remove_punctuation=True, keep_chars='_-.'): + # pylint: disable=too-many-arguments """Remove extended characters and diacritics from string and replace them with 'basic' ones - - :param str s: text to be cleaned. + + :param str value: text to be translated :param boolean escape_slashes: if True, slashes are also converted :param boolean force_lower: if True, result is automatically converted to lower case :param str spaces: character used to replace spaces @@ -91,39 +99,39 @@ :return: text without diacritics or special characters >>> from pyams_utils.unicode import translate_string - >>> input = 'Ceci est un test en Français !!!' - >>> translate_string(input) + >>> input_string = 'Ceci est un test en Français !!!' + >>> translate_string(input_string) 'ceci est un test en francais' - >>> translate_string(input, force_lower=False) + >>> translate_string(input_string, force_lower=False) 'Ceci est un test en Francais' - >>> translate_string(input, spaces='-') + >>> translate_string(input_string, spaces='-') 'ceci-est-un-test-en-francais' - >>> translate_string(input, remove_punctuation=False) + >>> translate_string(input_string, remove_punctuation=False) 'ceci est un test en francais !!!' - >>> translate_string(input, keep_chars='!') + >>> translate_string(input_string, keep_chars='!') 'ceci est un test en francais !!!' """ if escape_slashes: - s = s.replace("\\", "/").split("/")[-1] - s = s.strip() - if isinstance(s, bytes): - s = s.decode("utf-8", "replace") - s = s.translate(_unicodeTransTable) + value = value.replace("\\", "/").split("/")[-1] + value = value.strip() + if isinstance(value, bytes): + value = value.decode("utf-8", "replace") + value = value.translate(_UNICODE_TRANS_TABLE) if remove_punctuation: punctuation = ''.join(filter(lambda x: x not in keep_chars, - string.punctuation + removed_chars)) - s = ''.join(filter(lambda x: x not in punctuation, s)) + string.punctuation + _REMOVED_CHARS)) + value = ''.join(filter(lambda x: x not in punctuation, value)) if force_lower: - s = s.lower() - s = s.strip() + value = value.lower() + value = value.strip() if spaces != ' ': - s = s.replace(' ', spaces) - return s + value = value.replace(' ', spaces) + return value def nvl(value, default=''): """Get specified value, or an empty string if value is empty - + :param object value: value to be checked :param object default: default value to be returned if value is *false* :return: input value, or *default* if value is *false* @@ -141,7 +149,7 @@ def uninvl(value, default='', encoding='utf-8'): """Get specified value converted to unicode, or an empty unicode string if value is empty - + :param str/bytes value: the input to be checked :param default: str; default value :param encoding: str; encoding name to use for conversion @@ -161,13 +169,13 @@ return value try: return codecs.decode(value or default, encoding) - except: + except ValueError: return codecs.decode(value or default, 'latin1') def unidict(value, encoding='utf-8'): """Get specified dict with values converted to unicode - + :param dict value: input mapping of strings which may be converted to unicode :param str encoding: output encoding :return: dict; a new mapping with each value converted to unicode @@ -186,7 +194,7 @@ def unilist(value, encoding='utf-8'): """Get specified list with values converted to unicode - + :param list value: input list of strings which may be converted to unicode :param str encoding: output encoding :return: list; a new list with each value converted to unicode diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/vocabulary.py --- a/src/pyams_utils/vocabulary.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/vocabulary.py Fri Nov 22 18:51:37 2019 +0100 @@ -15,8 +15,6 @@ This module is used to handle vocabularies. """ -__docformat__ = 'restructuredtext' - import logging import venusian @@ -25,10 +23,13 @@ from zope.schema.vocabulary import getVocabularyRegistry -logger = logging.getLogger('PyAMS (utils)') +__docformat__ = 'restructuredtext' -class vocabulary_config: +LOGGER = logging.getLogger('PyAMS (utils)') + + +class vocabulary_config: # pylint: disable=invalid-name """Class decorator to define a vocabulary :param str name: name of the registered vocabulary @@ -48,7 +49,8 @@ '''ZEO connections vocabulary''' def __init__(self, context=None): - terms = [SimpleTerm(name, title=util.name) for name, util in get_utilities_for(IZEOConnection)] + terms = [SimpleTerm(name, title=util.name) + for name, util in get_utilities_for(IZEOConnection)] super(ZEOConnectionVocabulary, self).__init__(terms) You can then use such a vocabulary in any schema field: @@ -77,20 +79,21 @@ settings = self.__dict__.copy() depth = settings.pop('_depth', 0) - def callback(context, name, ob): - logger.debug('Registering class {0} as vocabulary with name "{1}"'.format(str(ob), self.name)) - directlyProvides(ob, IVocabularyFactory) - getVocabularyRegistry().register(self.name, ob) + def callback(context, name, obj): # pylint: disable=unused-argument + LOGGER.debug('Registering class {0} as vocabulary with name "{1}"'.format( + str(obj), self.name)) + directlyProvides(obj, IVocabularyFactory) + getVocabularyRegistry().register(self.name, obj) info = self.venusian.attach(wrapped, callback, category='pyams_vocabulary', 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 diff -r 0037199881fb -r cf2304af0fab src/pyams_utils/zodb.py --- a/src/pyams_utils/zodb.py Wed Nov 20 19:26:23 2019 +0100 +++ b/src/pyams_utils/zodb.py Fri Nov 22 18:51:37 2019 +0100 @@ -10,33 +10,34 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +""""PyAMS_utils.zodb module +This modules provides several utilities used to manage ZODB connections and persistent objects +""" -# import standard library +from ZEO import DB +from ZODB.interfaces import IConnection +from persistent import Persistent +from persistent.interfaces import IPersistent +from pyramid.events import subscriber +from pyramid_zodbconn import db_from_uri, get_uris +from transaction.interfaces import ITransactionManager +from zope.annotation.interfaces import IAttributeAnnotatable +from zope.container.contained import Contained +from zope.interface import implementer +from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent +from zope.schema import getFieldNames +from zope.schema.fieldproperty import FieldProperty +from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary -# import interfaces -from persistent.interfaces import IPersistent +from pyams_utils.adapter import adapter_config from pyams_utils.interfaces.site import IOptionalUtility from pyams_utils.interfaces.zeo import IZEOConnection -from transaction.interfaces import ITransactionManager -from ZODB.interfaces import IConnection -from zope.annotation.interfaces import IAttributeAnnotatable -from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent +from pyams_utils.registry import get_global_registry, get_utilities_for +from pyams_utils.vocabulary import vocabulary_config -# import packages -from persistent import Persistent -from pyams_utils.adapter import adapter_config -from pyams_utils.registry import get_utilities_for, get_global_registry -from pyams_utils.vocabulary import vocabulary_config -from pyramid.events import subscriber -from pyramid_zodbconn import get_uris, db_from_uri -from ZEO import DB -from zope.container.contained import Contained -from zope.interface import implementer -from zope.schema import getFieldNames -from zope.schema.fieldproperty import FieldProperty -from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm + +__docformat__ = 'restructuredtext' @adapter_config(context=IPersistent, provides=IConnection) @@ -53,19 +54,20 @@ cur = getattr(cur, '__parent__', None) if cur is None: return None - return cur._p_jar + return cur._p_jar # pylint: disable=protected-access # IPersistent adapters copied from zc.twist package # also register this for adapting from IConnection @adapter_config(context=IPersistent, provides=ITransactionManager) def persistent_transaction_manager(obj): + """Transaction manager adapter for persistent objects""" conn = IConnection(obj) # typically this will be # zope.keyreference.persistent.connectionOfPersistent try: return conn.transaction_manager except AttributeError: - return conn._txn_mgr + return conn._txn_mgr # pylint: disable=protected-access # or else we give up; who knows. transaction_manager is the more # recent spelling. @@ -75,13 +77,13 @@ # @implementer(IZEOConnection) -class ZEOConnection(object): +class ZEOConnection: """ZEO connection object This object can be used to store all settings to be able to open a ZEO connection. - Note that this class is required only for tasks specifically targeting a ZEO database connection (like a ZEO - packer scheduler task); for generic ZODB operations, just use a :class:`ZODBConnection` class defined through - Pyramid's configuration file. + Note that this class is required only for tasks specifically targeting a ZEO database + connection (like a ZEO packer scheduler task); for generic ZODB operations, just use a + :class:`ZODBConnection` class defined through Pyramid's configuration file. Note that a ZEO connection object is a context manager, so you can use it like this: @@ -157,6 +159,7 @@ @property def connection(self): + """Connection getter""" return self._connection # Context manager methods @@ -195,21 +198,22 @@ class ZEOConnectionVocabulary(SimpleVocabulary): """ZEO connections vocabulary""" - def __init__(self, context=None): - terms = [SimpleTerm(name, title=util.name) for name, util in get_utilities_for(IZEOConnection)] + def __init__(self, context=None): # pylint: disable=unused-argument + terms = [SimpleTerm(name, title=util.name) + for name, util in get_utilities_for(IZEOConnection)] super(ZEOConnectionVocabulary, self).__init__(terms) def get_connection_from_settings(settings=None): """Load connection matching registry settings""" if settings is None: - settings = get_global_registry().settings + settings = get_global_registry().settings # pylint: disable=no-member for name, uri in get_uris(settings): db = db_from_uri(uri, name, {}) return db.open() -class ZODBConnection(object): +class ZODBConnection: """ZODB connection wrapper Connections are extracted from Pyramid's settings file in *zodbconn.uri* entries. @@ -231,7 +235,7 @@ def __init__(self, name='', settings=None): self.name = name or '' if not settings: - settings = get_global_registry().settings + settings = get_global_registry().settings # pylint: disable=no-member self.settings = settings _connection = None @@ -240,14 +244,17 @@ @property def connection(self): + """Connection getter""" return self._connection @property def db(self): + """Database getter""" return self._db @property def storage(self): + """Storage getter""" return self._storage def get_connection(self): @@ -259,8 +266,10 @@ self._db = connection.db() self._storage = self.db.storage return connection + return None def close(self): + """Connection close""" self._connection.close() self._db.close() self._storage.close() @@ -278,16 +287,16 @@ class ZODBConnectionVocabulary(SimpleVocabulary): """ZODB connections vocabulary""" - def __init__(self, context=None): - settings = get_global_registry().settings + def __init__(self, context=None): # pylint: disable=unused-argument + settings = get_global_registry().settings # pylint: disable=no-member terms = [SimpleTerm(name, title=name) for name, uri in get_uris(settings)] super(ZODBConnectionVocabulary, self).__init__(terms) -volatile_marker = object() +VOLATILE_MARKER = object() -class volatile_property: +class volatile_property: # pylint: disable=invalid-name """Property decorator to define volatile attributes into persistent classes""" def __init__(self, fget, doc=None): @@ -300,8 +309,8 @@ if inst is None: return self attrname = '_v_{0}'.format(self.__name__) - value = getattr(inst, attrname, volatile_marker) - if value is volatile_marker: + value = getattr(inst, attrname, VOLATILE_MARKER) + if value is VOLATILE_MARKER: value = self.fget(inst) setattr(inst, attrname, value) return value