├── .editorconfig ├── .github └── workflows │ ├── pre-commit.yml │ └── tests.yml ├── .gitignore ├── .manylinux-install.sh ├── .manylinux.sh ├── .meta.toml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGES.rst ├── CONTRIBUTING.md ├── COPYRIGHT.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── buildout.cfg ├── docs ├── Makefile ├── api.rst ├── api │ ├── attributes.rst │ ├── cache.rst │ ├── collections.rst │ ├── interfaces.rst │ └── pickling.rst ├── changes.rst ├── conf.py ├── glossary.rst ├── index.rst ├── make.bat ├── requirements.txt └── using.rst ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── persistent │ ├── __init__.py │ ├── _compat.h │ ├── _compat.py │ ├── _ring_build.py │ ├── _timestamp.c │ ├── cPersistence.c │ ├── cPersistence.h │ ├── cPickleCache.c │ ├── dict.py │ ├── interfaces.py │ ├── list.py │ ├── mapping.py │ ├── persistence.py │ ├── picklecache.py │ ├── ring.c │ ├── ring.h │ ├── ring.py │ ├── tests │ ├── __init__.py │ ├── attrhooks.py │ ├── cucumbers.py │ ├── test__compat.py │ ├── test_compile_flags.py │ ├── test_docs.py │ ├── test_list.py │ ├── test_mapping.py │ ├── test_persistence.py │ ├── test_picklecache.py │ ├── test_ring.py │ ├── test_timestamp.py │ ├── test_wref.py │ └── utils.py │ ├── timestamp.py │ └── wref.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | # 4 | # EditorConfig Configuration file, for more details see: 5 | # http://EditorConfig.org 6 | # EditorConfig is a convention description, that could be interpreted 7 | # by multiple editors to enforce common coding conventions for specific 8 | # file types 9 | 10 | # top-most EditorConfig file: 11 | # Will ignore other EditorConfig files in Home directory or upper tree level. 12 | root = true 13 | 14 | 15 | [*] # For All Files 16 | # Unix-style newlines with a newline ending every file 17 | end_of_line = lf 18 | insert_final_newline = true 19 | trim_trailing_whitespace = true 20 | # Set default charset 21 | charset = utf-8 22 | # Indent style default 23 | indent_style = space 24 | # Max Line Length - a hard line wrap, should be disabled 25 | max_line_length = off 26 | 27 | [*.{py,cfg,ini}] 28 | # 4 space indentation 29 | indent_size = 4 30 | 31 | [*.{yml,zpt,pt,dtml,zcml}] 32 | # 2 space indentation 33 | indent_size = 2 34 | 35 | [{Makefile,.gitmodules}] 36 | # Tab indentation (no size specified, but view as 4 spaces) 37 | indent_style = tab 38 | indent_size = unset 39 | tab_width = unset 40 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | name: pre-commit 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - master 10 | # Allow to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | env: 14 | FORCE_COLOR: 1 15 | 16 | jobs: 17 | pre-commit: 18 | permissions: 19 | contents: read 20 | pull-requests: write 21 | name: linting 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.x 28 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd #v3.0.1 29 | with: 30 | extra_args: --all-files --show-diff-on-failure 31 | env: 32 | PRE_COMMIT_COLOR: always 33 | - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 #v1.1.0 34 | if: always() 35 | with: 36 | msg: Apply pre-commit code formatting 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | *.dll 4 | *.egg-info/ 5 | *.profraw 6 | *.pyc 7 | *.pyo 8 | *.so 9 | .coverage 10 | .coverage.* 11 | .eggs/ 12 | .installed.cfg 13 | .mr.developer.cfg 14 | .tox/ 15 | .vscode/ 16 | __pycache__/ 17 | bin/ 18 | build/ 19 | coverage.xml 20 | develop-eggs/ 21 | develop/ 22 | dist/ 23 | docs/_build 24 | eggs/ 25 | etc/ 26 | lib/ 27 | lib64 28 | log/ 29 | parts/ 30 | pyvenv.cfg 31 | testing.log 32 | var/ 33 | -------------------------------------------------------------------------------- /.manylinux-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Generated from: 3 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 4 | 5 | set -e -x 6 | 7 | # Running inside docker 8 | # Set a cache directory for pip. This was 9 | # mounted to be the same as it is outside docker so it 10 | # can be persisted. 11 | export XDG_CACHE_HOME="/cache" 12 | # XXX: This works for macOS, where everything bind-mounted 13 | # is seen as owned by root in the container. But when the host is Linux 14 | # the actual UIDs come through to the container, triggering 15 | # pip to disable the cache when it detects that the owner doesn't match. 16 | # The below is an attempt to fix that, taken from bcrypt. It seems to work on 17 | # Github Actions. 18 | if [ -n "$GITHUB_ACTIONS" ]; then 19 | echo Adjusting pip cache permissions 20 | mkdir -p $XDG_CACHE_HOME/pip 21 | chown -R $(whoami) $XDG_CACHE_HOME 22 | fi 23 | ls -ld /cache 24 | ls -ld /cache/pip 25 | 26 | # We need some libraries because we build wheels from scratch: 27 | yum -y install libffi-devel 28 | 29 | tox_env_map() { 30 | case $1 in 31 | *"cp39"*) echo 'py39';; 32 | *"cp310"*) echo 'py310';; 33 | *"cp311"*) echo 'py311';; 34 | *"cp312"*) echo 'py312';; 35 | *"cp313"*) echo 'py313';; 36 | *) echo 'py';; 37 | esac 38 | } 39 | 40 | # Compile wheels 41 | for PYBIN in /opt/python/*/bin; do 42 | if \ 43 | [[ "${PYBIN}" == *"cp39/"* ]] || \ 44 | [[ "${PYBIN}" == *"cp310/"* ]] || \ 45 | [[ "${PYBIN}" == *"cp311/"* ]] || \ 46 | [[ "${PYBIN}" == *"cp312/"* ]] || \ 47 | [[ "${PYBIN}" == *"cp313/"* ]] ; then 48 | "${PYBIN}/pip" install -e /io/ 49 | "${PYBIN}/pip" wheel /io/ -w wheelhouse/ 50 | if [ `uname -m` == 'aarch64' ]; then 51 | cd /io/ 52 | ${PYBIN}/pip install tox 53 | TOXENV=$(tox_env_map "${PYBIN}") 54 | ${PYBIN}/tox -e ${TOXENV} 55 | cd .. 56 | fi 57 | rm -rf /io/build /io/*.egg-info 58 | fi 59 | done 60 | 61 | # Bundle external shared libraries into the wheels 62 | for whl in wheelhouse/persistent*.whl; do 63 | auditwheel repair "$whl" -w /io/wheelhouse/ 64 | done 65 | -------------------------------------------------------------------------------- /.manylinux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Generated from: 3 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 4 | 5 | set -e -x 6 | 7 | # Mount the current directory as /io 8 | # Mount the pip cache directory as /cache 9 | # `pip cache` requires pip 20.1 10 | echo Setting up caching 11 | python --version 12 | python -mpip --version 13 | LCACHE="$(dirname `python -mpip cache dir`)" 14 | echo Sharing pip cache at $LCACHE $(ls -ld $LCACHE) 15 | 16 | docker run --rm -e GITHUB_ACTIONS -v "$(pwd)":/io -v "$LCACHE:/cache" $DOCKER_IMAGE $PRE_CMD /io/.manylinux-install.sh 17 | -------------------------------------------------------------------------------- /.meta.toml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | [meta] 4 | template = "c-code" 5 | commit-id = "3c1c588c" 6 | 7 | [python] 8 | with-windows = true 9 | with-pypy = true 10 | with-future-python = false 11 | with-docs = true 12 | with-sphinx-doctests = true 13 | with-macos = false 14 | 15 | [tox] 16 | use-flake8 = true 17 | coverage-command = [ 18 | "coverage run -m zope.testrunner --test-path=src {posargs:-vc}", 19 | "python -c 'import os, subprocess; subprocess.check_call(\"coverage run -a -m zope.testrunner --test-path=src\", env=dict(os.environ, PURE_PYTHON=\"0\"), shell=True)'", 20 | ] 21 | 22 | [coverage] 23 | fail-under = 94.9 24 | 25 | [coverage-run] 26 | source = "persistent" 27 | additional-config = [ 28 | "omit =", 29 | " _ring_build.py", 30 | ] 31 | 32 | [manifest] 33 | additional-rules = [ 34 | "include *.yaml", 35 | "include *.sh", 36 | "recursive-include docs *.bat", 37 | "recursive-include src *.h", 38 | ] 39 | 40 | [check-manifest] 41 | additional-ignores = [ 42 | "docs/_build/html/_sources/api/*", 43 | ] 44 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | minimum_pre_commit_version: '3.6' 4 | repos: 5 | - repo: https://github.com/pycqa/isort 6 | rev: "6.0.0" 7 | hooks: 8 | - id: isort 9 | - repo: https://github.com/hhatto/autopep8 10 | rev: "v2.3.2" 11 | hooks: 12 | - id: autopep8 13 | args: [--in-place, --aggressive, --aggressive] 14 | - repo: https://github.com/asottile/pyupgrade 15 | rev: v3.19.1 16 | hooks: 17 | - id: pyupgrade 18 | args: [--py39-plus] 19 | - repo: https://github.com/isidentical/teyit 20 | rev: 0.4.3 21 | hooks: 22 | - id: teyit 23 | - repo: https://github.com/PyCQA/flake8 24 | rev: "7.1.1" 25 | hooks: 26 | - id: flake8 27 | additional_dependencies: 28 | - flake8-debugger == 4.1.2 29 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | # Read the Docs configuration file 4 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 5 | 6 | # Required 7 | version: 2 8 | 9 | # Set the version of Python and other tools you might need 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.11" 14 | 15 | # Build documentation in the docs/ directory with Sphinx 16 | sphinx: 17 | configuration: docs/conf.py 18 | 19 | # We recommend specifying your dependencies to enable reproducible builds: 20 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | - method: pip 25 | path: . 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | # Contributing to zopefoundation projects 6 | 7 | The projects under the zopefoundation GitHub organization are open source and 8 | welcome contributions in different forms: 9 | 10 | * bug reports 11 | * code improvements and bug fixes 12 | * documentation improvements 13 | * pull request reviews 14 | 15 | For any changes in the repository besides trivial typo fixes you are required 16 | to sign the contributor agreement. See 17 | https://www.zope.dev/developer/becoming-a-committer.html for details. 18 | 19 | Please visit our [Developer 20 | Guidelines](https://www.zope.dev/developer/guidelines.html) if you'd like to 21 | contribute code changes and our [guidelines for reporting 22 | bugs](https://www.zope.dev/developer/reporting-bugs.html) if you want to file a 23 | bug report. 24 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Zope Foundation and Contributors -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Zope Public License (ZPL) Version 2.1 2 | 3 | A copyright notice accompanies this license document that identifies the 4 | copyright holders. 5 | 6 | This license has been certified as open source. It has also been designated as 7 | GPL compatible by the Free Software Foundation (FSF). 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions in source code must retain the accompanying copyright 13 | notice, this list of conditions, and the following disclaimer. 14 | 15 | 2. Redistributions in binary form must reproduce the accompanying copyright 16 | notice, this list of conditions, and the following disclaimer in the 17 | documentation and/or other materials provided with the distribution. 18 | 19 | 3. Names of the copyright holders must not be used to endorse or promote 20 | products derived from this software without prior written permission from the 21 | copyright holders. 22 | 23 | 4. The right to distribute this software or to use it for any purpose does not 24 | give you the right to use Servicemarks (sm) or Trademarks (tm) of the 25 | copyright 26 | holders. Use of them is covered by separate agreement with the copyright 27 | holders. 28 | 29 | 5. If any files are modified, you must cause the modified files to carry 30 | prominent notices stating that you changed the files and the date of any 31 | change. 32 | 33 | Disclaimer 34 | 35 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED 36 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 37 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 38 | EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 39 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 40 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 41 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 42 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 43 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 44 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 45 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | include *.md 4 | include *.rst 5 | include *.txt 6 | include buildout.cfg 7 | include tox.ini 8 | include .pre-commit-config.yaml 9 | 10 | recursive-include docs *.py 11 | recursive-include docs *.rst 12 | recursive-include docs *.txt 13 | recursive-include docs Makefile 14 | 15 | recursive-include src *.py 16 | include *.yaml 17 | include *.sh 18 | recursive-include docs *.bat 19 | recursive-include src *.h 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========================================================== 2 | ``persistent``: automatic persistence for Python objects 3 | =========================================================== 4 | 5 | .. image:: https://github.com/zopefoundation/persistent/actions/workflows/tests.yml/badge.svg 6 | :target: https://github.com/zopefoundation/persistent/actions/workflows/tests.yml 7 | 8 | .. image:: https://coveralls.io/repos/github/zopefoundation/persistent/badge.svg?branch=master 9 | :target: https://coveralls.io/github/zopefoundation/persistent?branch=master 10 | 11 | .. image:: https://readthedocs.org/projects/persistent/badge/?version=latest 12 | :target: https://persistent.readthedocs.io/en/latest/ 13 | :alt: Documentation Status 14 | 15 | .. image:: https://img.shields.io/pypi/v/persistent.svg 16 | :target: https://pypi.org/project/persistent/ 17 | :alt: Latest release 18 | 19 | .. image:: https://img.shields.io/pypi/pyversions/persistent.svg 20 | :target: https://pypi.org/project/persistent/ 21 | :alt: Python versions 22 | 23 | This package contains a generic persistence implementation for Python. It 24 | forms the core protocol for making objects interact "transparently" with 25 | a database such as the ZODB. 26 | 27 | Please see the Sphinx documentation (``docs/index.rst``) for further 28 | information, or view the documentation at Read The Docs, for either 29 | the latest (``https://persistent.readthedocs.io/en/latest/``) or stable 30 | release (``https://persistent.readthedocs.io/en/stable/``). 31 | 32 | .. note:: 33 | 34 | Use of this standalone ``persistent`` release is not recommended or 35 | supported with ZODB < 3.11. ZODB 3.10 and earlier bundle their own 36 | version of the ``persistent`` package. 37 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | develop = . 3 | parts = 4 | test 5 | scripts 6 | 7 | [test] 8 | recipe = zc.recipe.testrunner 9 | eggs = 10 | persistent [test] 11 | 12 | [scripts] 13 | recipe = zc.recipe.egg 14 | eggs = persistent [test] 15 | interpreter = py 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | :mod:`persistent` API documentation 2 | =================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | api/interfaces 8 | api/collections 9 | api/attributes 10 | api/pickling 11 | api/cache 12 | -------------------------------------------------------------------------------- /docs/api/attributes.rst: -------------------------------------------------------------------------------- 1 | Customizing Attribute Access 2 | ============================ 3 | 4 | Hooking :meth:`__getattr__` 5 | --------------------------- 6 | The __getattr__ method works pretty much the same for persistent 7 | classes as it does for other classes. No special handling is 8 | needed. If an object is a ghost, then it will be activated before 9 | __getattr__ is called. 10 | 11 | In this example, our objects returns a tuple with the attribute 12 | name, converted to upper case and the value of _p_changed, for any 13 | attribute that isn't handled by the default machinery. 14 | 15 | .. doctest:: 16 | 17 | >>> from persistent.tests.attrhooks import OverridesGetattr 18 | >>> o = OverridesGetattr() 19 | >>> o._p_changed 20 | False 21 | >>> o._p_oid 22 | >>> o._p_jar 23 | >>> o.spam 24 | ('SPAM', False) 25 | >>> o.spam = 1 26 | >>> o.spam 27 | 1 28 | 29 | We'll save the object, so it can be deactivated: 30 | 31 | .. doctest:: 32 | 33 | >>> from persistent.tests.attrhooks import _resettingJar 34 | >>> jar = _resettingJar() 35 | >>> jar.add(o) 36 | >>> o._p_deactivate() 37 | >>> o._p_changed 38 | 39 | And now, if we ask for an attribute it doesn't have, 40 | 41 | .. doctest:: 42 | 43 | >>> o.eggs 44 | ('EGGS', False) 45 | 46 | And we see that the object was activated before calling the 47 | :meth:`__getattr__` method. 48 | 49 | Hooking All Access 50 | ------------------ 51 | 52 | In this example, we'll provide an example that shows how to 53 | override the :meth:`__getattribute__`, :meth:`__setattr__`, and 54 | :meth:`__delattr__` methods. We'll create a class that stores it's 55 | attributes in a secret dictionary within the instance dictionary. 56 | 57 | The class will have the policy that variables with names starting 58 | with ``tmp_`` will be volatile. 59 | 60 | Our sample class takes initial values as keyword arguments to the constructor: 61 | 62 | .. doctest:: 63 | 64 | >>> from persistent.tests.attrhooks import VeryPrivate 65 | >>> o = VeryPrivate(x=1) 66 | 67 | 68 | Hooking :meth:`__getattribute__`` 69 | ################################# 70 | 71 | The :meth:`__getattribute__` method is called for all attribute 72 | accesses. It overrides the attribute access support inherited 73 | from Persistent. 74 | 75 | .. doctest:: 76 | 77 | >>> o._p_changed 78 | False 79 | >>> o._p_oid 80 | >>> o._p_jar 81 | >>> o.x 82 | 1 83 | >>> o.y 84 | Traceback (most recent call last): 85 | ... 86 | AttributeError: y 87 | 88 | Next, we'll save the object in a database so that we can deactivate it: 89 | 90 | .. doctest:: 91 | 92 | >>> from persistent.tests.attrhooks import _rememberingJar 93 | >>> jar = _rememberingJar() 94 | >>> jar.add(o) 95 | >>> o._p_deactivate() 96 | >>> o._p_changed 97 | 98 | And we'll get some data: 99 | 100 | .. doctest:: 101 | 102 | >>> o.x 103 | 1 104 | 105 | which activates the object: 106 | 107 | .. doctest:: 108 | 109 | >>> o._p_changed 110 | False 111 | 112 | It works for missing attributes too: 113 | 114 | .. doctest:: 115 | 116 | >>> o._p_deactivate() 117 | >>> o._p_changed 118 | 119 | >>> o.y 120 | Traceback (most recent call last): 121 | ... 122 | AttributeError: y 123 | 124 | >>> o._p_changed 125 | False 126 | 127 | 128 | Hooking :meth:`__setattr__`` 129 | ############################ 130 | 131 | The :meth:`__setattr__` method is called for all attribute 132 | assignments. It overrides the attribute assignment support 133 | inherited from Persistent. 134 | 135 | Implementors of :meth:`__setattr__` methods: 136 | 137 | 1. Must call Persistent._p_setattr first to allow it 138 | to handle some attributes and to make sure that the object 139 | is activated if necessary, and 140 | 141 | 2. Must set _p_changed to mark objects as changed. 142 | 143 | .. doctest:: 144 | 145 | >>> o = VeryPrivate() 146 | >>> o._p_changed 147 | False 148 | >>> o._p_oid 149 | >>> o._p_jar 150 | >>> o.x 151 | Traceback (most recent call last): 152 | ... 153 | AttributeError: x 154 | 155 | >>> o.x = 1 156 | >>> o.x 157 | 1 158 | 159 | Because the implementation doesn't store attributes directly 160 | in the instance dictionary, we don't have a key for the attribute: 161 | 162 | .. doctest:: 163 | 164 | >>> 'x' in o.__dict__ 165 | False 166 | 167 | Next, we'll give the object a "remembering" jar so we can 168 | deactivate it: 169 | 170 | .. doctest:: 171 | 172 | >>> jar = _rememberingJar() 173 | >>> jar.add(o) 174 | >>> o._p_deactivate() 175 | >>> o._p_changed 176 | 177 | We'll modify an attribute 178 | 179 | .. doctest:: 180 | 181 | >>> o.y = 2 182 | >>> o.y 183 | 2 184 | 185 | which reactivates it, and marks it as modified, because our 186 | implementation marked it as modified: 187 | 188 | .. doctest:: 189 | 190 | >>> o._p_changed 191 | True 192 | 193 | Now, if fake a commit: 194 | 195 | .. doctest:: 196 | 197 | >>> jar.fake_commit() 198 | >>> o._p_changed 199 | False 200 | 201 | And deactivate the object: 202 | 203 | .. doctest:: 204 | 205 | >>> o._p_deactivate() 206 | >>> o._p_changed 207 | 208 | and then set a variable with a name starting with ``tmp_``, 209 | The object will be activated, but not marked as modified, 210 | because our :meth:`__setattr__` implementation doesn't mark the 211 | object as changed if the name starts with ``tmp_``: 212 | 213 | .. doctest:: 214 | 215 | >>> o.tmp_foo = 3 216 | >>> o._p_changed 217 | False 218 | >>> o.tmp_foo 219 | 3 220 | 221 | 222 | Hooking :meth:`__delattr__`` 223 | ############################ 224 | 225 | The __delattr__ method is called for all attribute 226 | deletions. It overrides the attribute deletion support 227 | inherited from Persistent. 228 | 229 | Implementors of :meth:`__delattr__` methods: 230 | 231 | 1. Must call Persistent._p_delattr first to allow it 232 | to handle some attributes and to make sure that the object 233 | is activated if necessary, and 234 | 235 | 2. Must set _p_changed to mark objects as changed. 236 | 237 | .. doctest:: 238 | 239 | >>> o = VeryPrivate(x=1, y=2, tmp_z=3) 240 | >>> o._p_changed 241 | False 242 | >>> o._p_oid 243 | >>> o._p_jar 244 | >>> o.x 245 | 1 246 | >>> del o.x 247 | >>> o.x 248 | Traceback (most recent call last): 249 | ... 250 | AttributeError: x 251 | 252 | Next, we'll save the object in a jar so that we can 253 | deactivate it: 254 | 255 | .. doctest:: 256 | 257 | >>> jar = _rememberingJar() 258 | >>> jar.add(o) 259 | >>> o._p_deactivate() 260 | >>> o._p_changed 261 | 262 | If we delete an attribute: 263 | 264 | .. doctest:: 265 | 266 | >>> del o.y 267 | 268 | The object is activated. It is also marked as changed because 269 | our implementation marked it as changed. 270 | 271 | .. doctest:: 272 | 273 | >>> o._p_changed 274 | True 275 | >>> o.y 276 | Traceback (most recent call last): 277 | ... 278 | AttributeError: y 279 | 280 | >>> o.tmp_z 281 | 3 282 | 283 | Now, if fake a commit: 284 | 285 | .. doctest:: 286 | 287 | >>> jar.fake_commit() 288 | >>> o._p_changed 289 | False 290 | 291 | And deactivate the object: 292 | 293 | .. doctest:: 294 | 295 | >>> o._p_deactivate() 296 | >>> o._p_changed 297 | 298 | and then delete a variable with a name starting with ``tmp_``, 299 | The object will be activated, but not marked as modified, 300 | because our :meth:`__delattr__` implementation doesn't mark the 301 | object as changed if the name starts with ``tmp_``: 302 | 303 | .. doctest:: 304 | 305 | >>> del o.tmp_z 306 | >>> o._p_changed 307 | False 308 | >>> o.tmp_z 309 | Traceback (most recent call last): 310 | ... 311 | AttributeError: tmp_z 312 | 313 | If we attempt to delete ``_p_oid``, we find that we can't, and the 314 | object is also not activated or changed: 315 | 316 | .. doctest:: 317 | 318 | >>> del o._p_oid 319 | Traceback (most recent call last): 320 | ... 321 | ValueError: can't delete _p_oid of cached object 322 | >>> o._p_changed 323 | False 324 | 325 | We are allowed to delete ``_p_changed``, which sets it to ``None``: 326 | 327 | >>> del o._p_changed 328 | >>> o._p_changed is None 329 | True 330 | -------------------------------------------------------------------------------- /docs/api/cache.rst: -------------------------------------------------------------------------------- 1 | Caching Persistent Objects 2 | ========================== 3 | 4 | Creating Objects ``de novo`` 5 | ---------------------------- 6 | 7 | Creating ghosts from scratch, as opposed to ghostifying a non-ghost 8 | is rather tricky. :class:`~persistent.interfaces.IPeristent` doesn't 9 | really provide the right interface given that: 10 | 11 | - :meth:`_p_deactivate` and :meth:`_p_invalidate` are overridable, and 12 | could assume that the object's state is properly initialized. 13 | 14 | - Assigning :attr:`_p_changed` to None just calls :meth:`_p_deactivate`. 15 | 16 | - Deleting :attr:`_p_changed` just calls :meth:`_p_invalidate`. 17 | 18 | .. note:: 19 | 20 | The current cache implementation is intimately tied up with the 21 | persistence implementation and has internal access to the persistence 22 | state. The cache implementation can update the persistence state for 23 | newly created and uninitialized objects directly. 24 | 25 | The future persistence and cache implementations will be far more 26 | decoupled. The persistence implementation will only manage object 27 | state and generate object-usage events. The cache implementation(s) 28 | will be responsible for managing persistence-related (meta-)state, 29 | such as _p_state, _p_changed, _p_oid, etc. So in that future 30 | implementation, the cache will be more central to managing object 31 | persistence information. 32 | 33 | Caches have a :meth:`new_ghost` method that: 34 | 35 | - adds an object to the cache, and 36 | 37 | - initializes its persistence data. 38 | 39 | .. doctest:: 40 | 41 | >>> import persistent 42 | >>> from persistent.tests.utils import ResettingJar 43 | 44 | >>> class C(persistent.Persistent): 45 | ... pass 46 | 47 | >>> jar = ResettingJar() 48 | >>> cache = persistent.PickleCache(jar, 10, 100) 49 | >>> ob = C.__new__(C) 50 | >>> cache.new_ghost(b'1', ob) 51 | 52 | >>> ob._p_changed 53 | >>> ob._p_jar is jar 54 | True 55 | >>> ob._p_oid == b'1' 56 | True 57 | 58 | >>> cache.cache_non_ghost_count 59 | 0 60 | -------------------------------------------------------------------------------- /docs/api/collections.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Persistent Collections 3 | ======================== 4 | 5 | The ``persistent`` package provides two simple collections that are 6 | persistent and keep track of when they are mutated in place. 7 | 8 | .. autoclass:: persistent.mapping.PersistentMapping 9 | :members: 10 | :show-inheritance: 11 | 12 | .. autoclass:: persistent.list.PersistentList 13 | :members: 14 | :show-inheritance: 15 | -------------------------------------------------------------------------------- /docs/api/interfaces.rst: -------------------------------------------------------------------------------- 1 | :mod:`persistent.interfaces` 2 | =================================== 3 | 4 | .. automodule:: persistent.interfaces 5 | 6 | .. autointerface:: IPersistent 7 | :members: 8 | :member-order: bysource 9 | 10 | .. autointerface:: IPersistentDataManager 11 | :members: 12 | :member-order: bysource 13 | 14 | .. autointerface:: IPickleCache 15 | :members: 16 | :member-order: bysource 17 | 18 | Implementations 19 | =============== 20 | 21 | This package provides one implementation of :class:`IPersistent` that 22 | should be extended. 23 | 24 | .. autoclass:: persistent.Persistent 25 | :members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/api/pickling.rst: -------------------------------------------------------------------------------- 1 | Pickling Persistent Objects 2 | =========================== 3 | 4 | Persistent objects are designed to make the standard Python pickling 5 | machinery happy: 6 | 7 | .. doctest:: 8 | 9 | >>> import pickle 10 | >>> from persistent.tests.cucumbers import Simple 11 | >>> from persistent.tests.cucumbers import print_dict 12 | 13 | >>> x = Simple('x', aaa=1, bbb='foo') 14 | 15 | >>> print_dict(x.__getstate__()) 16 | {'__name__': 'x', 'aaa': 1, 'bbb': 'foo'} 17 | 18 | >>> f, (c,), state = x.__reduce__() 19 | >>> f.__name__ 20 | '__newobj__' 21 | >>> f.__module__.replace('_', '') # Normalize Python2/3 22 | 'copyreg' 23 | >>> c.__name__ 24 | 'Simple' 25 | 26 | >>> print_dict(state) 27 | {'__name__': 'x', 'aaa': 1, 'bbb': 'foo'} 28 | 29 | >>> import pickle 30 | >>> pickle.loads(pickle.dumps(x)) == x 31 | True 32 | >>> pickle.loads(pickle.dumps(x, 0)) == x 33 | True 34 | >>> pickle.loads(pickle.dumps(x, 1)) == x 35 | True 36 | 37 | >>> pickle.loads(pickle.dumps(x, 2)) == x 38 | True 39 | 40 | >>> x.__setstate__({'z': 1}) 41 | >>> x.__dict__ 42 | {'z': 1} 43 | 44 | This support even works well for derived classes which customize pickling 45 | by overriding :meth:`__getnewargs__`, :meth:`__getstate__` and 46 | :meth:`__setstate__`. 47 | 48 | .. doctest:: 49 | 50 | >>> from persistent.tests.cucumbers import Custom 51 | 52 | >>> x = Custom('x', 'y') 53 | >>> x.__getnewargs__() 54 | ('x', 'y') 55 | >>> x.a = 99 56 | 57 | >>> (f, (c, ax, ay), a) = x.__reduce__() 58 | >>> f.__name__ 59 | '__newobj__' 60 | >>> f.__module__.replace('_', '') # Normalize Python2/3 61 | 'copyreg' 62 | >>> c.__name__ 63 | 'Custom' 64 | >>> ax, ay, a 65 | ('x', 'y', 99) 66 | 67 | >>> pickle.loads(pickle.dumps(x)) == x 68 | True 69 | >>> pickle.loads(pickle.dumps(x, 0)) == x 70 | True 71 | >>> pickle.loads(pickle.dumps(x, 1)) == x 72 | True 73 | >>> pickle.loads(pickle.dumps(x, 2)) == x 74 | True 75 | 76 | The support works for derived classes which define :attr:`__slots__`. It 77 | ignores any slots which map onto the "persistent" namespace (prefixed with 78 | ``_p_``) or the "volatile" namespace (prefixed with ``_v_``): 79 | 80 | .. doctest:: 81 | 82 | >>> from persistent.tests.cucumbers import SubSlotted 83 | >>> x = SubSlotted('x', 'y', 'z') 84 | 85 | Note that we haven't yet assigned a value to the ``s4`` attribute: 86 | 87 | .. doctest:: 88 | 89 | >>> d, s = x.__getstate__() 90 | >>> d 91 | >>> print_dict(s) 92 | {'s1': 'x', 's2': 'y', 's3': 'z'} 93 | 94 | >>> import pickle 95 | >>> pickle.loads(pickle.dumps(x)) == x 96 | True 97 | >>> pickle.loads(pickle.dumps(x, 0)) == x 98 | True 99 | >>> pickle.loads(pickle.dumps(x, 1)) == x 100 | True 101 | >>> pickle.loads(pickle.dumps(x, 2)) == x 102 | True 103 | 104 | 105 | After assigning it: 106 | 107 | .. doctest:: 108 | 109 | >>> x.s4 = 'spam' 110 | 111 | >>> d, s = x.__getstate__() 112 | >>> d 113 | >>> print_dict(s) 114 | {'s1': 'x', 's2': 'y', 's3': 'z', 's4': 'spam'} 115 | 116 | >>> pickle.loads(pickle.dumps(x)) == x 117 | True 118 | >>> pickle.loads(pickle.dumps(x, 0)) == x 119 | True 120 | >>> pickle.loads(pickle.dumps(x, 1)) == x 121 | True 122 | >>> pickle.loads(pickle.dumps(x, 2)) == x 123 | True 124 | 125 | :class:`persistent.Persistent` supports derived classes which have base 126 | classes defining :attr:`__slots`, but which do not define attr:`__slots__` 127 | themselves: 128 | 129 | .. doctest:: 130 | 131 | >>> from persistent.tests.cucumbers import SubSubSlotted 132 | >>> x = SubSubSlotted('x', 'y', 'z') 133 | 134 | >>> d, s = x.__getstate__() 135 | >>> print_dict(d) 136 | {} 137 | >>> print_dict(s) 138 | {'s1': 'x', 's2': 'y', 's3': 'z'} 139 | 140 | >>> import pickle 141 | >>> pickle.loads(pickle.dumps(x)) == x 142 | True 143 | >>> pickle.loads(pickle.dumps(x, 0)) == x 144 | True 145 | >>> pickle.loads(pickle.dumps(x, 1)) == x 146 | True 147 | >>> pickle.loads(pickle.dumps(x, 2)) == x 148 | True 149 | 150 | >>> x.s4 = 'spam' 151 | >>> x.foo = 'bar' 152 | >>> x.baz = 'bam' 153 | 154 | >>> d, s = x.__getstate__() 155 | >>> print_dict(d) 156 | {'baz': 'bam', 'foo': 'bar'} 157 | >>> print_dict(s) 158 | {'s1': 'x', 's2': 'y', 's3': 'z', 's4': 'spam'} 159 | 160 | >>> pickle.loads(pickle.dumps(x)) == x 161 | True 162 | >>> pickle.loads(pickle.dumps(x, 0)) == x 163 | True 164 | >>> pickle.loads(pickle.dumps(x, 1)) == x 165 | True 166 | >>> pickle.loads(pickle.dumps(x, 2)) == x 167 | True 168 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import datetime 7 | 8 | 9 | # -- Project information ----------------------------------------------------- 10 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 11 | year = datetime.datetime.now().year 12 | 13 | project = 'persistent' 14 | copyright = f'2011-{year}, Zope Foundation and contributors' 15 | author = 'Zope Foundation and contributors' 16 | 17 | # -- General configuration --------------------------------------------------- 18 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 19 | 20 | extensions = [ 21 | 'sphinx.ext.autodoc', 22 | 'sphinx.ext.doctest', 23 | 'repoze.sphinx.autointerface', 24 | ] 25 | 26 | templates_path = ['_templates'] 27 | exclude_patterns = [] 28 | 29 | 30 | # -- Options for HTML output ------------------------------------------------- 31 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 32 | 33 | html_theme = 'sphinx_rtd_theme' 34 | # html_static_path = ['_static'] 35 | 36 | # Example configuration for intersphinx: refer to the Python standard library. 37 | intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} 38 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | data manager 10 | The object responsible for storing and loading an object's 11 | :term:`pickled data` in a backing store. Also called a :term:`jar`. 12 | 13 | jar 14 | Alias for :term:`data manager`: short for "pickle jar", because 15 | it traditionally holds the :term:`pickled data` of persistent objects. 16 | 17 | object cache 18 | An MRU cache for objects associated with a given :term:`data manager`. 19 | 20 | ghost 21 | An object whose :term:`pickled data` has not yet been loaded from its 22 | :term:`jar`. Accessing or mutating any of its attributes causes 23 | that data to be loaded, which is referred to as :term:`activation`. 24 | 25 | volatile attribute 26 | Attributes of a persistent object which are *not* captured as part 27 | of its :term:`pickled data`. These attributes thus disappear during 28 | :term:`deactivation` or :term:`invalidation`. 29 | 30 | pickled data 31 | The serialized data of a persistent object, stored in and retrieved 32 | from a backing store by a :term:`data manager`. 33 | 34 | activation 35 | Moving an object from the ``GHOST`` state to the ``UPTODATE`` state, 36 | load its :term:`pickled data` from its :term:`jar`. 37 | 38 | deactivation 39 | Moving an object from the ``UPTODATE`` state to the ``GHOST`` state, 40 | discarding its :term:`pickled data`. 41 | 42 | invalidation 43 | Moving an object from either the ``UPTODATE`` state or the ``CHANGED`` 44 | state to the ``GHOST`` state, discarding its :term:`pickled data`. 45 | 46 | object id 47 | The stable identifier that uniquely names a particular object. 48 | This is analogous to Python's `id`, but unlike `id`, object 49 | ids remain the same for a given object across different 50 | processes. 51 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :mod:`persistent`: automatic persistence for Python objects 2 | ============================================================ 3 | 4 | This package contains a generic persistence implementation for Python. It 5 | forms the core protocol for making objects interact "transparently" with 6 | a database such as the ZODB. 7 | 8 | Contents: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | using 14 | api 15 | changes 16 | glossary 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\persistent.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\persistent.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | repoze.sphinx.autointerface 3 | sphinx_rtd_theme>1 4 | docutils<0.19 5 | -------------------------------------------------------------------------------- /docs/using.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | Using :mod:`persistent` in your application 3 | ============================================= 4 | 5 | 6 | Inheriting from :class:`persistent.Persistent` 7 | ============================================== 8 | 9 | The basic mechanism for making your application's objects persistent 10 | is mix-in inheritance. Instances whose classes derive from 11 | :class:`persistent.Persistent` are automatically capable of being 12 | created as :term:`ghost` instances, being associated with a database 13 | connection (called the :term:`jar`), and notifying the connection when 14 | they have been changed. 15 | 16 | 17 | Relationship to a Data Manager and its Cache 18 | ============================================ 19 | 20 | Except immediately after their creation, persistent objects are normally 21 | associated with a :term:`data manager` (also referred to as a :term:`jar`). 22 | An object's data manager is stored in its ``_p_jar`` attribute. 23 | The data manager is responsible for loading and saving the state of the 24 | persistent object to some sort of backing store, including managing any 25 | interactions with transaction machinery. 26 | 27 | Each data manager maintains an :term:`object cache`, which keeps track of 28 | the currently loaded objects, as well as any objects they reference which 29 | have not yet been loaded: such an object is called a :term:`ghost`. 30 | The cache is stored on the data manager in its ``_cache`` attribute. 31 | 32 | A persistent object remains in the ghost state until the application 33 | attempts to access or mutate one of its attributes: at that point, the 34 | object requests that its data manager load its state. The persistent 35 | object also notifies the cache that it has been loaded, as well as on 36 | each subsequent attribute access. The cache keeps a "most-recently-used" 37 | list of its objects, and removes objects in least-recently-used order 38 | when it is asked to reduce its working set. 39 | 40 | The examples below use a stub data manager class: 41 | 42 | .. doctest:: 43 | 44 | >>> from zope.interface import implementer 45 | >>> from persistent.interfaces import IPersistentDataManager 46 | >>> @implementer(IPersistentDataManager) 47 | ... class DM(object): 48 | ... def __init__(self): 49 | ... self.registered = 0 50 | ... def register(self, ob): 51 | ... self.registered += 1 52 | ... def setstate(self, ob): 53 | ... ob.__setstate__({'x': 42}) 54 | 55 | .. note:: 56 | Notice that the ``DM`` class always sets the ``x`` attribute to the value 57 | ``42`` when activating an object. 58 | 59 | 60 | Persistent objects without a Data Manager 61 | ========================================= 62 | 63 | Before persistent instance has been associated with a a data manager ( 64 | i.e., its ``_p_jar`` is still ``None``). 65 | 66 | The examples below use a class, ``P``, defined as: 67 | 68 | .. doctest:: 69 | 70 | >>> from persistent import Persistent 71 | >>> from persistent.interfaces import GHOST, UPTODATE, CHANGED 72 | >>> class P(Persistent): 73 | ... def __init__(self): 74 | ... self.x = 0 75 | ... def inc(self): 76 | ... self.x += 1 77 | 78 | Instances of the derived ``P`` class which are not (yet) assigned to 79 | a :term:`data manager` behave as other Python instances, except that 80 | they have some extra attributes: 81 | 82 | .. doctest:: 83 | 84 | >>> p = P() 85 | >>> p.x 86 | 0 87 | 88 | The :attr:`_p_changed` attribute is a three-state flag: it can be 89 | one of ``None`` (the object is not loaded), ``False`` (the object has 90 | not been changed since it was loaded) or ``True`` (the object has been 91 | changed). Until the object is assigned a :term:`jar`, this attribute 92 | will always be ``False``. 93 | 94 | .. doctest:: 95 | 96 | >>> p._p_changed 97 | False 98 | 99 | The :attr:`_p_state` attribute is an integer, representing which of the 100 | "persistent lifecycle" states the object is in. Until the object is assigned 101 | a :term:`jar`, this attribute will always be ``0`` (the ``UPTODATE`` 102 | constant): 103 | 104 | .. doctest:: 105 | 106 | >>> p._p_state == UPTODATE 107 | True 108 | 109 | The :attr:`_p_jar` attribute is the object's :term:`data manager`. Since 110 | it has not yet been assigned, its value is ``None``: 111 | 112 | .. doctest:: 113 | 114 | >>> print(p._p_jar) 115 | None 116 | 117 | The :attr:`_p_oid` attribute is the :term:`object id`, a unique value 118 | normally assigned by the object's :term:`data manager`. Since the object 119 | has not yet been associated with its :term:`jar`, its value is ``None``: 120 | 121 | .. doctest:: 122 | 123 | >>> print(p._p_oid) 124 | None 125 | 126 | Without a data manager, modifying a persistent object has no effect on 127 | its ``_p_state`` or ``_p_changed``. 128 | 129 | .. doctest:: 130 | 131 | >>> p.inc() 132 | >>> p.inc() 133 | >>> p.x 134 | 2 135 | >>> p._p_changed 136 | False 137 | >>> p._p_state 138 | 0 139 | 140 | Try all sorts of different ways to change the object's state: 141 | 142 | .. doctest:: 143 | 144 | >>> p._p_deactivate() 145 | >>> p._p_state 146 | 0 147 | >>> p._p_changed 148 | False 149 | >>> p._p_changed = True 150 | >>> p._p_changed 151 | False 152 | >>> p._p_state 153 | 0 154 | >>> del p._p_changed 155 | >>> p._p_changed 156 | False 157 | >>> p._p_state 158 | 0 159 | >>> p.x 160 | 2 161 | 162 | 163 | Associating an Object with a Data Manager 164 | ========================================= 165 | 166 | Once associated with a data manager, a persistent object's behavior changes: 167 | 168 | .. doctest:: 169 | 170 | >>> p = P() 171 | >>> dm = DM() 172 | >>> p._p_oid = "00000012" 173 | >>> p._p_jar = dm 174 | >>> p._p_changed 175 | False 176 | >>> p._p_state 177 | 0 178 | >>> p.__dict__ 179 | {'x': 0} 180 | >>> dm.registered 181 | 0 182 | 183 | Modifying the object marks it as changed and registers it with the data 184 | manager. Subsequent modifications don't have additional side-effects. 185 | 186 | .. doctest:: 187 | 188 | >>> p.inc() 189 | >>> p.x 190 | 1 191 | >>> p.__dict__ 192 | {'x': 1} 193 | >>> p._p_changed 194 | True 195 | >>> p._p_state 196 | 1 197 | >>> dm.registered 198 | 1 199 | >>> p.inc() 200 | >>> p._p_changed 201 | True 202 | >>> p._p_state 203 | 1 204 | >>> dm.registered 205 | 1 206 | 207 | Object which register themselves with the data manager are candidates 208 | for storage to the backing store at a later point in time. 209 | 210 | Note that mutating a non-persistent attribute of a persistent object 211 | such as a :class:`dict` or :class:`list` will *not* cause the 212 | containing object to be changed. Instead you can either explicitly 213 | control the state as described below, or use a 214 | :class:`~.PersistentList` or :class:`~.PersistentMapping`. 215 | 216 | Explicitly controlling ``_p_state`` 217 | =================================== 218 | 219 | Persistent objects expose three methods for moving an object into and out 220 | of the "ghost" state:: :meth:`persistent.Persistent._p_activate`, 221 | :meth:`persistent.Persistent._p_deactivate`, and 222 | :meth:`persistent.Persistent._p_invalidate`: 223 | 224 | .. doctest:: 225 | 226 | >>> p = P() 227 | >>> p._p_oid = '00000012' 228 | >>> p._p_jar = DM() 229 | 230 | After being assigned a jar, the object is initially in the ``UPTODATE`` 231 | state: 232 | 233 | .. doctest:: 234 | 235 | >>> p._p_state 236 | 0 237 | 238 | From that state, ``_p_deactivate`` rests the object to the ``GHOST`` state: 239 | 240 | .. doctest:: 241 | 242 | >>> p._p_deactivate() 243 | >>> p._p_state 244 | -1 245 | 246 | From the ``GHOST`` state, ``_p_activate`` reloads the object's data and 247 | moves it to the ``UPTODATE`` state: 248 | 249 | .. doctest:: 250 | 251 | >>> p._p_activate() 252 | >>> p._p_state 253 | 0 254 | >>> p.x 255 | 42 256 | 257 | Changing the object puts it in the ``CHANGED`` state: 258 | 259 | .. doctest:: 260 | 261 | >>> p.inc() 262 | >>> p.x 263 | 43 264 | >>> p._p_state 265 | 1 266 | 267 | Attempting to deactivate in the ``CHANGED`` state is a no-op: 268 | 269 | .. doctest:: 270 | 271 | >>> p._p_deactivate() 272 | >>> p.__dict__ 273 | {'x': 43} 274 | >>> p._p_changed 275 | True 276 | >>> p._p_state 277 | 1 278 | 279 | ``_p_invalidate`` forces objects into the ``GHOST`` state; it works even on 280 | objects in the ``CHANGED`` state, which is the key difference between 281 | deactivation and invalidation: 282 | 283 | .. doctest:: 284 | 285 | >>> p._p_invalidate() 286 | >>> p.__dict__ 287 | {} 288 | >>> p._p_state 289 | -1 290 | 291 | You can manually reset the ``_p_changed`` field to ``False``: in this case, 292 | the object changes to the ``UPTODATE`` state but retains its modifications: 293 | 294 | .. doctest:: 295 | 296 | >>> p.inc() 297 | >>> p.x 298 | 43 299 | >>> p._p_changed = False 300 | >>> p._p_state 301 | 0 302 | >>> p._p_changed 303 | False 304 | >>> p.x 305 | 43 306 | 307 | For an object in the "ghost" state, assigning ``True`` (or any value which is 308 | coercible to ``True``) to its ``_p_changed`` attributes activates the object, 309 | which is exactly the same as calling ``_p_activate``: 310 | 311 | .. doctest:: 312 | 313 | >>> p._p_invalidate() 314 | >>> p._p_state 315 | -1 316 | >>> p._p_changed = True 317 | >>> p._p_changed 318 | True 319 | >>> p._p_state 320 | 1 321 | >>> p.x 322 | 42 323 | 324 | 325 | The pickling protocol 326 | ===================== 327 | 328 | Because persistent objects need to control how they are pickled and 329 | unpickled, the :class:`persistent.Persistent` base class overrides 330 | the implementations of ``__getstate__()`` and ``__setstate__()``: 331 | 332 | .. doctest:: 333 | 334 | >>> p = P() 335 | >>> dm = DM() 336 | >>> p._p_oid = "00000012" 337 | >>> p._p_jar = dm 338 | >>> p.__getstate__() 339 | {'x': 0} 340 | >>> p._p_state 341 | 0 342 | 343 | Calling ``__setstate__`` always leaves the object in the uptodate state. 344 | 345 | .. doctest:: 346 | 347 | >>> p.__setstate__({'x': 5}) 348 | >>> p._p_state 349 | 0 350 | 351 | A :term:`volatile attribute` is an attribute those whose name begins with a 352 | special prefix (``_v__``). Unlike normal attributes, volatile attributes do 353 | not get stored in the object's :term:`pickled data`. 354 | 355 | .. doctest:: 356 | 357 | >>> p._v_foo = 2 358 | >>> p.__getstate__() 359 | {'x': 5} 360 | 361 | Assigning to volatile attributes doesn't cause the object to be marked as 362 | changed: 363 | 364 | .. doctest:: 365 | 366 | >>> p._p_state 367 | 0 368 | 369 | The ``_p_serial`` attribute is not affected by calling setstate. 370 | 371 | .. doctest:: 372 | 373 | >>> p._p_serial = b"00000012" 374 | >>> p.__setstate__(p.__getstate__()) 375 | >>> p._p_serial 376 | b'00000012' 377 | 378 | 379 | Estimated Object Size 380 | ===================== 381 | 382 | We can store a size estimation in ``_p_estimated_size``. Its default is 0. 383 | The size estimation can be used by a cache associated with the data manager 384 | to help in the implementation of its replacement strategy or its size bounds. 385 | 386 | .. doctest:: 387 | 388 | >>> p._p_estimated_size 389 | 0 390 | >>> p._p_estimated_size = 1000 391 | >>> p._p_estimated_size 392 | 1024 393 | 394 | Huh? Why is the estimated size coming out different than what we put 395 | in? The reason is that the size isn't stored exactly. For backward 396 | compatibility reasons, the size needs to fit in 24 bits, so, 397 | internally, it is adjusted somewhat. 398 | 399 | Of course, the estimated size must not be negative. 400 | 401 | .. doctest:: 402 | 403 | >>> p._p_estimated_size = -1 404 | Traceback (most recent call last): 405 | ... 406 | ValueError: _p_estimated_size must not be negative 407 | 408 | 409 | Overriding the attribute protocol 410 | ================================= 411 | 412 | Subclasses which override the attribute-management methods provided by 413 | :class:`persistent.Persistent`, but must obey some constraints: 414 | 415 | 416 | :meth:`__getattribute__` 417 | When overriding ``__getattribute__``, the derived class implementation 418 | **must** first call :meth:`persistent.IPersistent._p_getattr`, passing the 419 | name being accessed. This method ensures that the object is activated, 420 | if needed, and handles the "special" attributes which do not require 421 | activation (e.g., ``_p_oid``, ``__class__``, ``__dict__``, etc.) 422 | If ``_p_getattr`` returns ``True``, the derived class implementation 423 | **must** delegate to the base class implementation for the attribute. 424 | 425 | :meth:`__setattr__` 426 | When overriding ``__setattr__``, the derived class implementation 427 | **must** first call :meth:`persistent.IPersistent._p_setattr`, passing the 428 | name being accessed and the value. This method ensures that the object is 429 | activated, if needed, and handles the "special" attributes which do not 430 | require activation (``_p_*``). If ``_p_setattr`` returns ``True``, the 431 | derived implementation must assume that the attribute value has been set by 432 | the base class. 433 | 434 | :meth:`__delattr__` 435 | When overriding ``__delattr__``, the derived class implementation 436 | **must** first call :meth:`persistent.IPersistent._p_delattr`, passing the 437 | name being accessed. This method ensures that the object is 438 | activated, if needed, and handles the "special" attributes which do not 439 | require activation (``_p_*``). If ``_p_delattr`` returns ``True``, the 440 | derived implementation must assume that the attribute has been deleted 441 | base class. 442 | 443 | :meth:`__getattr__` 444 | For the ``__getattr__`` method, the behavior is like that for regular Python 445 | classes and for earlier versions of ZODB 3. 446 | 447 | 448 | Implementing ``_p_repr`` 449 | ======================== 450 | 451 | Subclasses can implement ``_p_repr`` to provide a custom 452 | representation. If this method raises an exception, the default 453 | representation will be used. The benefit of implementing ``_p_repr`` 454 | instead of overriding ``__repr__`` is that it provides safer handling 455 | for objects that can't be activated because their persistent data is 456 | missing or their jar is closed. 457 | 458 | .. doctest:: 459 | 460 | >>> class P(Persistent): 461 | ... def _p_repr(self): 462 | ... return "Custom repr" 463 | 464 | >>> p = P() 465 | >>> print(repr(p)) 466 | Custom repr 467 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Generated from: 3 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 4 | 5 | [build-system] 6 | requires = [ 7 | "setuptools <= 75.6.0", 8 | "cffi; platform_python_implementation == 'CPython'", 9 | ] 10 | build-backend = "setuptools.build_meta" 11 | 12 | [tool.coverage.run] 13 | branch = true 14 | source = ["persistent"] 15 | relative_files = true 16 | omit = ["_ring_build.py"] 17 | 18 | [tool.coverage.report] 19 | fail_under = 94.9 20 | precision = 2 21 | ignore_errors = true 22 | show_missing = true 23 | exclude_lines = [ 24 | "pragma: no cover", 25 | "pragma: nocover", 26 | "except ImportError:", 27 | "raise NotImplementedError", 28 | "if __name__ == '__main__':", 29 | "self.fail", 30 | "raise AssertionError", 31 | "raise unittest.Skip", 32 | ] 33 | 34 | [tool.coverage.html] 35 | directory = "parts/htmlcov" 36 | 37 | [tool.coverage.paths] 38 | source = [ 39 | "src/", 40 | ".tox/*/lib/python*/site-packages/", 41 | ".tox/pypy*/site-packages/", 42 | ] 43 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | 4 | [zest.releaser] 5 | create-wheel = no 6 | 7 | [flake8] 8 | doctests = 1 9 | 10 | [check-manifest] 11 | ignore = 12 | .editorconfig 13 | .meta.toml 14 | docs/_build/html/_sources/* 15 | docs/_build/doctest/* 16 | docs/_build/html/_sources/api/* 17 | 18 | [isort] 19 | force_single_line = True 20 | combine_as_imports = True 21 | sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER 22 | known_third_party = docutils, pkg_resources, pytz 23 | known_zope = 24 | known_first_party = 25 | default_section = ZOPE 26 | line_length = 79 27 | lines_after_imports = 2 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2008 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | import os 16 | import platform 17 | 18 | from setuptools import Extension 19 | from setuptools import find_packages 20 | from setuptools import setup 21 | 22 | 23 | version = '6.2.dev0' 24 | 25 | here = os.path.abspath(os.path.dirname(__file__)) 26 | 27 | 28 | def _read_file(filename): 29 | with open(os.path.join(here, filename)) as f: 30 | return f.read() 31 | 32 | 33 | README = (_read_file('README.rst') + '\n\n' + _read_file('CHANGES.rst')) 34 | 35 | 36 | define_macros = ( 37 | # We currently use macros like PyBytes_AS_STRING 38 | # and internal functions like _PyObject_GetDictPtr 39 | # that make it impossible to use the stable (limited) API. 40 | # ('Py_LIMITED_API', '0x03050000'), 41 | ) 42 | ext_modules = [ 43 | Extension( 44 | name='persistent.cPersistence', 45 | sources=[ 46 | 'src/persistent/cPersistence.c', 47 | 'src/persistent/ring.c', 48 | ], 49 | depends=[ 50 | 'src/persistent/cPersistence.h', 51 | 'src/persistent/ring.h', 52 | 'src/persistent/ring.c', 53 | ], 54 | define_macros=list(define_macros), 55 | ), 56 | Extension( 57 | name='persistent.cPickleCache', 58 | sources=[ 59 | 'src/persistent/cPickleCache.c', 60 | 'src/persistent/ring.c', 61 | ], 62 | depends=[ 63 | 'src/persistent/cPersistence.h', 64 | 'src/persistent/ring.h', 65 | 'src/persistent/ring.c', 66 | ], 67 | define_macros=list(define_macros), 68 | ), 69 | Extension( 70 | name='persistent._timestamp', 71 | sources=[ 72 | 'src/persistent/_timestamp.c', 73 | ], 74 | define_macros=list(define_macros), 75 | ), 76 | ] 77 | 78 | is_pypy = platform.python_implementation() == 'PyPy' 79 | if is_pypy: 80 | # Header installation doesn't work on PyPy: 81 | # https://github.com/zopefoundation/persistent/issues/135 82 | headers = [] 83 | else: 84 | headers = [ 85 | 'src/persistent/cPersistence.h', 86 | 'src/persistent/ring.h', 87 | ] 88 | 89 | setup(name='persistent', 90 | version=version, 91 | description='Translucent persistent objects', 92 | long_description=README, 93 | long_description_content_type='text/x-rst', 94 | classifiers=[ 95 | "Development Status :: 6 - Mature", 96 | "License :: OSI Approved :: Zope Public License", 97 | "Programming Language :: Python", 98 | "Programming Language :: Python :: 3", 99 | "Programming Language :: Python :: 3.9", 100 | "Programming Language :: Python :: 3.10", 101 | "Programming Language :: Python :: 3.11", 102 | "Programming Language :: Python :: 3.12", 103 | "Programming Language :: Python :: 3.13", 104 | "Programming Language :: Python :: Implementation :: CPython", 105 | "Programming Language :: Python :: Implementation :: PyPy", 106 | "Framework :: ZODB", 107 | "Topic :: Database", 108 | "Topic :: Software Development :: Libraries :: Python Modules", 109 | "Operating System :: Microsoft :: Windows", 110 | "Operating System :: Unix", 111 | ], 112 | author="Zope Foundation and Contributors", 113 | author_email="zodb-dev@zope.org", 114 | url="https://github.com/zopefoundation/persistent/", 115 | project_urls={ 116 | 'Documentation': 'https://persistent.readthedocs.io', 117 | 'Issue Tracker': 'https://github.com/zopefoundation/' 118 | 'persistent/issues', 119 | 'Sources': 'https://github.com/zopefoundation/persistent', 120 | }, 121 | license="ZPL-2.1", 122 | packages=find_packages('src'), 123 | package_dir={'': 'src'}, 124 | include_package_data=True, 125 | zip_safe=False, 126 | ext_modules=ext_modules, 127 | cffi_modules=['src/persistent/_ring_build.py:ffi'], 128 | headers=headers, 129 | extras_require={ 130 | 'test': [ 131 | 'zope.testrunner', 132 | 'manuel', 133 | ], 134 | 'testing': (), 135 | 'docs': [ 136 | 'Sphinx', 137 | 'repoze.sphinx.autointerface', 138 | 'sphinx_rtd_theme', 139 | ], 140 | }, 141 | python_requires='>=3.9', 142 | install_requires=[ 143 | 'zope.deferredimport', 144 | 'zope.interface', 145 | "cffi ; platform_python_implementation == 'CPython'", 146 | ], 147 | entry_points={}) 148 | -------------------------------------------------------------------------------- /src/persistent/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Prefer C implementations of Persistent / PickleCache / TimeStamp. 15 | 16 | Fall back to pure Python implementations. 17 | """ 18 | 19 | import sys 20 | 21 | 22 | __all__ = [ 23 | 'IPersistent', 24 | 'Persistent', 25 | 'GHOST', 26 | 'UPTODATE', 27 | 'CHANGED', 28 | 'STICKY', 29 | 'PickleCache', 30 | 'TimeStamp', 31 | ] 32 | 33 | # Take care not to shadow the module names 34 | from persistent import interfaces as _interfaces 35 | from persistent import persistence as _persistence 36 | from persistent import picklecache as _picklecache 37 | from persistent import timestamp as _timestamp 38 | 39 | 40 | IPersistent = _interfaces.IPersistent 41 | Persistent = _persistence.Persistent 42 | GHOST = _interfaces.GHOST 43 | UPTODATE = _interfaces.UPTODATE 44 | CHANGED = _interfaces.CHANGED 45 | STICKY = _interfaces.STICKY 46 | PickleCache = _picklecache.PickleCache 47 | 48 | # BWC for TimeStamp. 49 | TimeStamp = _timestamp 50 | 51 | sys.modules['persistent.TimeStamp'] = sys.modules['persistent.timestamp'] 52 | -------------------------------------------------------------------------------- /src/persistent/_compat.h: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | 3 | Copyright (c) 2012 Zope Foundation and Contributors. 4 | All Rights Reserved. 5 | 6 | This software is subject to the provisions of the Zope Public License, 7 | Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | FOR A PARTICULAR PURPOSE 12 | 13 | ****************************************************************************/ 14 | 15 | #ifndef PERSISTENT__COMPAT_H 16 | #define PERSISTENT__COMPAT_H 17 | 18 | #include "Python.h" 19 | 20 | #define INTERN PyUnicode_InternFromString 21 | #define INTERN_INPLACE PyUnicode_InternInPlace 22 | #define NATIVE_CHECK_EXACT PyUnicode_CheckExact 23 | #define NATIVE_FROM_STRING_AND_SIZE PyUnicode_FromStringAndSize 24 | 25 | #define Py_TPFLAGS_HAVE_RICHCOMPARE 0 26 | 27 | #define INT_FROM_LONG(x) PyLong_FromLong(x) 28 | #define INT_CHECK(x) PyLong_Check(x) 29 | #define INT_AS_LONG(x) PyLong_AsLong(x) 30 | #define CAPI_CAPSULE_NAME "persistent.cPersistence.CAPI" 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/persistent/_compat.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2012 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | import os 16 | import sys 17 | import types 18 | 19 | from zope.interface import classImplements 20 | from zope.interface import implementedBy 21 | 22 | 23 | __all__ = [ 24 | 'use_c_impl', 25 | 'PYPY', 26 | ] 27 | 28 | 29 | PYPY = hasattr(sys, 'pypy_version_info') 30 | 31 | 32 | def _c_optimizations_required(): 33 | """ 34 | Return a true value if the C optimizations are required. 35 | 36 | This uses the ``PURE_PYTHON`` variable as documented in `use_c_impl`. 37 | """ 38 | pure_env = os.environ.get('PURE_PYTHON') 39 | require_c = pure_env == "0" 40 | return require_c 41 | 42 | 43 | def _c_optimizations_available(): 44 | """ 45 | Return the C optimization modules, if available, otherwise 46 | a false value. 47 | 48 | If the optimizations are required but not available, this 49 | raises the ImportError. Either all optimization modules are 50 | available or none are. 51 | 52 | This does not say whether they should be used or not. 53 | """ 54 | catch = () if _c_optimizations_required() else (ImportError,) 55 | try: 56 | from persistent import _timestamp 57 | from persistent import cPersistence 58 | from persistent import cPickleCache 59 | return { 60 | 'persistent.persistence': cPersistence, 61 | 'persistent.picklecache': cPickleCache, 62 | 'persistent.timestamp': _timestamp, 63 | } 64 | except catch: # pragma: no cover (only Jython doesn't build extensions) 65 | return {} 66 | 67 | 68 | def _c_optimizations_ignored(): 69 | """ 70 | The opposite of `_c_optimizations_required`. 71 | 72 | On PyPy, this always returns True. 73 | 74 | Otherwise, if ``$PURE_PYTHON`` is set to any non-empty value 75 | besides "0", optimizations are ignored. Setting ``$PURE_PYTHON`` 76 | to "1", for example, ignores optimizations. Setting ``$PURE_PYTHON`` to 77 | an empty value *does not* ignore optimizations. 78 | """ 79 | pure_env = os.environ.get('PURE_PYTHON') 80 | # The extensions can be compiled with PyPy 7.3, but they don't work. 81 | return PYPY or (pure_env and pure_env != "0") 82 | 83 | 84 | def _should_attempt_c_optimizations(): 85 | """ 86 | Return a true value if we should attempt to use the C optimizations. 87 | 88 | This takes into account whether we're on PyPy and the value of the 89 | ``PURE_PYTHON`` environment variable, as defined in `use_c_impl`. 90 | 91 | Note that setting ``PURE_PYTHON=0`` forces the use of C optimizations, 92 | even on PyPy. 93 | """ 94 | if _c_optimizations_required(): 95 | return True 96 | if PYPY: # pragma: no cover 97 | return False 98 | return not _c_optimizations_ignored() 99 | 100 | 101 | def use_c_impl(py_impl, name=None, globs=None, mod_name=None): 102 | """ 103 | Decorator. Given an object implemented in Python, with a name like 104 | ``Foo``, import the corresponding C implementation from 105 | ``persistent.c`` with the name ``Foo`` and use it instead 106 | (where ``NAME`` is the module name). 107 | 108 | This can also be used for constants and other things that do not 109 | have a name by passing the name as the second argument. 110 | 111 | Example:: 112 | 113 | @use_c_impl 114 | class Foo(object): 115 | ... 116 | 117 | GHOST = use_c_impl(12, 'GHOST') 118 | 119 | If the ``PURE_PYTHON`` environment variable is set to any value 120 | other than ``"0"``, or we're on PyPy, ignore the C implementation 121 | and return the Python version. If the C implementation cannot be 122 | imported, return the Python version. If ``PURE_PYTHON`` is set to 123 | 0, *require* the C implementation (let the ImportError propagate); 124 | note that PyPy can import the C implementation in this case (and 125 | all tests pass). 126 | 127 | In all cases, the Python version is kept available in the module 128 | globals with the name ``FooPy``. 129 | 130 | If the Python version is a class that implements interfaces, then 131 | the C version will be declared to also implement those interfaces. 132 | 133 | If the Python version is a class, then each function defined 134 | directly in that class will be replaced with a new version using 135 | globals that still use the original name of the class for the 136 | Python implementation. This lets the function bodies refer to the 137 | class using the name the class is defined with, as it would 138 | expect. (Only regular functions and static methods are handled.) 139 | However, it also means that mutating the module globals later on 140 | will not be visible to the methods of the class. In this example, 141 | ``Foo().method()`` will always return 1:: 142 | 143 | GLOBAL_OBJECT = 1 144 | @use_c_impl 145 | class Foo(object): 146 | def method(self): 147 | super(Foo, self).method() 148 | return GLOBAL_OBJECT 149 | GLOBAL_OBJECT = 2 150 | """ 151 | name = name or py_impl.__name__ 152 | globs = globs or sys._getframe(1).f_globals 153 | mod_name = mod_name or globs['__name__'] 154 | 155 | def find_impl(): 156 | if not _should_attempt_c_optimizations(): 157 | return py_impl 158 | 159 | c_opts = _c_optimizations_available() 160 | # only Jython doesn't build extensions: 161 | if not c_opts: # pragma: no cover 162 | return py_impl 163 | 164 | __traceback_info__ = c_opts 165 | c_opt = c_opts[mod_name] 166 | return getattr(c_opt, name) 167 | 168 | c_impl = find_impl() 169 | # Always make available by the FooPy name 170 | globs[name + 'Py'] = py_impl 171 | 172 | if c_impl is not py_impl and isinstance(py_impl, type): 173 | # Rebind the globals of all the functions to still see the 174 | # object under its original class name, so that references 175 | # in function bodies work as expected. 176 | py_attrs = vars(py_impl) 177 | new_globals = None 178 | for k, v in list(py_attrs.items()): 179 | static = isinstance(v, staticmethod) 180 | if static: 181 | # Often this is __new__ 182 | v = v.__func__ 183 | 184 | if not isinstance(v, types.FunctionType): 185 | continue 186 | 187 | if new_globals is None: 188 | new_globals = v.__globals__.copy() 189 | new_globals[py_impl.__name__] = py_impl 190 | v = types.FunctionType( 191 | v.__code__, 192 | new_globals, 193 | k, # name 194 | v.__defaults__, 195 | v.__closure__, 196 | ) 197 | if static: 198 | v = staticmethod(v) 199 | setattr(py_impl, k, v) 200 | # copy the interface declarations. 201 | implements = list(implementedBy(py_impl)) 202 | if implements: 203 | classImplements(c_impl, *implements) 204 | return c_impl 205 | -------------------------------------------------------------------------------- /src/persistent/_ring_build.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2018 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | import os 16 | 17 | from cffi import FFI 18 | 19 | 20 | this_dir = os.path.dirname(os.path.abspath(__file__)) 21 | 22 | ffi = FFI() 23 | with open(os.path.join(this_dir, 'ring.h')) as f: 24 | cdefs = f.read() 25 | 26 | # Define a structure with the same layout as CPersistentRing, 27 | # and an extra member. We'll cast between them to reuse the 28 | # existing functions. 29 | struct_def = """ 30 | typedef struct CPersistentRingCFFI_struct 31 | { 32 | struct CPersistentRing_struct *r_prev; 33 | struct CPersistentRing_struct *r_next; 34 | uintptr_t pobj_id; /* The id(PersistentPy object) */ 35 | } CPersistentRingCFFI; 36 | """ 37 | 38 | cdefs += struct_def + """ 39 | void cffi_ring_add(CPersistentRing* ring, void* elt); 40 | void cffi_ring_del(void* elt); 41 | void cffi_ring_move_to_head(CPersistentRing* ring, void* elt); 42 | """ 43 | 44 | ffi.cdef(cdefs) 45 | 46 | source = """ 47 | #include "ring.c" 48 | """ + struct_def + """ 49 | 50 | /* Like the other functions, but taking the CFFI version of the struct. This 51 | * saves casting at runtime in Python. 52 | */ 53 | #define cffi_ring_add(ring, elt) ring_add((CPersistentRing*)ring, (CPersistentRing*)elt) 54 | #define cffi_ring_del(elt) ring_del((CPersistentRing*)elt) 55 | #define cffi_ring_move_to_head(ring, elt) ring_move_to_head((CPersistentRing*)ring, (CPersistentRing*)elt) 56 | """ # noqa: E501 line too long 57 | 58 | ffi.set_source('persistent._ring', 59 | source, 60 | include_dirs=[this_dir]) 61 | 62 | if __name__ == '__main__': 63 | ffi.compile() 64 | -------------------------------------------------------------------------------- /src/persistent/_timestamp.c: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | 3 | Copyright (c) 2001, 2004 Zope Foundation and Contributors. 4 | All Rights Reserved. 5 | 6 | This software is subject to the provisions of the Zope Public License, 7 | Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | FOR A PARTICULAR PURPOSE 12 | 13 | ****************************************************************************/ 14 | 15 | #define PY_SSIZE_T_CLEAN 16 | #include "Python.h" 17 | #include "bytesobject.h" 18 | #include 19 | #include "_compat.h" 20 | 21 | 22 | PyObject *TimeStamp_FromDate(int, int, int, int, int, double); 23 | PyObject *TimeStamp_FromString(const char *); 24 | 25 | static char TimeStampModule_doc[] = 26 | "A 64-bit TimeStamp used as a ZODB serial number.\n" 27 | "\n" 28 | "$Id$\n"; 29 | 30 | 31 | /* A magic constant having the value 0.000000013969839. When an 32 | number of seconds between 0 and 59 is *divided* by this number, we get 33 | a number between 0 (for 0), 71582786 (for 1) and 4223384393 (for 59), 34 | all of which can be represented in a 32-bit unsigned integer, suitable 35 | for packing into 4 bytes using `TS_PACK_UINT32_INTO_BYTES`. 36 | To get (close to) the original seconds back, use 37 | `TS_UNPACK_UINT32_FROM_BYTES` and *multiply* by this number. 38 | */ 39 | #define TS_SECOND_BYTES_BIAS ((double)((double)60) / ((double)(0x10000)) / ((double)(0x10000))) 40 | #define TS_BASE_YEAR 1900 41 | #define TS_MINUTES_PER_DAY 1440 42 | /* We pretend there are always 31 days in a month; this has us using 43 | 372 days in a year in some calculations */ 44 | #define TS_DAYS_PER_MONTH 31 45 | #define TS_MONTHS_PER_YEAR 12 46 | #define TS_MINUTES_PER_MONTH (TS_DAYS_PER_MONTH * TS_MINUTES_PER_DAY) 47 | #define TS_MINUTES_PER_YEAR (TS_MINUTES_PER_MONTH * TS_MONTHS_PER_YEAR) 48 | 49 | /* The U suffixes matter on these constants to be sure 50 | the compiler generates the appropriate instructions when 51 | optimizations are enabled. On x86_64 GCC, if -fno-wrapv is given 52 | and -O is used, the compiler might choose to treat these as 32 bit 53 | signed quantities otherwise, producing incorrect results on 54 | some corner cases. See 55 | https://github.com/zopefoundation/persistent/issues/86 56 | */ 57 | 58 | /** 59 | * Given an unsigned int *v*, pack it into the four 60 | * unsigned char bytes beginning at *bytes*. If *v* is larger 61 | * than 2^31 (i.e., it doesn't fit in 32 bits), the results will 62 | * be invalid (the first byte will be 0.) 63 | * 64 | * The inverse is `TS_UNPACK_UINT32_FROM_BYTES`. This is a 65 | * lossy operation and may lose some lower-order precision. 66 | * 67 | */ 68 | #define TS_PACK_UINT32_INTO_BYTES(v, bytes) do { \ 69 | *(bytes) = v / 0x1000000U; \ 70 | *(bytes + 1) = (v % 0x1000000U) / 0x10000U; \ 71 | *(bytes + 2) = (v % 0x10000U) / 0x100U; \ 72 | *(bytes + 3) = v % 0x100U; \ 73 | } while (0) 74 | 75 | /** 76 | * Given a sequence of four unsigned chars beginning at *bytes* 77 | * as produced by `TS_PACK_UINT32_INTO_BYTES`, return the 78 | * original unsigned int. 79 | * 80 | * Remember this is a lossy operation, and the value you get back 81 | * may not exactly match the original value. If the original value 82 | * was greater than 2^31 it will definitely not match. 83 | */ 84 | #define TS_UNPACK_UINT32_FROM_BYTES(bytes) (*(bytes) * 0x1000000U + *(bytes + 1) * 0x10000U + *(bytes + 2) * 0x100U + *(bytes + 3)) 85 | 86 | typedef struct 87 | { 88 | PyObject_HEAD 89 | /* 90 | The first four bytes of data store the year, month, day, hour, and 91 | minute as the number of minutes since Jan 1 00:00. 92 | 93 | The final four bytes store the seconds since 00:00 as 94 | the number of microseconds. 95 | 96 | Both are normalized into those four bytes the same way with 97 | TS_[UN]PACK_UINT32_INTO|FROM_BYTES. 98 | */ 99 | 100 | unsigned char data[8]; 101 | } TimeStamp; 102 | 103 | /* The first dimension of the arrays below is non-leapyear / leapyear */ 104 | 105 | static char month_len[2][12] = 106 | { 107 | {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, 108 | {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} 109 | }; 110 | 111 | static short joff[2][12] = 112 | { 113 | {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}, 114 | {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335} 115 | }; 116 | 117 | static double gmoff=0; 118 | 119 | 120 | static int 121 | leap(int year) 122 | { 123 | return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); 124 | } 125 | 126 | static int 127 | days_in_month(int year, int month) 128 | { 129 | return month_len[leap(year)][month]; 130 | } 131 | 132 | static double 133 | TimeStamp_yad(int y) 134 | { 135 | double d, s; 136 | 137 | y -= TS_BASE_YEAR; 138 | 139 | d = (y - 1) * 365; 140 | if (y > 0) { 141 | s = 1.0; 142 | y -= 1; 143 | } else { 144 | s = -1.0; 145 | y = -y; 146 | } 147 | return d + s * (y / 4 - y / 100 + (y + 300) / 400); 148 | } 149 | 150 | static double 151 | TimeStamp_abst(int y, int mo, int d, int m, int s) 152 | { 153 | return (TimeStamp_yad(y) + joff[leap(y)][mo] + d) * 86400 + m * 60 + s; 154 | } 155 | 156 | static int 157 | TimeStamp_init_gmoff(void) 158 | { 159 | struct tm *t; 160 | time_t z=0; 161 | 162 | t = gmtime(&z); 163 | if (t == NULL) 164 | { 165 | PyErr_SetString(PyExc_SystemError, "gmtime failed"); 166 | return -1; 167 | } 168 | 169 | gmoff = TimeStamp_abst(t->tm_year + TS_BASE_YEAR, t->tm_mon, t->tm_mday - 1, 170 | t->tm_hour * 60 + t->tm_min, t->tm_sec); 171 | 172 | return 0; 173 | } 174 | 175 | static void 176 | TimeStamp_dealloc(TimeStamp *ts) 177 | { 178 | PyObject_Del(ts); 179 | } 180 | 181 | static PyObject* 182 | TimeStamp_richcompare(TimeStamp *self, TimeStamp *other, int op) 183 | { 184 | PyObject *result = NULL; 185 | int cmp; 186 | 187 | if (Py_TYPE(other) != Py_TYPE(self)) 188 | { 189 | result = Py_NotImplemented; 190 | } 191 | else 192 | { 193 | cmp = memcmp(self->data, other->data, 8); 194 | switch (op) { 195 | case Py_LT: 196 | result = (cmp < 0) ? Py_True : Py_False; 197 | break; 198 | case Py_LE: 199 | result = (cmp <= 0) ? Py_True : Py_False; 200 | break; 201 | case Py_EQ: 202 | result = (cmp == 0) ? Py_True : Py_False; 203 | break; 204 | case Py_NE: 205 | result = (cmp != 0) ? Py_True : Py_False; 206 | break; 207 | case Py_GT: 208 | result = (cmp > 0) ? Py_True : Py_False; 209 | break; 210 | case Py_GE: 211 | result = (cmp >= 0) ? Py_True : Py_False; 212 | break; 213 | } 214 | } 215 | 216 | Py_XINCREF(result); 217 | return result; 218 | } 219 | 220 | 221 | static Py_hash_t 222 | TimeStamp_hash(TimeStamp *self) 223 | { 224 | register unsigned char *p = (unsigned char *)self->data; 225 | register int len = 8; 226 | register long x = *p << 7; 227 | while (--len >= 0) 228 | x = (1000003*x) ^ *p++; 229 | x ^= 8; 230 | if (x == -1) 231 | x = -2; 232 | return x; 233 | } 234 | 235 | typedef struct 236 | { 237 | /* TODO: reverse-engineer what's in these things and comment them */ 238 | int y; 239 | int m; 240 | int d; 241 | int mi; 242 | } TimeStampParts; 243 | 244 | 245 | static void 246 | TimeStamp_unpack(TimeStamp *self, TimeStampParts *p) 247 | { 248 | unsigned int minutes_since_base; 249 | 250 | minutes_since_base = TS_UNPACK_UINT32_FROM_BYTES(self->data); 251 | p->y = minutes_since_base / TS_MINUTES_PER_YEAR + TS_BASE_YEAR; 252 | p->m = (minutes_since_base % TS_MINUTES_PER_YEAR) / TS_MINUTES_PER_MONTH + 1; 253 | p->d = (minutes_since_base % TS_MINUTES_PER_MONTH) / TS_MINUTES_PER_DAY + 1; 254 | p->mi = minutes_since_base % TS_MINUTES_PER_DAY; 255 | } 256 | 257 | static double 258 | TimeStamp_sec(TimeStamp *self) 259 | { 260 | unsigned int v; 261 | 262 | v = TS_UNPACK_UINT32_FROM_BYTES(self->data +4); 263 | return TS_SECOND_BYTES_BIAS * v; 264 | } 265 | 266 | static PyObject * 267 | TimeStamp_year(TimeStamp *self) 268 | { 269 | TimeStampParts p; 270 | TimeStamp_unpack(self, &p); 271 | return INT_FROM_LONG(p.y); 272 | } 273 | 274 | static PyObject * 275 | TimeStamp_month(TimeStamp *self) 276 | { 277 | TimeStampParts p; 278 | TimeStamp_unpack(self, &p); 279 | return INT_FROM_LONG(p.m); 280 | } 281 | 282 | static PyObject * 283 | TimeStamp_day(TimeStamp *self) 284 | { 285 | TimeStampParts p; 286 | TimeStamp_unpack(self, &p); 287 | return INT_FROM_LONG(p.d); 288 | } 289 | 290 | static PyObject * 291 | TimeStamp_hour(TimeStamp *self) 292 | { 293 | TimeStampParts p; 294 | TimeStamp_unpack(self, &p); 295 | return INT_FROM_LONG(p.mi / 60); 296 | } 297 | 298 | static PyObject * 299 | TimeStamp_minute(TimeStamp *self) 300 | { 301 | TimeStampParts p; 302 | TimeStamp_unpack(self, &p); 303 | return INT_FROM_LONG(p.mi % 60); 304 | } 305 | 306 | static PyObject * 307 | TimeStamp_second(TimeStamp *self) 308 | { 309 | return PyFloat_FromDouble(TimeStamp_sec(self)); 310 | } 311 | 312 | static PyObject * 313 | TimeStamp_timeTime(TimeStamp *self) 314 | { 315 | TimeStampParts p; 316 | TimeStamp_unpack(self, &p); 317 | return PyFloat_FromDouble(TimeStamp_abst(p.y, p.m - 1, p.d - 1, p.mi, 0) 318 | + TimeStamp_sec(self) - gmoff); 319 | } 320 | 321 | static PyObject * 322 | TimeStamp_raw(TimeStamp *self) 323 | { 324 | return PyBytes_FromStringAndSize((const char*)self->data, 8); 325 | } 326 | 327 | static PyObject * 328 | TimeStamp_repr(TimeStamp *self) 329 | { 330 | PyObject *raw, *result; 331 | raw = TimeStamp_raw(self); 332 | result = PyObject_Repr(raw); 333 | Py_DECREF(raw); 334 | return result; 335 | } 336 | 337 | static PyObject * 338 | TimeStamp_str(TimeStamp *self) 339 | { 340 | char buf[128]; 341 | TimeStampParts p; 342 | int len; 343 | 344 | TimeStamp_unpack(self, &p); 345 | len =sprintf(buf, "%4.4d-%2.2d-%2.2d %2.2d:%2.2d:%09.6f", 346 | p.y, p.m, p.d, p.mi / 60, p.mi % 60, 347 | TimeStamp_sec(self)); 348 | 349 | return NATIVE_FROM_STRING_AND_SIZE(buf, len); 350 | } 351 | 352 | 353 | static PyObject * 354 | TimeStamp_laterThan(TimeStamp *self, PyObject *obj) 355 | { 356 | TimeStamp *o = NULL; 357 | TimeStampParts p; 358 | unsigned char new[8]; 359 | int i; 360 | 361 | if (Py_TYPE(obj) != Py_TYPE(self)) 362 | { 363 | PyErr_SetString(PyExc_TypeError, "expected TimeStamp object"); 364 | return NULL; 365 | } 366 | o = (TimeStamp *)obj; 367 | if (memcmp(self->data, o->data, 8) > 0) 368 | { 369 | Py_INCREF(self); 370 | return (PyObject *)self; 371 | } 372 | 373 | memcpy(new, o->data, 8); 374 | for (i = 7; i > 3; i--) 375 | { 376 | if (new[i] == 255) 377 | new[i] = 0; 378 | else 379 | { 380 | new[i]++; 381 | return TimeStamp_FromString((const char*)new); 382 | } 383 | } 384 | 385 | /* All but the first two bytes are the same. Need to increment 386 | the year, month, and day explicitly. */ 387 | TimeStamp_unpack(o, &p); 388 | if (p.mi >= 1439) 389 | { 390 | p.mi = 0; 391 | if (p.d == month_len[leap(p.y)][p.m - 1]) 392 | { 393 | p.d = 1; 394 | if (p.m == 12) 395 | { 396 | p.m = 1; 397 | p.y++; 398 | } 399 | else 400 | p.m++; 401 | } 402 | else 403 | p.d++; 404 | } 405 | else 406 | p.mi++; 407 | 408 | return TimeStamp_FromDate(p.y, p.m, p.d, p.mi / 60, p.mi % 60, 0); 409 | } 410 | 411 | static struct PyMethodDef TimeStamp_methods[] = 412 | { 413 | {"year", (PyCFunction)TimeStamp_year, METH_NOARGS}, 414 | {"minute", (PyCFunction)TimeStamp_minute, METH_NOARGS}, 415 | {"month", (PyCFunction)TimeStamp_month, METH_NOARGS}, 416 | {"day", (PyCFunction)TimeStamp_day, METH_NOARGS}, 417 | {"hour", (PyCFunction)TimeStamp_hour, METH_NOARGS}, 418 | {"second", (PyCFunction)TimeStamp_second, METH_NOARGS}, 419 | {"timeTime", (PyCFunction)TimeStamp_timeTime, METH_NOARGS}, 420 | {"laterThan", (PyCFunction)TimeStamp_laterThan, METH_O}, 421 | {"raw", (PyCFunction)TimeStamp_raw, METH_NOARGS}, 422 | {NULL, NULL}, 423 | }; 424 | 425 | #define DEFERRED_ADDRESS(ADDR) 0 426 | 427 | static PyTypeObject TimeStamp_type = 428 | { 429 | PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(NULL), 0) 430 | "persistent.TimeStamp", 431 | sizeof(TimeStamp), /* tp_basicsize */ 432 | 0, /* tp_itemsize */ 433 | (destructor)TimeStamp_dealloc, /* tp_dealloc */ 434 | 0, /* tp_print */ 435 | 0, /* tp_getattr */ 436 | 0, /* tp_setattr */ 437 | 0, /* tp_compare */ 438 | (reprfunc)TimeStamp_repr, /* tp_repr */ 439 | 0, /* tp_as_number */ 440 | 0, /* tp_as_sequence */ 441 | 0, /* tp_as_mapping */ 442 | (hashfunc)TimeStamp_hash, /* tp_hash */ 443 | 0, /* tp_call */ 444 | (reprfunc)TimeStamp_str, /* tp_str */ 445 | 0, /* tp_getattro */ 446 | 0, /* tp_setattro */ 447 | 0, /* tp_as_buffer */ 448 | Py_TPFLAGS_DEFAULT | 449 | Py_TPFLAGS_BASETYPE | 450 | Py_TPFLAGS_HAVE_RICHCOMPARE, /* tp_flags */ 451 | 0, /* tp_doc */ 452 | 0, /* tp_traverse */ 453 | 0, /* tp_clear */ 454 | (richcmpfunc)&TimeStamp_richcompare, /* tp_richcompare */ 455 | 0, /* tp_weaklistoffset */ 456 | 0, /* tp_iter */ 457 | 0, /* tp_iternext */ 458 | TimeStamp_methods, /* tp_methods */ 459 | 0, /* tp_members */ 460 | 0, /* tp_getset */ 461 | 0, /* tp_base */ 462 | 0, /* tp_dict */ 463 | 0, /* tp_descr_get */ 464 | 0, /* tp_descr_set */ 465 | }; 466 | 467 | PyObject * 468 | TimeStamp_FromString(const char *buf) 469 | { 470 | /* buf must be exactly 8 characters */ 471 | TimeStamp *ts = (TimeStamp *)PyObject_New(TimeStamp, &TimeStamp_type); 472 | memcpy(ts->data, buf, 8); 473 | return (PyObject *)ts; 474 | } 475 | 476 | #define CHECK_RANGE(VAR, LO, HI) if ((VAR) < (LO) || (VAR) > (HI)) { \ 477 | return PyErr_Format(PyExc_ValueError, \ 478 | # VAR " must be between %d and %d: %d", \ 479 | (LO), (HI), (VAR)); \ 480 | } 481 | 482 | PyObject * 483 | TimeStamp_FromDate(int year, int month, int day, int hour, int min, 484 | double sec) 485 | { 486 | 487 | TimeStamp *ts = NULL; 488 | int d; 489 | unsigned int years_since_base; 490 | unsigned int months_since_base; 491 | unsigned int days_since_base; 492 | unsigned int hours_since_base; 493 | unsigned int minutes_since_base; 494 | unsigned int v; 495 | 496 | if (year < TS_BASE_YEAR) 497 | return PyErr_Format(PyExc_ValueError, 498 | "year must be greater than %d: %d", TS_BASE_YEAR, year); 499 | CHECK_RANGE(month, 1, 12); 500 | d = days_in_month(year, month - 1); 501 | if (day < 1 || day > d) 502 | return PyErr_Format(PyExc_ValueError, 503 | "day must be between 1 and %d: %d", d, day); 504 | CHECK_RANGE(hour, 0, 23); 505 | CHECK_RANGE(min, 0, 59); 506 | /* Seconds are allowed to be anything, so chill 507 | If we did want to be pickly, 60 would be a better choice. 508 | if (sec < 0 || sec > 59) 509 | return PyErr_Format(PyExc_ValueError, 510 | "second must be between 0 and 59: %f", sec); 511 | */ 512 | ts = (TimeStamp *)PyObject_New(TimeStamp, &TimeStamp_type); 513 | /* months come in 1-based, hours and minutes come in 0-based */ 514 | /* The base time is Jan 1, 00:00 of TS_BASE_YEAR */ 515 | years_since_base = year - TS_BASE_YEAR; 516 | months_since_base = years_since_base * TS_MONTHS_PER_YEAR + (month - 1); 517 | days_since_base = months_since_base * TS_DAYS_PER_MONTH + (day - 1); 518 | hours_since_base = days_since_base * 24 + hour; 519 | minutes_since_base = hours_since_base * 60 + min; 520 | 521 | TS_PACK_UINT32_INTO_BYTES(minutes_since_base, ts->data); 522 | 523 | sec /= TS_SECOND_BYTES_BIAS; 524 | v = (unsigned int)sec; 525 | TS_PACK_UINT32_INTO_BYTES(v, ts->data + 4); 526 | return (PyObject *)ts; 527 | } 528 | 529 | PyObject * 530 | TimeStamp_TimeStamp(PyObject *obj, PyObject *args) 531 | { 532 | char *buf = NULL; 533 | Py_ssize_t len = 0; 534 | int y, mo, d, h = 0, m = 0; 535 | double sec = 0; 536 | 537 | if (PyArg_ParseTuple(args, "y#", &buf, &len)) 538 | { 539 | if (len != 8) 540 | { 541 | PyErr_SetString(PyExc_ValueError, 542 | "8-byte array expected"); 543 | return NULL; 544 | } 545 | return TimeStamp_FromString(buf); 546 | } 547 | PyErr_Clear(); 548 | 549 | if (!PyArg_ParseTuple(args, "iii|iid", &y, &mo, &d, &h, &m, &sec)) 550 | return NULL; 551 | return TimeStamp_FromDate(y, mo, d, h, m, sec); 552 | } 553 | 554 | static PyMethodDef TimeStampModule_functions[] = 555 | { 556 | {"TimeStamp", TimeStamp_TimeStamp, METH_VARARGS}, 557 | {NULL, NULL}, 558 | }; 559 | 560 | static struct PyModuleDef moduledef = 561 | { 562 | PyModuleDef_HEAD_INIT, 563 | "_timestamp", /* m_name */ 564 | TimeStampModule_doc, /* m_doc */ 565 | -1, /* m_size */ 566 | TimeStampModule_functions, /* m_methods */ 567 | NULL, /* m_reload */ 568 | NULL, /* m_traverse */ 569 | NULL, /* m_clear */ 570 | NULL, /* m_free */ 571 | }; 572 | 573 | 574 | static PyObject* 575 | module_init(void) 576 | { 577 | PyObject *module; 578 | 579 | if (TimeStamp_init_gmoff() < 0) 580 | return NULL; 581 | 582 | module = PyModule_Create(&moduledef); 583 | if (module == NULL) 584 | return NULL; 585 | 586 | ((PyObject*)&TimeStamp_type)->ob_type = &PyType_Type; 587 | TimeStamp_type.tp_getattro = PyObject_GenericGetAttr; 588 | 589 | return module; 590 | } 591 | 592 | PyMODINIT_FUNC PyInit__timestamp(void) 593 | { 594 | return module_init(); 595 | } 596 | -------------------------------------------------------------------------------- /src/persistent/cPersistence.h: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | 3 | Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | All Rights Reserved. 5 | 6 | This software is subject to the provisions of the Zope Public License, 7 | Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | FOR A PARTICULAR PURPOSE 12 | 13 | ****************************************************************************/ 14 | 15 | #ifndef CPERSISTENCE_H 16 | #define CPERSISTENCE_H 17 | 18 | #include "_compat.h" 19 | #include "bytesobject.h" 20 | 21 | #include "ring.h" 22 | 23 | #define CACHE_HEAD \ 24 | PyObject_HEAD \ 25 | CPersistentRing ring_home; \ 26 | int non_ghost_count; \ 27 | Py_ssize_t total_estimated_size; 28 | 29 | struct ccobject_head_struct; 30 | 31 | typedef struct ccobject_head_struct PerCache; 32 | 33 | /* How big is a persistent object? 34 | 35 | 12 PyGC_Head is two pointers and an int 36 | 8 PyObject_HEAD is an int and a pointer 37 | 38 | 12 jar, oid, cache pointers 39 | 8 ring struct 40 | 8 serialno 41 | 4 state + extra 42 | 4 size info 43 | 44 | (56) so far 45 | 46 | 4 dict ptr 47 | 4 weaklist ptr 48 | ------------------------- 49 | 68 only need 62, but obmalloc rounds up to multiple of eight 50 | 51 | Even a ghost requires 64 bytes. It's possible to make a persistent 52 | instance with slots and no dict, which changes the storage needed. 53 | 54 | */ 55 | 56 | #define cPersistent_HEAD \ 57 | PyObject_HEAD \ 58 | PyObject *jar; \ 59 | PyObject *oid; \ 60 | PerCache *cache; \ 61 | CPersistentRing ring; \ 62 | char serial[8]; \ 63 | signed state:8; \ 64 | unsigned estimated_size:24; 65 | 66 | /* We recently added estimated_size. We originally added it as a new 67 | unsigned long field after a signed char state field and a 68 | 3-character reserved field. This didn't work because there 69 | are packages in the wild that have their own copies of cPersistence.h 70 | that didn't see the update. 71 | 72 | To get around this, we used the reserved space by making 73 | estimated_size a 24-bit bit field in the space occupied by the old 74 | 3-character reserved field. To fit in 24 bits, we made the units 75 | of estimated_size 64-character blocks. This allows is to handle up 76 | to a GB. We should never see that, but to be paranoid, we also 77 | truncate sizes greater than 1GB. We also set the minimum size to 78 | 64 bytes. 79 | 80 | We use the _estimated_size_in_24_bits and _estimated_size_in_bytes 81 | macros both to avoid repetition and to make intent a little clearer. 82 | */ 83 | #define _estimated_size_in_24_bits(I) ((I) > 1073741696 ? 16777215 : (I)/64+1) 84 | #define _estimated_size_in_bytes(I) ((I)*64) 85 | 86 | #define cPersistent_GHOST_STATE -1 87 | #define cPersistent_UPTODATE_STATE 0 88 | #define cPersistent_CHANGED_STATE 1 89 | #define cPersistent_STICKY_STATE 2 90 | 91 | typedef struct { 92 | cPersistent_HEAD 93 | } cPersistentObject; 94 | 95 | typedef void (*percachedelfunc)(PerCache *, PyObject *); 96 | 97 | typedef struct { 98 | PyTypeObject *pertype; 99 | getattrofunc getattro; 100 | setattrofunc setattro; 101 | int (*changed)(cPersistentObject*); 102 | void (*accessed)(cPersistentObject*); 103 | void (*ghostify)(cPersistentObject*); 104 | int (*setstate)(PyObject*); 105 | percachedelfunc percachedel; 106 | int (*readCurrent)(cPersistentObject*); 107 | } cPersistenceCAPIstruct; 108 | 109 | #define cPersistenceType cPersistenceCAPI->pertype 110 | 111 | #ifndef DONT_USE_CPERSISTENCECAPI 112 | static cPersistenceCAPIstruct *cPersistenceCAPI; 113 | #endif 114 | 115 | #define cPersistanceModuleName "cPersistence" 116 | 117 | #define PER_TypeCheck(O) PyObject_TypeCheck((O), cPersistenceCAPI->pertype) 118 | 119 | #define PER_USE_OR_RETURN(O,R) {if((O)->state==cPersistent_GHOST_STATE && cPersistenceCAPI->setstate((PyObject*)(O)) < 0) return (R); else if ((O)->state==cPersistent_UPTODATE_STATE) (O)->state=cPersistent_STICKY_STATE;} 120 | 121 | #define PER_CHANGED(O) (cPersistenceCAPI->changed((cPersistentObject*)(O))) 122 | 123 | #define PER_READCURRENT(O, E) \ 124 | if (cPersistenceCAPI->readCurrent((cPersistentObject*)(O)) < 0) { E; } 125 | 126 | #define PER_GHOSTIFY(O) (cPersistenceCAPI->ghostify((cPersistentObject*)(O))) 127 | 128 | /* If the object is sticky, make it non-sticky, so that it can be ghostified. 129 | The value is not meaningful 130 | */ 131 | #define PER_ALLOW_DEACTIVATION(O) ((O)->state==cPersistent_STICKY_STATE && ((O)->state=cPersistent_UPTODATE_STATE)) 132 | 133 | #define PER_PREVENT_DEACTIVATION(O) ((O)->state==cPersistent_UPTODATE_STATE && ((O)->state=cPersistent_STICKY_STATE)) 134 | 135 | /* 136 | Make a persistent object usable from C by: 137 | 138 | - Making sure it is not a ghost 139 | 140 | - Making it sticky. 141 | 142 | IMPORTANT: If you call this and don't call PER_ALLOW_DEACTIVATION, 143 | your object will not be ghostified. 144 | 145 | PER_USE returns a 1 on success and 0 failure, where failure means 146 | error. 147 | */ 148 | #define PER_USE(O) \ 149 | (((O)->state != cPersistent_GHOST_STATE \ 150 | || (cPersistenceCAPI->setstate((PyObject*)(O)) >= 0)) \ 151 | ? (((O)->state==cPersistent_UPTODATE_STATE) \ 152 | ? ((O)->state=cPersistent_STICKY_STATE) : 1) : 0) 153 | 154 | #define PER_ACCESSED(O) (cPersistenceCAPI->accessed((cPersistentObject*)(O))) 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /src/persistent/dict.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | from zope.deferredimport import deprecated 15 | 16 | 17 | deprecated( 18 | "`persistent.dict.PersistentDict` is deprecated. Use" 19 | " `persistent.mapping.PersistentMapping` instead." 20 | " This backward compatibility shim will be removed in persistent" 21 | " version 7.", 22 | PersistentDict='persistent.mapping:PersistentMapping', 23 | ) 24 | -------------------------------------------------------------------------------- /src/persistent/list.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | 15 | """Python implementation of persistent list.""" 16 | import sys 17 | from collections import UserList 18 | 19 | import persistent 20 | 21 | 22 | # The slice object you get when you write list[:] 23 | _SLICE_ALL = slice(None, None, None) 24 | 25 | 26 | class PersistentList(UserList, persistent.Persistent): 27 | """A persistent wrapper for list objects. 28 | 29 | Mutating instances of this class will cause them to be marked 30 | as changed and automatically persisted. 31 | 32 | .. versionchanged:: 4.5.2 33 | Using the `clear` method, or deleting a slice (e.g., ``del inst[:]`` or 34 | ``del inst[x:x]``) now only results in marking the instance as changed 35 | if it actually removed items. 36 | """ 37 | __super_getitem = UserList.__getitem__ 38 | __super_setitem = UserList.__setitem__ 39 | __super_delitem = UserList.__delitem__ 40 | __super_iadd = UserList.__iadd__ 41 | __super_imul = UserList.__imul__ 42 | __super_append = UserList.append 43 | __super_insert = UserList.insert 44 | __super_pop = UserList.pop 45 | __super_remove = UserList.remove 46 | __super_reverse = UserList.reverse 47 | __super_sort = UserList.sort 48 | __super_extend = UserList.extend 49 | __super_clear = ( 50 | UserList.clear 51 | if hasattr(UserList, 'clear') 52 | else lambda inst: inst.__delitem__(_SLICE_ALL) 53 | ) 54 | 55 | if sys.version_info[:3] < (3, 7, 4): # pragma: no cover 56 | # Prior to 3.7.4, Python 3 failed to properly 57 | # return an instance of the same class. 58 | # See https://bugs.python.org/issue27639 59 | # and https://github.com/zopefoundation/persistent/issues/112. 60 | # We only define the special method on the necessary versions to avoid 61 | # any speed penalty. 62 | def __getitem__(self, item): 63 | result = self.__super_getitem(item) 64 | if isinstance(item, slice): 65 | result = self.__class__(result) 66 | return result 67 | 68 | if sys.version_info[:3] < (3, 7, 4): # pragma: no cover 69 | # Likewise for __copy__. 70 | # See 71 | # https://github.com/python/cpython/commit/3645d29a1dc2102fdb0f5f0c0129ff2295bcd768 72 | def __copy__(self): 73 | inst = self.__class__.__new__(self.__class__) 74 | inst.__dict__.update(self.__dict__) 75 | # Create a copy and avoid triggering descriptors 76 | inst.__dict__["data"] = self.__dict__["data"][:] 77 | return inst 78 | 79 | def __setitem__(self, i, item): 80 | self.__super_setitem(i, item) 81 | self._p_changed = 1 82 | 83 | def __delitem__(self, i): 84 | # If they write del list[:] but we're empty, 85 | # no need to mark us changed. Likewise with 86 | # a slice that's empty, like list[1:1]. 87 | len_before = len(self.data) 88 | self.__super_delitem(i) 89 | if len(self.data) != len_before: 90 | self._p_changed = 1 91 | 92 | def __iadd__(self, other): 93 | L = self.__super_iadd(other) 94 | self._p_changed = 1 95 | return L 96 | 97 | def __imul__(self, n): 98 | L = self.__super_imul(n) 99 | self._p_changed = 1 100 | return L 101 | 102 | def append(self, item): 103 | self.__super_append(item) 104 | self._p_changed = 1 105 | 106 | def clear(self): 107 | """ 108 | Remove all items from the list. 109 | 110 | .. versionchanged:: 4.5.2 111 | Now marks the list as changed. 112 | """ 113 | needs_changed = bool(self) 114 | self.__super_clear() 115 | if needs_changed: 116 | self._p_changed = 1 117 | 118 | def insert(self, i, item): 119 | self.__super_insert(i, item) 120 | self._p_changed = 1 121 | 122 | def pop(self, i=-1): 123 | rtn = self.__super_pop(i) 124 | self._p_changed = 1 125 | return rtn 126 | 127 | def remove(self, item): 128 | self.__super_remove(item) 129 | self._p_changed = 1 130 | 131 | def reverse(self): 132 | self.__super_reverse() 133 | self._p_changed = 1 134 | 135 | def sort(self, *args, **kwargs): 136 | self.__super_sort(*args, **kwargs) 137 | self._p_changed = 1 138 | 139 | def extend(self, other): 140 | self.__super_extend(other) 141 | self._p_changed = 1 142 | -------------------------------------------------------------------------------- /src/persistent/mapping.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | 15 | """Python implementation of persistent base types.""" 16 | from collections import UserDict as IterableUserDict 17 | 18 | import persistent 19 | 20 | 21 | class default: 22 | 23 | def __init__(self, func): 24 | self.func = func 25 | 26 | def __get__(self, inst, class_): 27 | if inst is None: 28 | return self 29 | return self.func(inst) 30 | 31 | 32 | class PersistentMapping(IterableUserDict, persistent.Persistent): 33 | """A persistent wrapper for mapping objects. 34 | 35 | This class allows wrapping of mapping objects so that object 36 | changes are registered. As a side effect, mapping objects may be 37 | subclassed. 38 | 39 | A subclass of PersistentMapping or any code that adds new 40 | attributes should not create an attribute named _container. This 41 | is reserved for backwards compatibility reasons. 42 | """ 43 | 44 | # UserDict provides all of the mapping behavior. The 45 | # PersistentMapping class is responsible marking the persistent 46 | # state as changed when a method actually changes the state. At 47 | # the mapping API evolves, we may need to add more methods here. 48 | 49 | __super_delitem = IterableUserDict.__delitem__ 50 | __super_setitem = IterableUserDict.__setitem__ 51 | __super_clear = IterableUserDict.clear 52 | __super_update = IterableUserDict.update 53 | __super_setdefault = IterableUserDict.setdefault 54 | __super_pop = IterableUserDict.pop 55 | __super_popitem = IterableUserDict.popitem 56 | 57 | # Be sure to make a deep copy of our ``data`` (See PersistentList.) See 58 | # https://github.com/python/cpython/commit/3645d29a1dc2102fdb0f5f0c0129ff2295bcd768 59 | # This was fixed in CPython 3.7.4, but we can't rely on that because it 60 | # doesn't handle our old ``_container`` appropriately (it goes directly to 61 | # ``self.__dict__``, bypassing the descriptor). The code here was initially 62 | # based on the version found in 3.7.4. 63 | 64 | def __copy__(self): 65 | inst = self.__class__.__new__(self.__class__) 66 | inst.__dict__.update(self.__dict__) 67 | # Create a copy and avoid triggering descriptors 68 | if '_container' in inst.__dict__: 69 | # BWC for ZODB < 3.3. 70 | data = inst.__dict__.pop('_container') 71 | else: 72 | data = inst.__dict__['data'] 73 | inst.__dict__["data"] = data.copy() 74 | return inst 75 | 76 | def __delitem__(self, key): 77 | self.__super_delitem(key) 78 | self._p_changed = 1 79 | 80 | def __setitem__(self, key, v): 81 | self.__super_setitem(key, v) 82 | self._p_changed = 1 83 | 84 | def clear(self): 85 | """ 86 | Remove all data from this dictionary. 87 | 88 | .. versionchanged:: 4.5.2 89 | If there was nothing to remove, this object is no 90 | longer marked as modified. 91 | """ 92 | # Historically this method always marked ourself as changed, 93 | # so if there was a _container it was persisted as data. We want 94 | # to preserve that, even if we won't make any modifications otherwise. 95 | needs_changed = '_container' in self.__dict__ or bool(self) 96 | # Python 2 implements this by directly calling self.data.clear(), 97 | # but Python 3 does so by repeatedly calling self.popitem() 98 | self.__super_clear() 99 | if needs_changed: 100 | self._p_changed = 1 101 | 102 | def update(self, *args, **kwargs): 103 | """ 104 | D.update([E, ]**F) -> None. 105 | 106 | .. versionchanged:: 4.5.2 107 | Now accepts arbitrary keyword arguments. In the special case 108 | of a keyword argument named ``b`` that is a dictionary, 109 | the behaviour will change. 110 | """ 111 | self.__super_update(*args, **kwargs) 112 | self._p_changed = 1 113 | 114 | def setdefault(self, key, default=None): 115 | # We could inline all of UserDict's implementation into the 116 | # method here, but I'd rather not depend at all on the 117 | # implementation in UserDict (simple as it is). 118 | if key not in self.data: 119 | self._p_changed = 1 120 | return self.__super_setdefault(key, default=default) 121 | 122 | def pop(self, key, *args, **kwargs): 123 | self._p_changed = 1 124 | return self.__super_pop(key, *args, **kwargs) 125 | 126 | def popitem(self): 127 | """ 128 | Remove an item. 129 | 130 | .. versionchanged:: 4.5.2 131 | No longer marks this object as modified if it was empty 132 | and an exception raised. 133 | """ 134 | result = self.__super_popitem() 135 | self._p_changed = 1 136 | return result 137 | 138 | # Old implementations (prior to 2001; see 139 | # https://github.com/zopefoundation/ZODB/commit/c64281cf2830b569eed4f211630a8a61d22a0f0b#diff-b0f568e20f51129c10a096abad27c64a) 140 | # used ``_container`` rather than ``data``. Use a descriptor to provide 141 | # ``data`` when we have ``_container`` instead 142 | 143 | @default 144 | def data(self): 145 | # We don't want to cause a write on read, so we're careful not to 146 | # do anything that would cause us to become marked as changed, however, 147 | # if we're modified, then the saved record will have data, not 148 | # _container. 149 | data = self.__dict__.pop('_container') 150 | self.__dict__['data'] = data 151 | 152 | return data 153 | -------------------------------------------------------------------------------- /src/persistent/ring.c: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | 3 | Copyright (c) 2003 Zope Foundation and Contributors. 4 | All Rights Reserved. 5 | 6 | This software is subject to the provisions of the Zope Public License, 7 | Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | FOR A PARTICULAR PURPOSE 12 | 13 | ****************************************************************************/ 14 | 15 | #define RING_C "$Id$\n" 16 | 17 | /* Support routines for the doubly-linked list of cached objects. 18 | 19 | The cache stores a doubly-linked list of persistent objects, with 20 | space for the pointers allocated in the objects themselves. The cache 21 | stores the distinguished head of the list, which is not a valid 22 | persistent object. 23 | 24 | The next pointers traverse the ring in order starting with the least 25 | recently used object. The prev pointers traverse the ring in order 26 | starting with the most recently used object. 27 | 28 | */ 29 | 30 | #include "Python.h" 31 | #include "ring.h" 32 | 33 | void 34 | ring_add(CPersistentRing *ring, CPersistentRing *elt) 35 | { 36 | assert(!elt->r_next); 37 | elt->r_next = ring; 38 | elt->r_prev = ring->r_prev; 39 | ring->r_prev->r_next = elt; 40 | ring->r_prev = elt; 41 | } 42 | 43 | void 44 | ring_del(CPersistentRing *elt) 45 | { 46 | assert(elt->r_next); 47 | elt->r_next->r_prev = elt->r_prev; 48 | elt->r_prev->r_next = elt->r_next; 49 | elt->r_next = NULL; 50 | elt->r_prev = NULL; 51 | } 52 | 53 | void 54 | ring_move_to_head(CPersistentRing *ring, CPersistentRing *elt) 55 | { 56 | assert(elt->r_next); 57 | elt->r_prev->r_next = elt->r_next; 58 | elt->r_next->r_prev = elt->r_prev; 59 | elt->r_next = ring; 60 | elt->r_prev = ring->r_prev; 61 | ring->r_prev->r_next = elt; 62 | ring->r_prev = elt; 63 | } 64 | -------------------------------------------------------------------------------- /src/persistent/ring.h: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | 3 | Copyright (c) 2003 Zope Foundation and Contributors. 4 | All Rights Reserved. 5 | 6 | This software is subject to the provisions of the Zope Public License, 7 | Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | FOR A PARTICULAR PURPOSE 12 | 13 | ****************************************************************************/ 14 | 15 | /* Support routines for the doubly-linked list of cached objects. 16 | 17 | The cache stores a headed, doubly-linked, circular list of persistent 18 | objects, with space for the pointers allocated in the objects themselves. 19 | The cache stores the distinguished head of the list, which is not a valid 20 | persistent object. The other list members are non-ghost persistent 21 | objects, linked in LRU (least-recently used) order. 22 | 23 | The r_next pointers traverse the ring starting with the least recently used 24 | object. The r_prev pointers traverse the ring starting with the most 25 | recently used object. 26 | 27 | Obscure: While each object is pointed at twice by list pointers (once by 28 | its predecessor's r_next, again by its successor's r_prev), the refcount 29 | on the object is bumped only by 1. This leads to some possibly surprising 30 | sequences of incref and decref code. Note that since the refcount is 31 | bumped at least once, the list does hold a strong reference to each 32 | object in it. 33 | */ 34 | 35 | typedef struct CPersistentRing_struct 36 | { 37 | struct CPersistentRing_struct *r_prev; 38 | struct CPersistentRing_struct *r_next; 39 | } CPersistentRing; 40 | 41 | 42 | /* The list operations here take constant time independent of the 43 | * number of objects in the list: 44 | */ 45 | 46 | /* Add elt as the most recently used object. elt must not already be 47 | * in the list, although this isn't checked. 48 | */ 49 | void ring_add(CPersistentRing *ring, CPersistentRing *elt); 50 | 51 | /* Remove elt from the list. elt must already be in the list, although 52 | * this isn't checked. 53 | */ 54 | void ring_del(CPersistentRing *elt); 55 | 56 | /* elt must already be in the list, although this isn't checked. It's 57 | * unlinked from its current position, and relinked into the list as the 58 | * most recently used object (which is arguably the tail of the list 59 | * instead of the head -- but the name of this function could be argued 60 | * either way). This is equivalent to 61 | * 62 | * ring_del(elt); 63 | * ring_add(ring, elt); 64 | * 65 | * but may be a little quicker. 66 | */ 67 | void ring_move_to_head(CPersistentRing *ring, CPersistentRing *elt); 68 | -------------------------------------------------------------------------------- /src/persistent/ring.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2015 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | from zope.interface import Interface 16 | from zope.interface import implementer 17 | 18 | from persistent import _ring 19 | 20 | 21 | class IRing(Interface): 22 | """Conceptually, a doubly-linked list for efficiently keeping track of 23 | least- and most-recently used :class:`persistent.interfaces.IPersistent` 24 | objects. 25 | 26 | This is meant to be used by the :class:`persistent.picklecache.PickleCache` 27 | and should not be considered a public API. This interface documentation 28 | exists to assist development of the picklecache and alternate 29 | implementations by explaining assumptions and performance requirements. 30 | """ 31 | 32 | def __len__(): 33 | """Return the number of persistent objects stored in the ring. 34 | 35 | Should be constant time. 36 | """ 37 | 38 | def __contains__(object): 39 | """Answer whether the given persistent object is found in the ring. 40 | 41 | Must not rely on object equality or object hashing, but only 42 | identity or the `_p_oid`. Should be constant time. 43 | """ 44 | 45 | def add(object): 46 | """Add the persistent object to the ring as most-recently used. 47 | 48 | When an object is in the ring, the ring holds a strong 49 | reference to it so it can be deactivated later by the pickle 50 | cache. Should be constant time. 51 | 52 | The object should not already be in the ring, but this is not 53 | necessarily enforced. 54 | """ 55 | 56 | def delete(object): 57 | """Remove the object from the ring if it is present. 58 | 59 | Returns a true value if it was present and a false value 60 | otherwise. An ideal implementation should be constant time, 61 | but linear time is allowed. 62 | """ 63 | 64 | def move_to_head(object): 65 | """Place the object as the most recently used object in the ring. 66 | 67 | The object should already be in the ring, but this is not 68 | necessarily enforced, and attempting to move an object that is 69 | not in the ring has undefined consequences. An ideal 70 | implementation should be constant time, but linear time is 71 | allowed. 72 | """ 73 | 74 | def __iter__(): 75 | """Iterate over each persistent object in the ring, in the order of 76 | least recently used to most recently used. 77 | 78 | Mutating the ring while an iteration is in progress has 79 | undefined consequences. 80 | """ 81 | 82 | 83 | ffi = _ring.ffi 84 | _FFI_RING = _ring.lib 85 | 86 | _OGA = object.__getattribute__ 87 | _OSA = object.__setattr__ 88 | 89 | _handles = set() 90 | 91 | 92 | @implementer(IRing) 93 | class _CFFIRing: 94 | """A ring backed by a C implementation. All operations are constant time. 95 | 96 | It is only available on platforms with ``cffi`` installed. 97 | """ 98 | 99 | __slots__ = ('ring_home', 'ring_to_obj', 'cleanup_func') 100 | 101 | def __init__(self, cleanup_func=None): 102 | node = self.ring_home = ffi.new("CPersistentRing*") 103 | node.r_next = node 104 | node.r_prev = node 105 | 106 | self.cleanup_func = cleanup_func 107 | 108 | # The Persistent objects themselves are responsible for keeping 109 | # the CFFI nodes alive, but we need to be able to detect whether 110 | # or not any given object is in our ring, plus know how many there are. 111 | # In addition, once an object enters the ring, it must be kept alive 112 | # so that it can be deactivated. 113 | # Note that because this is a strong reference to the persistent 114 | # object, its cleanup function --- triggered by the ``ffi.gc`` object 115 | # it owns --- will never be fired while it is in this dict. 116 | self.ring_to_obj = {} 117 | 118 | def ring_node_for(self, persistent_object, create=True): 119 | ring_data = _OGA(persistent_object, '_Persistent__ring') 120 | if ring_data is None: 121 | if not create: 122 | return None 123 | 124 | if self.cleanup_func: 125 | node = ffi.new('CPersistentRingCFFI*') 126 | node.pobj_id = ffi.cast('uintptr_t', id(persistent_object)) 127 | gc_ptr = ffi.gc(node, self.cleanup_func) 128 | else: 129 | node = ffi.new("CPersistentRing*") 130 | gc_ptr = None 131 | ring_data = ( 132 | node, 133 | gc_ptr, 134 | ) 135 | _OSA(persistent_object, '_Persistent__ring', ring_data) 136 | 137 | return ring_data[0] 138 | 139 | def __len__(self): 140 | return len(self.ring_to_obj) 141 | 142 | def __contains__(self, pobj): 143 | node = self.ring_node_for(pobj, False) 144 | return node and node in self.ring_to_obj 145 | 146 | def add(self, pobj): 147 | node = self.ring_node_for(pobj) 148 | _FFI_RING.cffi_ring_add(self.ring_home, node) 149 | self.ring_to_obj[node] = pobj 150 | 151 | def delete(self, pobj): 152 | its_node = self.ring_node_for(pobj, False) 153 | our_obj = self.ring_to_obj.pop(its_node, self) 154 | if its_node is not None and our_obj is not self and its_node.r_next: 155 | _FFI_RING.cffi_ring_del(its_node) 156 | return 1 157 | return None 158 | 159 | def delete_node(self, node): 160 | # Minimal sanity checking, assumes we're called from iter. 161 | self.ring_to_obj.pop(node) 162 | _FFI_RING.cffi_ring_del(node) 163 | 164 | def move_to_head(self, pobj): 165 | node = self.ring_node_for(pobj, False) 166 | _FFI_RING.cffi_ring_move_to_head(self.ring_home, node) 167 | 168 | def iteritems(self): 169 | head = self.ring_home 170 | here = head.r_next 171 | ring_to_obj = self.ring_to_obj 172 | while here != head: 173 | # We allow mutation during iteration, which means 174 | # we must get the next ``here`` value before 175 | # yielding, just in case the current value is 176 | # removed. 177 | current = here 178 | here = here.r_next 179 | pobj = ring_to_obj[current] 180 | yield current, pobj 181 | 182 | def __iter__(self): 183 | for _, v in self.iteritems(): 184 | yield v 185 | 186 | 187 | # Export the best available implementation 188 | Ring = _CFFIRing 189 | -------------------------------------------------------------------------------- /src/persistent/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /src/persistent/tests/attrhooks.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2004 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Overriding attr methods 15 | 16 | Examples for overriding attribute access methods. 17 | """ 18 | 19 | from persistent import Persistent 20 | 21 | 22 | def _resettingJar(): 23 | from persistent.tests.utils import ResettingJar 24 | return ResettingJar() 25 | 26 | 27 | def _rememberingJar(): 28 | from persistent.tests.utils import RememberingJar 29 | return RememberingJar() 30 | 31 | 32 | class OverridesGetattr(Persistent): 33 | """Example of overriding __getattr__ 34 | """ 35 | 36 | def __getattr__(self, name): 37 | """Get attributes that can't be gotten the usual way 38 | """ 39 | # Don't pretend we have any special attributes. 40 | if name.startswith("__") and name.endswrith("__"): 41 | raise AttributeError(name) # pragma: no cover 42 | return name.upper(), self._p_changed 43 | 44 | 45 | class VeryPrivate(Persistent): 46 | """Example of overriding __getattribute__, __setattr__, and __delattr__ 47 | """ 48 | 49 | def __init__(self, **kw): 50 | self.__dict__['__secret__'] = kw.copy() 51 | 52 | def __getattribute__(self, name): 53 | """Get an attribute value 54 | 55 | See the very important note in the comment below! 56 | """ 57 | ################################################################# 58 | # IMPORTANT! READ THIS! 8-> 59 | # 60 | # We *always* give Persistent a chance first. 61 | # Persistent handles certain special attributes, like _p_ 62 | # attributes. In particular, the base class handles __dict__ 63 | # and __class__. 64 | # 65 | # We call _p_getattr. If it returns True, then we have to 66 | # use Persistent.__getattribute__ to get the value. 67 | # 68 | ################################################################# 69 | if Persistent._p_getattr(self, name): 70 | return Persistent.__getattribute__(self, name) 71 | 72 | # Data should be in our secret dictionary: 73 | secret = self.__dict__['__secret__'] 74 | if name in secret: 75 | return secret[name] 76 | 77 | # Maybe it's a method: 78 | meth = getattr(self.__class__, name, None) 79 | if meth is None: 80 | raise AttributeError(name) 81 | 82 | return meth.__get__(self, self.__class__) 83 | 84 | def __setattr__(self, name, value): 85 | """Set an attribute value 86 | """ 87 | ################################################################# 88 | # IMPORTANT! READ THIS! 8-> 89 | # 90 | # We *always* give Persistent a chance first. 91 | # Persistent handles certain special attributes, like _p_ 92 | # attributes. 93 | # 94 | # We call _p_setattr. If it returns True, then we are done. 95 | # It has already set the attribute. 96 | # 97 | ################################################################# 98 | if Persistent._p_setattr(self, name, value): 99 | return 100 | 101 | self.__dict__['__secret__'][name] = value 102 | 103 | if not name.startswith('tmp_'): 104 | self._p_changed = 1 105 | 106 | def __delattr__(self, name): 107 | """Delete an attribute value 108 | """ 109 | ################################################################# 110 | # IMPORTANT! READ THIS! 8-> 111 | # 112 | # We *always* give Persistent a chance first. 113 | # Persistent handles certain special attributes, like _p_ 114 | # attributes. 115 | # 116 | # We call _p_delattr. If it returns True, then we are done. 117 | # It has already deleted the attribute. 118 | # 119 | ################################################################# 120 | if Persistent._p_delattr(self, name): 121 | return 122 | 123 | del self.__dict__['__secret__'][name] 124 | 125 | if not name.startswith('tmp_'): 126 | self._p_changed = 1 127 | -------------------------------------------------------------------------------- /src/persistent/tests/cucumbers.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2003 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | # Example objects for pickling. 15 | 16 | from persistent import Persistent 17 | 18 | 19 | def print_dict(d): 20 | d = sorted(d.items()) 21 | print('{%s}' % (', '.join( 22 | [(f'{k!r}: {v!r}') for (k, v) in d] 23 | ))) 24 | 25 | 26 | def cmpattrs(self, other, *attrs): 27 | result = 0 28 | for attr in attrs: 29 | if attr[:3] in ('_v_', '_p_'): 30 | raise AssertionError("_v_ and _p_ attrs not allowed") 31 | lhs = getattr(self, attr, None) 32 | rhs = getattr(other, attr, None) 33 | result += lhs != rhs 34 | return result 35 | 36 | 37 | class Simple(Persistent): 38 | def __init__(self, name, **kw): 39 | self.__name__ = name 40 | self.__dict__.update(kw) 41 | self._v_favorite_color = 'blue' 42 | self._p_foo = 'bar' 43 | 44 | @property 45 | def _attrs(self): 46 | return list(self.__dict__.keys()) 47 | 48 | def __eq__(self, other): 49 | return cmpattrs(self, other, '__class__', *self._attrs) == 0 50 | 51 | 52 | class Custom(Simple): 53 | 54 | def __new__(cls, x, y): 55 | r = Persistent.__new__(cls) 56 | r.x, r.y = x, y 57 | return r 58 | 59 | def __init__(self, x, y): 60 | self.a = 42 61 | 62 | def __getnewargs__(self): 63 | return self.x, self.y 64 | 65 | def __getstate__(self): 66 | return self.a 67 | 68 | def __setstate__(self, a): 69 | self.a = a 70 | 71 | 72 | class Slotted(Persistent): 73 | 74 | __slots__ = 's1', 's2', '_p_splat', '_v_eek' 75 | 76 | def __init__(self, s1, s2): 77 | self.s1, self.s2 = s1, s2 78 | self._v_eek = 1 79 | self._p_splat = 2 80 | 81 | @property 82 | def _attrs(self): 83 | raise NotImplementedError() 84 | 85 | def __eq__(self, other): 86 | return cmpattrs(self, other, '__class__', *self._attrs) == 0 87 | 88 | 89 | class SubSlotted(Slotted): 90 | 91 | __slots__ = 's3', 's4' 92 | 93 | def __init__(self, s1, s2, s3): 94 | Slotted.__init__(self, s1, s2) 95 | self.s3 = s3 96 | 97 | @property 98 | def _attrs(self): 99 | return ('s1', 's2', 's3', 's4') 100 | 101 | 102 | class SubSubSlotted(SubSlotted): 103 | 104 | def __init__(self, s1, s2, s3, **kw): 105 | SubSlotted.__init__(self, s1, s2, s3) 106 | self.__dict__.update(kw) 107 | self._v_favorite_color = 'blue' 108 | self._p_foo = 'bar' 109 | 110 | @property 111 | def _attrs(self): 112 | return ['s1', 's2', 's3', 's4'] + list(self.__dict__.keys()) 113 | -------------------------------------------------------------------------------- /src/persistent/tests/test__compat.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """ 15 | Tests for ``persistent._compat`` 16 | 17 | """ 18 | 19 | import os 20 | import unittest 21 | 22 | from persistent import _compat as compat 23 | 24 | 25 | class TestCOptimizationsFuncs(unittest.TestCase): 26 | 27 | def setUp(self): 28 | self.env_val = os.environ.get('PURE_PYTHON', self) 29 | self.orig_pypy = compat.PYPY 30 | compat.PYPY = False 31 | 32 | def tearDown(self): 33 | compat.PYPY = self.orig_pypy 34 | if self.env_val is not self: 35 | # Reset to what it was to begin with. 36 | os.environ['PURE_PYTHON'] = self.env_val 37 | else: # pragma: no cover 38 | # It wasn't present before, make sure it's not present now. 39 | os.environ.pop('PURE_PYTHON', None) 40 | 41 | self.env_val = None 42 | 43 | def _set_env(self, val): 44 | if val is not None: 45 | os.environ['PURE_PYTHON'] = val 46 | else: 47 | os.environ.pop('PURE_PYTHON', None) 48 | 49 | def test_ignored_no_env_val(self): 50 | self._set_env(None) 51 | self.assertFalse(compat._c_optimizations_ignored()) 52 | 53 | def test_ignored_zero(self): 54 | self._set_env('0') 55 | self.assertFalse(compat._c_optimizations_ignored()) 56 | 57 | def test_ignored_empty(self): 58 | self._set_env('') 59 | self.assertFalse(compat._c_optimizations_ignored()) 60 | 61 | def test_ignored_other_values(self): 62 | for val in "1", "yes", "hi": 63 | self._set_env(val) 64 | self.assertTrue(compat._c_optimizations_ignored()) 65 | 66 | def test_ignored_pypy(self): 67 | # No matter what the environment variable is, PyPy always ignores 68 | compat.PYPY = True 69 | for val in None, "", "0", "1", "yes": 70 | __traceback_info__ = val 71 | self._set_env(val) 72 | self.assertTrue(compat._c_optimizations_ignored()) 73 | 74 | def test_required(self): 75 | for val, expected in ( 76 | ('', False), 77 | ('0', True), 78 | ('1', False), 79 | ('Yes', False) 80 | ): 81 | self._set_env(val) 82 | self.assertEqual(expected, compat._c_optimizations_required()) 83 | 84 | def test_should_attempt(self): 85 | for val, expected in ( 86 | (None, True), 87 | ('', True), 88 | ('0', True), 89 | ('1', False), 90 | ('Yes', False) 91 | ): 92 | self._set_env(val) 93 | self.assertEqual( 94 | expected, compat._should_attempt_c_optimizations()) 95 | 96 | def test_should_attempt_pypy(self): 97 | compat.PYPY = True 98 | for val, expected in ( 99 | (None, False), 100 | ('', False), 101 | ('0', True), 102 | ('1', False), 103 | ('Yes', False) 104 | ): 105 | __traceback_info__ = val 106 | self._set_env(val) 107 | self.assertEqual( 108 | expected, compat._should_attempt_c_optimizations()) 109 | -------------------------------------------------------------------------------- /src/persistent/tests/test_compile_flags.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2022 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | import struct 15 | import unittest 16 | 17 | import persistent # noqa: try to load a C module for side effects 18 | 19 | 20 | class TestFloatingPoint(unittest.TestCase): 21 | 22 | def test_no_fast_math_optimization(self): 23 | # Building with -Ofast enables -ffast-math, which sets certain FPU 24 | # flags that can cause breakage elsewhere. A library such as BTrees 25 | # has no business changing global FPU flags for the entire process. 26 | zero_bits = struct.unpack("!Q", struct.pack("!d", 0.0))[0] 27 | next_up = zero_bits + 1 28 | smallest_subnormal = struct.unpack("!d", struct.pack("!Q", next_up))[0] 29 | self.assertNotEqual(smallest_subnormal, 0.0) 30 | -------------------------------------------------------------------------------- /src/persistent/tests/test_docs.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """ 15 | Tests for the documentation. 16 | """ 17 | 18 | 19 | import doctest 20 | import os.path 21 | import unittest 22 | 23 | import manuel.capture 24 | import manuel.codeblock 25 | import manuel.doctest 26 | import manuel.ignore 27 | import manuel.testing 28 | 29 | 30 | def test_suite(): 31 | here = os.path.dirname(os.path.abspath(__file__)) 32 | while not os.path.exists(os.path.join(here, 'setup.py')): 33 | prev, here = here, os.path.dirname(here) 34 | if here == prev: 35 | # Let's avoid infinite loops at root 36 | raise AssertionError('could not find my setup.py') 37 | 38 | docs = os.path.join(here, 'docs', 'api') 39 | 40 | files_to_test = ( 41 | 'cache.rst', 42 | 'attributes.rst', 43 | 'pickling.rst', 44 | ) 45 | paths = [os.path.join(docs, f) for f in files_to_test] 46 | 47 | m = manuel.ignore.Manuel() 48 | m += manuel.doctest.Manuel(optionflags=( 49 | doctest.NORMALIZE_WHITESPACE 50 | | doctest.ELLIPSIS 51 | | doctest.IGNORE_EXCEPTION_DETAIL 52 | )) 53 | m += manuel.codeblock.Manuel() 54 | m += manuel.capture.Manuel() 55 | 56 | suite = unittest.TestSuite() 57 | suite.addTest( 58 | manuel.testing.TestSuite( 59 | m, 60 | *paths 61 | ) 62 | ) 63 | 64 | return suite 65 | -------------------------------------------------------------------------------- /src/persistent/tests/test_list.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Tests for PersistentList 15 | """ 16 | 17 | import unittest 18 | 19 | from persistent.tests.utils import TrivialJar 20 | from persistent.tests.utils import copy_test 21 | 22 | 23 | l0 = [] 24 | l1 = [0] 25 | l2 = [0, 1] 26 | 27 | 28 | class OtherList: 29 | def __init__(self, initlist): 30 | self.__data = initlist 31 | 32 | def __len__(self): 33 | return len(self.__data) 34 | 35 | def __getitem__(self, i): 36 | return self.__data[i] 37 | 38 | 39 | class TestPList(unittest.TestCase): 40 | 41 | def _getTargetClass(self): 42 | from persistent.list import PersistentList 43 | return PersistentList 44 | 45 | def _makeJar(self): 46 | return TrivialJar() 47 | 48 | def _makeOne(self, *args): 49 | inst = self._getTargetClass()(*args) 50 | inst._p_jar = self._makeJar() 51 | return inst 52 | 53 | def test_volatile_attributes_not_persisted(self): 54 | # http://www.zope.org/Collectors/Zope/2052 55 | m = self._getTargetClass()() 56 | m.foo = 'bar' 57 | m._v_baz = 'qux' 58 | state = m.__getstate__() 59 | self.assertIn('foo', state) 60 | self.assertNotIn('_v_baz', state) 61 | 62 | def testTheWorld(self): 63 | # Test constructors 64 | pl = self._getTargetClass() 65 | u = pl() 66 | u0 = pl(l0) 67 | u1 = pl(l1) 68 | u2 = pl(l2) 69 | 70 | uu = pl(u) 71 | uu0 = pl(u0) 72 | uu1 = pl(u1) 73 | uu2 = pl(u2) 74 | 75 | pl(tuple(u)) 76 | pl(OtherList(u0)) 77 | pl("this is also a sequence") 78 | 79 | # Test __repr__ 80 | eq = self.assertEqual 81 | 82 | eq(str(u0), str(l0), "str(u0) == str(l0)") 83 | eq(repr(u1), repr(l1), "repr(u1) == repr(l1)") 84 | 85 | # Test __cmp__ and __len__ 86 | try: 87 | cmp 88 | except NameError: 89 | def cmp(a, b): 90 | if a == b: 91 | return 0 92 | if a < b: 93 | return -1 94 | return 1 95 | 96 | def mycmp(a, b): 97 | r = cmp(a, b) 98 | if r < 0: 99 | return -1 100 | if r > 0: 101 | return 1 102 | return r 103 | 104 | to_test = [l0, l1, l2, u, u0, u1, u2, uu, uu0, uu1, uu2] 105 | for a in to_test: 106 | for b in to_test: 107 | eq(mycmp(a, b), mycmp(len(a), len(b)), 108 | "mycmp(a, b) == mycmp(len(a), len(b))") 109 | 110 | # Test __getitem__ 111 | 112 | for i, val in enumerate(u2): 113 | eq(val, i, "u2[i] == i") 114 | 115 | # Test __setitem__ 116 | 117 | uu2[0] = 0 118 | uu2[1] = 100 119 | with self.assertRaises(IndexError): 120 | uu2[2] = 200 121 | 122 | # Test __delitem__ 123 | 124 | del uu2[1] 125 | del uu2[0] 126 | with self.assertRaises(IndexError): 127 | del uu2[0] 128 | 129 | # Test __getslice__ 130 | 131 | for i in range(-3, 4): 132 | eq(u2[:i], l2[:i], "u2[:i] == l2[:i]") 133 | eq(u2[i:], l2[i:], "u2[i:] == l2[i:]") 134 | for j in range(-3, 4): 135 | eq(u2[i:j], l2[i:j], "u2[i:j] == l2[i:j]") 136 | 137 | # Test __setslice__ 138 | 139 | for i in range(-3, 4): 140 | u2[:i] = l2[:i] 141 | eq(u2, l2, "u2 == l2") 142 | u2[i:] = l2[i:] 143 | eq(u2, l2, "u2 == l2") 144 | for j in range(-3, 4): 145 | u2[i:j] = l2[i:j] 146 | eq(u2, l2, "u2 == l2") 147 | 148 | uu2 = u2[:] 149 | uu2[:0] = [-2, -1] 150 | eq(uu2, [-2, -1, 0, 1], "uu2 == [-2, -1, 0, 1]") 151 | uu2[0:] = [] 152 | eq(uu2, [], "uu2 == []") 153 | 154 | # Test __contains__ 155 | for i in u2: 156 | self.assertIn(i, u2, "i in u2") 157 | for i in min(u2) - 1, max(u2) + 1: 158 | self.assertNotIn(i, u2, "i not in u2") 159 | 160 | # Test __delslice__ 161 | 162 | uu2 = u2[:] 163 | del uu2[1:2] 164 | del uu2[0:1] 165 | eq(uu2, [], "uu2 == []") 166 | 167 | uu2 = u2[:] 168 | del uu2[1:] 169 | del uu2[:1] 170 | eq(uu2, [], "uu2 == []") 171 | 172 | # Test __add__, __radd__, __mul__ and __rmul__ 173 | 174 | # self.assertTrue(u1 + [] == [] + u1 == u1, "u1 + [] == [] + u1 == u1") 175 | self.assertEqual(u1 + [1], u2, "u1 + [1] == u2") 176 | # self.assertTrue([-1] + u1 == [-1, 0], "[-1] + u1 == [-1, 0]") 177 | self.assertTrue(u2 == u2 * 1 == 1 * u2, "u2 == u2*1 == 1*u2") 178 | self.assertTrue(u2 + u2 == u2 * 2 == 2 * u2, "u2+u2 == u2*2 == 2*u2") 179 | self.assertTrue( 180 | u2 + u2 + u2 == u2 * 3 == 3 * u2, 181 | "u2+u2+u2 == u2*3 == 3*u2") 182 | 183 | # Test append 184 | 185 | u = u1[:] 186 | u.append(1) 187 | eq(u, u2, "u == u2") 188 | 189 | # Test insert 190 | 191 | u = u2[:] 192 | u.insert(0, -1) 193 | eq(u, [-1, 0, 1], "u == [-1, 0, 1]") 194 | 195 | # Test pop 196 | 197 | u = pl([0, -1, 1]) 198 | u.pop() 199 | eq(u, [0, -1], "u == [0, -1]") 200 | u.pop(0) 201 | eq(u, [-1], "u == [-1]") 202 | 203 | # Test remove 204 | 205 | u = u2[:] 206 | u.remove(1) 207 | eq(u, u1, "u == u1") 208 | 209 | # Test count 210 | u = u2 * 3 211 | eq(u.count(0), 3, "u.count(0) == 3") 212 | eq(u.count(1), 3, "u.count(1) == 3") 213 | eq(u.count(2), 0, "u.count(2) == 0") 214 | 215 | # Test index 216 | 217 | eq(u2.index(0), 0, "u2.index(0) == 0") 218 | eq(u2.index(1), 1, "u2.index(1) == 1") 219 | with self.assertRaises(ValueError): 220 | u2.index(2) 221 | 222 | # Test reverse 223 | 224 | u = u2[:] 225 | u.reverse() 226 | eq(u, [1, 0], "u == [1, 0]") 227 | u.reverse() 228 | eq(u, u2, "u == u2") 229 | 230 | # Test sort 231 | 232 | u = pl([1, 0]) 233 | u.sort() 234 | eq(u, u2, "u == u2") 235 | 236 | # Test keyword arguments to sort 237 | u.sort(key=lambda x: -x) 238 | eq(u, [1, 0], "u == [1, 0]") 239 | 240 | u.sort(reverse=True) 241 | eq(u, [1, 0], "u == [1, 0]") 242 | 243 | # Passing any other keyword arguments results in a TypeError 244 | with self.assertRaises(TypeError): 245 | u.sort(blah=True) 246 | 247 | # Test extend 248 | 249 | u = u1[:] 250 | u.extend(u2) 251 | eq(u, u1 + u2, "u == u1 + u2") 252 | 253 | # Test iadd 254 | u = u1[:] 255 | u += u2 256 | eq(u, u1 + u2, "u == u1 + u2") 257 | 258 | # Test imul 259 | u = u1[:] 260 | u *= 3 261 | eq(u, u1 + u1 + u1, "u == u1 + u1 + u1") 262 | 263 | def test_setslice(self): 264 | inst = self._makeOne() 265 | self.assertFalse(inst._p_changed) 266 | inst[:] = [1, 2, 3] 267 | self.assertEqual(inst, [1, 2, 3]) 268 | self.assertTrue(inst._p_changed) 269 | 270 | def test_delslice_all_nonempty_list(self): 271 | # Delete everything from a non-empty list 272 | inst = self._makeOne([1, 2, 3]) 273 | self.assertFalse(inst._p_changed) 274 | self.assertEqual(inst, [1, 2, 3]) 275 | del inst[:] 276 | self.assertEqual(inst, []) 277 | self.assertTrue(inst._p_changed) 278 | 279 | def test_delslice_sub_nonempty_list(self): 280 | # delete a sub-list from a non-empty list 281 | inst = self._makeOne([0, 1, 2, 3]) 282 | self.assertFalse(inst._p_changed) 283 | del inst[1:2] 284 | self.assertEqual(inst, [0, 2, 3]) 285 | self.assertTrue(inst._p_changed) 286 | 287 | def test_delslice_empty_nonempty_list(self): 288 | # delete an empty sub-list from a non-empty list 289 | inst = self._makeOne([0, 1, 2, 3]) 290 | self.assertFalse(inst._p_changed) 291 | del inst[1:1] 292 | self.assertEqual(inst, [0, 1, 2, 3]) 293 | self.assertFalse(inst._p_changed) 294 | 295 | def test_delslice_all_empty_list(self): 296 | inst = self._makeOne([]) 297 | self.assertFalse(inst._p_changed) 298 | self.assertEqual(inst, []) 299 | del inst[:] 300 | self.assertEqual(inst, []) 301 | self.assertFalse(inst._p_changed) 302 | 303 | def test_iadd(self): 304 | inst = self._makeOne() 305 | self.assertFalse(inst._p_changed) 306 | inst += [1, 2, 3] 307 | self.assertEqual(inst, [1, 2, 3]) 308 | self.assertTrue(inst._p_changed) 309 | 310 | def test_extend(self): 311 | inst = self._makeOne() 312 | self.assertFalse(inst._p_changed) 313 | inst.extend([1, 2, 3]) 314 | self.assertEqual(inst, [1, 2, 3]) 315 | self.assertTrue(inst._p_changed) 316 | 317 | def test_imul(self): 318 | inst = self._makeOne([1]) 319 | self.assertFalse(inst._p_changed) 320 | inst *= 2 321 | self.assertEqual(inst, [1, 1]) 322 | self.assertTrue(inst._p_changed) 323 | 324 | def test_append(self): 325 | inst = self._makeOne() 326 | self.assertFalse(inst._p_changed) 327 | inst.append(1) 328 | self.assertEqual(inst, [1]) 329 | self.assertTrue(inst._p_changed) 330 | 331 | def test_clear_nonempty(self): 332 | inst = self._makeOne([1, 2, 3, 4]) 333 | self.assertFalse(inst._p_changed) 334 | inst.clear() 335 | self.assertEqual(inst, []) 336 | self.assertTrue(inst._p_changed) 337 | 338 | def test_clear_empty(self): 339 | inst = self._makeOne([]) 340 | self.assertFalse(inst._p_changed) 341 | inst.clear() 342 | self.assertEqual(inst, []) 343 | self.assertFalse(inst._p_changed) 344 | 345 | def test_insert(self): 346 | inst = self._makeOne() 347 | self.assertFalse(inst._p_changed) 348 | inst.insert(0, 1) 349 | self.assertEqual(inst, [1]) 350 | self.assertTrue(inst._p_changed) 351 | 352 | def test_remove(self): 353 | inst = self._makeOne([1]) 354 | self.assertFalse(inst._p_changed) 355 | inst.remove(1) 356 | self.assertEqual(inst, []) 357 | self.assertTrue(inst._p_changed) 358 | 359 | def test_reverse(self): 360 | inst = self._makeOne([2, 1]) 361 | self.assertFalse(inst._p_changed) 362 | inst.reverse() 363 | self.assertEqual(inst, [1, 2]) 364 | self.assertTrue(inst._p_changed) 365 | 366 | def test_getslice_same_class(self): 367 | class MyList(self._getTargetClass()): 368 | pass 369 | 370 | inst = MyList() 371 | inst._p_jar = self._makeJar() 372 | # Entire thing, empty. 373 | inst2 = inst[:] 374 | self.assertIsNot(inst, inst2) 375 | self.assertEqual(inst, inst2) 376 | self.assertIsInstance(inst2, MyList) 377 | # The _p_jar is *not* propagated. 378 | self.assertIsNotNone(inst._p_jar) 379 | self.assertIsNone(inst2._p_jar) 380 | 381 | # Partial 382 | inst.extend((1, 2, 3)) 383 | inst2 = inst[1:2] 384 | self.assertEqual(inst2, [2]) 385 | self.assertIsInstance(inst2, MyList) 386 | self.assertIsNone(inst2._p_jar) 387 | 388 | def test_copy(self): 389 | inst = self._makeOne() 390 | inst.append(42) 391 | copy_test(self, inst) 392 | -------------------------------------------------------------------------------- /src/persistent/tests/test_mapping.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | import unittest 15 | 16 | from persistent.tests.utils import TrivialJar 17 | from persistent.tests.utils import copy_test 18 | 19 | 20 | class Test_default(unittest.TestCase): 21 | 22 | def _getTargetClass(self): 23 | from persistent.mapping import default 24 | return default 25 | 26 | def _makeOne(self, func): 27 | return self._getTargetClass()(func) 28 | 29 | def test___get___from_class(self): 30 | def _test(inst): 31 | raise AssertionError("Must not be caled") 32 | 33 | descr = self._makeOne(_test) 34 | 35 | class Foo: 36 | testing = descr 37 | self.assertIs(Foo.testing, descr) 38 | 39 | def test___get___from_instance(self): 40 | _called_with = [] 41 | 42 | def _test(inst): 43 | _called_with.append(inst) 44 | return 'TESTING' 45 | descr = self._makeOne(_test) 46 | 47 | class Foo: 48 | testing = descr 49 | foo = Foo() 50 | self.assertEqual(foo.testing, 'TESTING') 51 | self.assertEqual(_called_with, [foo]) 52 | 53 | 54 | class PersistentMappingTests(unittest.TestCase): 55 | 56 | def _getTargetClass(self): 57 | from persistent.mapping import PersistentMapping 58 | return PersistentMapping 59 | 60 | def _makeJar(self): 61 | return TrivialJar() 62 | 63 | def _makeOne(self, *args, **kwargs): 64 | inst = self._getTargetClass()(*args, **kwargs) 65 | inst._p_jar = self._makeJar() 66 | return inst 67 | 68 | def test_volatile_attributes_not_persisted(self): 69 | # http://www.zope.org/Collectors/Zope/2052 70 | m = self._makeOne() 71 | m.foo = 'bar' 72 | m._v_baz = 'qux' 73 | state = m.__getstate__() 74 | self.assertIn('foo', state) 75 | self.assertNotIn('_v_baz', state) 76 | 77 | def testTheWorld(self): 78 | # Test constructors 79 | l0 = {} 80 | l1 = {0: 0} 81 | l2 = {0: 0, 1: 1} 82 | u = self._makeOne() 83 | u0 = self._makeOne(l0) 84 | u1 = self._makeOne(l1) 85 | u2 = self._makeOne(l2) 86 | 87 | uu = self._makeOne(u) 88 | uu0 = self._makeOne(u0) 89 | uu1 = self._makeOne(u1) 90 | uu2 = self._makeOne(u2) 91 | 92 | class OtherMapping(dict): 93 | def __init__(self, initmapping): 94 | dict.__init__(self) 95 | self.__data = initmapping 96 | 97 | def items(self): 98 | raise AssertionError("Not called") 99 | self._makeOne(OtherMapping(u0)) 100 | self._makeOne([(0, 0), (1, 1)]) 101 | 102 | # Test __repr__ 103 | eq = self.assertEqual 104 | 105 | eq(str(u0), str(l0), "str(u0) == str(l0)") 106 | eq(repr(u1), repr(l1), "repr(u1) == repr(l1)") 107 | 108 | # Test __cmp__ and __len__ 109 | try: 110 | cmp 111 | except NameError: 112 | def cmp(a, b): 113 | if a == b: 114 | return 0 115 | if hasattr(a, 'items'): 116 | a = sorted(a.items()) 117 | b = sorted(b.items()) 118 | if a < b: 119 | return -1 120 | return 1 121 | 122 | def mycmp(a, b): 123 | r = cmp(a, b) 124 | if r < 0: 125 | return -1 126 | if r > 0: 127 | return 1 128 | return r 129 | 130 | to_test = [l0, l1, l2, u, u0, u1, u2, uu, uu0, uu1, uu2] 131 | for a in to_test: 132 | for b in to_test: 133 | eq(mycmp(a, b), mycmp(len(a), len(b)), 134 | "mycmp(a, b) == mycmp(len(a), len(b))") 135 | 136 | # Test __getitem__ 137 | 138 | for i, val in enumerate(u2): 139 | eq(val, i, "u2[i] == i") 140 | 141 | # Test get 142 | 143 | for i in range(len(u2)): 144 | eq(u2.get(i), i, "u2.get(i) == i") 145 | eq(u2.get(i, 5), i, "u2.get(i, 5) == i") 146 | 147 | for i in min(u2) - 1, max(u2) + 1: 148 | eq(u2.get(i), None, "u2.get(i) == None") 149 | eq(u2.get(i, 5), 5, "u2.get(i, 5) == 5") 150 | 151 | # Test __setitem__ 152 | 153 | uu2[0] = 0 154 | uu2[1] = 100 155 | uu2[2] = 200 156 | 157 | # Test __delitem__ 158 | 159 | del uu2[1] 160 | del uu2[0] 161 | with self.assertRaises(KeyError): 162 | del uu2[0] 163 | 164 | # Test __contains__ 165 | for i in u2: 166 | self.assertIn(i, u2, "i in u2") 167 | for i in min(u2) - 1, max(u2) + 1: 168 | self.assertNotIn(i, u2, "i not in u2") 169 | 170 | # Test update 171 | 172 | l_ = {"a": "b"} 173 | u = self._makeOne(l_) 174 | u.update(u2) 175 | for i in u: 176 | self.assertTrue(i in l_ or i in u2, "i in l or i in u2") 177 | for i in l_: 178 | self.assertIn(i, u, "i in u") 179 | for i in u2: 180 | self.assertIn(i, u, "i in u") 181 | 182 | # Test setdefault 183 | 184 | x = u2.setdefault(0, 5) 185 | eq(x, 0, "u2.setdefault(0, 5) == 0") 186 | 187 | x = u2.setdefault(5, 5) 188 | eq(x, 5, "u2.setdefault(5, 5) == 5") 189 | self.assertIn(5, u2, "5 in u2") 190 | 191 | # Test pop 192 | 193 | x = u2.pop(1) 194 | eq(x, 1, "u2.pop(1) == 1") 195 | self.assertNotIn(1, u2, "1 not in u2") 196 | 197 | with self.assertRaises(KeyError): 198 | u2.pop(1) 199 | 200 | x = u2.pop(1, 7) 201 | eq(x, 7, "u2.pop(1, 7) == 7") 202 | 203 | # Test popitem 204 | 205 | items = list(u2.items()) 206 | key, value = u2.popitem() 207 | self.assertIn((key, value), items, "key, value in items") 208 | self.assertNotIn(key, u2, "key not in u2") 209 | 210 | # Test clear 211 | 212 | u2.clear() 213 | eq(u2, {}, "u2 == {}") 214 | 215 | def test___repr___converts_legacy_container_attr(self): 216 | # In the past, PM used a _container attribute. For some time, the 217 | # implementation continued to use a _container attribute in pickles 218 | # (__get/setstate__) to be compatible with older releases. This isn't 219 | # really necessary any more. In fact, releases for which this might 220 | # matter can no longer share databases with current releases. Because 221 | # releases as recent as 3.9.0b5 still use _container in saved state, we 222 | # need to accept such state, but we stop producing it. 223 | pm = self._makeOne() 224 | self.assertEqual(pm.__dict__, {'data': {}}) 225 | # Make it look like an older instance 226 | pm.__dict__.clear() 227 | pm.__dict__['_container'] = {'a': 1} 228 | self.assertEqual(pm.__dict__, {'_container': {'a': 1}}) 229 | pm._p_changed = 0 230 | self.assertEqual(repr(pm), "{'a': 1}") 231 | self.assertEqual(pm.__dict__, {'data': {'a': 1}}) 232 | self.assertEqual(pm.__getstate__(), {'data': {'a': 1}}) 233 | 234 | def test_update_keywords(self): 235 | # Prior to https://github.com/zopefoundation/persistent/issues/126, 236 | # PersistentMapping didn't accept keyword arguments to update as 237 | # the builtin dict and the UserDict do. 238 | # Here we make sure it does. We use some names that have been 239 | # seen to be special in signatures as well to make sure that 240 | # we don't interpret them incorrectly. 241 | pm = self._makeOne() 242 | # Our older implementation was ``def update(self, b)``, so ``b`` 243 | # is potentially a keyword argument in the wild; the behaviour in that 244 | # corner case has changed. 245 | pm.update(b={'a': 42}) 246 | self.assertEqual(pm, {'b': {'a': 42}}) 247 | 248 | pm = self._makeOne() 249 | # Our previous implementation would explode with a TypeError 250 | pm.update(b=42) 251 | self.assertEqual(pm, {'b': 42}) 252 | 253 | pm = self._makeOne() 254 | # ``other`` shows up in a Python 3 signature. 255 | pm.update(other=42) 256 | self.assertEqual(pm, {'other': 42}) 257 | pm = self._makeOne() 258 | pm.update(other={'a': 42}) 259 | self.assertEqual(pm, {'other': {'a': 42}}) 260 | 261 | pm = self._makeOne() 262 | pm.update(a=1, b=2) 263 | self.assertEqual(pm, {'a': 1, 'b': 2}) 264 | 265 | def test_clear_nonempty(self): 266 | pm = self._makeOne({'a': 42}) 267 | self.assertFalse(pm._p_changed) 268 | pm.clear() 269 | self.assertTrue(pm._p_changed) 270 | 271 | def test_clear_empty(self): 272 | pm = self._makeOne() 273 | self.assertFalse(pm._p_changed) 274 | pm.clear() 275 | self.assertFalse(pm._p_changed) 276 | 277 | def test_clear_no_jar(self): 278 | # https://github.com/zopefoundation/persistent/issues/139 279 | self._makeOne = self._getTargetClass() 280 | self.test_clear_empty() 281 | 282 | pm = self._makeOne(a=42) 283 | pm.clear() 284 | self.assertFalse(pm._p_changed) 285 | 286 | def test_clear_empty_legacy_container(self): 287 | pm = self._makeOne() 288 | pm.__dict__['_container'] = pm.__dict__.pop('data') 289 | self.assertFalse(pm._p_changed) 290 | pm.clear() 291 | # Migration happened 292 | self.assertIn('data', pm.__dict__) 293 | # and we are marked as changed. 294 | self.assertTrue(pm._p_changed) 295 | 296 | def test_copy(self): 297 | pm = self._makeOne() 298 | pm['key'] = 42 299 | copy = copy_test(self, pm) 300 | self.assertEqual(42, copy['key']) 301 | 302 | def test_copy_legacy_container(self): 303 | pm = self._makeOne() 304 | pm['key'] = 42 305 | pm.__dict__['_container'] = pm.__dict__.pop('data') 306 | 307 | self.assertNotIn('data', pm.__dict__) 308 | self.assertIn('_container', pm.__dict__) 309 | 310 | copy = copy_test(self, pm) 311 | self.assertNotIn('_container', copy.__dict__) 312 | self.assertIn('data', copy.__dict__) 313 | self.assertEqual(42, copy['key']) 314 | 315 | 316 | class Test_legacy_PersistentDict(unittest.TestCase): 317 | 318 | def _getTargetClass(self): 319 | from persistent.dict import PersistentDict 320 | return PersistentDict 321 | 322 | def test_PD_is_alias_to_PM(self): 323 | from persistent.mapping import PersistentMapping 324 | self.assertIs(self._getTargetClass(), PersistentMapping) 325 | -------------------------------------------------------------------------------- /src/persistent/tests/test_ring.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2015 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | import unittest 15 | 16 | from .. import ring 17 | 18 | 19 | class DummyPersistent: 20 | _p_oid = None 21 | _Persistent__ring = None 22 | __next_oid = 0 23 | 24 | @classmethod 25 | def _next_oid(cls): 26 | cls.__next_oid += 1 27 | return cls.__next_oid 28 | 29 | def __init__(self): 30 | self._p_oid = self._next_oid() 31 | 32 | def __repr__(self): # pragma: no cover 33 | return f"" 34 | 35 | 36 | class CFFIRingTests(unittest.TestCase): 37 | 38 | def _getTargetClass(self): 39 | return ring._CFFIRing 40 | 41 | def _makeOne(self): 42 | return self._getTargetClass()() 43 | 44 | def test_empty_len(self): 45 | self.assertEqual(0, len(self._makeOne())) 46 | 47 | def test_empty_contains(self): 48 | r = self._makeOne() 49 | self.assertNotIn(DummyPersistent(), r) 50 | 51 | def test_empty_iter(self): 52 | self.assertEqual([], list(self._makeOne())) 53 | 54 | def test_add_one_len1(self): 55 | r = self._makeOne() 56 | p = DummyPersistent() 57 | r.add(p) 58 | self.assertEqual(1, len(r)) 59 | 60 | def test_add_one_contains(self): 61 | r = self._makeOne() 62 | p = DummyPersistent() 63 | r.add(p) 64 | self.assertIn(p, r) 65 | 66 | def test_delete_one_len0(self): 67 | r = self._makeOne() 68 | p = DummyPersistent() 69 | r.add(p) 70 | r.delete(p) 71 | self.assertEqual(0, len(r)) 72 | 73 | def test_delete_one_multiple(self): 74 | r = self._makeOne() 75 | p = DummyPersistent() 76 | r.add(p) 77 | r.delete(p) 78 | self.assertEqual(0, len(r)) 79 | self.assertNotIn(p, r) 80 | 81 | r.delete(p) 82 | self.assertEqual(0, len(r)) 83 | self.assertNotIn(p, r) 84 | 85 | def test_delete_from_wrong_ring(self): 86 | r1 = self._makeOne() 87 | r2 = self._makeOne() 88 | p1 = DummyPersistent() 89 | p2 = DummyPersistent() 90 | 91 | r1.add(p1) 92 | r2.add(p2) 93 | 94 | r2.delete(p1) 95 | 96 | self.assertEqual(1, len(r1)) 97 | self.assertEqual(1, len(r2)) 98 | 99 | self.assertEqual([p1], list(r1)) 100 | self.assertEqual([p2], list(r2)) 101 | 102 | def test_move_to_head(self): 103 | r = self._makeOne() 104 | p1 = DummyPersistent() 105 | p2 = DummyPersistent() 106 | p3 = DummyPersistent() 107 | 108 | r.add(p1) 109 | r.add(p2) 110 | r.add(p3) 111 | __traceback_info__ = [ 112 | p1._Persistent__ring, 113 | p2._Persistent__ring, 114 | p3._Persistent__ring, 115 | ] 116 | self.assertEqual(3, len(r)) 117 | self.assertEqual([p1, p2, p3], list(r)) 118 | 119 | r.move_to_head(p1) 120 | self.assertEqual([p2, p3, p1], list(r)) 121 | 122 | r.move_to_head(p3) 123 | self.assertEqual([p2, p1, p3], list(r)) 124 | 125 | r.move_to_head(p3) 126 | self.assertEqual([p2, p1, p3], list(r)) 127 | -------------------------------------------------------------------------------- /src/persistent/tests/test_timestamp.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2011 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | import unittest 15 | from contextlib import contextmanager 16 | 17 | from persistent.tests.utils import skipIfNoCExtension 18 | 19 | 20 | class Test__UTC(unittest.TestCase): 21 | 22 | def _getTargetClass(self): 23 | from persistent.timestamp import _UTC 24 | return _UTC 25 | 26 | def _makeOne(self, *args, **kw): 27 | return self._getTargetClass()(*args, **kw) 28 | 29 | def test_tzname(self): 30 | utc = self._makeOne() 31 | self.assertEqual(utc.tzname(None), 'UTC') 32 | 33 | def test_utcoffset(self): 34 | from datetime import timedelta 35 | utc = self._makeOne() 36 | self.assertEqual(utc.utcoffset(None), timedelta(0)) 37 | 38 | def test_dst(self): 39 | utc = self._makeOne() 40 | self.assertEqual(utc.dst(None), None) 41 | 42 | def test_fromutc(self): 43 | import datetime 44 | source = datetime.datetime.now(self._getTargetClass()()) 45 | utc = self._makeOne() 46 | self.assertEqual(utc.fromutc(source), source) 47 | 48 | 49 | class TimeStampTestsMixin: 50 | # Tests that work for either implementation. 51 | 52 | def _getTargetClass(self): 53 | raise NotImplementedError 54 | 55 | def _makeOne(self, *args, **kw): 56 | return self._getTargetClass()(*args, **kw) 57 | 58 | def test_ctor_invalid_arglist(self): 59 | BAD_ARGS = [(), 60 | (1,), 61 | (1, 2), 62 | (1, 2, 3), 63 | (1, 2, 3, 4), 64 | (1, 2, 3, 4, 5), 65 | ('1', '2', '3', '4', '5', '6'), 66 | (1, 2, 3, 4, 5, 6, 7), 67 | (b'123',), 68 | ] 69 | for args in BAD_ARGS: 70 | with self.assertRaises((TypeError, ValueError)): 71 | self._makeOne(*args) 72 | 73 | def test_ctor_from_invalid_strings(self): 74 | BAD_ARGS = ['' 75 | '\x00', 76 | '\x00' * 2, 77 | '\x00' * 3, 78 | '\x00' * 4, 79 | '\x00' * 5, 80 | '\x00' * 7, 81 | ] 82 | for args in BAD_ARGS: 83 | self.assertRaises((TypeError, ValueError), self._makeOne, *args) 84 | 85 | def test_ctor_from_string(self): 86 | from persistent.timestamp import _makeUTC 87 | ZERO = _makeUTC(1900, 1, 1, 0, 0, 0) 88 | EPOCH = _makeUTC(1970, 1, 1, 0, 0, 0) 89 | DELTA = ZERO - EPOCH 90 | DELTA_SECS = DELTA.days * 86400 + DELTA.seconds 91 | SERIAL = b'\x00' * 8 92 | ts = self._makeOne(SERIAL) 93 | self.assertEqual(ts.raw(), SERIAL) 94 | self.assertEqual(ts.year(), 1900) 95 | self.assertEqual(ts.month(), 1) 96 | self.assertEqual(ts.day(), 1) 97 | self.assertEqual(ts.hour(), 0) 98 | self.assertEqual(ts.minute(), 0) 99 | self.assertEqual(ts.second(), 0.0) 100 | self.assertEqual(ts.timeTime(), DELTA_SECS) 101 | 102 | def test_ctor_from_string_non_zero(self): 103 | before = self._makeOne(2011, 2, 16, 14, 37, 22.80544) 104 | after = self._makeOne(before.raw()) 105 | self.assertEqual(before.raw(), after.raw()) 106 | self.assertEqual(before.timeTime(), 1297867042.80544) 107 | 108 | def test_ctor_from_elements(self): 109 | from persistent.timestamp import _makeUTC 110 | ZERO = _makeUTC(1900, 1, 1, 0, 0, 0) 111 | EPOCH = _makeUTC(1970, 1, 1, 0, 0, 0) 112 | DELTA = ZERO - EPOCH 113 | DELTA_SECS = DELTA.days * 86400 + DELTA.seconds 114 | SERIAL = b'\x00' * 8 115 | ts = self._makeOne(1900, 1, 1, 0, 0, 0.0) 116 | self.assertEqual(ts.raw(), SERIAL) 117 | self.assertEqual(ts.year(), 1900) 118 | self.assertEqual(ts.month(), 1) 119 | self.assertEqual(ts.day(), 1) 120 | self.assertEqual(ts.hour(), 0) 121 | self.assertEqual(ts.minute(), 0) 122 | self.assertEqual(ts.second(), 0.0) 123 | self.assertEqual(ts.timeTime(), DELTA_SECS) 124 | 125 | def test_laterThan_invalid(self): 126 | ERRORS = (ValueError, TypeError) 127 | SERIAL = b'\x01' * 8 128 | ts = self._makeOne(SERIAL) 129 | self.assertRaises(ERRORS, ts.laterThan, None) 130 | self.assertRaises(ERRORS, ts.laterThan, '') 131 | self.assertRaises(ERRORS, ts.laterThan, ()) 132 | self.assertRaises(ERRORS, ts.laterThan, []) 133 | self.assertRaises(ERRORS, ts.laterThan, {}) 134 | self.assertRaises(ERRORS, ts.laterThan, object()) 135 | 136 | def test_laterThan_self_is_earlier(self): 137 | SERIAL1 = b'\x01' * 8 138 | SERIAL2 = b'\x02' * 8 139 | ts1 = self._makeOne(SERIAL1) 140 | ts2 = self._makeOne(SERIAL2) 141 | later = ts1.laterThan(ts2) 142 | self.assertEqual(later.raw(), b'\x02' * 7 + b'\x03') 143 | 144 | def test_laterThan_self_is_later(self): 145 | SERIAL1 = b'\x01' * 8 146 | SERIAL2 = b'\x02' * 8 147 | ts1 = self._makeOne(SERIAL1) 148 | ts2 = self._makeOne(SERIAL2) 149 | later = ts2.laterThan(ts1) 150 | self.assertIs(later, ts2) 151 | 152 | def test_repr(self): 153 | SERIAL = b'\x01' * 8 154 | ts = self._makeOne(SERIAL) 155 | self.assertEqual(repr(ts), repr(SERIAL)) 156 | 157 | def test_comparisons_to_non_timestamps(self): 158 | import operator 159 | 160 | # Check the corner cases when comparing non-comparable types 161 | ts = self._makeOne(2011, 2, 16, 14, 37, 22.0) 162 | 163 | def check_common(op, passes): 164 | if passes == 'neither': 165 | self.assertFalse(op(ts, None)) 166 | self.assertFalse(op(None, ts)) 167 | return True 168 | 169 | if passes == 'both': 170 | self.assertTrue(op(ts, None)) 171 | self.assertTrue(op(None, ts)) 172 | return True 173 | return False 174 | 175 | def check(op, passes): 176 | self.assertRaises(TypeError, op, ts, None) 177 | self.assertRaises(TypeError, op, None, ts) 178 | 179 | for op_name, passes in (('lt', 'second'), 180 | ('gt', 'first'), 181 | ('le', 'second'), 182 | ('ge', 'first'), 183 | ('eq', 'neither'), 184 | ('ne', 'both')): 185 | op = getattr(operator, op_name) 186 | if not check_common(op, passes): 187 | check(op, passes) 188 | 189 | 190 | class Instant: 191 | # Namespace to hold some constants. 192 | 193 | # A particular instant in time. 194 | now = 1229959248.3 195 | 196 | # That instant in time split as the result of this expression: 197 | # (time.gmtime(now)[:5] + (now % 60,)) 198 | now_ts_args = (2008, 12, 22, 15, 20, 48.299999952316284) 199 | 200 | # We happen to know that on a 32-bit platform, the hashcode 201 | # of a TimeStamp at that instant should be exactly 202 | # -1419374591 203 | # and the 64-bit should be exactly: 204 | # -3850693964765720575 205 | bit_32_hash = -1419374591 206 | bit_64_hash = -3850693964765720575 207 | 208 | MAX_32_BITS = 2 ** 31 - 1 209 | MAX_64_BITS = 2 ** 63 - 1 210 | 211 | def __init__(self): 212 | from persistent import timestamp as MUT 213 | self.MUT = MUT 214 | self.orig_maxint = MUT._MAXINT 215 | 216 | self.is_32_bit_hash = self.orig_maxint == self.MAX_32_BITS 217 | 218 | self.orig_c_long = None 219 | self.c_int64 = None 220 | self.c_int32 = None 221 | if MUT.c_long is not None: 222 | import ctypes 223 | self.orig_c_long = MUT.c_long 224 | self.c_int32 = ctypes.c_int32 225 | self.c_int64 = ctypes.c_int64 226 | # win32, even on 64-bit long, has funny sizes 227 | self.is_32_bit_hash = self.c_int32 == ctypes.c_long 228 | self.expected_hash = ( 229 | self.bit_32_hash if self.is_32_bit_hash else self.bit_64_hash) 230 | 231 | @contextmanager 232 | def _use_hash(self, maxint, c_long): 233 | try: 234 | self.MUT._MAXINT = maxint 235 | self.MUT.c_long = c_long 236 | yield 237 | finally: 238 | self.MUT._MAXINT = self.orig_maxint 239 | self.MUT.c_long = self.orig_c_long 240 | 241 | def use_32bit(self): 242 | return self._use_hash(self.MAX_32_BITS, self.c_int32) 243 | 244 | def use_64bit(self): 245 | return self._use_hash(self.MAX_64_BITS, self.c_int64) 246 | 247 | 248 | class pyTimeStampTests(TimeStampTestsMixin, unittest.TestCase): 249 | # Tests specific to the Python implementation 250 | 251 | def _getTargetClass(self): 252 | from persistent.timestamp import TimeStampPy 253 | return TimeStampPy 254 | 255 | def test_py_hash_32_64_bit(self): 256 | # Fake out the python version to think it's on a 32-bit 257 | # platform and test the same; also verify 64 bit 258 | instant = Instant() 259 | 260 | with instant.use_32bit(): 261 | py = self._makeOne(*Instant.now_ts_args) 262 | self.assertEqual(hash(py), Instant.bit_32_hash) 263 | 264 | with instant.use_64bit(): 265 | # call __hash__ directly to avoid interpreter truncation 266 | # in hash() on 32-bit platforms 267 | self.assertEqual(py.__hash__(), Instant.bit_64_hash) 268 | 269 | self.assertEqual(py.__hash__(), instant.expected_hash) 270 | 271 | 272 | class CTimeStampTests(TimeStampTestsMixin, unittest.TestCase): 273 | 274 | def _getTargetClass(self): 275 | from persistent.timestamp import TimeStamp 276 | return TimeStamp 277 | 278 | def test_hash_32_or_64_bit(self): 279 | ts = self._makeOne(*Instant.now_ts_args) 280 | self.assertIn(hash(ts), (Instant.bit_32_hash, Instant.bit_64_hash)) 281 | 282 | 283 | @skipIfNoCExtension 284 | class PyAndCComparisonTests(unittest.TestCase): 285 | """ 286 | Compares C and Python implementations. 287 | """ 288 | 289 | def _make_many_instants(self): 290 | # Given the above data, return many slight variations on 291 | # it to test matching 292 | yield Instant.now_ts_args 293 | for i in range(2000): 294 | yield Instant.now_ts_args[:-1] + ( 295 | Instant.now_ts_args[-1] + (i % 60.0) / 100.0, ) 296 | 297 | def _makeC(self, *args, **kwargs): 298 | from persistent._compat import _c_optimizations_available as get_c 299 | return get_c()['persistent.timestamp'].TimeStamp(*args, **kwargs) 300 | 301 | def _makePy(self, *args, **kwargs): 302 | from persistent.timestamp import TimeStampPy 303 | return TimeStampPy(*args, **kwargs) 304 | 305 | def _make_C_and_Py(self, *args, **kwargs): 306 | return self._makeC(*args, **kwargs), self._makePy(*args, **kwargs) 307 | 308 | def test_reprs_equal(self): 309 | for args in self._make_many_instants(): 310 | c, py = self._make_C_and_Py(*args) 311 | self.assertEqual(repr(c), repr(py)) 312 | 313 | def test_strs_equal(self): 314 | for args in self._make_many_instants(): 315 | c, py = self._make_C_and_Py(*args) 316 | self.assertEqual(str(c), str(py)) 317 | 318 | def test_raw_equal(self): 319 | c, py = self._make_C_and_Py(*Instant.now_ts_args) 320 | self.assertEqual(c.raw(), py.raw()) 321 | 322 | def test_equal(self): 323 | c, py = self._make_C_and_Py(*Instant.now_ts_args) 324 | self.assertEqual(c, py) 325 | 326 | def test_hash_equal(self): 327 | c, py = self._make_C_and_Py(*Instant.now_ts_args) 328 | self.assertEqual(hash(c), hash(py)) 329 | 330 | def test_hash_equal_constants(self): 331 | # The simple constants make it easier to diagnose 332 | # a difference in algorithms 333 | is_32_bit = Instant().is_32_bit_hash 334 | 335 | c, py = self._make_C_and_Py(b'\x00\x00\x00\x00\x00\x00\x00\x00') 336 | self.assertEqual(hash(c), 8) 337 | self.assertEqual(hash(c), hash(py)) 338 | 339 | c, py = self._make_C_and_Py(b'\x00\x00\x00\x00\x00\x00\x00\x01') 340 | self.assertEqual(hash(c), 9) 341 | self.assertEqual(hash(c), hash(py)) 342 | 343 | c, py = self._make_C_and_Py(b'\x00\x00\x00\x00\x00\x00\x01\x00') 344 | self.assertEqual(hash(c), 1000011) 345 | self.assertEqual(hash(c), hash(py)) 346 | 347 | # overflow kicks in here on 32-bit platforms 348 | c, py = self._make_C_and_Py(b'\x00\x00\x00\x00\x00\x01\x00\x00') 349 | expected = -721379967 if is_32_bit else 1000006000001 350 | self.assertEqual(hash(c), expected) 351 | self.assertEqual(hash(c), hash(py)) 352 | 353 | c, py = self._make_C_and_Py(b'\x00\x00\x00\x00\x01\x00\x00\x00') 354 | expected = 583896275 if is_32_bit else 1000009000027000019 355 | self.assertEqual(hash(c), expected) 356 | self.assertEqual(hash(c), hash(py)) 357 | 358 | # Overflow kicks in at this point on 64-bit platforms 359 | c, py = self._make_C_and_Py(b'\x00\x00\x00\x01\x00\x00\x00\x00') 360 | expected = 1525764953 if is_32_bit else -4442925868394654887 361 | self.assertEqual(hash(c), expected) 362 | self.assertEqual(hash(c), hash(py)) 363 | 364 | c, py = self._make_C_and_Py(b'\x00\x00\x01\x00\x00\x00\x00\x00') 365 | expected = -429739973 if is_32_bit else -3993531167153147845 366 | self.assertEqual(hash(c), expected) 367 | self.assertEqual(hash(c), hash(py)) 368 | 369 | c, py = self._make_C_and_Py(b'\x01\x00\x00\x00\x00\x00\x00\x00') 370 | expected = 263152323 if is_32_bit else -3099646879006235965 371 | self.assertEqual(hash(c), expected) 372 | self.assertEqual(hash(c), hash(py)) 373 | 374 | def test_ordering(self): 375 | small_c = self._makeC(b'\x00\x00\x00\x00\x00\x00\x00\x01') 376 | small_py = self._makePy(b'\x00\x00\x00\x00\x00\x00\x00\x01') 377 | 378 | big_c = self._makeC(b'\x01\x00\x00\x00\x00\x00\x00\x00') 379 | big_py = self._makePy(b'\x01\x00\x00\x00\x00\x00\x00\x00') 380 | 381 | self.assertLess(small_py, big_py) 382 | self.assertLessEqual(small_py, big_py) 383 | 384 | self.assertLess(small_py, big_c) 385 | self.assertLessEqual(small_py, big_c) 386 | self.assertLessEqual(small_py, small_c) 387 | 388 | self.assertLess(small_c, big_c) 389 | self.assertLessEqual(small_c, big_c) 390 | 391 | self.assertLessEqual(small_c, big_py) 392 | self.assertGreater(big_c, small_py) 393 | self.assertGreaterEqual(big_c, big_py) 394 | 395 | self.assertNotEqual(big_c, small_py) 396 | self.assertNotEqual(small_py, big_c) 397 | 398 | self.assertNotEqual(big_c, small_py) 399 | self.assertNotEqual(small_py, big_c) 400 | 401 | def test_seconds_precision(self, seconds=6.123456789): 402 | # https://github.com/zopefoundation/persistent/issues/41 403 | args = (2001, 2, 3, 4, 5, seconds) 404 | c = self._makeC(*args) 405 | py = self._makePy(*args) 406 | 407 | self.assertEqual(c, py) 408 | self.assertEqual(c.second(), py.second()) 409 | 410 | py2 = self._makePy(c.raw()) 411 | self.assertEqual(py2, c) 412 | 413 | c2 = self._makeC(c.raw()) 414 | self.assertEqual(c2, c) 415 | 416 | def test_seconds_precision_half(self): 417 | # make sure our rounding matches 418 | self.test_seconds_precision(seconds=6.5) 419 | self.test_seconds_precision(seconds=6.55) 420 | self.test_seconds_precision(seconds=6.555) 421 | self.test_seconds_precision(seconds=6.5555) 422 | self.test_seconds_precision(seconds=6.55555) 423 | self.test_seconds_precision(seconds=6.555555) 424 | self.test_seconds_precision(seconds=6.5555555) 425 | self.test_seconds_precision(seconds=6.55555555) 426 | self.test_seconds_precision(seconds=6.555555555) 427 | 428 | 429 | def test_suite(): 430 | return unittest.defaultTestLoader.loadTestsFromName(__name__) 431 | -------------------------------------------------------------------------------- /src/persistent/tests/test_wref.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2003 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | import unittest 15 | 16 | 17 | class WeakRefTests(unittest.TestCase): 18 | 19 | def _getTargetClass(self): 20 | from persistent.wref import WeakRef 21 | return WeakRef 22 | 23 | def _makeOne(self, ob): 24 | return self._getTargetClass()(ob) 25 | 26 | def test_ctor_target_wo_jar(self): 27 | target = _makeTarget() 28 | wref = self._makeOne(target) 29 | self.assertIs(wref._v_ob, target) 30 | self.assertEqual(wref.oid, b'OID') 31 | self.assertIsNone(wref.dm) 32 | self.assertNotIn('database_name', wref.__dict__) 33 | 34 | def test_ctor_target_w_jar(self): 35 | target = _makeTarget() 36 | target._p_jar = jar = _makeJar() 37 | wref = self._makeOne(target) 38 | self.assertIs(wref._v_ob, target) 39 | self.assertEqual(wref.oid, b'OID') 40 | self.assertIs(wref.dm, jar) 41 | self.assertEqual(wref.database_name, 'testing') 42 | 43 | def test___call___target_in_volatile(self): 44 | target = _makeTarget() 45 | target._p_jar = _makeJar() 46 | wref = self._makeOne(target) 47 | self.assertIs(wref(), target) 48 | 49 | def test___call___target_in_jar(self): 50 | target = _makeTarget() 51 | target._p_jar = jar = _makeJar() 52 | jar[target._p_oid] = target 53 | wref = self._makeOne(target) 54 | del wref._v_ob 55 | self.assertIs(wref(), target) 56 | 57 | def test___call___target_not_in_jar(self): 58 | target = _makeTarget() 59 | target._p_jar = _makeJar() 60 | wref = self._makeOne(target) 61 | del wref._v_ob 62 | self.assertIsNone(wref()) 63 | 64 | def test___hash___w_target(self): 65 | target = _makeTarget() 66 | target._p_jar = _makeJar() 67 | wref = self._makeOne(target) 68 | self.assertEqual(hash(wref), hash(target)) 69 | 70 | def test___hash___wo_target(self): 71 | target = _makeTarget() 72 | target._p_jar = _makeJar() 73 | wref = self._makeOne(target) 74 | del wref._v_ob 75 | self.assertRaises(TypeError, hash, wref) 76 | 77 | def test___eq___w_non_weakref(self): 78 | target = _makeTarget() 79 | lhs = self._makeOne(target) 80 | self.assertNotEqual(lhs, object()) 81 | # Test belt-and-suspenders directly 82 | self.assertFalse(lhs.__eq__(object())) 83 | 84 | def test___eq___w_both_same_target(self): 85 | target = _makeTarget() 86 | lhs = self._makeOne(target) 87 | _makeTarget() 88 | rhs = self._makeOne(target) 89 | self.assertEqual(lhs, rhs) 90 | 91 | def test___eq___w_both_different_targets(self): 92 | lhs_target = _makeTarget(oid='LHS') 93 | lhs = self._makeOne(lhs_target) 94 | rhs_target = _makeTarget(oid='RHS') 95 | rhs = self._makeOne(rhs_target) 96 | self.assertNotEqual(lhs, rhs) 97 | 98 | def test___eq___w_lhs_gone_target_not_in_jar(self): 99 | target = _makeTarget() 100 | target._p_jar = _makeJar() 101 | lhs = self._makeOne(target) 102 | del lhs._v_ob 103 | rhs = self._makeOne(target) 104 | self.assertRaises(TypeError, lambda: lhs == rhs) 105 | 106 | def test___eq___w_lhs_gone_target_in_jar(self): 107 | target = _makeTarget() 108 | target._p_jar = jar = _makeJar() 109 | jar[target._p_oid] = target 110 | lhs = self._makeOne(target) 111 | del lhs._v_ob 112 | _makeTarget() 113 | rhs = self._makeOne(target) 114 | self.assertEqual(lhs, rhs) 115 | 116 | def test___eq___w_rhs_gone_target_not_in_jar(self): 117 | target = _makeTarget() 118 | target._p_jar = _makeJar() 119 | lhs = self._makeOne(target) 120 | rhs = self._makeOne(target) 121 | del rhs._v_ob 122 | self.assertRaises(TypeError, lambda: lhs == rhs) 123 | 124 | def test___eq___w_rhs_gone_target_in_jar(self): 125 | target = _makeTarget() 126 | target._p_jar = jar = _makeJar() 127 | jar[target._p_oid] = target 128 | lhs = self._makeOne(target) 129 | rhs = self._makeOne(target) 130 | del rhs._v_ob 131 | self.assertEqual(lhs, rhs) 132 | 133 | 134 | class PersistentWeakKeyDictionaryTests(unittest.TestCase): 135 | 136 | def _getTargetClass(self): 137 | from persistent.wref import PersistentWeakKeyDictionary 138 | return PersistentWeakKeyDictionary 139 | 140 | def _makeOne(self, adict, **kw): 141 | return self._getTargetClass()(adict, **kw) 142 | 143 | def test_ctor_w_adict_none_no_kwargs(self): 144 | pwkd = self._makeOne(None) 145 | self.assertEqual(pwkd.data, {}) 146 | 147 | def test_ctor_w_adict_as_dict(self): 148 | jar = _makeJar() 149 | key = jar['key'] = _makeTarget(oid='KEY') 150 | key._p_jar = jar 151 | value = jar['value'] = _makeTarget(oid='VALUE') 152 | value._p_jar = jar 153 | pwkd = self._makeOne({key: value}) 154 | self.assertIs(pwkd[key], value) 155 | 156 | def test_ctor_w_adict_as_items(self): 157 | jar = _makeJar() 158 | key = jar['key'] = _makeTarget(oid='KEY') 159 | key._p_jar = jar 160 | value = jar['value'] = _makeTarget(oid='VALUE') 161 | value._p_jar = jar 162 | pwkd = self._makeOne([(key, value)]) 163 | self.assertIs(pwkd[key], value) 164 | 165 | def test___getstate___empty(self): 166 | pwkd = self._makeOne(None) 167 | self.assertEqual(pwkd.__getstate__(), {'data': []}) 168 | 169 | def test___getstate___filled(self): 170 | from persistent.wref import WeakRef 171 | jar = _makeJar() 172 | key = jar['key'] = _makeTarget(oid='KEY') 173 | key._p_jar = jar 174 | value = jar['value'] = _makeTarget(oid='VALUE') 175 | value._p_jar = jar 176 | pwkd = self._makeOne([(key, value)]) 177 | self.assertEqual(pwkd.__getstate__(), 178 | {'data': [(WeakRef(key), value)]}) 179 | 180 | def test___setstate___empty(self): 181 | from persistent.wref import WeakRef 182 | jar = _makeJar() 183 | KEY = b'KEY' 184 | KEY2 = b'KEY2' 185 | KEY3 = b'KEY3' 186 | VALUE = b'VALUE' 187 | VALUE2 = b'VALUE2' 188 | VALUE3 = b'VALUE3' 189 | key = jar[KEY] = _makeTarget(oid=KEY) 190 | key._p_jar = jar 191 | kref = WeakRef(key) 192 | value = jar[VALUE] = _makeTarget(oid=VALUE) 193 | value._p_jar = jar 194 | key2 = _makeTarget(oid=KEY2) 195 | key2._p_jar = jar # not findable 196 | kref2 = WeakRef(key2) 197 | del kref2._v_ob # force a miss 198 | value2 = jar[VALUE2] = _makeTarget(oid=VALUE2) 199 | value2._p_jar = jar 200 | key3 = jar[KEY3] = _makeTarget(oid=KEY3) # findable 201 | key3._p_jar = jar 202 | kref3 = WeakRef(key3) 203 | del kref3._v_ob # force a miss, but win in the lookup 204 | value3 = jar[VALUE3] = _makeTarget(oid=VALUE3) 205 | value3._p_jar = jar 206 | pwkd = self._makeOne(None) 207 | pwkd.__setstate__({'data': 208 | [(kref, value), (kref2, value2), (kref3, value3)]}) 209 | self.assertIs(pwkd[key], value) 210 | self.assertIsNone(pwkd.get(key2)) 211 | self.assertIs(pwkd[key3], value3) 212 | 213 | def test___setitem__(self): 214 | jar = _makeJar() 215 | key = jar['key'] = _makeTarget(oid='KEY') 216 | key._p_jar = jar 217 | value = jar['value'] = _makeTarget(oid='VALUE') 218 | value._p_jar = jar 219 | pwkd = self._makeOne(None) 220 | pwkd[key] = value 221 | self.assertIs(pwkd[key], value) 222 | 223 | def test___getitem___miss(self): 224 | jar = _makeJar() 225 | key = jar['key'] = _makeTarget(oid='KEY') 226 | key._p_jar = jar 227 | value = jar['value'] = _makeTarget(oid='VALUE') 228 | value._p_jar = jar 229 | pwkd = self._makeOne(None) 230 | 231 | def _try(): 232 | return pwkd[key] 233 | self.assertRaises(KeyError, _try) 234 | 235 | def test___delitem__(self): 236 | jar = _makeJar() 237 | key = jar['key'] = _makeTarget(oid='KEY') 238 | key._p_jar = jar 239 | value = jar['value'] = _makeTarget(oid='VALUE') 240 | value._p_jar = jar 241 | pwkd = self._makeOne([(key, value)]) 242 | del pwkd[key] 243 | self.assertIsNone(pwkd.get(key)) 244 | 245 | def test___delitem___miss(self): 246 | jar = _makeJar() 247 | key = jar['key'] = _makeTarget(oid='KEY') 248 | key._p_jar = jar 249 | value = jar['value'] = _makeTarget(oid='VALUE') 250 | value._p_jar = jar 251 | pwkd = self._makeOne(None) 252 | 253 | def _try(): 254 | del pwkd[key] 255 | self.assertRaises(KeyError, _try) 256 | 257 | def test_get_miss_w_explicit_default(self): 258 | jar = _makeJar() 259 | key = jar['key'] = _makeTarget(oid='KEY') 260 | key._p_jar = jar 261 | value = jar['value'] = _makeTarget(oid='VALUE') 262 | value._p_jar = jar 263 | pwkd = self._makeOne(None) 264 | self.assertIs(pwkd.get(key, value), value) 265 | 266 | def test___contains___miss(self): 267 | jar = _makeJar() 268 | key = jar['key'] = _makeTarget(oid='KEY') 269 | key._p_jar = jar 270 | pwkd = self._makeOne(None) 271 | self.assertNotIn(key, pwkd) 272 | 273 | def test___contains___hit(self): 274 | jar = _makeJar() 275 | key = jar['key'] = _makeTarget(oid='KEY') 276 | key._p_jar = jar 277 | value = jar['value'] = _makeTarget(oid='VALUE') 278 | value._p_jar = jar 279 | pwkd = self._makeOne([(key, value)]) 280 | self.assertIn(key, pwkd) 281 | 282 | def test___iter___empty(self): 283 | _makeJar() 284 | pwkd = self._makeOne(None) 285 | self.assertEqual(list(pwkd), []) 286 | 287 | def test___iter___filled(self): 288 | jar = _makeJar() 289 | key = jar['key'] = _makeTarget(oid='KEY') 290 | key._p_jar = jar 291 | value = jar['value'] = _makeTarget(oid='VALUE') 292 | value._p_jar = jar 293 | pwkd = self._makeOne([(key, value)]) 294 | self.assertEqual(list(pwkd), [key]) 295 | 296 | def test_update_w_other_pwkd(self): 297 | jar = _makeJar() 298 | key = jar['key'] = _makeTarget(oid='KEY') 299 | key._p_jar = jar 300 | value = jar['value'] = _makeTarget(oid='VALUE') 301 | value._p_jar = jar 302 | source = self._makeOne([(key, value)]) 303 | target = self._makeOne(None) 304 | target.update(source) 305 | self.assertIs(target[key], value) 306 | 307 | def test_update_w_dict(self): 308 | jar = _makeJar() 309 | key = jar['key'] = _makeTarget(oid='KEY') 310 | key._p_jar = jar 311 | value = jar['value'] = _makeTarget(oid='VALUE') 312 | value._p_jar = jar 313 | source = dict([(key, value)]) 314 | target = self._makeOne(None) 315 | target.update(source) 316 | self.assertIs(target[key], value) 317 | 318 | 319 | def _makeTarget(oid=b'OID'): 320 | from persistent import Persistent 321 | 322 | class Derived(Persistent): 323 | def __hash__(self): 324 | return hash(self._p_oid) 325 | 326 | def __eq__(self, other): 327 | return self._p_oid == other._p_oid 328 | 329 | def __repr__(self): # pragma: no cover 330 | return 'Derived: %s' % self._p_oid 331 | derived = Derived() 332 | derived._p_oid = oid 333 | return derived 334 | 335 | 336 | def _makeJar(): 337 | class _DB: 338 | database_name = 'testing' 339 | 340 | class _Jar(dict): 341 | def db(self): return _DB() 342 | return _Jar() 343 | 344 | 345 | def test_suite(): 346 | return unittest.defaultTestLoader.loadTestsFromName(__name__) 347 | -------------------------------------------------------------------------------- /src/persistent/tests/utils.py: -------------------------------------------------------------------------------- 1 | class TrivialJar: 2 | """ 3 | Jar that only supports registering objects so ``_p_changed`` 4 | can be tested. 5 | """ 6 | 7 | def register(self, ob): 8 | """Does nothing""" 9 | 10 | 11 | class ResettingJar: 12 | """Testing stub for _p_jar attribute. 13 | """ 14 | 15 | def __init__(self): 16 | from zope.interface import directlyProvides 17 | 18 | from persistent import PickleCache # XXX stub it! 19 | from persistent.interfaces import IPersistentDataManager 20 | self.cache = self._cache = PickleCache(self) 21 | self.oid = 1 22 | self.registered = {} 23 | directlyProvides(self, IPersistentDataManager) 24 | 25 | def add(self, obj): 26 | import struct 27 | obj._p_oid = struct.pack(">Q", self.oid) 28 | self.oid += 1 29 | obj._p_jar = self 30 | self.cache[obj._p_oid] = obj 31 | 32 | # the following methods must be implemented to be a jar 33 | 34 | def setstate(self, obj): 35 | # Trivial setstate() implementation that just re-initializes 36 | # the object. This isn't what setstate() is supposed to do, 37 | # but it suffices for the tests. 38 | obj.__class__.__init__(obj) 39 | 40 | 41 | class RememberingJar: 42 | """Testing stub for _p_jar attribute. 43 | """ 44 | 45 | def __init__(self): 46 | from persistent import PickleCache # XXX stub it! 47 | self.cache = PickleCache(self) 48 | self.oid = 1 49 | self.registered = {} 50 | 51 | def add(self, obj): 52 | import struct 53 | obj._p_oid = struct.pack(">Q", self.oid) 54 | self.oid += 1 55 | obj._p_jar = self 56 | self.cache[obj._p_oid] = obj 57 | # Remember object's state for later. 58 | self.obj = obj 59 | self.remembered = obj.__getstate__() 60 | 61 | def fake_commit(self): 62 | self.remembered = self.obj.__getstate__() 63 | self.obj._p_changed = 0 64 | 65 | # the following methods must be implemented to be a jar 66 | 67 | def register(self, obj): 68 | self.registered[obj] = 1 69 | 70 | def setstate(self, obj): 71 | # Trivial setstate() implementation that resets the object's 72 | # state as of the time it was added to the jar. 73 | # This isn't what setstate() is supposed to do, 74 | # but it suffices for the tests. 75 | obj.__setstate__(self.remembered) 76 | 77 | 78 | def copy_test(self, obj): 79 | import copy 80 | 81 | # Test copy.copy. Do this first, because depending on the 82 | # version of Python, `UserDict.copy()` may wind up 83 | # mutating the original object's ``data`` (due to our 84 | # BWC with ``_container``). This shows up only as a failure 85 | # of coverage. 86 | obj.test = [1234] # Make sure instance vars are also copied. 87 | obj_copy = copy.copy(obj) 88 | self.assertIsNot(obj.data, obj_copy.data) 89 | self.assertEqual(obj.data, obj_copy.data) 90 | self.assertIs(obj.test, obj_copy.test) 91 | 92 | # Test internal copy 93 | obj_copy = obj.copy() 94 | self.assertIsNot(obj.data, obj_copy.data) 95 | self.assertEqual(obj.data, obj_copy.data) 96 | 97 | return obj_copy 98 | 99 | 100 | def skipIfNoCExtension(o): 101 | import unittest 102 | 103 | from persistent._compat import _c_optimizations_available 104 | from persistent._compat import _c_optimizations_ignored 105 | from persistent._compat import _should_attempt_c_optimizations 106 | 107 | if _should_attempt_c_optimizations( 108 | ) and not _c_optimizations_available(): # pragma: no cover 109 | return unittest.expectedFailure(o) 110 | return unittest.skipIf( 111 | _c_optimizations_ignored() or not _c_optimizations_available(), 112 | "The C extension is not available" 113 | )(o) 114 | -------------------------------------------------------------------------------- /src/persistent/timestamp.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2011 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | __all__ = ('TimeStamp',) 15 | 16 | import datetime 17 | import functools 18 | import math 19 | import struct 20 | import sys 21 | from datetime import timezone 22 | 23 | from persistent._compat import use_c_impl 24 | 25 | 26 | _RAWTYPE = bytes 27 | _MAXINT = sys.maxsize 28 | 29 | _ZERO = b'\x00' * 8 30 | 31 | __all__ = [ 32 | 'TimeStamp', 33 | 'TimeStampPy', 34 | ] 35 | 36 | try: 37 | # Make sure to overflow and wraparound just 38 | # like the C code does. 39 | from ctypes import c_long 40 | except ImportError: # pragma: no cover 41 | # XXX: This is broken on 64-bit windows, where 42 | # sizeof(long) != sizeof(Py_ssize_t) 43 | # sizeof(long) == 4, sizeof(Py_ssize_t) == 8 44 | # It can be fixed by setting _MAXINT = 2 ** 31 - 1 on all 45 | # win32 platforms, but then that breaks PyPy3 64 bit for an unknown 46 | # reason. 47 | c_long = None 48 | 49 | def _wraparound(x): 50 | return int(((x + (_MAXINT + 1)) & ((_MAXINT << 1) + 1)) 51 | - (_MAXINT + 1)) 52 | else: 53 | def _wraparound(x): 54 | return c_long(x).value 55 | 56 | 57 | def _UTC(): 58 | return timezone.utc 59 | 60 | 61 | def _makeUTC(y, mo, d, h, mi, s): 62 | s = round(s, 6) # microsecond precision, to match the C implementation 63 | usec, sec = math.modf(s) 64 | sec = int(sec) 65 | usec = int(usec * 1e6) 66 | return datetime.datetime(y, mo, d, h, mi, sec, usec, tzinfo=_UTC()) 67 | 68 | 69 | _EPOCH = _makeUTC(1970, 1, 1, 0, 0, 0) 70 | 71 | _TS_SECOND_BYTES_BIAS = 60.0 / (1 << 16) / (1 << 16) 72 | 73 | 74 | def _makeRaw(year, month, day, hour, minute, second): 75 | a = (((year - 1900) * 12 + month - 1) * 31 + day - 1) 76 | a = (a * 24 + hour) * 60 + minute 77 | # Don't round() this; the C version just truncates 78 | b = int(second / _TS_SECOND_BYTES_BIAS) 79 | return struct.pack('>II', a, b) 80 | 81 | 82 | def _parseRaw(octets): 83 | a, b = struct.unpack('>II', octets) 84 | minute = a % 60 85 | hour = a // 60 % 24 86 | day = a // (60 * 24) % 31 + 1 87 | month = a // (60 * 24 * 31) % 12 + 1 88 | year = a // (60 * 24 * 31 * 12) + 1900 89 | second = b * _TS_SECOND_BYTES_BIAS 90 | return (year, month, day, hour, minute, second) 91 | 92 | 93 | @use_c_impl 94 | @functools.total_ordering 95 | class TimeStamp: 96 | __slots__ = ('_raw', '_elements') 97 | 98 | def __init__(self, *args): 99 | self._elements = None 100 | if len(args) == 1: 101 | raw = args[0] 102 | if not isinstance(raw, _RAWTYPE): 103 | raise TypeError('Raw octets must be of type: %s' % _RAWTYPE) 104 | if len(raw) != 8: 105 | raise TypeError('Raw must be 8 octets') 106 | self._raw = raw 107 | elif len(args) == 6: 108 | self._raw = _makeRaw(*args) 109 | # Note that we don't preserve the incoming arguments in 110 | # self._elements, we derive them from the raw value. This is 111 | # because the incoming seconds value could have more precision than 112 | # would survive in the raw data, so we must be consistent. 113 | else: 114 | raise TypeError('Pass either a single 8-octet arg ' 115 | 'or 5 integers and a float') 116 | 117 | self._elements = _parseRaw(self._raw) 118 | 119 | def raw(self): 120 | return self._raw 121 | 122 | def __repr__(self): 123 | return repr(self._raw) 124 | 125 | def __str__(self): 126 | return "%4.4d-%2.2d-%2.2d %2.2d:%2.2d:%09.6f" % ( 127 | self.year(), self.month(), self.day(), 128 | self.hour(), self.minute(), 129 | self.second()) 130 | 131 | def year(self): 132 | return self._elements[0] 133 | 134 | def month(self): 135 | return self._elements[1] 136 | 137 | def day(self): 138 | return self._elements[2] 139 | 140 | def hour(self): 141 | return self._elements[3] 142 | 143 | def minute(self): 144 | return self._elements[4] 145 | 146 | def second(self): 147 | return self._elements[5] 148 | 149 | def timeTime(self): 150 | """ -> seconds since epoch, as a float. 151 | """ 152 | delta = _makeUTC(*self._elements) - _EPOCH 153 | return delta.days * 86400 + delta.seconds + delta.microseconds / 1e6 154 | 155 | def laterThan(self, other): 156 | """ Return a timestamp instance which is later than 'other'. 157 | 158 | If self already qualifies, return self. 159 | 160 | Otherwise, return a new instance one moment later than 'other'. 161 | """ 162 | if not isinstance(other, self.__class__): 163 | raise ValueError() 164 | if self._raw > other._raw: 165 | return self 166 | a, b = struct.unpack('>II', other._raw) 167 | later = struct.pack('>II', a, b + 1) 168 | return self.__class__(later) 169 | 170 | def __eq__(self, other): 171 | try: 172 | return self.raw() == other.raw() 173 | except AttributeError: 174 | return NotImplemented 175 | 176 | def __ne__(self, other): 177 | try: 178 | return self.raw() != other.raw() 179 | except AttributeError: 180 | return NotImplemented 181 | 182 | def __hash__(self): 183 | # Match the C implementation 184 | a = bytearray(self._raw) 185 | x = a[0] << 7 186 | for i in a: 187 | x = (1000003 * x) ^ i 188 | x ^= 8 189 | 190 | x = _wraparound(x) 191 | 192 | if x == -1: # pragma: no cover 193 | # The C version has this condition, but it's not clear 194 | # why; it's also not immediately obvious what bytestring 195 | # would generate this---hence the no-cover 196 | x = -2 197 | return x 198 | 199 | def __lt__(self, other): 200 | try: 201 | return self.raw() < other.raw() 202 | except AttributeError: 203 | return NotImplemented 204 | 205 | 206 | # This name is bound by the ``@use_c_impl`` decorator to the class defined 207 | # above. We make sure and list it statically, though, to help out linters. 208 | TimeStampPy = TimeStampPy # noqa: F821 undefined name 'TimeStampPy' 209 | -------------------------------------------------------------------------------- /src/persistent/wref.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2003 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """ZODB-based persistent weakrefs 15 | """ 16 | 17 | from persistent import Persistent 18 | 19 | 20 | WeakRefMarker = object() 21 | 22 | 23 | class WeakRef: 24 | """Persistent weak references 25 | 26 | Persistent weak references are used much like Python weak 27 | references. The major difference is that you can't specify an 28 | object to be called when the object is removed from the database. 29 | """ 30 | # We set _p_oid to a marker so that the serialization system can 31 | # provide special handling of weakrefs. 32 | _p_oid = WeakRefMarker 33 | 34 | def __init__(self, ob): 35 | self._v_ob = ob 36 | self.oid = ob._p_oid 37 | self.dm = ob._p_jar 38 | if self.dm is not None: 39 | self.database_name = self.dm.db().database_name 40 | 41 | def __call__(self): 42 | try: 43 | return self._v_ob 44 | except AttributeError: 45 | try: 46 | self._v_ob = self.dm[self.oid] 47 | except (KeyError, AttributeError): 48 | return None 49 | return self._v_ob 50 | 51 | def __hash__(self): 52 | self = self() 53 | if self is None: 54 | raise TypeError('Weakly-referenced object has gone away') 55 | return hash(self) 56 | 57 | def __eq__(self, other): 58 | if not isinstance(other, WeakRef): 59 | return False 60 | self = self() 61 | if self is None: 62 | raise TypeError('Weakly-referenced object has gone away') 63 | other = other() 64 | if other is None: 65 | raise TypeError('Weakly-referenced object has gone away') 66 | 67 | return self == other 68 | 69 | 70 | class PersistentWeakKeyDictionary(Persistent): 71 | """Persistent weak key dictionary 72 | 73 | This is akin to WeakKeyDictionaries. Note, however, that removal 74 | of items is extremely lazy. 75 | """ 76 | # TODO: It's expensive trying to load dead objects from the database. 77 | # It would be helpful if the data manager/connection cached these. 78 | 79 | def __init__(self, adict=None, **kwargs): 80 | self.data = {} 81 | if adict is not None: 82 | keys = getattr(adict, "keys", None) 83 | if keys is None: 84 | adict = dict(adict) 85 | self.update(adict) 86 | # XXX 'kwargs' is pointless, because keys must be strings, but we 87 | # are going to try (and fail) to wrap a WeakRef around them. 88 | if kwargs: # pragma: no cover 89 | self.update(kwargs) 90 | 91 | def __getstate__(self): 92 | state = Persistent.__getstate__(self) 93 | state['data'] = list(state['data'].items()) 94 | return state 95 | 96 | def __setstate__(self, state): 97 | state['data'] = { 98 | k: v for (k, v) in state['data'] 99 | if k() is not None 100 | } 101 | Persistent.__setstate__(self, state) 102 | 103 | def __setitem__(self, key, value): 104 | self.data[WeakRef(key)] = value 105 | 106 | def __getitem__(self, key): 107 | return self.data[WeakRef(key)] 108 | 109 | def __delitem__(self, key): 110 | del self.data[WeakRef(key)] 111 | 112 | def get(self, key, default=None): 113 | """D.get(k[, d]) -> D[k] if k in D, else d. 114 | """ 115 | return self.data.get(WeakRef(key), default) 116 | 117 | def __contains__(self, key): 118 | return WeakRef(key) in self.data 119 | 120 | def __iter__(self): 121 | for k in self.data: 122 | yield k() 123 | 124 | def update(self, adict): 125 | if isinstance(adict, PersistentWeakKeyDictionary): 126 | self.data.update(adict.data) 127 | else: 128 | for k, v in adict.items(): 129 | self.data[WeakRef(k)] = v 130 | 131 | # TODO: May need more methods and tests. 132 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/c-code 3 | [tox] 4 | minversion = 4.0 5 | envlist = 6 | release-check 7 | lint 8 | py39,py39-pure 9 | py310,py310-pure 10 | py311,py311-pure 11 | py312,py312-pure 12 | py313,py313-pure 13 | pypy3 14 | docs 15 | coverage 16 | 17 | [testenv] 18 | deps = 19 | setuptools <= 75.6.0 20 | setenv = 21 | pure: PURE_PYTHON=1 22 | !pure-!pypy3: PURE_PYTHON=0 23 | commands = 24 | zope-testrunner --test-path=src {posargs:-vc} 25 | sphinx-build -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest 26 | extras = 27 | test 28 | docs 29 | 30 | [testenv:setuptools-latest] 31 | basepython = python3 32 | deps = 33 | git+https://github.com/pypa/setuptools.git\#egg=setuptools 34 | 35 | [testenv:coverage] 36 | basepython = python3 37 | allowlist_externals = 38 | mkdir 39 | deps = 40 | coverage[toml] 41 | setenv = 42 | PURE_PYTHON=1 43 | commands = 44 | mkdir -p {toxinidir}/parts/htmlcov 45 | coverage run -m zope.testrunner --test-path=src {posargs:-vc} 46 | python -c 'import os, subprocess; subprocess.check_call("coverage run -a -m zope.testrunner --test-path=src", env=dict(os.environ, PURE_PYTHON="0"), shell=True)' 47 | coverage html 48 | coverage report 49 | 50 | [testenv:release-check] 51 | description = ensure that the distribution is ready to release 52 | basepython = python3 53 | skip_install = true 54 | deps = 55 | setuptools <= 75.6.0 56 | cffi; platform_python_implementation == 'CPython' 57 | twine 58 | build 59 | check-manifest 60 | check-python-versions >= 0.20.0 61 | wheel 62 | commands_pre = 63 | commands = 64 | check-manifest 65 | check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml 66 | python -m build --sdist --no-isolation 67 | twine check dist/* 68 | 69 | [testenv:lint] 70 | description = This env runs all linters configured in .pre-commit-config.yaml 71 | basepython = python3 72 | skip_install = true 73 | deps = 74 | pre-commit 75 | commands_pre = 76 | commands = 77 | pre-commit run --all-files --show-diff-on-failure 78 | 79 | [testenv:docs] 80 | basepython = python3 81 | skip_install = false 82 | commands_pre = 83 | commands = 84 | sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html 85 | sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest 86 | --------------------------------------------------------------------------------