diff --git a/.coveragerc b/.coveragerc index fdbb39f..9b3659e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,3 +8,9 @@ exclude_lines = raise NotImplementedError self.fail raise AssertionError + +[paths] +source = + src/ + .tox/*/lib/python*/site-packages/ + .tox/pypy*/site-packages/ diff --git a/.gitignore b/.gitignore index d71ba67..fc24307 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__ *.so .tox .coverage +.coverage.* htmlcov/ nosetests.xml coverage.xml diff --git a/.travis.yml b/.travis.yml index 83f4c84..9243a61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,11 @@ env: TWINE_USERNAME: zope.wheelbuilder TWINE_PASSWORD: secure: "G1ORcUIV439Iws2toGhxPvYvQKQt6L1wyXfIMspz2xtjJvQ2D1XrqK8dRYyQ0YaGLY/OAI8BrEdTp/l7jrhiG0gXMeAs0k1KFbp7ATahcVT8rWOzvcLGMAiYRVloQ3rz5x5HjMm0CWpDjo1MAeJMmesyq8RlmYzaMu4aw1mBd/Y=" + jobs: + # We want to require the C extensions to build and function + # everywhere (except where we specifically opt-out) + - PURE_PYTHON: 0 + - PURE_PYTHON: 1 python: - 2.7 @@ -12,13 +17,14 @@ python: - 3.6 - 3.7 - 3.8 - - pypy - - pypy3 jobs: include: - - name: "Python: 2.7, pure (no C extensions)" - python: 2.7 + # Don't test C extensions on PyPy. + - python: pypy + env: PURE_PYTHON=1 + + - python: pypy3 env: PURE_PYTHON=1 # manylinux wheel builds @@ -73,7 +79,6 @@ install: - pip install -U pip setuptools - pip install -U coverage coveralls - pip install -U -e .[test] - - if [[ $PURE_PYTHON ]]; then pip install -U -e .[zodb,zcml]; fi script: - python --version diff --git a/CHANGES.rst b/CHANGES.rst index c958fd4..4034406 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,10 +2,15 @@ Changes ========= -4.3.1 (unreleased) +4.4.0 (unreleased) ================== -- Nothing changed yet. +- Support the ``PURE_PYTHON`` environment variable at runtime instead + of just at wheel build time. A value of 0 forces the C extensions to + be used failing if they aren't present. Any other value forces the + Python implementation to be used, ignoring the C extensions. + +- Drop support for the deprecated ``python setup.py test`` command. 4.3.0 (2019-11-11) @@ -73,7 +78,7 @@ 4.1.0 (2015-05-22) ================== -- Make ``zope.container._proxy.PytContainedProxyBase`` inherit +- Make ``zope.container._proxy.PyContainedProxyBase`` inherit directly from ``zope.proxy.AbstractProxyBase`` as well as ``persistent.Persistent``, removing a bunch of redundant code, and fixing bugs in interaction with pure-Python persistence. See: diff --git a/docs/conf.py b/docs/conf.py index ff0d1b3..db982ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,6 +22,13 @@ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' +import os +import sys +import pkg_resources + + +sys.path.append(os.path.abspath('../src')) +rqmt = pkg_resources.require('zope.container')[0] # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -31,6 +38,7 @@ 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', + 'sphinx.ext.extlinks', 'repoze.sphinx.autointerface', ] @@ -55,13 +63,10 @@ # built documents. # The full version, including alpha/beta/rc tags. -import io -with io.open('../version.txt') as f: - release = f.read().strip() - # The short X.Y version. -version = u'.'.join(release.split('.')[:2]) - +version = '%s.%s' % tuple(map(int, rqmt.version.split('.')[:2])) +# The full version, including alpha/beta/rc tags. +release = rqmt.version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -79,7 +84,7 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +default_role = 'obj' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True @@ -106,7 +111,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -270,12 +275,13 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'https://docs.python.org/': None, - 'http://zodb.readthedocs.io/en/latest/': None, - 'http://persistent.readthedocs.io/en/latest/': None, - 'http://zopesize.readthedocs.io/en/latest/': None, - 'http://zopesite.readthedocs.io/en/latest/': None, - 'http://zopefilerepresentation.readthedocs.io/en/latest/': None, - 'http://zopelifecycleevent.readthedocs.io/en/latest/': None, + 'https://zodb.readthedocs.io/en/latest/': None, + 'https://persistent.readthedocs.io/en/latest/': None, + 'https://zopesize.readthedocs.io/en/latest/': None, + 'https://zopesite.readthedocs.io/en/latest/': None, + 'https://zopefilerepresentation.readthedocs.io/en/latest/': None, + 'https://zopelifecycleevent.readthedocs.io/en/latest/': None, + 'https://zopeinterface.readthedocs.io/en/latest/': None, } extlinks = {'issue': ('https://github.com/zopefoundation/zope.container/issues/%s', @@ -283,5 +289,13 @@ 'pr': ('https://github.com/zopefoundation/zope.container/pull/%s', 'pull request #')} -autodoc_default_flags = ['members', 'show-inheritance'] + +# Sphinx 1.8+ prefers this to `autodoc_default_flags`. It's documented that +# either True or None mean the same thing as just setting the flag, but +# only None works in 1.8 (True works in 2.0) +autodoc_default_options = { + 'members': None, + 'show-inheritance': None, +} +autodoc_member_order = 'groupwise' autoclass_content = 'both' diff --git a/setup.py b/setup.py index 628f936..533422d 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ """Setup for zope.container package """ import os -import platform from setuptools import setup, find_packages, Extension @@ -29,39 +28,14 @@ def read(*rnames): return f.read() -def alltests(): - import sys - import unittest - # use the zope.testrunner machinery to find all the - # test suites we've put under ourselves - import zope.testrunner.find - import zope.testrunner.options - here = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')) - args = sys.argv[:] - defaults = ["--test-path", here] - options = zope.testrunner.options.get_options(args, defaults) - suites = list(zope.testrunner.find.find_suites(options)) - return unittest.TestSuite(suites) - - -# PyPy cannot correctly build the C optimizations, and even if it -# could they would be anti-optimizations (the C extension -# compatibility layer is known-slow, and defeats JIT opportunities). -py_impl = getattr(platform, 'python_implementation', lambda: None) -pure_python = os.environ.get('PURE_PYTHON', False) -is_pypy = py_impl() == 'PyPy' - -if pure_python or is_pypy: - ext_modules = [] -else: - ext_modules = [ - Extension( - "zope.container._zope_container_contained", - [os.path.join("src", "zope", "container", - "_zope_container_contained.c")], - include_dirs=['include'], - ), - ] +ext_modules = [ + Extension( + "zope.container._zope_container_contained", + [os.path.join("src", "zope", "container", + "_zope_container_contained.c")], + include_dirs=['include'], + ), +] install_requires = [ 'BTrees', @@ -85,9 +59,31 @@ def alltests(): 'setuptools', ] +extras = { + 'docs': [ + 'Sphinx', + 'repoze.sphinx.autointerface', + 'sphinx_rtd_theme', + ], + 'test': [ + 'zope.testing', + 'zope.testrunner', + ], + 'zcml': [ + 'zope.component[zcml]', + 'zope.configuration', + 'zope.security[zcml]>=4.0.0a3', + ], + 'zodb': [ + 'ZODB>=3.10', + ], +} + +extras['test'] += (extras['zodb'] + extras['zcml']) + setup(name='zope.container', - version=read('version.txt').strip(), + version='4.4.0.dev0', author='Zope Foundation and Contributors', author_email='zope-dev@zope.org', description='Zope Container', @@ -117,36 +113,23 @@ def alltests(): 'Topic :: Internet :: WWW/HTTP', 'Framework :: Zope :: 3', ], - url='http://github.com/zopefoundation/zope.container', + url='https://github.com/zopefoundation/zope.container', license='ZPL 2.1', packages=find_packages('src'), package_dir={'': 'src'}, namespace_packages=['zope'], ext_modules=ext_modules, - extras_require={ - 'docs': [ - 'Sphinx', - 'repoze.sphinx.autointerface', - ], - 'test': [ - 'zope.testing', - 'zope.testrunner', - ], - 'zcml': [ - 'zope.component[zcml]', - 'zope.configuration', - 'zope.security[zcml]>=4.0.0a3', - ], - 'zodb': [ - 'ZODB>=3.10', - ], - }, + extras_require=extras, install_requires=install_requires, - tests_require=[ - 'zope.testing', - 'zope.testrunner', - ], - test_suite='__main__.alltests', + tests_require=extras['test'], include_package_data=True, zip_safe=False, + python_requires=', '.join([ + '>=2.7', + '!=3.0.*', + '!=3.1.*', + '!=3.2.*', + '!=3.3.*', + '!=3.4.*', + ]), ) diff --git a/src/zope/container/_proxy.py b/src/zope/container/_proxy.py index ce4e01a..64bfdfe 100644 --- a/src/zope/container/_proxy.py +++ b/src/zope/container/_proxy.py @@ -13,31 +13,34 @@ ############################################################################## from zope.proxy import AbstractPyProxyBase - -_MARKER = object() +from zope.container._util import use_c_impl from persistent import Persistent + +_MARKER = object() + def _special_name(name): "attribute names we delegate to Persistent for" return (name.startswith('_Persistent') or name.startswith('_p_') or name.startswith('_v_') - or name in PyContainedProxyBase.__slots__) + or name in ContainedProxyBase.__slots__) -class PyContainedProxyBase(AbstractPyProxyBase, Persistent): +@use_c_impl +class ContainedProxyBase(AbstractPyProxyBase, Persistent): """Persistent proxy """ __slots__ = ('_wrapped', '__parent__', '__name__', '__weakref__') def __new__(cls, obj): - inst = super(PyContainedProxyBase, cls).__new__(cls, obj) + inst = super(ContainedProxyBase, cls).__new__(cls, obj) inst.__parent__ = None inst.__name__ = None return inst def __init__(self, obj): - super(PyContainedProxyBase, self).__init__(obj) + super(ContainedProxyBase, self).__init__(obj) self.__parent__ = None self.__name__ = None @@ -57,7 +60,7 @@ def __getstate__(self): return (self.__parent__, self.__name__) def __getnewargs__(self): - return self._wrapped, + return (self._wrapped,) def _p_invalidate(self): # The superclass wants to clear the __dict__, which @@ -80,10 +83,16 @@ def __getattribute__(self, name): # activated return Persistent.__getattribute__(self, name) - if name in ('__reduce__', '__reduce_ex__', '__getstate__', '__setstate__', '__getnewargs__'): + if name in ( + '__reduce__', + '__reduce_ex__', + '__getstate__', + '__setstate__', + '__getnewargs__', + ): return object.__getattribute__(self, name) - return super(PyContainedProxyBase,self).__getattribute__(name) + return super(ContainedProxyBase, self).__getattribute__(name) def __setattr__(self, name, value): if _special_name(name): @@ -91,17 +100,19 @@ def __setattr__(self, name, value): # so that _p_changed gets set, in addition to the _p values themselves return Persistent.__setattr__(self, name, value) - return super(PyContainedProxyBase, self).__setattr__(name, value) + return super(ContainedProxyBase, self).__setattr__(name, value) -def py_getProxiedObject(obj): - if isinstance(obj, PyContainedProxyBase): +@use_c_impl +def getProxiedObject(obj): + if isinstance(obj, ContainedProxyBase): return obj._wrapped return obj -def py_setProxiedObject(obj, new_value): - if not isinstance(obj, PyContainedProxyBase): +@use_c_impl +def setProxiedObject(obj, new_value): + if not isinstance(obj, ContainedProxyBase): raise TypeError('Not a proxy') old, obj._wrapped = obj._wrapped, new_value return old diff --git a/src/zope/container/_util.py b/src/zope/container/_util.py new file mode 100644 index 0000000..c6065ea --- /dev/null +++ b/src/zope/container/_util.py @@ -0,0 +1,185 @@ +############################################################################## +# +# Copyright (c) 2020 Zope Foundation and Contributors. +# 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. +# +############################################################################## + +import os +import sys +import types + +from zope.interface import implementedBy +from zope.interface import classImplements + +def _c_optimizations_required(): + """ + Return a true value if the C optimizations are required. + + This uses the ``PURE_PYTHON`` variable as documented in `_use_c_impl`. + """ + pure_env = os.environ.get('PURE_PYTHON') + require_c = pure_env == "0" + return require_c + + +def _c_optimizations_available(): + """ + Return the C optimization module, if available, otherwise + a false value. + + If the optimizations are required but not available, this + raises the ImportError. + + This does not say whether they should be used or not. + """ + catch = () if _c_optimizations_required() else (ImportError,) + try: + from zope.container import _zope_container_contained as c_opt + return c_opt + except catch: # pragma: no cover (they build everywhere) + return False + + +def _c_optimizations_ignored(): + """ + The opposite of `_c_optimizations_required`. + """ + pure_env = os.environ.get('PURE_PYTHON') + return pure_env is not None and pure_env != "0" + + +def _should_attempt_c_optimizations(): + """ + Return a true value if we should attempt to use the C optimizations. + + This takes into account whether we're on PyPy and the value of the + ``PURE_PYTHON`` environment variable, as defined in `_use_c_impl`. + """ + is_pypy = hasattr(sys, 'pypy_version_info') + + if _c_optimizations_required(): + return True + if is_pypy: + return False + return not _c_optimizations_ignored() + + +def use_c_impl(py_impl, name=None, globs=None): + """ + Decorator. Given an object implemented in Python, with a name like + ``Foo``, import the corresponding C implementation from + ``persistent.c`` with the name ``Foo`` and use it instead + (where ``NAME`` is the module name). + + This can also be used for constants and other things that do not + have a name by passing the name as the second argument. + + Example:: + + @use_c_impl + class Foo(object): + ... + + GHOST = use_c_impl(12, 'GHOST') + + If the ``PURE_PYTHON`` environment variable is set to any value + other than ``"0"``, or we're on PyPy, ignore the C implementation + and return the Python version. If the C implementation cannot be + imported, return the Python version. If ``PURE_PYTHON`` is set to + 0, *require* the C implementation (let the ImportError propagate); + note that PyPy can import the C implementation in this case (and + all tests pass). + + In all cases, the Python version is kept available in the module + globals with the name ``FooPy``. + + If the Python version is a class that implements interfaces, then + the C version will be declared to also implement those interfaces. + + If the Python version is a class, then each function defined + directly in that class will be replaced with a new version using + globals that still use the original name of the class for the + Python implementation. This lets the function bodies refer to the + class using the name the class is defined with, as it would + expect. (Only regular functions and static methods are handled.) + However, it also means that mutating the module globals later on + will not be visible to the methods of the class. In this example, + ``Foo().method()`` will always return 1:: + + GLOBAL_OBJECT = 1 + @use_c_impl + class Foo(object): + def method(self): + super(Foo, self).method() + return GLOBAL_OBJECT + GLOBAL_OBJECT = 2 + """ + name = name or py_impl.__name__ + globs = globs or sys._getframe(1).f_globals # pylint:disable=protected-access + + def find_impl(): + if not _should_attempt_c_optimizations(): + return py_impl + + c_opts = _c_optimizations_available() + if not c_opts: # pragma: no cover (only Jython doesn't build extensions) + return py_impl + + __traceback_info__ = c_opts + return getattr(c_opts, name) + + c_impl = find_impl() + # Always make available by the FooPy name + globs[name + 'Py'] = py_impl + + if c_impl is not py_impl and isinstance(py_impl, type): + # Rebind the globals of all the functions to still see the + # object under its original class name, so that references + # in function bodies work as expected. + py_attrs = vars(py_impl) + new_globals = None + for k, v in list(py_attrs.items()): + static = isinstance(v, staticmethod) + if static: + # Often this is __new__ + v = v.__func__ + + if not isinstance(v, types.FunctionType): + continue + + # Somewhat surprisingly, on Python 2, while + # ``Class.function`` results in a + # ``types.UnboundMethodType`` (``instancemethed``) object, + # ``Class.__dict__["function"]`` returns a + # ``types.FunctionType``, just like ``Class.function`` + # (and the dictionary access, of course) does on Python 3. + # The upshot is, we don't need different version-dependent + # code. Hooray! + if new_globals is None: + new_globals = v.__globals__.copy() + new_globals[py_impl.__name__] = py_impl + # On Python 2, all arguments are optional, but an Python 3, all + # are required. + v = types.FunctionType( + v.__code__, + new_globals, + k, # name + v.__defaults__, + v.__closure__, + ) + if static: + v = staticmethod(v) + setattr(py_impl, k, v) + # copy the interface declarations. + implements = list(implementedBy(py_impl)) + if implements: + classImplements(c_impl, *implements) + return c_impl diff --git a/src/zope/container/contained.py b/src/zope/container/contained.py index f5043e9..224e8d5 100644 --- a/src/zope/container/contained.py +++ b/src/zope/container/contained.py @@ -13,53 +13,51 @@ ############################################################################## """Classes to support implementing `IContained` """ -import os -import sys +# pylint:disable=too-many-lines +from six import text_type + import zope.component + import zope.interface.declarations from zope.interface import providedBy, Interface from zope.interface.declarations import getObjectSpecification -from zope.interface.declarations import ObjectSpecification +from zope.interface.declarations import Provides + from zope.event import notify + from zope.location.interfaces import ILocation, ISublocations +from zope.location.interfaces import IContained + from zope.security.checker import selectChecker, CombinedChecker + from zope.lifecycleevent import ObjectModifiedEvent +from zope.lifecycleevent import ObjectMovedEvent +from zope.lifecycleevent import ObjectAddedEvent +from zope.lifecycleevent import ObjectRemovedEvent + from zope.container.i18n import ZopeMessageFactory as _ -from zope.location.interfaces import IContained from zope.container.interfaces import INameChooser from zope.container.interfaces import IReservedNames, NameReserved from zope.container.interfaces import IContainerModifiedEvent -from zope.container._proxy import py_getProxiedObject as getProxiedObject -from zope.container._proxy import py_setProxiedObject as setProxiedObject -from zope.container._proxy import PyContainedProxyBase as ContainedProxyBase -if not os.getenv('PURE_PYTHON'): - try: - from zope.container._zope_container_contained import ContainedProxyBase - except ImportError: # PyPy - pass - else: - from zope.container._zope_container_contained import getProxiedObject - from zope.container._zope_container_contained import setProxiedObject +from zope.container._proxy import getProxiedObject +from zope.container._proxy import ContainedProxyBase -from zope.lifecycleevent import ObjectMovedEvent -from zope.lifecycleevent import ObjectAddedEvent -from zope.lifecycleevent import ObjectRemovedEvent try: from ZODB.interfaces import IBroken -except ImportError: - class IBroken(Interface): +except ImportError: # pragma: no cover + class IBroken(Interface): # pylint:disable=inherit-non-class pass -from six import text_type - -PY3 = sys.version_info[0] >= 3 @zope.interface.implementer(IContained) class Contained(object): - """Stupid mix-in that defines `__parent__` and `__name__` attributes""" + """ + Simple mix-in that defines ``__parent__`` and ``__name__`` + attributes and implements `IContained`. + """ __parent__ = __name__ = None @@ -70,7 +68,7 @@ class ContainerModifiedEvent(ObjectModifiedEvent): -def dispatchToSublocations(object, event): +def dispatchToSublocations(object, event): # pylint:disable=redefined-builtin """Dispatch an event to sublocations of a given object When a move event happens for an object, it's important to notify @@ -193,15 +191,15 @@ def sublocations(self): yield container[key] -def containedEvent(object, container, name=None): +def containedEvent(object, container, name=None): # pylint:disable=redefined-builtin """Establish the containment of the object in the container The object and necessary event are returned. The object may be a `ContainedProxy` around the original object. The event is an added event, a moved event, or None. - If the object implements `IContained`, simply set its `__parent__` - and `__name__` attributes: + If the object implements `IContained`, simply set its ``__parent__`` + and ``__name__`` attributes: >>> container = {} >>> item = Contained() @@ -257,8 +255,8 @@ def containedEvent(object, container, name=None): >>> event.oldName u'foo' - If the `object` implements `ILocation`, but not `IContained`, set its - `__parent__` and `__name__` attributes *and* declare that it + If the *object* implements `ILocation`, but not `IContained`, set its + ``__parent__`` and ``__name__`` attributes *and* declare that it implements `IContained`: >>> from zope.location import Location @@ -276,7 +274,7 @@ def containedEvent(object, container, name=None): True - If the `object` doesn't even implement `ILocation`, put a + If the *object* doesn't even implement `ILocation`, put a `ContainedProxy` around it: >>> item = [] @@ -326,7 +324,7 @@ def containedEvent(object, container, name=None): return object, event -def contained(object, container, name=None): +def contained(object, container, name=None): # pylint:disable=redefined-builtin """Establish the containment of the object in the container Just return the contained object without an event. This is a convenience @@ -338,7 +336,7 @@ def contained(object, container, name=None): """ return containedEvent(object, container, name)[0] -def notifyContainerModified(object, *descriptions): +def notifyContainerModified(object, *descriptions): # pylint:disable=redefined-builtin """Notify that the container was modified.""" notify(ContainerModifiedEvent(object, *descriptions)) @@ -360,14 +358,14 @@ def checkAndConvertName(name): raise ValueError("empty names are not allowed") return name -def setitem(container, setitemf, name, object): - """Helper function to set an item and generate needed events +def setitem(container, setitemf, name, object): # pylint:disable=redefined-builtin + r"""Helper function to set an item and generate needed events This helper is needed, in part, because the events need to get - published after the `object` has been added to the `container`. + published after the *object* has been added to the *container*. - If the item implements `IContained`, simply set its `__parent__` - and `__name__` attributes: + If the item implements `IContained`, simply set its ``__parent__`` + and ``__name__`` attributes: >>> class IItem(zope.interface.Interface): ... pass @@ -432,8 +430,8 @@ def setitem(container, setitemf, name, object): >>> item.moved is event 1 - We can suppress events and hooks by setting the `__parent__` and - `__name__` first: + We can suppress events and hooks by setting the ``__parent__`` and + ``__name__`` first: >>> item = Item() >>> item.__parent__, item.__name__ = container, 'c2' @@ -488,7 +486,7 @@ def setitem(container, setitemf, name, object): If the object implements `ILocation`, but not `IContained`, set it's - `__parent__` and `__name__` attributes *and* declare that it + ``__parent__`` and ``__name__`` attributes *and* declare that it implements `IContained`: >>> from zope.location import Location @@ -546,8 +544,7 @@ def setitem(container, setitemf, name, object): ... TypeError: name not unicode or ascii string - >>> c = bytes([200]) if PY3 else chr(200) - >>> setitem(container, container.__setitem__, b'hello ' + c, item) + >>> setitem(container, container.__setitem__, b'hello \xc8', item) Traceback (most recent call last): ... TypeError: name not unicode or ascii string @@ -581,9 +578,9 @@ def setitem(container, setitemf, name, object): notifyContainerModified(container) fixing_up = False -def uncontained(object, container, name=None): - """Clear the containment relationship between the `object` and - the `container`. +def uncontained(object, container, name=None): # pylint:disable=redefined-builtin + """Clear the containment relationship between the *object* and + the *container*. If we run this using the testing framework, we'll use `getEvents` to track the events generated: @@ -710,7 +707,7 @@ class NameChooser(object): def __init__(self, context): self.context = context - def checkName(self, name, object): + def checkName(self, name, object): # pylint:disable=redefined-builtin """See zope.container.interfaces.INameChooser We create and populate a dummy container @@ -793,7 +790,7 @@ def checkName(self, name, object): return True - def chooseName(self, name, object): + def chooseName(self, name, object): # pylint:disable=redefined-builtin """See zope.container.interfaces.INameChooser The name chooser is expected to choose a name without error @@ -921,7 +918,7 @@ def __get__(self, inst, cls=None): # Use type rather than __class__ because inst is a proxy and # will return the proxied object's class. cls = type(inst) - return ObjectSpecification(provided, cls) + return Provides(cls, provided) class DecoratedSecurityCheckerDescriptor(object): @@ -989,6 +986,13 @@ def __delete__(self, inst): @zope.interface.implementer(IContained) class ContainedProxy(ContainedProxyBase): + """ + Wraps an object to implement :class:`zope.container.interfaces.IContained` + with a new ``__name__`` and ``__parent__``. + + The new object provides everything the wrapped object did, plus + `IContained` and `IPersistent`. + """ # Prevent proxies from having their own instance dictionaries: __slots__ = () diff --git a/src/zope/container/tests/test_contained.py b/src/zope/container/tests/test_contained.py index cc103e2..fc625d3 100644 --- a/src/zope/container/tests/test_contained.py +++ b/src/zope/container/tests/test_contained.py @@ -62,7 +62,7 @@ def test_declarations_on_ContainedProxy(self): from zope.container.interfaces import IContained from persistent.interfaces import IPersistent - class I1(zope.interface.Interface): + class I1(zope.interface.Interface): # pylint:disable=inherit-non-class pass @zope.interface.implementer(I1) class C(object): @@ -86,7 +86,7 @@ class C(object): self.assertEqual(tuple(zope.interface.providedBy(p)), (I1, IContained, IPersistent)) - class I2(zope.interface.Interface): + class I2(zope.interface.Interface): # pylint:disable=inherit-non-class pass zope.interface.directlyProvides(c, I2) self.assertEqual(tuple(zope.interface.providedBy(p)), @@ -94,7 +94,7 @@ class I2(zope.interface.Interface): # We can declare interfaces through the proxy: - class I3(zope.interface.Interface): + class I3(zope.interface.Interface): # pylint:disable=inherit-non-class pass zope.interface.directlyProvides(p, I3) self.assertEqual(tuple(zope.interface.providedBy(p)), @@ -104,10 +104,10 @@ class I3(zope.interface.Interface): def test_ContainedProxy_instances_have_no_instance_dictionaries(self): # Make sure that proxies don't introduce extra instance dictionaries class C(object): - pass + def __init__(self, x): + self.x = x - c = C() - c.x = 1 + c = C(1) self.assertEqual(dict(c.__dict__), {'x': 1}) p = ContainedProxy(c) @@ -120,8 +120,8 @@ class C(object): self.assertIs(p.__dict__, c.__dict__) def test_get_set_ProxiedObject(self): - from zope.container.contained import getProxiedObject - from zope.container.contained import setProxiedObject + from zope.container._proxy import getProxiedObject + from zope.container._proxy import setProxiedObject proxy = ContainedProxy(self) self.assertIs(self, getProxiedObject(proxy)) diff --git a/src/zope/container/tests/test_contained_zodb.py b/src/zope/container/tests/test_contained_zodb.py index 0851cf7..6044648 100644 --- a/src/zope/container/tests/test_contained_zodb.py +++ b/src/zope/container/tests/test_contained_zodb.py @@ -17,23 +17,19 @@ import gc import unittest -try: - from ZODB.DemoStorage import DemoStorage - from ZODB.DB import DB - import transaction - HAVE_ZODB = True -except ImportError: # pragma: no cover - HAVE_ZODB = False +from ZODB.DemoStorage import DemoStorage +from ZODB.DB import DB +import transaction from persistent import Persistent from zope.container.contained import ContainedProxy +# pylint:disable=protected-access class MyOb(Persistent): - pass + ob = None -@unittest.skipUnless(HAVE_ZODB, "Needs ZODB") class TestContainedZODB(unittest.TestCase): def setUp(self): diff --git a/tox.ini b/tox.ini index 7fe092d..f68395e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,55 +1,38 @@ [tox] -envlist = py27,py35,py36,py37,py38,pypy,py27-zodb,pypy-zodb,py27-pure-zodb,docs,coverage +envlist = py27,py35,py36,py37,py38,pypy,pypy3,py27-pure,docs,coverage +# Note: if you add new Python versions, please add them to +# [testenv:coverage] depends as well! [testenv] commands = - zope-testrunner --test-path=src [] + coverage run -p -m zope.testrunner --test-path=src [] +extras = + test deps = - .[test] - -[testenv:py27-zodb] -basepython = - python2.7 -deps = - {[testenv]deps} - .[zodb] - -[testenv:py27-pure-zodb] -basepython = - python2.7 + coverage setenv = - PURE_PYTHON = 1 - PIP_CACHE_DIR = {envdir}/.cache -deps = - {[testenv:py27-zodb]deps} - - -[testenv:pypy-zodb] -basepython = - pypy -deps = - {[testenv:py27-zodb]deps} - + PURE_PYTHON=0 + pure: PURE_PYTHON=1 + pypy: PURE_PYTHON=1 + pypy3: PURE_PYTHON=1 [testenv:coverage] -usedevelop = true -basepython = - python3.6 -setenv = - PURE_PYTHON = 1 - PIP_CACHE_DIR = {envdir}/.cache - +skip_install = true commands = - coverage run -m zope.testrunner --test-path=src [] - coverage report -deps = - {[testenv:py27-zodb]deps} - coverage + coverage erase + coverage combine + coverage html -i + coverage xml -i + coverage report --fail-under=100 --show-missing +# parallel mode: make sure all builds complete before we run this one +depends = + py27,py35,py36,py37,py38,pypy,pypy3 +parallel_show_output = true [testenv:docs] basepython = - python2.7 + python3 commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html -deps = - .[docs] +extras = + docs diff --git a/version.txt b/version.txt deleted file mode 100644 index a93dc8e..0000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -4.3.1.dev0