├── .coveragerc ├── .gitignore ├── .gitreview ├── .mailmap ├── .pre-commit-config.yaml ├── .stestr.conf ├── .zuul.yaml ├── CONTRIBUTING.rst ├── HACKING.rst ├── LICENSE ├── README.rst ├── bindep.txt ├── doc ├── requirements.txt └── source │ ├── conf.py │ ├── configuration │ └── index.rst │ ├── contributor │ └── index.rst │ ├── index.rst │ ├── install │ └── index.rst │ ├── reference │ ├── base.rst │ ├── exception.rst │ ├── fields.rst │ ├── fixture.rst │ └── index.rst │ └── user │ ├── examples.rst │ ├── history.rst │ ├── index.rst │ └── usage.rst ├── oslo_versionedobjects ├── __init__.py ├── _i18n.py ├── _options.py ├── _utils.py ├── base.py ├── examples │ ├── __init__.py │ └── iot_bulb.py ├── exception.py ├── fields.py ├── fixture.py ├── locale │ └── en_GB │ │ └── LC_MESSAGES │ │ └── oslo_versionedobjects.po ├── test.py └── tests │ ├── __init__.py │ ├── obj_fixtures.py │ ├── test_exception.py │ ├── test_fields.py │ ├── test_fixture.py │ └── test_objects.py ├── pyproject.toml ├── releasenotes ├── notes │ ├── add-reno-996dd44974d53238.yaml │ ├── drop-python27-support-b3e377b0dcfa4f5c.yaml │ ├── remove-py38-f4e8c7ce18a5914b.yaml │ └── update_md5_for_fips-e5a8f8f438ac81fb.yaml └── source │ ├── 2023.1.rst │ ├── 2023.2.rst │ ├── 2024.1.rst │ ├── 2024.2.rst │ ├── 2025.1.rst │ ├── _static │ └── .placeholder │ ├── _templates │ └── .placeholder │ ├── conf.py │ ├── index.rst │ ├── locale │ └── en_GB │ │ └── LC_MESSAGES │ │ └── releasenotes.po │ ├── ocata.rst │ ├── pike.rst │ ├── queens.rst │ ├── rocky.rst │ ├── stein.rst │ ├── train.rst │ ├── unreleased.rst │ ├── ussuri.rst │ ├── victoria.rst │ ├── wallaby.rst │ ├── xena.rst │ ├── yoga.rst │ └── zed.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = oslo_versionedobjects 4 | omit = oslo_versionedobjects/tests/* 5 | 6 | [report] 7 | ignore_errors = True 8 | precision = 2 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Add patterns in here to exclude files created by tools integrated with this 2 | # repository, such as test frameworks from the project's recommended workflow, 3 | # rendered documentation and package builds. 4 | # 5 | # Don't add patterns to exclude files created by preferred personal tools 6 | # (editors, IDEs, your operating system itself even). These should instead be 7 | # maintained outside the repository, for example in a ~/.gitignore file added 8 | # with: 9 | # 10 | # git config --global core.excludesfile '~/.gitignore' 11 | 12 | # Bytecompiled Python 13 | *.py[cod] 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Packages 19 | *.egg* 20 | *.egg-info 21 | dist 22 | build 23 | eggs 24 | parts 25 | bin 26 | var 27 | sdist 28 | develop-eggs 29 | .installed.cfg 30 | lib 31 | lib64 32 | 33 | # Installer logs 34 | pip-log.txt 35 | 36 | # Unit test / coverage reports 37 | .coverage 38 | cover 39 | .tox 40 | .stestr/ 41 | 42 | # Translations 43 | *.mo 44 | 45 | # Complexity 46 | output/*.html 47 | output/*/index.html 48 | 49 | # Sphinx 50 | doc/build 51 | 52 | # pbr generates these 53 | AUTHORS 54 | ChangeLog 55 | 56 | # reno build 57 | RELEASENOTES.rst 58 | releasenotes/build 59 | releasenotes/notes/reno.cache 60 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/oslo.versionedobjects.git 5 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # Format is: 2 | # 3 | # 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | # Replaces or checks mixed line ending 7 | - id: mixed-line-ending 8 | args: ['--fix', 'lf'] 9 | exclude: '.*\.(svg)$' 10 | # Forbid files which have a UTF-8 byte-order marker 11 | - id: check-byte-order-marker 12 | # Checks that non-binary executables have a proper shebang 13 | - id: check-executables-have-shebangs 14 | # Check for files that contain merge conflict strings. 15 | - id: check-merge-conflict 16 | # Check for debugger imports and py37+ breakpoint() 17 | # calls in python source 18 | - id: debug-statements 19 | - id: check-yaml 20 | files: .*\.(yaml|yml)$ 21 | - repo: https://opendev.org/openstack/hacking 22 | rev: 7.0.0 23 | hooks: 24 | - id: hacking 25 | additional_dependencies: [] 26 | - repo: https://github.com/PyCQA/bandit 27 | rev: 1.7.10 28 | hooks: 29 | - id: bandit 30 | args: ['-x', 'tests', '--skip', 'B303'] 31 | - repo: https://github.com/asottile/pyupgrade 32 | rev: v3.18.0 33 | hooks: 34 | - id: pyupgrade 35 | args: [--py3-only] 36 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./oslo_versionedobjects/tests 3 | top_path=./ 4 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - project: 2 | check: 3 | jobs: 4 | - oslo.versionedobjects-src-grenade-multinode 5 | templates: 6 | - check-requirements 7 | - lib-forward-testing-python3 8 | - openstack-python3-jobs 9 | - periodic-stable-jobs 10 | - publish-openstack-docs-pti 11 | - release-notes-jobs-python3 12 | 13 | - job: 14 | name: oslo.versionedobjects-src-grenade-multinode 15 | parent: grenade-multinode 16 | voting: false 17 | irrelevant-files: 18 | - ^.*\.rst$ 19 | - ^doc/.*$ 20 | - ^releasenotes/.*$ 21 | - ^.git.*$ 22 | - ^.*/locale/.*po$ 23 | - ^(test-|)requirements.txt$ 24 | - ^setup.cfg$ 25 | - ^\.pre-commit-config\.yaml$ 26 | required-projects: 27 | - opendev.org/openstack/oslo.versionedobjects 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you would like to contribute to the development of oslo's libraries, 2 | first you must take a look to this page: 3 | 4 | https://specs.openstack.org/openstack/oslo-specs/specs/policy/contributing.html 5 | 6 | If you would like to contribute to the development of OpenStack, 7 | you must follow the steps in this page: 8 | http://docs.openstack.org/infra/manual/developers.html 9 | 10 | Once those steps have been completed, changes to OpenStack 11 | should be submitted for review via the Gerrit tool, following 12 | the workflow documented at: 13 | http://docs.openstack.org/infra/manual/developers.html#development-workflow 14 | 15 | Pull requests submitted through GitHub will be ignored. 16 | 17 | Bugs should be filed on Launchpad, not GitHub: 18 | https://bugs.launchpad.net/oslo.versionedobjects 19 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | oslo.versionedobjects Style Commandments 2 | ======================================== 3 | 4 | Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Team and repository tags 3 | ======================== 4 | 5 | .. image:: https://governance.openstack.org/tc/badges/oslo.versionedobjects.svg 6 | :target: https://governance.openstack.org/tc/reference/tags/index.html 7 | 8 | .. Change things from this point on 9 | 10 | ===================== 11 | oslo.versionedobjects 12 | ===================== 13 | 14 | .. image:: https://img.shields.io/pypi/v/oslo.versionedobjects.svg 15 | :target: https://pypi.org/project/oslo.versionedobjects/ 16 | :alt: Latest Version 17 | 18 | .. image:: https://img.shields.io/pypi/dm/oslo.versionedobjects.svg 19 | :target: https://pypi.org/project/oslo.versionedobjects/ 20 | :alt: Downloads 21 | 22 | The oslo.versionedobjects library provides a generic versioned object model 23 | that is RPC-friendly, with inbuilt serialization, field typing, and remotable 24 | method calls. It can be used to define a data model within a project 25 | independent of external APIs or database schema for the purposes of providing 26 | upgrade compatibility across distributed services. 27 | 28 | * Free software: Apache license 29 | * Documentation: https://docs.openstack.org/oslo.versionedobjects/latest 30 | * Source: http://opendev.org/openstack/oslo.versionedobjects 31 | * Bugs: http://bugs.launchpad.net/oslo.versionedobjects 32 | * Release notes: https://docs.openstack.org/releasenotes/oslo.versionedobjects/ 33 | -------------------------------------------------------------------------------- /bindep.txt: -------------------------------------------------------------------------------- 1 | # This is a cross-platform list tracking distribution packages needed by tests; 2 | # see http://docs.openstack.org/infra/bindep/ for additional information. 3 | 4 | locales [platform:debian] 5 | python3-all-dev [platform:ubuntu !platform:ubuntu-precise] 6 | python3-dev [platform:dpkg] 7 | python3-devel [platform:fedora] 8 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | # These are needed for docs generation 2 | openstackdocstheme>=2.2.1 # Apache-2.0 3 | sphinx>=2.0.0 # BSD 4 | reno>=3.1.0 # Apache-2.0 5 | 6 | fixtures>=3.0.0 # Apache-2.0/BSD 7 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import sys 18 | 19 | sys.path.insert(0, os.path.abspath('../..')) 20 | # -- General configuration ---------------------------------------------------- 21 | 22 | # Add any Sphinx extension module names here, as strings. They can be 23 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 24 | extensions = [ 25 | 'sphinx.ext.autodoc', 26 | 'sphinx.ext.extlinks', 27 | 'openstackdocstheme', 28 | 'oslo_config.sphinxext', 29 | ] 30 | 31 | # openstackdocstheme options 32 | openstackdocs_repo_name = 'openstack/oslo.versionedobjects' 33 | openstackdocs_bug_project = 'oslo.versionedobjects' 34 | openstackdocs_bug_tag = '' 35 | 36 | # autodoc generation is a bit aggressive and a nuisance when doing heavy 37 | # text edit cycles. 38 | # execute "export SPHINX_DEBUG=1" in your terminal to disable 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = 'oslo.versionedobjects' 48 | copyright = '2014, OpenStack Foundation' 49 | source_tree = 'https://opendev.org/openstack/%s' % project 50 | 51 | # If true, '()' will be appended to :func: etc. cross-reference text. 52 | add_function_parentheses = True 53 | 54 | # If true, the current module name will be prepended to all description 55 | # unit titles (such as .. function::). 56 | add_module_names = True 57 | 58 | # Shortened external links. 59 | extlinks = { 60 | 'example': (source_tree + 61 | '/%s/examples/%%s.py' % project.replace(".", "_"), None), 62 | } 63 | 64 | # The name of the Pygments (syntax highlighting) style to use. 65 | pygments_style = 'native' 66 | 67 | # -- Options for HTML output -------------------------------------------------- 68 | 69 | # The theme to use for HTML and HTML Help pages. Major themes that come with 70 | # Sphinx are currently 'default' and 'sphinxdoc'. 71 | # html_theme_path = ["."] 72 | # html_theme = '_theme' 73 | # html_static_path = ['static'] 74 | html_theme = 'openstackdocs' 75 | 76 | 77 | # Output file base name for HTML help builder. 78 | htmlhelp_basename = '%sdoc' % project 79 | 80 | # Grouping the document tree into LaTeX files. List of tuples 81 | # (source start file, target name, title, author, documentclass 82 | # [howto/manual]). 83 | latex_documents = [ 84 | ('index', 85 | '%s.tex' % project, 86 | '%s Documentation' % project, 87 | 'OpenStack Foundation', 'manual'), 88 | ] 89 | -------------------------------------------------------------------------------- /doc/source/configuration/index.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Configuration Options 3 | ======================= 4 | 5 | oslo.versionedobjects uses oslo.config to define and manage 6 | configuration options to allow the deployer to control how an 7 | application using oslo.versionedobjects behaves. 8 | 9 | .. show-options:: oslo.versionedobjects 10 | -------------------------------------------------------------------------------- /doc/source/contributor/index.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Contributing 3 | ============== 4 | 5 | .. include:: ../../../CONTRIBUTING.rst 6 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | oslo.versionedobjects 3 | ======================= 4 | 5 | The oslo.versionedobjects library provides a generic versioned object model 6 | that is RPC-friendly, with inbuilt serialization, field typing, and remotable 7 | method calls. It can be used to define a data model within a project 8 | independent of external APIs or database schema for the purposes of providing 9 | upgrade compatibility across distributed services. 10 | 11 | * Free software: Apache license 12 | * Documentation: https://docs.openstack.org/oslo.versionedobjects/latest/ 13 | * Source: https://opendev.org/openstack/oslo.versionedobjects 14 | * Bugs: https://bugs.launchpad.net/oslo.versionedobjects 15 | 16 | ---- 17 | 18 | Contents 19 | ======== 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | install/index 25 | user/index 26 | configuration/index 27 | reference/index 28 | contributor/index 29 | 30 | Release Notes 31 | ============= 32 | 33 | Read also the `oslo.versionedobjects Release Notes 34 | `_. 35 | 36 | Indices and tables 37 | ================== 38 | 39 | * :ref:`genindex` 40 | * :ref:`modindex` 41 | * :ref:`search` 42 | 43 | -------------------------------------------------------------------------------- /doc/source/install/index.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Installation 3 | ============== 4 | 5 | At the command line:: 6 | 7 | $ pip install oslo.versionedobjects 8 | 9 | To use ``oslo_versionedobjects.fixture``, some additional dependencies 10 | are needed. They can be installed using the ``fixtures`` extra:: 11 | 12 | $ pip install 'oslo.versionedobjects[fixtures]' 13 | -------------------------------------------------------------------------------- /doc/source/reference/base.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | base 3 | ============= 4 | 5 | .. automodule:: oslo_versionedobjects.base 6 | :members: 7 | -------------------------------------------------------------------------------- /doc/source/reference/exception.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | exception 3 | ========= 4 | 5 | .. automodule:: oslo_versionedobjects.exception 6 | :members: 7 | -------------------------------------------------------------------------------- /doc/source/reference/fields.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | fields 3 | ============= 4 | 5 | .. automodule:: oslo_versionedobjects.fields 6 | :members: 7 | -------------------------------------------------------------------------------- /doc/source/reference/fixture.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Fixture 3 | ========= 4 | 5 | .. automodule:: oslo_versionedobjects.fixture 6 | :members: 7 | :undoc-members: 8 | 9 | ObjectVersionChecker 10 | ~~~~~~~~~~~~~~~~~~~~ 11 | 12 | Fingerprints 13 | ------------ 14 | 15 | One function of the ObjectVersionChecker is to generate fingerprints of versioned objects. 16 | These fingerprints are a combination of the object's version and a hash of the 17 | RPC-critical attributes of the object: fields and remotable methods. 18 | 19 | The test_hashes() method is used to retrieve the expected and actual fingerprints 20 | of the objects. When using this method to assert the versions of objects in a 21 | local project, the expected fingerprints are the fingerprints of the previous 22 | state of the objects. These fingerprints are defined locally in the project and 23 | passed to test_hashes(). The actual fingerprints are the dynamically-generated 24 | fingerprints of the current state of the objects. If the expected and actual 25 | fingerprints do not match on an object, this means the RPC contract that was 26 | previously defined in the object is no longer the same. Because of this, the 27 | object's version must be updated. When the version is updated and the tests are 28 | run again, a new fingerprint for the object is generated. This fingerprint 29 | should be written over the previous version of the fingerprint. This shows the 30 | newly generated fingerprint is now the most recent state of the object. 31 | -------------------------------------------------------------------------------- /doc/source/reference/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | API Reference 3 | ============= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :glob: 8 | 9 | * 10 | -------------------------------------------------------------------------------- /doc/source/user/examples.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Examples 3 | ========== 4 | 5 | IOT lightbulb 6 | ============= 7 | 8 | .. note:: 9 | 10 | Full source located at :example:`iot_bulb`. 11 | 12 | .. literalinclude:: ../../../oslo_versionedobjects/examples/iot_bulb.py 13 | :language: python 14 | :linenos: 15 | :lines: 14- 16 | 17 | Expected (or similar) output:: 18 | 19 | The __str__() output of this new object: IOTLightbulb(manufactured_on=2017-03-15T23:25:01Z,serial='abc-123') 20 | The 'serial' field of the object: abc-123 21 | Primitive representation of this object: {'versioned_object.version': '1.0', 'versioned_object.changes': ['serial', 'manufactured_on'], 'versioned_object.name': 'IOTLightbulb', 'versioned_object.data': {'serial': u'abc-123', 'manufactured_on': '2017-03-15T23:25:01Z'}, 'versioned_object.namespace': 'versionedobjects.examples'} 22 | The __str__() output of this new (reconstructed) object: IOTLightbulb(manufactured_on=2017-03-15T23:25:01Z,serial='abc-123') 23 | After serial number change, the set of fields that have been mutated is: set(['serial']) 24 | -------------------------------------------------------------------------------- /doc/source/user/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../ChangeLog 2 | -------------------------------------------------------------------------------- /doc/source/user/index.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Using oslo.versionedobjects 3 | =========================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | usage 9 | examples 10 | 11 | .. history contains a lot of sections, toctree with maxdepth 1 is used. 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | history 16 | -------------------------------------------------------------------------------- /doc/source/user/usage.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Usage 3 | ======= 4 | 5 | Incorporating oslo.versionedobjects into your projects can be accomplished in 6 | the following steps: 7 | 8 | 1. `Add oslo.versionedobjects to requirements`_ 9 | 2. `Create objects subdirectory and a base.py inside it`_ 10 | 3. `Create base object with the project namespace`_ 11 | 4. `Create other base objects if needed`_ 12 | 5. `Implement objects and place them in objects/\*.py`_ 13 | 6. `Implement extra fields in objects/fields.py`_ 14 | 7. `Create object registry and register all objects`_ 15 | 8. `Create and attach the object serializer`_ 16 | 9. `Implement the indirection API`_ 17 | 18 | 19 | Add oslo.versionedobjects to requirements 20 | ----------------------------------------- 21 | 22 | To use oslo.versionedobjects in an OpenStack project remember to add it to the 23 | requirements.txt 24 | 25 | 26 | Create objects subdirectory and a base.py inside it 27 | --------------------------------------------------- 28 | 29 | Objects reside in the `/objects` directory and this is the place 30 | from which all objects should be imported. 31 | 32 | Start the implementation by creating `objects/base.py` with these main 33 | classes: 34 | 35 | 36 | Create base object with the project namespace 37 | --------------------------------------------- 38 | 39 | :class:`oslo_versionedobjects.base.VersionedObject` 40 | 41 | The VersionedObject base class for the project. You have to fill up the 42 | `OBJ_PROJECT_NAMESPACE` property. `OBJ_SERIAL_NAMESPACE` is used only for 43 | backward compatibility and should not be set in new projects. 44 | 45 | 46 | Create other base objects if needed 47 | ----------------------------------- 48 | 49 | class:`oslo_versionedobjects.base.VersionedPersistentObject` 50 | 51 | A mixin class for persistent objects can be created, defining repeated fields 52 | like `created_at`, `updated_at`. Fields are defined in the fields property 53 | (which is a dict). 54 | 55 | If objects were previously passed as dicts (a common situation), a 56 | :class:`oslo_versionedobjects.base.VersionedObjectDictCompat` can be used as a 57 | mixin class to support dict operations. 58 | 59 | Implement objects and place them in objects/\*.py 60 | ------------------------------------------------- 61 | 62 | Objects classes should be created for all resources/objects passed via RPC 63 | as IDs or dicts in order to: 64 | 65 | * spare the database (or other resource) from extra calls 66 | * pass objects instead of dicts, which are tagged with their version 67 | * handle all object versions in one place (the `obj_make_compatible` method) 68 | 69 | To make sure all objects are accessible at all times, you should import them 70 | in __init__.py in the objects/ directory. 71 | 72 | 73 | Implement extra fields in objects/fields.py 74 | ------------------------------------------- 75 | 76 | New field types can be implemented by inheriting from 77 | :class:`oslo_versionedobjects.field.Field` and overwriting the `from_primitive` 78 | and `to_primitive` methods. 79 | 80 | By subclassing :class:`oslo_versionedobjects.fields.AutoTypedField` you can 81 | stack multiple fields together, making sure even nested data structures are 82 | being validated. 83 | 84 | 85 | Create object registry and register all objects 86 | ----------------------------------------------- 87 | 88 | :class:`oslo_versionedobjects.base.VersionedObjectRegistry` 89 | 90 | The place where all objects are registered. All object classes should be 91 | registered by the :attr:`oslo_versionedobjects.base.ObjectRegistry.register` 92 | class decorator. 93 | 94 | 95 | 96 | Create and attach the object serializer 97 | --------------------------------------- 98 | 99 | :class:`oslo_versionedobjects.base.VersionedObjectSerializer` 100 | 101 | To transfer objects by RPC, subclass the 102 | :class:`oslo_versionedobjects.base.VersionedObjectSerializer` setting the 103 | OBJ_BASE_CLASS property to the previously defined Object class. 104 | 105 | Connect the serializer to oslo_messaging: 106 | 107 | .. code:: python 108 | 109 | serializer = RequestContextSerializer(objects_base.MagnumObjectSerializer()) 110 | target = messaging.Target(topic=topic, server=server) 111 | self._server = messaging.get_rpc_server(transport, target, handlers, serializer=serializer) 112 | 113 | 114 | Implement the indirection API 115 | ----------------------------- 116 | 117 | :class:`oslo_versionedobjects.base.VersionedObjectIndirectionAPI` 118 | 119 | oslo.versionedobjects supports `remotable` method calls. These are calls 120 | of the object methods and classmethods which can be executed locally or 121 | remotely depending on the configuration. Setting the indirection_api as a 122 | property of an object relays the calls to decorated methods through the 123 | defined RPC API. The attachment of the indirection_api should be handled 124 | by configuration at startup time. 125 | 126 | Second function of the indirection API is backporting. When the object 127 | serializer attempts to deserialize an object with a future version, not 128 | supported by the current instance, it calls the object_backport method in an 129 | attempt to backport the object to a version which can then be handled as 130 | normal. 131 | 132 | -------------------------------------------------------------------------------- /oslo_versionedobjects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/oslo.versionedobjects/5c2d02ef021d16da8e14799bde5c7a8e7c811756/oslo_versionedobjects/__init__.py -------------------------------------------------------------------------------- /oslo_versionedobjects/_i18n.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | """oslo.i18n integration module. 14 | 15 | See https://docs.openstack.org/oslo.i18n/latest/user/index.html 16 | 17 | """ 18 | 19 | import oslo_i18n 20 | 21 | 22 | _translators = oslo_i18n.TranslatorFactory(domain='oslo_versionedobjects') 23 | 24 | # The primary translation function using the well-known name "_" 25 | _ = _translators.primary 26 | -------------------------------------------------------------------------------- /oslo_versionedobjects/_options.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import copy 14 | 15 | from oslo_versionedobjects import exception 16 | 17 | 18 | def list_opts(): 19 | """Returns a list of oslo.config options available in the library. 20 | 21 | The returned list includes all oslo.config options which may be registered 22 | at runtime by the library. 23 | 24 | Each element of the list is a tuple. The first element is the name of the 25 | group under which the list of elements in the second element will be 26 | registered. A group name of None corresponds to the [DEFAULT] group in 27 | config files. 28 | 29 | The purpose of this is to allow tools like the Oslo sample config file 30 | generator to discover the options exposed to users by this library. 31 | 32 | :returns: a list of (group_name, opts) tuples 33 | """ 34 | return [('oslo_versionedobjects', copy.deepcopy(exception.exc_log_opts))] 35 | -------------------------------------------------------------------------------- /oslo_versionedobjects/_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010 United States Government as represented by the 2 | # Administrator of the National Aeronautics and Space Administration. 3 | # Copyright 2011 Justin Santa Barbara 4 | # All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | """Utilities and helper functions.""" 19 | 20 | # ISO 8601 extended time format without microseconds 21 | _ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' 22 | 23 | 24 | def isotime(at): 25 | """Stringify time in ISO 8601 format.""" 26 | st = at.strftime(_ISO8601_TIME_FORMAT) 27 | tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' 28 | # Need to handle either iso8601 or python UTC format 29 | st += ('Z' if tz in ['UTC', 'UTC+00:00'] else tz) 30 | return st 31 | -------------------------------------------------------------------------------- /oslo_versionedobjects/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/oslo.versionedobjects/5c2d02ef021d16da8e14799bde5c7a8e7c811756/oslo_versionedobjects/examples/__init__.py -------------------------------------------------------------------------------- /oslo_versionedobjects/examples/iot_bulb.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from datetime import datetime 14 | 15 | from oslo_versionedobjects import base 16 | from oslo_versionedobjects import fields as obj_fields 17 | 18 | # INTRO: This example shows how a object (a plain-old-python-object) with 19 | # some associated fields can be used, and some of its built-in methods can 20 | # be used to convert that object into a primitive and back again (as well 21 | # as determine simple changes on it. 22 | 23 | 24 | # Ensure that we always register our object with an object registry, 25 | # so that it can be deserialized from its primitive form. 26 | @base.VersionedObjectRegistry.register 27 | class IOTLightbulb(base.VersionedObject): 28 | """Simple light bulb class with some data about it.""" 29 | 30 | VERSION = '1.0' # Initial version 31 | 32 | #: Namespace these examples will use. 33 | OBJ_PROJECT_NAMESPACE = 'versionedobjects.examples' 34 | 35 | #: Required fields this object **must** declare. 36 | fields = { 37 | 'serial': obj_fields.StringField(), 38 | 'manufactured_on': obj_fields.DateTimeField(), 39 | } 40 | 41 | 42 | # Now do some basic operations on a light bulb. 43 | bulb = IOTLightbulb(serial='abc-123', manufactured_on=datetime.now()) 44 | print("The __str__() output of this new object: %s" % bulb) 45 | print("The 'serial' field of the object: %s" % bulb.serial) 46 | bulb_prim = bulb.obj_to_primitive() 47 | print("Primitive representation of this object: %s" % bulb_prim) 48 | 49 | # Now convert the primitive back to an object (isn't it easy!) 50 | bulb = IOTLightbulb.obj_from_primitive(bulb_prim) 51 | 52 | bulb.obj_reset_changes() 53 | print("The __str__() output of this new (reconstructed)" 54 | " object: %s" % bulb) 55 | 56 | # Mutating a field and showing what changed. 57 | bulb.serial = 'abc-124' 58 | print("After serial number change, the set of fields that" 59 | " have been mutated is: %s" % bulb.obj_what_changed()) 60 | -------------------------------------------------------------------------------- /oslo_versionedobjects/exception.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010 United States Government as represented by the 2 | # Administrator of the National Aeronautics and Space Administration. 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """VersionedObjects base exception handling. 18 | 19 | Includes decorator for re-raising VersionedObjects-type exceptions. 20 | 21 | SHOULD include dedicated exception logging. 22 | 23 | """ 24 | 25 | import functools 26 | import inspect 27 | import logging 28 | 29 | from oslo_config import cfg 30 | from oslo_utils import excutils 31 | import webob.exc 32 | 33 | from oslo_versionedobjects._i18n import _ 34 | 35 | LOG = logging.getLogger(__name__) 36 | 37 | exc_log_opts = [ 38 | cfg.BoolOpt('fatal_exception_format_errors', 39 | default=False, 40 | help='Make exception message format errors fatal'), 41 | ] 42 | 43 | CONF = cfg.CONF 44 | CONF.register_opts(exc_log_opts, group='oslo_versionedobjects') 45 | 46 | 47 | class ConvertedException(webob.exc.WSGIHTTPException): 48 | def __init__(self, code=0, title="", explanation=""): 49 | self.code = code 50 | self.title = title 51 | self.explanation = explanation 52 | super().__init__() 53 | 54 | 55 | def _cleanse_dict(original): 56 | """Strip all admin_password, new_pass, rescue_pass keys from a dict.""" 57 | return {k: v for k, v in original.items() if "_pass" not in k} 58 | 59 | 60 | def wrap_exception(notifier=None, get_notifier=None): 61 | """Catch all exceptions in wrapped method 62 | 63 | This decorator wraps a method to catch any exceptions that may 64 | get thrown. It also optionally sends the exception to the notification 65 | system. 66 | """ 67 | def inner(f): 68 | def wrapped(self, context, *args, **kw): 69 | # Don't store self or context in the payload, it now seems to 70 | # contain confidential information. 71 | try: 72 | return f(self, context, *args, **kw) 73 | except Exception as e: 74 | with excutils.save_and_reraise_exception(): 75 | if notifier or get_notifier: 76 | payload = dict(exception=e) 77 | call_dict = inspect.getcallargs(f, self, context, 78 | *args, **kw) 79 | cleansed = _cleanse_dict(call_dict) 80 | payload.update({'args': cleansed}) 81 | 82 | # If f has multiple decorators, they must use 83 | # functools.wraps to ensure the name is 84 | # propagated. 85 | event_type = f.__name__ 86 | 87 | (notifier or get_notifier()).error(context, 88 | event_type, 89 | payload) 90 | 91 | return functools.wraps(f)(wrapped) 92 | return inner 93 | 94 | 95 | class VersionedObjectsException(Exception): 96 | """Base VersionedObjects Exception 97 | 98 | To correctly use this class, inherit from it and define 99 | a 'msg_fmt' property. That msg_fmt will get printf'd 100 | with the keyword arguments provided to the constructor. 101 | 102 | """ 103 | msg_fmt = _("An unknown exception occurred.") 104 | code = 500 105 | headers = {} 106 | safe = False 107 | 108 | def __init__(self, message=None, **kwargs): 109 | self.kwargs = kwargs 110 | 111 | if 'code' not in self.kwargs: 112 | try: 113 | self.kwargs['code'] = self.code 114 | except AttributeError: 115 | pass 116 | 117 | if not message: 118 | try: 119 | message = self.msg_fmt % kwargs 120 | except Exception: 121 | # kwargs doesn't match a variable in the message 122 | # log the issue and the kwargs 123 | LOG.exception('Exception in string format operation') 124 | for name, value in kwargs.items(): 125 | LOG.error("{}: {}".format(name, value)) # noqa 126 | 127 | if CONF.oslo_versionedobjects.fatal_exception_format_errors: 128 | raise 129 | else: 130 | # at least get the core message out if something happened 131 | message = self.msg_fmt 132 | 133 | super().__init__(message) 134 | 135 | def format_message(self): 136 | # NOTE(mrodden): use the first argument to the python Exception object 137 | # which should be our full VersionedObjectsException message, 138 | # (see __init__) 139 | return self.args[0] 140 | 141 | 142 | class ObjectActionError(VersionedObjectsException): 143 | msg_fmt = _('Object action %(action)s failed because: %(reason)s') 144 | 145 | 146 | class ObjectFieldInvalid(VersionedObjectsException): 147 | msg_fmt = _('Field %(field)s of %(objname)s is not an instance of Field') 148 | 149 | 150 | class OrphanedObjectError(VersionedObjectsException): 151 | msg_fmt = _('Cannot call %(method)s on orphaned %(objtype)s object') 152 | 153 | 154 | class IncompatibleObjectVersion(VersionedObjectsException): 155 | msg_fmt = _('Version %(objver)s of %(objname)s is not supported, ' 156 | 'supported version is %(supported)s') 157 | 158 | 159 | class ReadOnlyFieldError(VersionedObjectsException): 160 | msg_fmt = _('Cannot modify readonly field %(field)s') 161 | 162 | 163 | class UnsupportedObjectError(VersionedObjectsException): 164 | msg_fmt = _('Unsupported object type %(objtype)s') 165 | 166 | 167 | class EnumRequiresValidValuesError(VersionedObjectsException): 168 | msg_fmt = _('Enum fields require a list of valid_values') 169 | 170 | 171 | class EnumValidValuesInvalidError(VersionedObjectsException): 172 | msg_fmt = _('Enum valid values are not valid') 173 | 174 | 175 | class EnumFieldInvalid(VersionedObjectsException): 176 | msg_fmt = _('%(typename)s in %(fieldname)s is not an instance of Enum') 177 | 178 | 179 | class EnumFieldUnset(VersionedObjectsException): 180 | msg_fmt = _('%(fieldname)s missing field type') 181 | 182 | 183 | class InvalidTargetVersion(VersionedObjectsException): 184 | msg_fmt = _('Invalid target version %(version)s') 185 | 186 | 187 | class TargetBeforeSubobjectExistedException(VersionedObjectsException): 188 | msg_fmt = _("No subobject existed at version %(target_version)s") 189 | 190 | 191 | class UnregisteredSubobject(VersionedObjectsException): 192 | msg_fmt = _("%(child_objname)s is referenced by %(parent_objname)s but " 193 | "is not registered") 194 | -------------------------------------------------------------------------------- /oslo_versionedobjects/fields.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Red Hat, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import abc 16 | from collections import abc as collections_abc 17 | import datetime 18 | import re 19 | import uuid 20 | import warnings 21 | 22 | import copy 23 | import netaddr 24 | from oslo_utils import strutils 25 | from oslo_utils import timeutils 26 | from oslo_utils import versionutils 27 | 28 | from oslo_versionedobjects._i18n import _ 29 | from oslo_versionedobjects import _utils 30 | from oslo_versionedobjects import exception 31 | 32 | 33 | class KeyTypeError(TypeError): 34 | def __init__(self, expected, value): 35 | super().__init__( 36 | _('Key %(key)s must be of type %(expected)s not %(actual)s' 37 | ) % {'key': repr(value), 38 | 'expected': expected.__name__, 39 | 'actual': value.__class__.__name__, 40 | }) 41 | 42 | 43 | class ElementTypeError(TypeError): 44 | def __init__(self, expected, key, value): 45 | super().__init__( 46 | _('Element %(key)s:%(val)s must be of type %(expected)s' 47 | ' not %(actual)s' 48 | ) % {'key': key, 49 | 'val': repr(value), 50 | 'expected': expected, 51 | 'actual': value.__class__.__name__, 52 | }) 53 | 54 | 55 | class AbstractFieldType(metaclass=abc.ABCMeta): 56 | @abc.abstractmethod 57 | def coerce(self, obj, attr, value): 58 | """This is called to coerce (if possible) a value on assignment. 59 | 60 | This method should convert the value given into the designated type, 61 | or throw an exception if this is not possible. 62 | 63 | :param:obj: The VersionedObject on which an attribute is being set 64 | :param:attr: The name of the attribute being set 65 | :param:value: The value being set 66 | :returns: A properly-typed value 67 | """ 68 | pass 69 | 70 | @abc.abstractmethod 71 | def from_primitive(self, obj, attr, value): 72 | """This is called to deserialize a value. 73 | 74 | This method should deserialize a value from the form given by 75 | to_primitive() to the designated type. 76 | 77 | :param:obj: The VersionedObject on which the value is to be set 78 | :param:attr: The name of the attribute which will hold the value 79 | :param:value: The serialized form of the value 80 | :returns: The natural form of the value 81 | """ 82 | pass 83 | 84 | @abc.abstractmethod 85 | def to_primitive(self, obj, attr, value): 86 | """This is called to serialize a value. 87 | 88 | This method should serialize a value to the form expected by 89 | from_primitive(). 90 | 91 | :param:obj: The VersionedObject on which the value is set 92 | :param:attr: The name of the attribute holding the value 93 | :param:value: The natural form of the value 94 | :returns: The serialized form of the value 95 | """ 96 | pass 97 | 98 | @abc.abstractmethod 99 | def describe(self): 100 | """Returns a string describing the type of the field.""" 101 | pass 102 | 103 | @abc.abstractmethod 104 | def stringify(self, value): 105 | """Returns a short stringified version of a value.""" 106 | pass 107 | 108 | 109 | class FieldType(AbstractFieldType): 110 | @staticmethod 111 | def coerce(obj, attr, value): 112 | return value 113 | 114 | @staticmethod 115 | def from_primitive(obj, attr, value): 116 | return value 117 | 118 | @staticmethod 119 | def to_primitive(obj, attr, value): 120 | return value 121 | 122 | def describe(self): 123 | return self.__class__.__name__ 124 | 125 | def stringify(self, value): 126 | return str(value) 127 | 128 | def get_schema(self): 129 | raise NotImplementedError() 130 | 131 | 132 | class UnspecifiedDefault: 133 | pass 134 | 135 | 136 | class Field: 137 | def __init__(self, field_type, nullable=False, 138 | default=UnspecifiedDefault, read_only=False): 139 | self._type = field_type 140 | self._nullable = nullable 141 | self._default = default 142 | self._read_only = read_only 143 | 144 | def __repr__(self): 145 | if isinstance(self._default, set): 146 | # TODO(stephenfin): Drop this when we switch from 147 | # 'inspect.getargspec' to 'inspect.getfullargspec', since our 148 | # hashes will have to change anyway 149 | # make a py27 and py35 compatible representation. See bug 1771804 150 | default = 'set([%s])' % ','.join( 151 | sorted([str(v) for v in self._default]) 152 | ) 153 | else: 154 | default = str(self._default) 155 | return '{}(default={},nullable={})'.format( 156 | self._type.__class__.__name__, default, self._nullable) 157 | 158 | @property 159 | def nullable(self): 160 | return self._nullable 161 | 162 | @property 163 | def default(self): 164 | return self._default 165 | 166 | @property 167 | def read_only(self): 168 | return self._read_only 169 | 170 | def _null(self, obj, attr): 171 | if self.nullable: 172 | return None 173 | elif self._default != UnspecifiedDefault: 174 | # NOTE(danms): We coerce the default value each time the field 175 | # is set to None as our contract states that we'll let the type 176 | # examine the object and attribute name at that time. 177 | return self._type.coerce(obj, attr, copy.deepcopy(self._default)) 178 | else: 179 | raise ValueError(_("Field `%s' cannot be None") % attr) 180 | 181 | def coerce(self, obj, attr, value): 182 | """Coerce a value to a suitable type. 183 | 184 | This is called any time you set a value on an object, like: 185 | 186 | foo.myint = 1 187 | 188 | and is responsible for making sure that the value (1 here) is of 189 | the proper type, or can be sanely converted. 190 | 191 | This also handles the potentially nullable or defaultable 192 | nature of the field and calls the coerce() method on a 193 | FieldType to actually do the coercion. 194 | 195 | :param:obj: The object being acted upon 196 | :param:attr: The name of the attribute/field being set 197 | :param:value: The value being set 198 | :returns: The properly-typed value 199 | """ 200 | if value is None: 201 | return self._null(obj, attr) 202 | else: 203 | return self._type.coerce(obj, attr, value) 204 | 205 | def from_primitive(self, obj, attr, value): 206 | """Deserialize a value from primitive form. 207 | 208 | This is responsible for deserializing a value from primitive 209 | into regular form. It calls the from_primitive() method on a 210 | FieldType to do the actual deserialization. 211 | 212 | :param:obj: The object being acted upon 213 | :param:attr: The name of the attribute/field being deserialized 214 | :param:value: The value to be deserialized 215 | :returns: The deserialized value 216 | """ 217 | if value is None: 218 | return None 219 | else: 220 | return self._type.from_primitive(obj, attr, value) 221 | 222 | def to_primitive(self, obj, attr, value): 223 | """Serialize a value to primitive form. 224 | 225 | This is responsible for serializing a value to primitive 226 | form. It calls to_primitive() on a FieldType to do the actual 227 | serialization. 228 | 229 | :param:obj: The object being acted upon 230 | :param:attr: The name of the attribute/field being serialized 231 | :param:value: The value to be serialized 232 | :returns: The serialized value 233 | """ 234 | if value is None: 235 | return None 236 | else: 237 | return self._type.to_primitive(obj, attr, value) 238 | 239 | def describe(self): 240 | """Return a short string describing the type of this field.""" 241 | name = self._type.describe() 242 | prefix = self.nullable and 'Nullable' or '' 243 | return prefix + name 244 | 245 | def stringify(self, value): 246 | if value is None: 247 | return 'None' 248 | else: 249 | return self._type.stringify(value) 250 | 251 | def get_schema(self): 252 | schema = self._type.get_schema() 253 | schema.update({'readonly': self.read_only}) 254 | if self.nullable: 255 | schema['type'].append('null') 256 | default = self.default 257 | if default != UnspecifiedDefault: 258 | schema.update({'default': default}) 259 | return schema 260 | 261 | 262 | class String(FieldType): 263 | @staticmethod 264 | def coerce(obj, attr, value): 265 | # FIXME(danms): We should really try to avoid the need to do this 266 | accepted_types = (int, float, str, datetime.datetime) 267 | if isinstance(value, accepted_types): 268 | return str(value) 269 | 270 | raise ValueError(_('A string is required in field %(attr)s, ' 271 | 'not a %(type)s') % 272 | {'attr': attr, 'type': type(value).__name__}) 273 | 274 | @staticmethod 275 | def stringify(value): 276 | return '\'%s\'' % value 277 | 278 | def get_schema(self): 279 | return {'type': ['string']} 280 | 281 | 282 | class SensitiveString(String): 283 | """A string field type that may contain sensitive (password) information. 284 | 285 | Passwords in the string value are masked when stringified. 286 | """ 287 | def stringify(self, value): 288 | return super().stringify( 289 | strutils.mask_password(value)) 290 | 291 | 292 | class VersionPredicate(String): 293 | @staticmethod 294 | def coerce(obj, attr, value): 295 | if not isinstance(value, str): 296 | raise ValueError(_('Version %(val)s should be a string type, not ' 297 | '%(real_type)s') % 298 | {'val': value, 'real_type': type(value)}) 299 | try: 300 | versionutils.VersionPredicate(value) 301 | except ValueError: 302 | raise ValueError(_('Version %(val)s is not a valid predicate in ' 303 | 'field %(attr)s') % 304 | {'val': value, 'attr': attr}) 305 | return value 306 | 307 | 308 | class Enum(String): 309 | def __init__(self, valid_values, **kwargs): 310 | if not valid_values: 311 | raise exception.EnumRequiresValidValuesError() 312 | try: 313 | # Test validity of the values 314 | for value in valid_values: 315 | super().coerce(None, 'init', value) 316 | except (TypeError, ValueError): 317 | raise exception.EnumValidValuesInvalidError() 318 | self._valid_values = valid_values 319 | super().__init__(**kwargs) 320 | 321 | @property 322 | def valid_values(self): 323 | return copy.copy(self._valid_values) 324 | 325 | def coerce(self, obj, attr, value): 326 | if value not in self._valid_values: 327 | msg = _("Field value %s is invalid") % value 328 | raise ValueError(msg) 329 | return super().coerce(obj, attr, value) 330 | 331 | def stringify(self, value): 332 | if value not in self._valid_values: 333 | msg = _("Field value %s is invalid") % value 334 | raise ValueError(msg) 335 | return super().stringify(value) 336 | 337 | def get_schema(self): 338 | schema = super().get_schema() 339 | schema['enum'] = self._valid_values 340 | return schema 341 | 342 | 343 | class StringPattern(FieldType): 344 | def get_schema(self): 345 | if hasattr(self, "PATTERN"): 346 | return {'type': ['string'], 'pattern': self.PATTERN} 347 | else: 348 | msg = _("%s has no pattern") % self.__class__.__name__ 349 | raise AttributeError(msg) 350 | 351 | 352 | class UUID(StringPattern): 353 | 354 | PATTERN = (r'^[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]' 355 | r'{4}-?[a-fA-F0-9]{12}$') 356 | 357 | @staticmethod 358 | def coerce(obj, attr, value): 359 | # FIXME(danms): We should actually verify the UUIDness here 360 | with warnings.catch_warnings(): 361 | # Change the warning action only if no other filter exists 362 | # for this warning to allow the client to define other action 363 | # like 'error' for this warning. 364 | warnings.filterwarnings(action="once", append=True) 365 | try: 366 | uuid.UUID("%s" % value) 367 | except Exception: 368 | # This is to ensure no breaking behaviour for current 369 | # users 370 | warnings.warn("%s is an invalid UUID. Using UUIDFields " 371 | "with invalid UUIDs is no longer " 372 | "supported, and will be removed in a future " 373 | "release. Please update your " 374 | "code to input valid UUIDs or accept " 375 | "ValueErrors for invalid UUIDs. See " 376 | "https://docs.openstack.org/oslo.versionedobjects/latest/reference/fields.html#oslo_versionedobjects.fields.UUIDField " # noqa 377 | "for further details" % 378 | repr(value).encode('utf8'), 379 | FutureWarning) 380 | 381 | return "%s" % value 382 | 383 | 384 | class MACAddress(StringPattern): 385 | 386 | PATTERN = r'^[0-9a-f]{2}(:[0-9a-f]{2}){5}$' 387 | _REGEX = re.compile(PATTERN) 388 | 389 | @staticmethod 390 | def coerce(obj, attr, value): 391 | if isinstance(value, str): 392 | lowered = value.lower().replace('-', ':') 393 | if MACAddress._REGEX.match(lowered): 394 | return lowered 395 | raise ValueError(_("Malformed MAC %s") % (value,)) 396 | 397 | 398 | class PCIAddress(StringPattern): 399 | 400 | PATTERN = r'^[0-9a-f]{4}:[0-9a-f]{2}:[0-1][0-9a-f].[0-7]$' 401 | _REGEX = re.compile(PATTERN) 402 | 403 | @staticmethod 404 | def coerce(obj, attr, value): 405 | if isinstance(value, str): 406 | newvalue = value.lower() 407 | if PCIAddress._REGEX.match(newvalue): 408 | return newvalue 409 | raise ValueError(_("Malformed PCI address %s") % (value,)) 410 | 411 | 412 | class Integer(FieldType): 413 | @staticmethod 414 | def coerce(obj, attr, value): 415 | return int(value) 416 | 417 | def get_schema(self): 418 | return {'type': ['integer']} 419 | 420 | 421 | class NonNegativeInteger(FieldType): 422 | @staticmethod 423 | def coerce(obj, attr, value): 424 | v = int(value) 425 | if v < 0: 426 | raise ValueError(_('Value must be >= 0 for field %s') % attr) 427 | return v 428 | 429 | def get_schema(self): 430 | return {'type': ['integer'], 'minimum': 0} 431 | 432 | 433 | class Float(FieldType): 434 | def coerce(self, obj, attr, value): 435 | return float(value) 436 | 437 | def get_schema(self): 438 | return {'type': ['number']} 439 | 440 | 441 | class NonNegativeFloat(FieldType): 442 | @staticmethod 443 | def coerce(obj, attr, value): 444 | v = float(value) 445 | if v < 0: 446 | raise ValueError(_('Value must be >= 0 for field %s') % attr) 447 | return v 448 | 449 | def get_schema(self): 450 | return {'type': ['number'], 'minimum': 0} 451 | 452 | 453 | class Boolean(FieldType): 454 | @staticmethod 455 | def coerce(obj, attr, value): 456 | return bool(value) 457 | 458 | def get_schema(self): 459 | return {'type': ['boolean']} 460 | 461 | 462 | class FlexibleBoolean(Boolean): 463 | @staticmethod 464 | def coerce(obj, attr, value): 465 | return strutils.bool_from_string(value) 466 | 467 | 468 | class DateTime(FieldType): 469 | def __init__(self, tzinfo_aware=True, *args, **kwargs): 470 | self.tzinfo_aware = tzinfo_aware 471 | super().__init__(*args, **kwargs) 472 | 473 | def coerce(self, obj, attr, value): 474 | if isinstance(value, str): 475 | # NOTE(danms): Being tolerant of isotime strings here will help us 476 | # during our objects transition 477 | value = timeutils.parse_isotime(value) 478 | elif not isinstance(value, datetime.datetime): 479 | raise ValueError(_('A datetime.datetime is required ' 480 | 'in field %(attr)s, not a %(type)s') % 481 | {'attr': attr, 'type': type(value).__name__}) 482 | 483 | if value.utcoffset() is None and self.tzinfo_aware: 484 | # NOTE(danms): Legacy objects from sqlalchemy are stored in UTC, 485 | # but are returned without a timezone attached. 486 | # As a transitional aid, assume a tz-naive object is in UTC. 487 | value = value.replace(tzinfo=datetime.timezone.utc) 488 | elif not self.tzinfo_aware: 489 | value = value.replace(tzinfo=None) 490 | return value 491 | 492 | def from_primitive(self, obj, attr, value): 493 | return self.coerce(obj, attr, timeutils.parse_isotime(value)) 494 | 495 | def get_schema(self): 496 | return {'type': ['string'], 'format': 'date-time'} 497 | 498 | @staticmethod 499 | def to_primitive(obj, attr, value): 500 | return _utils.isotime(value) 501 | 502 | @staticmethod 503 | def stringify(value): 504 | return _utils.isotime(value) 505 | 506 | 507 | class IPAddress(StringPattern): 508 | @staticmethod 509 | def coerce(obj, attr, value): 510 | try: 511 | return netaddr.IPAddress(value) 512 | except netaddr.AddrFormatError as e: 513 | raise ValueError(str(e)) 514 | 515 | def from_primitive(self, obj, attr, value): 516 | return self.coerce(obj, attr, value) 517 | 518 | @staticmethod 519 | def to_primitive(obj, attr, value): 520 | return str(value) 521 | 522 | 523 | class IPV4Address(IPAddress): 524 | @staticmethod 525 | def coerce(obj, attr, value): 526 | result = IPAddress.coerce(obj, attr, value) 527 | if result.version != 4: 528 | raise ValueError(_('Network "%(val)s" is not valid ' 529 | 'in field %(attr)s') % 530 | {'val': value, 'attr': attr}) 531 | return result 532 | 533 | def get_schema(self): 534 | return {'type': ['string'], 'format': 'ipv4'} 535 | 536 | 537 | class IPV6Address(IPAddress): 538 | @staticmethod 539 | def coerce(obj, attr, value): 540 | result = IPAddress.coerce(obj, attr, value) 541 | if result.version != 6: 542 | raise ValueError(_('Network "%(val)s" is not valid ' 543 | 'in field %(attr)s') % 544 | {'val': value, 'attr': attr}) 545 | return result 546 | 547 | def get_schema(self): 548 | return {'type': ['string'], 'format': 'ipv6'} 549 | 550 | 551 | class IPV4AndV6Address(IPAddress): 552 | @staticmethod 553 | def coerce(obj, attr, value): 554 | result = IPAddress.coerce(obj, attr, value) 555 | if result.version != 4 and result.version != 6: 556 | raise ValueError(_('Network "%(val)s" is not valid ' 557 | 'in field %(attr)s') % 558 | {'val': value, 'attr': attr}) 559 | return result 560 | 561 | def get_schema(self): 562 | return {'oneOf': [IPV4Address().get_schema(), 563 | IPV6Address().get_schema()]} 564 | 565 | 566 | class IPNetwork(IPAddress): 567 | @staticmethod 568 | def coerce(obj, attr, value): 569 | try: 570 | return netaddr.IPNetwork(value) 571 | except netaddr.AddrFormatError as e: 572 | raise ValueError(str(e)) 573 | 574 | 575 | class IPV4Network(IPNetwork): 576 | 577 | PATTERN = (r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-' 578 | r'9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][' 579 | r'0-9]|3[0-2]))$') 580 | 581 | @staticmethod 582 | def coerce(obj, attr, value): 583 | try: 584 | return netaddr.IPNetwork(value, version=4) 585 | except netaddr.AddrFormatError as e: 586 | raise ValueError(str(e)) 587 | 588 | 589 | class IPV6Network(IPNetwork): 590 | 591 | def __init__(self, *args, **kwargs): 592 | super().__init__(*args, **kwargs) 593 | self.PATTERN = self._create_pattern() 594 | 595 | @staticmethod 596 | def coerce(obj, attr, value): 597 | try: 598 | return netaddr.IPNetwork(value, version=6) 599 | except netaddr.AddrFormatError as e: 600 | raise ValueError(str(e)) 601 | 602 | def _create_pattern(self): 603 | ipv6seg = '[0-9a-fA-F]{1,4}' 604 | ipv4seg = '(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])' 605 | 606 | return ( 607 | # Pattern based on answer to 608 | # http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses 609 | '^' 610 | # 1:2:3:4:5:6:7:8 611 | '(' + ipv6seg + ':){7,7}' + ipv6seg + '|' 612 | # 1:: 1:2:3:4:5:6:7:: 613 | '(' + ipv6seg + ':){1,7}:|' 614 | # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 615 | '(' + ipv6seg + ':){1,6}:' + ipv6seg + '|' 616 | # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 617 | '(' + ipv6seg + ':){1,5}(:' + ipv6seg + '){1,2}|' 618 | # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 619 | '(' + ipv6seg + ':){1,4}(:' + ipv6seg + '){1,3}|' 620 | # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 621 | '(' + ipv6seg + ':){1,3}(:' + ipv6seg + '){1,4}|' 622 | # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 623 | '(' + ipv6seg + ':){1,2}(:' + ipv6seg + '){1,5}|' + 624 | # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 625 | ipv6seg + ':((:' + ipv6seg + '){1,6})|' 626 | # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: 627 | ':((:' + ipv6seg + '){1,7}|:)|' 628 | # fe80::7:8%eth0 fe80::7:8%1 629 | 'fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|' 630 | # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 631 | '::(ffff(:0{1,4}){0,1}:){0,1}' 632 | '(' + ipv4seg + r'\.){3,3}' + 633 | ipv4seg + '|' 634 | # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 635 | '(' + ipv6seg + ':){1,4}:' 636 | '(' + ipv4seg + r'\.){3,3}' + 637 | ipv4seg + 638 | # /128 639 | r'(\/(d|dd|1[0-1]d|12[0-8]))$' 640 | ) 641 | 642 | 643 | class CompoundFieldType(FieldType): 644 | def __init__(self, element_type, **field_args): 645 | self._element_type = Field(element_type, **field_args) 646 | 647 | 648 | class List(CompoundFieldType): 649 | def coerce(self, obj, attr, value): 650 | 651 | if (not isinstance(value, collections_abc.Iterable) or 652 | isinstance(value, (str, collections_abc.Mapping))): 653 | raise ValueError(_('A list is required in field %(attr)s, ' 654 | 'not a %(type)s') % 655 | {'attr': attr, 'type': type(value).__name__}) 656 | coerced_list = CoercedList() 657 | coerced_list.enable_coercing(self._element_type, obj, attr) 658 | coerced_list.extend(value) 659 | return coerced_list 660 | 661 | def to_primitive(self, obj, attr, value): 662 | return [self._element_type.to_primitive(obj, attr, x) for x in value] 663 | 664 | def from_primitive(self, obj, attr, value): 665 | return [self._element_type.from_primitive(obj, attr, x) for x in value] 666 | 667 | def stringify(self, value): 668 | return '[%s]' % ( 669 | ','.join([self._element_type.stringify(x) for x in value])) 670 | 671 | def get_schema(self): 672 | return {'type': ['array'], 'items': self._element_type.get_schema()} 673 | 674 | 675 | class Dict(CompoundFieldType): 676 | def coerce(self, obj, attr, value): 677 | if not isinstance(value, dict): 678 | raise ValueError(_('A dict is required in field %(attr)s, ' 679 | 'not a %(type)s') % 680 | {'attr': attr, 'type': type(value).__name__}) 681 | coerced_dict = CoercedDict() 682 | coerced_dict.enable_coercing(self._element_type, obj, attr) 683 | coerced_dict.update(value) 684 | return coerced_dict 685 | 686 | def to_primitive(self, obj, attr, value): 687 | primitive = {} 688 | for key, element in value.items(): 689 | primitive[key] = self._element_type.to_primitive( 690 | obj, '{}["{}"]'.format(attr, key), element) 691 | return primitive 692 | 693 | def from_primitive(self, obj, attr, value): 694 | concrete = {} 695 | for key, element in value.items(): 696 | concrete[key] = self._element_type.from_primitive( 697 | obj, '{}["{}"]'.format(attr, key), element) 698 | return concrete 699 | 700 | def stringify(self, value): 701 | return '{%s}' % ( 702 | ','.join(['{}={}'.format(key, self._element_type.stringify(val)) 703 | for key, val in sorted(value.items())])) 704 | 705 | def get_schema(self): 706 | return {'type': ['object'], 707 | 'additionalProperties': self._element_type.get_schema()} 708 | 709 | 710 | class DictProxyField: 711 | """Descriptor allowing us to assign pinning data as a dict of key_types 712 | 713 | This allows us to have an object field that will be a dict of key_type 714 | keys, allowing that will convert back to string-keyed dict. 715 | 716 | This will take care of the conversion while the dict field will make sure 717 | that we store the raw json-serializable data on the object. 718 | 719 | key_type should return a type that unambiguously responds to str 720 | so that calling key_type on it yields the same thing. 721 | """ 722 | def __init__(self, dict_field_name, key_type=int): 723 | self._fld_name = dict_field_name 724 | self._key_type = key_type 725 | 726 | def __get__(self, obj, obj_type): 727 | if obj is None: 728 | return self 729 | if getattr(obj, self._fld_name) is None: 730 | return 731 | return {self._key_type(k): v 732 | for k, v in getattr(obj, self._fld_name).items()} 733 | 734 | def __set__(self, obj, val): 735 | if val is None: 736 | setattr(obj, self._fld_name, val) 737 | else: 738 | setattr(obj, self._fld_name, {str(k): v for k, v in val.items()}) 739 | 740 | 741 | class Set(CompoundFieldType): 742 | def coerce(self, obj, attr, value): 743 | if not isinstance(value, set): 744 | raise ValueError(_('A set is required in field %(attr)s, ' 745 | 'not a %(type)s') % 746 | {'attr': attr, 'type': type(value).__name__}) 747 | coerced_set = CoercedSet() 748 | coerced_set.enable_coercing(self._element_type, obj, attr) 749 | coerced_set.update(value) 750 | return coerced_set 751 | 752 | def to_primitive(self, obj, attr, value): 753 | return tuple( 754 | self._element_type.to_primitive(obj, attr, x) for x in value) 755 | 756 | def from_primitive(self, obj, attr, value): 757 | return {self._element_type.from_primitive(obj, attr, x) 758 | for x in value} 759 | 760 | def stringify(self, value): 761 | return 'set([%s])' % ( 762 | ','.join([self._element_type.stringify(x) for x in value])) 763 | 764 | def get_schema(self): 765 | return {'type': ['array'], 'uniqueItems': True, 766 | 'items': self._element_type.get_schema()} 767 | 768 | 769 | class Object(FieldType): 770 | def __init__(self, obj_name, subclasses=False, **kwargs): 771 | self._obj_name = obj_name 772 | self._subclasses = subclasses 773 | super().__init__(**kwargs) 774 | 775 | @staticmethod 776 | def _get_all_obj_names(obj): 777 | obj_names = [] 778 | for parent in obj.__class__.mro(): 779 | # Skip mix-ins which are not versioned object subclasses 780 | if not hasattr(parent, "obj_name"): 781 | continue 782 | obj_names.append(parent.obj_name()) 783 | return obj_names 784 | 785 | def coerce(self, obj, attr, value): 786 | try: 787 | obj_name = value.obj_name() 788 | except AttributeError: 789 | obj_name = "" 790 | 791 | if self._subclasses: 792 | obj_names = self._get_all_obj_names(value) 793 | else: 794 | obj_names = [obj_name] 795 | 796 | if self._obj_name not in obj_names: 797 | if not obj_name: 798 | # If we're not dealing with an object, it's probably a 799 | # primitive so get it's type for the message below. 800 | obj_name = type(value).__name__ 801 | obj_mod = '' 802 | if hasattr(obj, '__module__'): 803 | obj_mod = ''.join([obj.__module__, '.']) 804 | val_mod = '' 805 | if hasattr(value, '__module__'): 806 | val_mod = ''.join([value.__module__, '.']) 807 | raise ValueError(_('An object of type %(type)s is required ' 808 | 'in field %(attr)s, not a %(valtype)s') % 809 | {'type': ''.join([obj_mod, self._obj_name]), 810 | 'attr': attr, 'valtype': ''.join([val_mod, 811 | obj_name])}) 812 | return value 813 | 814 | @staticmethod 815 | def to_primitive(obj, attr, value): 816 | return value.obj_to_primitive() 817 | 818 | @staticmethod 819 | def from_primitive(obj, attr, value): 820 | # FIXME(danms): Avoid circular import from base.py 821 | from oslo_versionedobjects import base as obj_base 822 | # NOTE (ndipanov): If they already got hydrated by the serializer, just 823 | # pass them back unchanged 824 | if isinstance(value, obj_base.VersionedObject): 825 | return value 826 | return obj.obj_from_primitive(value, obj._context) 827 | 828 | def describe(self): 829 | return "Object<%s>" % self._obj_name 830 | 831 | def stringify(self, value): 832 | if 'uuid' in value.fields: 833 | ident = '(%s)' % (value.obj_attr_is_set('uuid') and value.uuid or 834 | 'UNKNOWN') 835 | elif 'id' in value.fields: 836 | ident = '(%s)' % (value.obj_attr_is_set('id') and value.id or 837 | 'UNKNOWN') 838 | else: 839 | ident = '' 840 | 841 | return '{}{}'.format(value.obj_name(), ident) 842 | 843 | def get_schema(self): 844 | from oslo_versionedobjects import base as obj_base 845 | obj_classes = obj_base.VersionedObjectRegistry.obj_classes() 846 | if self._obj_name in obj_classes: 847 | cls = obj_classes[self._obj_name][0] 848 | namespace_key = cls._obj_primitive_key('namespace') 849 | name_key = cls._obj_primitive_key('name') 850 | version_key = cls._obj_primitive_key('version') 851 | data_key = cls._obj_primitive_key('data') 852 | changes_key = cls._obj_primitive_key('changes') 853 | field_schemas = {key: field.get_schema() 854 | for key, field in cls.fields.items()} 855 | required_fields = [key for key, field in sorted(cls.fields.items()) 856 | if not field.nullable] 857 | schema = { 858 | 'type': ['object'], 859 | 'properties': { 860 | namespace_key: { 861 | 'type': 'string' 862 | }, 863 | name_key: { 864 | 'type': 'string' 865 | }, 866 | version_key: { 867 | 'type': 'string' 868 | }, 869 | changes_key: { 870 | 'type': 'array', 871 | 'items': { 872 | 'type': 'string' 873 | } 874 | }, 875 | data_key: { 876 | 'type': 'object', 877 | 'description': 'fields of %s' % self._obj_name, 878 | 'properties': field_schemas, 879 | }, 880 | }, 881 | 'required': [namespace_key, name_key, version_key, data_key] 882 | } 883 | 884 | if required_fields: 885 | schema['properties'][data_key]['required'] = required_fields 886 | 887 | return schema 888 | else: 889 | raise exception.UnsupportedObjectError(objtype=self._obj_name) 890 | 891 | 892 | class AutoTypedField(Field): 893 | AUTO_TYPE = None 894 | 895 | def __init__(self, **kwargs): 896 | super().__init__(self.AUTO_TYPE, **kwargs) 897 | 898 | 899 | class StringField(AutoTypedField): 900 | AUTO_TYPE = String() 901 | 902 | 903 | class SensitiveStringField(AutoTypedField): 904 | """Field type that masks passwords when the field is stringified.""" 905 | AUTO_TYPE = SensitiveString() 906 | 907 | 908 | class VersionPredicateField(AutoTypedField): 909 | AUTO_TYPE = VersionPredicate() 910 | 911 | 912 | class BaseEnumField(AutoTypedField): 913 | '''Base class for all enum field types 914 | 915 | This class should not be directly instantiated. Instead 916 | subclass it and set AUTO_TYPE to be a SomeEnum() 917 | where SomeEnum is a subclass of Enum. 918 | ''' 919 | 920 | def __init__(self, **kwargs): 921 | if self.AUTO_TYPE is None: 922 | raise exception.EnumFieldUnset( 923 | fieldname=self.__class__.__name__) 924 | 925 | if not isinstance(self.AUTO_TYPE, Enum): 926 | raise exception.EnumFieldInvalid( 927 | typename=self.AUTO_TYPE.__class__.__name__, 928 | fieldname=self.__class__.__name__) 929 | 930 | super().__init__(**kwargs) 931 | 932 | def __repr__(self): 933 | valid_values = self._type.valid_values 934 | args = { 935 | 'nullable': self._nullable, 936 | 'default': self._default, 937 | } 938 | args.update({'valid_values': valid_values}) 939 | return '{}({})'.format(self._type.__class__.__name__, 940 | ','.join(['{}={}'.format(k, v) 941 | for k, v in sorted(args.items())])) 942 | 943 | @property 944 | def valid_values(self): 945 | """Return the list of valid values for the field.""" 946 | return self._type.valid_values 947 | 948 | 949 | class EnumField(BaseEnumField): 950 | '''Anonymous enum field type 951 | 952 | This class allows for anonymous enum types to be 953 | declared, simply by passing in a list of valid values 954 | to its constructor. It is generally preferable though, 955 | to create an explicit named enum type by sub-classing 956 | the BaseEnumField type directly. 957 | ''' 958 | 959 | def __init__(self, valid_values, **kwargs): 960 | self.AUTO_TYPE = Enum(valid_values=valid_values) 961 | super().__init__(**kwargs) 962 | 963 | 964 | class StateMachine(EnumField): 965 | """A mixin that can be applied to an EnumField to enforce a state machine 966 | 967 | e.g: Setting the code below on a field will ensure an object cannot 968 | transition from ERROR to ACTIVE 969 | 970 | :example: 971 | .. code-block:: python 972 | 973 | class FakeStateMachineField(fields.EnumField, fields.StateMachine): 974 | 975 | ACTIVE = 'ACTIVE' 976 | PENDING = 'PENDING' 977 | ERROR = 'ERROR' 978 | DELETED = 'DELETED' 979 | 980 | ALLOWED_TRANSITIONS = { 981 | ACTIVE: { 982 | PENDING, 983 | ERROR, 984 | DELETED, 985 | }, 986 | PENDING: { 987 | ACTIVE, 988 | ERROR 989 | }, 990 | ERROR: { 991 | PENDING, 992 | }, 993 | DELETED: {} # This is a terminal state 994 | } 995 | 996 | _TYPES = (ACTIVE, PENDING, ERROR, DELETED) 997 | 998 | def __init__(self, **kwargs): 999 | super(FakeStateMachineField, self).__init__( 1000 | self._TYPES, **kwargs) 1001 | 1002 | """ 1003 | # This is dict of states, that have dicts of states an object is 1004 | # allowed to transition to 1005 | 1006 | ALLOWED_TRANSITIONS = {} 1007 | 1008 | def _my_name(self, obj): 1009 | for name, field in obj.fields.items(): 1010 | if field == self: 1011 | return name 1012 | return 'unknown' 1013 | 1014 | def coerce(self, obj, attr, value): 1015 | super().coerce(obj, attr, value) 1016 | my_name = self._my_name(obj) 1017 | msg = _("%(object)s.%(name)s is not allowed to transition out of " 1018 | "%(value)s state") 1019 | 1020 | if attr in obj: 1021 | current_value = getattr(obj, attr) 1022 | else: 1023 | return value 1024 | 1025 | if current_value in self.ALLOWED_TRANSITIONS: 1026 | 1027 | if value in self.ALLOWED_TRANSITIONS[current_value]: 1028 | return value 1029 | else: 1030 | msg = _( 1031 | "%(object)s.%(name)s is not allowed to transition out of " 1032 | "'%(current_value)s' state to '%(value)s' state, choose " 1033 | "from %(options)r") 1034 | msg = msg % { 1035 | 'object': obj.obj_name(), 1036 | 'name': my_name, 1037 | 'current_value': current_value, 1038 | 'value': value, 1039 | 'options': [x for x in self.ALLOWED_TRANSITIONS[current_value]] 1040 | } 1041 | raise ValueError(msg) 1042 | 1043 | 1044 | class UUIDField(AutoTypedField): 1045 | """UUID Field Type 1046 | 1047 | .. warning:: 1048 | 1049 | This class does not actually validate UUIDs. This will happen in a 1050 | future major version of oslo.versionedobjects 1051 | 1052 | To validate that you have valid UUIDs you need to do the following in 1053 | your own objects/fields.py 1054 | 1055 | :Example: 1056 | .. code-block:: python 1057 | 1058 | import oslo_versionedobjects.fields as ovo_fields 1059 | 1060 | class UUID(ovo_fields.UUID): 1061 | def coerce(self, obj, attr, value): 1062 | uuid.UUID(value) 1063 | return str(value) 1064 | 1065 | 1066 | class UUIDField(ovo_fields.AutoTypedField): 1067 | AUTO_TYPE = UUID() 1068 | 1069 | and then in your objects use 1070 | ``.object.fields.UUIDField``. 1071 | 1072 | This will become default behaviour in the future. 1073 | """ 1074 | AUTO_TYPE = UUID() 1075 | 1076 | 1077 | class MACAddressField(AutoTypedField): 1078 | AUTO_TYPE = MACAddress() 1079 | 1080 | 1081 | class PCIAddressField(AutoTypedField): 1082 | AUTO_TYPE = PCIAddress() 1083 | 1084 | 1085 | class IntegerField(AutoTypedField): 1086 | AUTO_TYPE = Integer() 1087 | 1088 | 1089 | class NonNegativeIntegerField(AutoTypedField): 1090 | AUTO_TYPE = NonNegativeInteger() 1091 | 1092 | 1093 | class FloatField(AutoTypedField): 1094 | AUTO_TYPE = Float() 1095 | 1096 | 1097 | class NonNegativeFloatField(AutoTypedField): 1098 | AUTO_TYPE = NonNegativeFloat() 1099 | 1100 | 1101 | # This is a strict interpretation of boolean 1102 | # values using Python's semantics for truth/falsehood 1103 | class BooleanField(AutoTypedField): 1104 | AUTO_TYPE = Boolean() 1105 | 1106 | 1107 | # This is a flexible interpretation of boolean 1108 | # values using common user friendly semantics for 1109 | # truth/falsehood. ie strings like 'yes', 'no', 1110 | # 'on', 'off', 't', 'f' get mapped to values you 1111 | # would expect. 1112 | class FlexibleBooleanField(AutoTypedField): 1113 | AUTO_TYPE = FlexibleBoolean() 1114 | 1115 | 1116 | class DateTimeField(AutoTypedField): 1117 | def __init__(self, tzinfo_aware=True, **kwargs): 1118 | self.AUTO_TYPE = DateTime(tzinfo_aware=tzinfo_aware) 1119 | super().__init__(**kwargs) 1120 | 1121 | 1122 | class DictOfStringsField(AutoTypedField): 1123 | AUTO_TYPE = Dict(String()) 1124 | 1125 | 1126 | class DictOfNullableStringsField(AutoTypedField): 1127 | AUTO_TYPE = Dict(String(), nullable=True) 1128 | 1129 | 1130 | class DictOfIntegersField(AutoTypedField): 1131 | AUTO_TYPE = Dict(Integer()) 1132 | 1133 | 1134 | class ListOfStringsField(AutoTypedField): 1135 | AUTO_TYPE = List(String()) 1136 | 1137 | 1138 | class DictOfListOfStringsField(AutoTypedField): 1139 | AUTO_TYPE = Dict(List(String())) 1140 | 1141 | 1142 | class ListOfEnumField(AutoTypedField): 1143 | def __init__(self, valid_values, **kwargs): 1144 | self.AUTO_TYPE = List(Enum(valid_values)) 1145 | super().__init__(**kwargs) 1146 | 1147 | def __repr__(self): 1148 | valid_values = self._type._element_type._type.valid_values 1149 | args = { 1150 | 'nullable': self._nullable, 1151 | 'default': self._default, 1152 | } 1153 | args.update({'valid_values': valid_values}) 1154 | return '{}({})'.format(self._type.__class__.__name__, 1155 | ','.join(['{}={}'.format(k, v) 1156 | for k, v in sorted(args.items())])) 1157 | 1158 | 1159 | class SetOfIntegersField(AutoTypedField): 1160 | AUTO_TYPE = Set(Integer()) 1161 | 1162 | 1163 | class ListOfSetsOfIntegersField(AutoTypedField): 1164 | AUTO_TYPE = List(Set(Integer())) 1165 | 1166 | 1167 | class ListOfIntegersField(AutoTypedField): 1168 | AUTO_TYPE = List(Integer()) 1169 | 1170 | 1171 | class ListOfDictOfNullableStringsField(AutoTypedField): 1172 | AUTO_TYPE = List(Dict(String(), nullable=True)) 1173 | 1174 | 1175 | class ObjectField(AutoTypedField): 1176 | def __init__(self, objtype, subclasses=False, **kwargs): 1177 | self.AUTO_TYPE = Object(objtype, subclasses) 1178 | self.objname = objtype 1179 | super().__init__(**kwargs) 1180 | 1181 | 1182 | class ListOfObjectsField(AutoTypedField): 1183 | def __init__(self, objtype, subclasses=False, **kwargs): 1184 | self.AUTO_TYPE = List(Object(objtype, subclasses)) 1185 | self.objname = objtype 1186 | super().__init__(**kwargs) 1187 | 1188 | 1189 | class ListOfUUIDField(AutoTypedField): 1190 | AUTO_TYPE = List(UUID()) 1191 | 1192 | 1193 | class IPAddressField(AutoTypedField): 1194 | AUTO_TYPE = IPAddress() 1195 | 1196 | 1197 | class IPV4AddressField(AutoTypedField): 1198 | AUTO_TYPE = IPV4Address() 1199 | 1200 | 1201 | class IPV6AddressField(AutoTypedField): 1202 | AUTO_TYPE = IPV6Address() 1203 | 1204 | 1205 | class IPV4AndV6AddressField(AutoTypedField): 1206 | AUTO_TYPE = IPV4AndV6Address() 1207 | 1208 | 1209 | class IPNetworkField(AutoTypedField): 1210 | AUTO_TYPE = IPNetwork() 1211 | 1212 | 1213 | class IPV4NetworkField(AutoTypedField): 1214 | AUTO_TYPE = IPV4Network() 1215 | 1216 | 1217 | class IPV6NetworkField(AutoTypedField): 1218 | AUTO_TYPE = IPV6Network() 1219 | 1220 | 1221 | class CoercedCollectionMixin: 1222 | def __init__(self, *args, **kwargs): 1223 | self._element_type = None 1224 | self._obj = None 1225 | self._field = None 1226 | super().__init__(*args, **kwargs) 1227 | 1228 | def enable_coercing(self, element_type, obj, field): 1229 | self._element_type = element_type 1230 | self._obj = obj 1231 | self._field = field 1232 | 1233 | 1234 | class CoercedList(CoercedCollectionMixin, list): 1235 | """List which coerces its elements 1236 | 1237 | List implementation which overrides all element-adding methods and 1238 | coercing the element(s) being added to the required element type 1239 | """ 1240 | def _coerce_item(self, index, item): 1241 | if hasattr(self, "_element_type") and self._element_type is not None: 1242 | att_name = "%s[%i]" % (self._field, index) 1243 | return self._element_type.coerce(self._obj, att_name, item) 1244 | else: 1245 | return item 1246 | 1247 | def __setitem__(self, i, y): 1248 | if type(i) is slice: # compatibility with py3 and [::] slices 1249 | start = i.start or 0 1250 | step = i.step or 1 1251 | coerced_items = [self._coerce_item(start + index * step, item) 1252 | for index, item in enumerate(y)] 1253 | super().__setitem__(i, coerced_items) 1254 | else: 1255 | super().__setitem__(i, self._coerce_item(i, y)) 1256 | 1257 | def append(self, x): 1258 | super().append(self._coerce_item(len(self) + 1, x)) 1259 | 1260 | def extend(self, t): 1261 | coerced_items = [self._coerce_item(len(self) + index, item) 1262 | for index, item in enumerate(t)] 1263 | super().extend(coerced_items) 1264 | 1265 | def insert(self, i, x): 1266 | super().insert(i, self._coerce_item(i, x)) 1267 | 1268 | def __iadd__(self, y): 1269 | coerced_items = [self._coerce_item(len(self) + index, item) 1270 | for index, item in enumerate(y)] 1271 | return super().__iadd__(coerced_items) 1272 | 1273 | def __setslice__(self, i, j, y): 1274 | coerced_items = [self._coerce_item(i + index, item) 1275 | for index, item in enumerate(y)] 1276 | return super().__setslice__(i, j, coerced_items) 1277 | 1278 | 1279 | class CoercedDict(CoercedCollectionMixin, dict): 1280 | """Dict which coerces its values 1281 | 1282 | Dict implementation which overrides all element-adding methods and 1283 | coercing the element(s) being added to the required element type 1284 | """ 1285 | 1286 | def _coerce_dict(self, d): 1287 | res = {} 1288 | for key, element in d.items(): 1289 | res[key] = self._coerce_item(key, element) 1290 | return res 1291 | 1292 | def _coerce_item(self, key, item): 1293 | if not isinstance(key, str): 1294 | raise KeyTypeError(str, key) 1295 | if hasattr(self, "_element_type") and self._element_type is not None: 1296 | att_name = "{}[{}]".format(self._field, key) 1297 | return self._element_type.coerce(self._obj, att_name, item) 1298 | else: 1299 | return item 1300 | 1301 | def __setitem__(self, key, value): 1302 | super().__setitem__(key, 1303 | self._coerce_item(key, value)) 1304 | 1305 | def update(self, other=None, **kwargs): 1306 | if other is not None: 1307 | super().update(self._coerce_dict(other), 1308 | **self._coerce_dict(kwargs)) 1309 | else: 1310 | super().update(**self._coerce_dict(kwargs)) 1311 | 1312 | def setdefault(self, key, default=None): 1313 | return super().setdefault(key, 1314 | self._coerce_item(key, 1315 | default)) 1316 | 1317 | 1318 | class CoercedSet(CoercedCollectionMixin, set): 1319 | """Set which coerces its values 1320 | 1321 | Dict implementation which overrides all element-adding methods and 1322 | coercing the element(s) being added to the required element type 1323 | """ 1324 | def _coerce_element(self, element): 1325 | if hasattr(self, "_element_type") and self._element_type is not None: 1326 | return self._element_type.coerce(self._obj, 1327 | "{}[{}]".format( 1328 | self._field, element), 1329 | element) 1330 | else: 1331 | return element 1332 | 1333 | def _coerce_iterable(self, values): 1334 | coerced = set() 1335 | for element in values: 1336 | coerced.add(self._coerce_element(element)) 1337 | return coerced 1338 | 1339 | def add(self, value): 1340 | return super().add(self._coerce_element(value)) 1341 | 1342 | def update(self, values): 1343 | return super().update(self._coerce_iterable(values)) 1344 | 1345 | def symmetric_difference_update(self, values): 1346 | return super().symmetric_difference_update( 1347 | self._coerce_iterable(values)) 1348 | 1349 | def __ior__(self, y): 1350 | return super().__ior__(self._coerce_iterable(y)) 1351 | 1352 | def __ixor__(self, y): 1353 | return super().__ixor__(self._coerce_iterable(y)) 1354 | -------------------------------------------------------------------------------- /oslo_versionedobjects/fixture.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | """Fixtures for writing tests for code using oslo.versionedobjects 13 | 14 | .. note:: 15 | 16 | This module has several extra dependencies not needed at runtime 17 | for production code, and therefore not installed by default. To 18 | ensure those dependencies are present for your tests, add 19 | ``oslo.versionedobjects[fixtures]`` to your list of test dependencies. 20 | 21 | """ 22 | 23 | from collections import namedtuple 24 | from collections import OrderedDict 25 | import copy 26 | import datetime 27 | import hashlib 28 | import inspect 29 | import logging 30 | from reprlib import recursive_repr 31 | from unittest import mock 32 | 33 | import fixtures 34 | from oslo_utils import versionutils as vutils 35 | 36 | from oslo_versionedobjects import base 37 | from oslo_versionedobjects import fields 38 | 39 | 40 | LOG = logging.getLogger(__name__) 41 | 42 | 43 | def compare_obj(test, obj, db_obj, subs=None, allow_missing=None, 44 | comparators=None): 45 | """Compare a VersionedObject and a dict-like database object. 46 | 47 | This automatically converts TZ-aware datetimes and iterates over 48 | the fields of the object. 49 | 50 | :param test: The TestCase doing the comparison 51 | :param obj: The VersionedObject to examine 52 | :param db_obj: The dict-like database object to use as reference 53 | :param subs: A dict of objkey=dbkey field substitutions 54 | :param allow_missing: A list of fields that may not be in db_obj 55 | :param comparators: Map of comparator functions to use for certain fields 56 | """ 57 | 58 | subs = subs or {} 59 | allow_missing = allow_missing or [] 60 | comparators = comparators or {} 61 | 62 | for key in obj.fields: 63 | db_key = subs.get(key, key) 64 | 65 | # If this is an allow_missing key and it's missing in either obj or 66 | # db_obj, just skip it 67 | if key in allow_missing: 68 | if key not in obj or db_key not in db_obj: 69 | continue 70 | 71 | # If the value isn't set on the object, and also isn't set on the 72 | # db_obj, we'll skip the value check, unset in both is equal 73 | if not obj.obj_attr_is_set(key) and db_key not in db_obj: 74 | continue 75 | # If it's set on the object and not on the db_obj, they aren't equal 76 | elif obj.obj_attr_is_set(key) and db_key not in db_obj: 77 | raise AssertionError(("%s (db_key: %s) is set on the object, but " 78 | "not on the db_obj, so the objects are not " 79 | "equal") 80 | % (key, db_key)) 81 | # If it's set on the db_obj and not the object, they aren't equal 82 | elif not obj.obj_attr_is_set(key) and db_key in db_obj: 83 | raise AssertionError(("%s (db_key: %s) is set on the db_obj, but " 84 | "not on the object, so the objects are not " 85 | "equal") 86 | % (key, db_key)) 87 | 88 | # All of the checks above have safeguarded us, so we know we will 89 | # get an obj_val and db_val without issue 90 | obj_val = getattr(obj, key) 91 | db_val = db_obj[db_key] 92 | if isinstance(obj_val, datetime.datetime): 93 | obj_val = obj_val.replace(tzinfo=None) 94 | 95 | if isinstance(db_val, datetime.datetime): 96 | db_val = obj_val.replace(tzinfo=None) 97 | 98 | if key in comparators: 99 | comparator = comparators[key] 100 | comparator(db_val, obj_val) 101 | else: 102 | test.assertEqual(db_val, obj_val) 103 | 104 | 105 | class OsloOrderedDict(OrderedDict): 106 | """Oslo version of OrderedDict for Python consistency.""" 107 | 108 | @recursive_repr() 109 | def __repr__(self): 110 | if not self: 111 | return '%s()' % self.__class__.__bases__[0].__name__ 112 | # NOTE(jamespage): 113 | # Python >= 3.12 uses a dict instead of a list which changes the 114 | # repr of the versioned object and its associated hash value 115 | # Switch back to using list an use super class name. 116 | return '{}({!r})'.format( 117 | self.__class__.__bases__[0].__name__, list(self.items()) 118 | ) 119 | 120 | 121 | class FakeIndirectionAPI(base.VersionedObjectIndirectionAPI): 122 | def __init__(self, serializer=None): 123 | super().__init__() 124 | self._ser = serializer or base.VersionedObjectSerializer() 125 | 126 | def _get_changes(self, orig_obj, new_obj): 127 | updates = dict() 128 | for name, field in new_obj.fields.items(): 129 | if not new_obj.obj_attr_is_set(name): 130 | continue 131 | if (not orig_obj.obj_attr_is_set(name) or 132 | getattr(orig_obj, name) != getattr(new_obj, name)): 133 | updates[name] = field.to_primitive(new_obj, name, 134 | getattr(new_obj, name)) 135 | return updates 136 | 137 | def _canonicalize_args(self, context, args, kwargs): 138 | args = tuple( 139 | [self._ser.deserialize_entity( 140 | context, self._ser.serialize_entity(context, arg)) 141 | for arg in args]) 142 | kwargs = { 143 | argname: self._ser.deserialize_entity( 144 | context, self._ser.serialize_entity(context, arg)) 145 | for argname, arg in kwargs.items()} 146 | return args, kwargs 147 | 148 | def object_action(self, context, objinst, objmethod, args, kwargs): 149 | objinst = self._ser.deserialize_entity( 150 | context, self._ser.serialize_entity( 151 | context, objinst)) 152 | objmethod = str(objmethod) 153 | args, kwargs = self._canonicalize_args(context, args, kwargs) 154 | original = objinst.obj_clone() 155 | with mock.patch('oslo_versionedobjects.base.VersionedObject.' 156 | 'indirection_api', new=None): 157 | result = getattr(objinst, objmethod)(*args, **kwargs) 158 | updates = self._get_changes(original, objinst) 159 | updates['obj_what_changed'] = objinst.obj_what_changed() 160 | return updates, result 161 | 162 | def object_class_action(self, context, objname, objmethod, objver, 163 | args, kwargs): 164 | objname = str(objname) 165 | objmethod = str(objmethod) 166 | objver = str(objver) 167 | args, kwargs = self._canonicalize_args(context, args, kwargs) 168 | cls = base.VersionedObject.obj_class_from_name(objname, objver) 169 | with mock.patch('oslo_versionedobjects.base.VersionedObject.' 170 | 'indirection_api', new=None): 171 | result = getattr(cls, objmethod)(context, *args, **kwargs) 172 | return (base.VersionedObject.obj_from_primitive( 173 | result.obj_to_primitive(target_version=objver), 174 | context=context) 175 | if isinstance(result, base.VersionedObject) else result) 176 | 177 | def object_class_action_versions(self, context, objname, objmethod, 178 | object_versions, args, kwargs): 179 | objname = str(objname) 180 | objmethod = str(objmethod) 181 | object_versions = {str(o): str(v) for o, v in object_versions.items()} 182 | args, kwargs = self._canonicalize_args(context, args, kwargs) 183 | objver = object_versions[objname] 184 | cls = base.VersionedObject.obj_class_from_name(objname, objver) 185 | with mock.patch('oslo_versionedobjects.base.VersionedObject.' 186 | 'indirection_api', new=None): 187 | result = getattr(cls, objmethod)(context, *args, **kwargs) 188 | return (base.VersionedObject.obj_from_primitive( 189 | result.obj_to_primitive(target_version=objver), 190 | context=context) 191 | if isinstance(result, base.VersionedObject) else result) 192 | 193 | def object_backport(self, context, objinst, target_version): 194 | raise Exception('not supported') 195 | 196 | 197 | class IndirectionFixture(fixtures.Fixture): 198 | def __init__(self, indirection_api=None): 199 | self.indirection_api = indirection_api or FakeIndirectionAPI() 200 | 201 | def setUp(self): 202 | super().setUp() 203 | self.useFixture(fixtures.MonkeyPatch( 204 | 'oslo_versionedobjects.base.VersionedObject.indirection_api', 205 | self.indirection_api)) 206 | 207 | 208 | class ObjectHashMismatch(Exception): 209 | def __init__(self, expected, actual): 210 | self.expected = expected 211 | self.actual = actual 212 | 213 | def __str__(self): 214 | return 'Hashes have changed for %s' % ( 215 | ','.join(set(self.expected.keys() + self.actual.keys()))) 216 | 217 | 218 | CompatArgSpec = namedtuple( 219 | 'ArgSpec', ('args', 'varargs', 'keywords', 'defaults')) 220 | 221 | 222 | def get_method_spec(method): 223 | """Get a stable and compatible method spec. 224 | 225 | Newer features in Python3 (kw-only arguments and annotations) are 226 | not supported or representable with inspect.getargspec() but many 227 | object hashes are already recorded using that method. This attempts 228 | to return something compatible with getargspec() when possible (i.e. 229 | when those features are not used), and otherwise just returns the 230 | newer getfullargspec() representation. 231 | """ 232 | fullspec = inspect.getfullargspec(method) 233 | if any([fullspec.kwonlyargs, fullspec.kwonlydefaults, 234 | fullspec.annotations]): 235 | # Method uses newer-than-getargspec() features, so return the 236 | # newer full spec 237 | return fullspec 238 | else: 239 | return CompatArgSpec(fullspec.args, fullspec.varargs, 240 | fullspec.varkw, fullspec.defaults) 241 | 242 | 243 | class ObjectVersionChecker: 244 | def __init__(self, obj_classes=base.VersionedObjectRegistry.obj_classes()): 245 | self.obj_classes = obj_classes 246 | 247 | def _find_remotable_method(self, cls, thing, parent_was_remotable=False): 248 | """Follow a chain of remotable things down to the original function.""" 249 | if isinstance(thing, classmethod): 250 | return self._find_remotable_method(cls, thing.__get__(None, cls)) 251 | elif (inspect.ismethod(thing) or 252 | inspect.isfunction(thing)) and hasattr(thing, 'remotable'): 253 | return self._find_remotable_method(cls, thing.original_fn, 254 | parent_was_remotable=True) 255 | elif parent_was_remotable: 256 | # We must be the first non-remotable thing underneath a stack of 257 | # remotable things (i.e. the actual implementation method) 258 | return thing 259 | else: 260 | # This means the top-level thing never hit a remotable layer 261 | return None 262 | 263 | def _get_fingerprint(self, obj_name, extra_data_func=None): 264 | obj_class = self.obj_classes[obj_name][0] 265 | obj_fields = list(obj_class.fields.items()) 266 | obj_fields.sort() 267 | methods = [] 268 | for name in dir(obj_class): 269 | thing = getattr(obj_class, name) 270 | if inspect.ismethod(thing) or inspect.isfunction(thing) \ 271 | or isinstance(thing, classmethod): 272 | method = self._find_remotable_method(obj_class, thing) 273 | if method: 274 | methods.append((name, get_method_spec(method))) 275 | methods.sort() 276 | # NOTE(danms): Things that need a version bump are any fields 277 | # and their types, or the signatures of any remotable methods. 278 | # Of course, these are just the mechanical changes we can detect, 279 | # but many other things may require a version bump (method behavior 280 | # and return value changes, for example). 281 | if hasattr(obj_class, 'child_versions'): 282 | relevant_data = (obj_fields, methods, 283 | OsloOrderedDict( 284 | sorted(obj_class.child_versions.items()))) 285 | else: 286 | relevant_data = (obj_fields, methods) 287 | 288 | if extra_data_func: 289 | relevant_data += extra_data_func(obj_class) 290 | 291 | fingerprint = '{}-{}'.format(obj_class.VERSION, hashlib.md5( 292 | bytes(repr(relevant_data).encode()), 293 | usedforsecurity=False).hexdigest()) 294 | return fingerprint 295 | 296 | def get_hashes(self, extra_data_func=None): 297 | """Return a dict of computed object hashes. 298 | 299 | :param extra_data_func: a function that is given the object class 300 | which gathers more relevant data about the 301 | class that is needed in versioning. Returns 302 | a tuple containing the extra data bits. 303 | """ 304 | 305 | fingerprints = {} 306 | for obj_name in sorted(self.obj_classes): 307 | fingerprints[obj_name] = self._get_fingerprint( 308 | obj_name, extra_data_func=extra_data_func) 309 | return fingerprints 310 | 311 | def test_hashes(self, expected_hashes, extra_data_func=None): 312 | fingerprints = self.get_hashes(extra_data_func=extra_data_func) 313 | 314 | stored = set(expected_hashes.items()) 315 | computed = set(fingerprints.items()) 316 | changed = stored.symmetric_difference(computed) 317 | expected = {} 318 | actual = {} 319 | for name, hash in changed: 320 | expected[name] = expected_hashes.get(name) 321 | actual[name] = fingerprints.get(name) 322 | 323 | return expected, actual 324 | 325 | def _get_dependencies(self, tree, obj_class): 326 | obj_name = obj_class.obj_name() 327 | if obj_name in tree: 328 | return 329 | 330 | for name, field in obj_class.fields.items(): 331 | if isinstance(field._type, fields.Object): 332 | sub_obj_name = field._type._obj_name 333 | sub_obj_class = self.obj_classes[sub_obj_name][0] 334 | self._get_dependencies(tree, sub_obj_class) 335 | tree.setdefault(obj_name, {}) 336 | tree[obj_name][sub_obj_name] = sub_obj_class.VERSION 337 | 338 | def get_dependency_tree(self): 339 | tree = {} 340 | for obj_name in self.obj_classes.keys(): 341 | self._get_dependencies(tree, self.obj_classes[obj_name][0]) 342 | return tree 343 | 344 | def test_relationships(self, expected_tree): 345 | actual_tree = self.get_dependency_tree() 346 | 347 | stored = {(x, str(y)) for x, y in expected_tree.items()} 348 | computed = {(x, str(y)) for x, y in actual_tree.items()} 349 | changed = stored.symmetric_difference(computed) 350 | expected = {} 351 | actual = {} 352 | for name, deps in changed: 353 | expected[name] = expected_tree.get(name) 354 | actual[name] = actual_tree.get(name) 355 | 356 | return expected, actual 357 | 358 | def _test_object_compatibility(self, obj_class, manifest=None, 359 | init_args=None, init_kwargs=None): 360 | init_args = init_args or [] 361 | init_kwargs = init_kwargs or {} 362 | version = vutils.convert_version_to_tuple(obj_class.VERSION) 363 | kwargs = {'version_manifest': manifest} if manifest else {} 364 | for n in range(version[1] + 1): 365 | test_version = '%d.%d' % (version[0], n) 366 | # Run the test with OS_DEBUG=True to see this. 367 | LOG.debug('testing obj: %s version: %s' % 368 | (obj_class.obj_name(), test_version)) 369 | kwargs['target_version'] = test_version 370 | obj_class(*init_args, **init_kwargs).obj_to_primitive(**kwargs) 371 | 372 | def test_compatibility_routines(self, use_manifest=False, init_args=None, 373 | init_kwargs=None): 374 | """Test obj_make_compatible() on all object classes. 375 | 376 | :param use_manifest: a boolean that determines if the version 377 | manifest should be passed to obj_make_compatible 378 | :param init_args: a dictionary of the format {obj_class: [arg1, arg2]} 379 | that will be used to pass arguments to init on the 380 | given obj_class. If no args are needed, the 381 | obj_class does not need to be added to the dict 382 | :param init_kwargs: a dictionary of the format 383 | {obj_class: {'kwarg1': val1}} that will be used to 384 | pass kwargs to init on the given obj_class. If no 385 | kwargs are needed, the obj_class does not need to 386 | be added to the dict 387 | """ 388 | # Iterate all object classes and verify that we can run 389 | # obj_make_compatible with every older version than current. 390 | # This doesn't actually test the data conversions, but it at least 391 | # makes sure the method doesn't blow up on something basic like 392 | # expecting the wrong version format. 393 | init_args = init_args or {} 394 | init_kwargs = init_kwargs or {} 395 | for obj_name in self.obj_classes: 396 | obj_classes = self.obj_classes[obj_name] 397 | if use_manifest: 398 | manifest = base.obj_tree_get_versions(obj_name) 399 | else: 400 | manifest = None 401 | 402 | for obj_class in obj_classes: 403 | args_for_init = init_args.get(obj_class, []) 404 | kwargs_for_init = init_kwargs.get(obj_class, {}) 405 | self._test_object_compatibility(obj_class, manifest=manifest, 406 | init_args=args_for_init, 407 | init_kwargs=kwargs_for_init) 408 | 409 | def _test_relationships_in_order(self, obj_class): 410 | for field, versions in obj_class.obj_relationships.items(): 411 | last_my_version = (0, 0) 412 | last_child_version = (0, 0) 413 | for my_version, child_version in versions: 414 | _my_version = vutils.convert_version_to_tuple(my_version) 415 | _ch_version = vutils.convert_version_to_tuple(child_version) 416 | if not (last_my_version < _my_version and 417 | last_child_version <= _ch_version): 418 | raise AssertionError(('Object %s relationship %s->%s for ' 419 | 'field %s is out of order') % ( 420 | obj_class.obj_name(), 421 | my_version, child_version, 422 | field)) 423 | last_my_version = _my_version 424 | last_child_version = _ch_version 425 | 426 | def test_relationships_in_order(self): 427 | # Iterate all object classes and verify that we can run 428 | # obj_make_compatible with every older version than current. 429 | # This doesn't actually test the data conversions, but it at least 430 | # makes sure the method doesn't blow up on something basic like 431 | # expecting the wrong version format. 432 | for obj_name in self.obj_classes: 433 | obj_classes = self.obj_classes[obj_name] 434 | for obj_class in obj_classes: 435 | self._test_relationships_in_order(obj_class) 436 | 437 | 438 | class VersionedObjectRegistryFixture(fixtures.Fixture): 439 | """Use a VersionedObjectRegistry as a temp registry pattern fixture. 440 | 441 | The pattern solution is to backup the object registry, register 442 | a class locally, and then restore the original registry. This could be 443 | used for test objects that do not need to be registered permanently but 444 | will have calls which lookup registration. 445 | """ 446 | 447 | def setUp(self): 448 | super().setUp() 449 | self._base_test_obj_backup = copy.deepcopy( 450 | base.VersionedObjectRegistry._registry._obj_classes) 451 | self.addCleanup(self._restore_obj_registry) 452 | 453 | @staticmethod 454 | def register(cls_name): 455 | base.VersionedObjectRegistry.register(cls_name) 456 | 457 | def _restore_obj_registry(self): 458 | base.VersionedObjectRegistry._registry._obj_classes = \ 459 | self._base_test_obj_backup 460 | 461 | 462 | class StableObjectJsonFixture(fixtures.Fixture): 463 | """Fixture that makes sure we get stable JSON object representations. 464 | 465 | Since objects contain things like set(), which can't be converted to 466 | JSON, we have some situations where the representation isn't fully 467 | deterministic. This doesn't matter at all at runtime, but does to 468 | unit tests that try to assert things at a low level. 469 | 470 | This fixture mocks the obj_to_primitive() call and makes sure to 471 | sort the list of changed fields (which came from a set) before 472 | returning it to the caller. 473 | """ 474 | def __init__(self): 475 | self._original_otp = base.VersionedObject.obj_to_primitive 476 | 477 | def setUp(self): 478 | super().setUp() 479 | 480 | def _doit(obj, *args, **kwargs): 481 | result = self._original_otp(obj, *args, **kwargs) 482 | changes_key = obj._obj_primitive_key('changes') 483 | if changes_key in result: 484 | result[changes_key].sort() 485 | return result 486 | 487 | self.useFixture(fixtures.MonkeyPatch( 488 | 'oslo_versionedobjects.base.VersionedObject.obj_to_primitive', 489 | _doit)) 490 | -------------------------------------------------------------------------------- /oslo_versionedobjects/locale/en_GB/LC_MESSAGES/oslo_versionedobjects.po: -------------------------------------------------------------------------------- 1 | # Translations template for oslo.versionedobjects. 2 | # Copyright (C) 2015 ORGANIZATION 3 | # This file is distributed under the same license as the 4 | # oslo.versionedobjects project. 5 | # 6 | # Translators: 7 | # Andi Chandler , 2015 8 | # Andi Chandler , 2016. #zanata 9 | # Andreas Jaeger , 2016. #zanata 10 | # Andi Chandler , 2017. #zanata 11 | # Andi Chandler , 2018. #zanata 12 | # Andi Chandler , 2020. #zanata 13 | msgid "" 14 | msgstr "" 15 | "Project-Id-Version: oslo.versionedobjects VERSION\n" 16 | "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" 17 | "POT-Creation-Date: 2020-05-21 17:27+0000\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "PO-Revision-Date: 2020-05-04 09:27+0000\n" 22 | "Last-Translator: Andi Chandler \n" 23 | "Language: en_GB\n" 24 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 25 | "Generated-By: Babel 2.0\n" 26 | "X-Generator: Zanata 4.3.3\n" 27 | "Language-Team: English (United Kingdom)\n" 28 | 29 | #, python-format 30 | msgid "" 31 | "%(child_objname)s is referenced by %(parent_objname)s but is not registered" 32 | msgstr "" 33 | "%(child_objname)s is referenced by %(parent_objname)s but is not registered" 34 | 35 | #, python-format 36 | msgid "%(fieldname)s missing field type" 37 | msgstr "%(fieldname)s missing field type" 38 | 39 | #, python-format 40 | msgid "%(object)s.%(name)s is not allowed to transition out of %(value)s state" 41 | msgstr "" 42 | "%(object)s.%(name)s is not allowed to transition out of %(value)s state" 43 | 44 | #, python-format 45 | msgid "" 46 | "%(object)s.%(name)s is not allowed to transition out of '%(current_value)s' " 47 | "state to '%(value)s' state, choose from %(options)r" 48 | msgstr "" 49 | "%(object)s.%(name)s is not allowed to transition out of '%(current_value)s' " 50 | "state to '%(value)s' state, choose from %(options)r" 51 | 52 | #, python-format 53 | msgid "%(objname)s object has no attribute '%(attrname)s'" 54 | msgstr "%(objname)s object has no attribute '%(attrname)s'" 55 | 56 | #, python-format 57 | msgid "%(typename)s in %(fieldname)s is not an instance of Enum" 58 | msgstr "%(typename)s in %(fieldname)s is not an instance of Enum" 59 | 60 | #, python-format 61 | msgid "%s has no pattern" 62 | msgstr "%s has no pattern" 63 | 64 | #, python-format 65 | msgid "A datetime.datetime is required in field %(attr)s, not a %(type)s" 66 | msgstr "A datetime.datetime is required in field %(attr)s, not a %(type)s" 67 | 68 | #, python-format 69 | msgid "A dict is required in field %(attr)s, not a %(type)s" 70 | msgstr "A dict is required in field %(attr)s, not a %(type)s" 71 | 72 | #, python-format 73 | msgid "A list is required in field %(attr)s, not a %(type)s" 74 | msgstr "A list is required in field %(attr)s, not a %(type)s" 75 | 76 | #, python-format 77 | msgid "A set is required in field %(attr)s, not a %(type)s" 78 | msgstr "A set is required in field %(attr)s, not a %(type)s" 79 | 80 | #, python-format 81 | msgid "A string is required in field %(attr)s, not a %(type)s" 82 | msgstr "A string is required in field %(attr)s, not a %(type)s" 83 | 84 | #, python-format 85 | msgid "" 86 | "An object of type %(type)s is required in field %(attr)s, not a %(valtype)s" 87 | msgstr "" 88 | "An object of type %(type)s is required in field %(attr)s, not a %(valtype)s" 89 | 90 | msgid "An unknown exception occurred." 91 | msgstr "An unknown exception occurred." 92 | 93 | #, python-format 94 | msgid "Cannot call %(method)s on orphaned %(objtype)s object" 95 | msgstr "Cannot call %(method)s on orphaned %(objtype)s object" 96 | 97 | #, python-format 98 | msgid "Cannot load '%s' in the base class" 99 | msgstr "Cannot load '%s' in the base class" 100 | 101 | #, python-format 102 | msgid "Cannot modify readonly field %(field)s" 103 | msgstr "Cannot modify readonly field %(field)s" 104 | 105 | msgid "Cannot save anything in the base class" 106 | msgstr "Cannot save anything in the base class" 107 | 108 | #, python-format 109 | msgid "Element %(key)s:%(val)s must be of type %(expected)s not %(actual)s" 110 | msgstr "Element %(key)s:%(val)s must be of type %(expected)s not %(actual)s" 111 | 112 | msgid "Enum fields require a list of valid_values" 113 | msgstr "Enum fields require a list of valid_values" 114 | 115 | msgid "Enum valid values are not valid" 116 | msgstr "Enum valid values are not valid" 117 | 118 | #, python-format 119 | msgid "Field %(field)s of %(objname)s is not an instance of Field" 120 | msgstr "Field %(field)s of %(objname)s is not an instance of Field" 121 | 122 | #, python-format 123 | msgid "Field `%s' cannot be None" 124 | msgstr "Field `%s' cannot be None" 125 | 126 | #, python-format 127 | msgid "Field value %s is invalid" 128 | msgstr "Field value %s is invalid" 129 | 130 | #, python-format 131 | msgid "Invalid target version %(version)s" 132 | msgstr "Invalid target version %(version)s" 133 | 134 | #, python-format 135 | msgid "Key %(key)s must be of type %(expected)s not %(actual)s" 136 | msgstr "Key %(key)s must be of type %(expected)s not %(actual)s" 137 | 138 | #, python-format 139 | msgid "Malformed MAC %s" 140 | msgstr "Malformed MAC %s" 141 | 142 | #, python-format 143 | msgid "Malformed PCI address %s" 144 | msgstr "Malformed PCI address %s" 145 | 146 | #, python-format 147 | msgid "Network \"%(val)s\" is not valid in field %(attr)s" 148 | msgstr "Network \"%(val)s\" is not valid in field %(attr)s" 149 | 150 | #, python-format 151 | msgid "No subobject existed at version %(target_version)s" 152 | msgstr "No subobject existed at version %(target_version)s" 153 | 154 | #, python-format 155 | msgid "Object action %(action)s failed because: %(reason)s" 156 | msgstr "Object action %(action)s failed because: %(reason)s" 157 | 158 | #, python-format 159 | msgid "Unsupported object type %(objtype)s" 160 | msgstr "Unsupported object type %(objtype)s" 161 | 162 | #, python-format 163 | msgid "Value must be >= 0 for field %s" 164 | msgstr "Value must be >= 0 for field %s" 165 | 166 | #, python-format 167 | msgid "" 168 | "Version %(objver)s of %(objname)s is not supported, supported version is " 169 | "%(supported)s" 170 | msgstr "" 171 | "Version %(objver)s of %(objname)s is not supported, supported version is " 172 | "%(supported)s" 173 | 174 | #, python-format 175 | msgid "Version %(val)s is not a valid predicate in field %(attr)s" 176 | msgstr "Version %(val)s is not a valid predicate in field %(attr)s" 177 | -------------------------------------------------------------------------------- /oslo_versionedobjects/test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010 United States Government as represented by the 2 | # Administrator of the National Aeronautics and Space Administration. 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """Base classes for our unit tests. 18 | 19 | Some black magic for inline callbacks. 20 | 21 | """ 22 | 23 | import eventlet # noqa 24 | eventlet.monkey_patch(os=False) # noqa 25 | 26 | import functools # noqa: E402 27 | import inspect # noqa: E402 28 | import os # noqa: E402 29 | from unittest import mock # noqa: E402 30 | 31 | import fixtures # noqa: E402 32 | from oslo_concurrency import lockutils # noqa: E402 33 | from oslo_config import cfg # noqa: E402 34 | from oslo_config import fixture as config_fixture # noqa: E402 35 | from oslo_log.fixture import logging_error # noqa: E402 36 | import testtools # noqa: E402 37 | 38 | from oslo_versionedobjects.tests import obj_fixtures # noqa: E402 39 | 40 | 41 | CONF = cfg.CONF 42 | 43 | 44 | class TestingException(Exception): 45 | pass 46 | 47 | 48 | class skipIf: 49 | def __init__(self, condition, reason): 50 | self.condition = condition 51 | self.reason = reason 52 | 53 | def __call__(self, func_or_cls): 54 | condition = self.condition 55 | reason = self.reason 56 | if inspect.isfunction(func_or_cls): 57 | @functools.wraps(func_or_cls) 58 | def wrapped(*args, **kwargs): 59 | if condition: 60 | raise testtools.TestCase.skipException(reason) 61 | return func_or_cls(*args, **kwargs) 62 | 63 | return wrapped 64 | elif inspect.isclass(func_or_cls): 65 | orig_func = getattr(func_or_cls, 'setUp') 66 | 67 | @functools.wraps(orig_func) 68 | def new_func(self, *args, **kwargs): 69 | if condition: 70 | raise testtools.TestCase.skipException(reason) 71 | orig_func(self, *args, **kwargs) 72 | 73 | func_or_cls.setUp = new_func 74 | return func_or_cls 75 | else: 76 | raise TypeError('skipUnless can be used only with functions or ' 77 | 'classes') 78 | 79 | 80 | def _patch_mock_to_raise_for_invalid_assert_calls(): 81 | def raise_for_invalid_assert_calls(wrapped): 82 | def wrapper(_self, name): 83 | valid_asserts = [ 84 | 'assert_called_with', 85 | 'assert_called_once_with', 86 | 'assert_has_calls', 87 | 'assert_any_calls'] 88 | 89 | if name.startswith('assert') and name not in valid_asserts: 90 | raise AttributeError('%s is not a valid mock assert method' 91 | % name) 92 | 93 | return wrapped(_self, name) 94 | return wrapper 95 | mock.Mock.__getattr__ = raise_for_invalid_assert_calls( 96 | mock.Mock.__getattr__) 97 | 98 | 99 | # NOTE(gibi): needs to be called only once at import time 100 | # to patch the mock lib 101 | _patch_mock_to_raise_for_invalid_assert_calls() 102 | 103 | 104 | class TestCase(testtools.TestCase): 105 | """Test case base class for all unit tests.""" 106 | REQUIRES_LOCKING = False 107 | 108 | TIMEOUT_SCALING_FACTOR = 1 109 | 110 | def setUp(self): 111 | """Run before each test method to initialize test environment.""" 112 | super().setUp() 113 | self.useFixture(obj_fixtures.Timeout( 114 | os.environ.get('OS_TEST_TIMEOUT', 0), 115 | self.TIMEOUT_SCALING_FACTOR)) 116 | 117 | self.useFixture(fixtures.NestedTempfile()) 118 | self.useFixture(fixtures.TempHomeDir()) 119 | self.useFixture(obj_fixtures.TranslationFixture()) 120 | self.useFixture(logging_error.get_logging_handle_error_fixture()) 121 | 122 | self.useFixture(obj_fixtures.OutputStreamCapture()) 123 | 124 | self.useFixture(obj_fixtures.StandardLogging()) 125 | 126 | # NOTE(sdague): because of the way we were using the lock 127 | # wrapper we eneded up with a lot of tests that started 128 | # relying on global external locking being set up for them. We 129 | # consider all of these to be *bugs*. Tests should not require 130 | # global external locking, or if they do, they should 131 | # explicitly set it up themselves. 132 | # 133 | # The following REQUIRES_LOCKING class parameter is provided 134 | # as a bridge to get us there. No new tests should be added 135 | # that require it, and existing classes and tests should be 136 | # fixed to not need it. 137 | if self.REQUIRES_LOCKING: 138 | lock_path = self.useFixture(fixtures.TempDir()).path 139 | self.fixture = self.useFixture( 140 | config_fixture.Config(lockutils.CONF)) 141 | self.fixture.config(lock_path=lock_path, 142 | group='oslo_concurrency') 143 | 144 | # NOTE(blk-u): WarningsFixture must be after the Database fixture 145 | # because sqlalchemy-migrate messes with the warnings filters. 146 | self.useFixture(obj_fixtures.WarningsFixture()) 147 | 148 | self.addCleanup(self._clear_attrs) 149 | self.useFixture(fixtures.EnvironmentVariable('http_proxy')) 150 | 151 | def _clear_attrs(self): 152 | # Delete attributes that don't start with _ so they don't pin 153 | # memory around unnecessarily for the duration of the test 154 | # suite 155 | for key in [k for k in self.__dict__.keys() if k[0] != '_']: 156 | del self.__dict__[key] 157 | 158 | def assertPublicAPISignatures(self, baseinst, inst): 159 | def get_public_apis(inst): 160 | methods = {} 161 | for (name, value) in inspect.getmembers(inst, inspect.ismethod): 162 | if name.startswith("_"): 163 | continue 164 | methods[name] = value 165 | return methods 166 | 167 | baseclass = baseinst.__class__.__name__ 168 | basemethods = get_public_apis(baseinst) 169 | implmethods = get_public_apis(inst) 170 | 171 | extranames = [] 172 | for name in sorted(implmethods.keys()): 173 | if name not in basemethods: 174 | extranames.append(name) 175 | 176 | self.assertEqual([], extranames, 177 | "public APIs not listed in base class %s" % 178 | baseclass) 179 | 180 | for name in sorted(implmethods.keys()): 181 | baseargs = inspect.getfullargspec(basemethods[name]) 182 | implargs = inspect.getfullargspec(implmethods[name]) 183 | 184 | self.assertEqual(baseargs, implargs, 185 | "%s args don't match base class %s" % 186 | (name, baseclass)) 187 | 188 | 189 | class APICoverage: 190 | 191 | cover_api = None 192 | 193 | def test_api_methods(self): 194 | self.assertTrue(self.cover_api is not None) 195 | api_methods = [x for x in dir(self.cover_api) 196 | if not x.startswith('_')] 197 | test_methods = [x[5:] for x in dir(self) 198 | if x.startswith('test_')] 199 | self.assertThat( 200 | test_methods, 201 | testtools.matchers.ContainsAll(api_methods)) 202 | 203 | 204 | class BaseHookTestCase(TestCase): 205 | def assert_has_hook(self, expected_name, func): 206 | self.assertTrue(hasattr(func, '__hook_name__')) 207 | self.assertEqual(expected_name, func.__hook_name__) 208 | -------------------------------------------------------------------------------- /oslo_versionedobjects/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/oslo.versionedobjects/5c2d02ef021d16da8e14799bde5c7a8e7c811756/oslo_versionedobjects/tests/__init__.py -------------------------------------------------------------------------------- /oslo_versionedobjects/tests/obj_fixtures.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010 United States Government as represented by the 2 | # Administrator of the National Aeronautics and Space Administration. 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """Fixtures for VersionedObject tests.""" 18 | 19 | import gettext 20 | import logging 21 | import os 22 | import warnings 23 | 24 | import fixtures 25 | from oslo_config import cfg 26 | 27 | 28 | _TRUE_VALUES = ('True', 'true', '1', 'yes') 29 | 30 | CONF = cfg.CONF 31 | DB_SCHEMA = "" 32 | 33 | 34 | class TranslationFixture(fixtures.Fixture): 35 | """Use gettext NullTranslation objects in tests.""" 36 | 37 | def setUp(self): 38 | super().setUp() 39 | nulltrans = gettext.NullTranslations() 40 | gettext_fixture = fixtures.MonkeyPatch('gettext.translation', 41 | lambda *x, **y: nulltrans) 42 | self.gettext_patcher = self.useFixture(gettext_fixture) 43 | 44 | 45 | class NullHandler(logging.Handler): 46 | """custom default NullHandler to attempt to format the record. 47 | 48 | Used in conjunction with 49 | log_fixture.get_logging_handle_error_fixture to detect formatting errors in 50 | debug level logs without saving the logs. 51 | """ 52 | def handle(self, record): 53 | self.format(record) 54 | 55 | def emit(self, record): 56 | pass 57 | 58 | def createLock(self): 59 | self.lock = None 60 | 61 | 62 | class StandardLogging(fixtures.Fixture): 63 | """Setup Logging redirection for tests. 64 | 65 | There are a number of things we want to handle with logging in tests: 66 | 67 | * Redirect the logging to somewhere that we can test or dump it later. 68 | 69 | * Ensure that as many DEBUG messages as possible are actually 70 | executed, to ensure they are actually syntactically valid (they 71 | often have not been). 72 | 73 | * Ensure that we create useful output for tests that doesn't 74 | overwhelm the testing system (which means we can't capture the 75 | 100 MB of debug logging on every run). 76 | 77 | To do this we create a logger fixture at the root level, which 78 | defaults to INFO and create a Null Logger at DEBUG which lets 79 | us execute log messages at DEBUG but not keep the output. 80 | 81 | To support local debugging OS_DEBUG=True can be set in the 82 | environment, which will print out the full debug logging. 83 | 84 | There are also a set of overrides for particularly verbose 85 | modules to be even less than INFO. 86 | 87 | """ 88 | 89 | def setUp(self): 90 | super().setUp() 91 | 92 | # set root logger to debug 93 | root = logging.getLogger() 94 | root.setLevel(logging.DEBUG) 95 | 96 | # supports collecting debug level for local runs 97 | if os.environ.get('OS_DEBUG') in _TRUE_VALUES: 98 | level = logging.DEBUG 99 | else: 100 | level = logging.INFO 101 | 102 | # Collect logs 103 | fs = '%(asctime)s %(levelname)s [%(name)s] %(message)s' 104 | self.logger = self.useFixture( 105 | fixtures.FakeLogger(format=fs, level=None)) 106 | # TODO(sdague): why can't we send level through the fake 107 | # logger? Tests prove that it breaks, but it's worth getting 108 | # to the bottom of. 109 | root.handlers[0].setLevel(level) 110 | 111 | if level > logging.DEBUG: 112 | # Just attempt to format debug level logs, but don't save them 113 | handler = NullHandler() 114 | self.useFixture(fixtures.LogHandler(handler, nuke_handlers=False)) 115 | handler.setLevel(logging.DEBUG) 116 | 117 | 118 | class OutputStreamCapture(fixtures.Fixture): 119 | """Capture output streams during tests. 120 | 121 | This fixture captures errant printing to stderr / stdout during 122 | the tests and lets us see those streams at the end of the test 123 | runs instead. Useful to see what was happening during failed 124 | tests. 125 | """ 126 | def setUp(self): 127 | super().setUp() 128 | if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: 129 | self.out = self.useFixture(fixtures.StringStream('stdout')) 130 | self.useFixture( 131 | fixtures.MonkeyPatch('sys.stdout', self.out.stream)) 132 | if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: 133 | self.err = self.useFixture(fixtures.StringStream('stderr')) 134 | self.useFixture( 135 | fixtures.MonkeyPatch('sys.stderr', self.err.stream)) 136 | 137 | @property 138 | def stderr(self): 139 | return self.err._details["stderr"].as_text() 140 | 141 | @property 142 | def stdout(self): 143 | return self.out._details["stdout"].as_text() 144 | 145 | 146 | class Timeout(fixtures.Fixture): 147 | """Setup per test timeouts. 148 | 149 | In order to avoid test deadlocks we support setting up a test 150 | timeout parameter read from the environment. In almost all 151 | cases where the timeout is reached this means a deadlock. 152 | 153 | A class level TIMEOUT_SCALING_FACTOR also exists, which allows 154 | extremely long tests to specify they need more time. 155 | """ 156 | 157 | def __init__(self, timeout, scaling=1): 158 | super().__init__() 159 | try: 160 | self.test_timeout = int(timeout) 161 | except ValueError: 162 | # If timeout value is invalid do not set a timeout. 163 | self.test_timeout = 0 164 | if scaling >= 1: 165 | self.test_timeout *= scaling 166 | else: 167 | raise ValueError('scaling value must be >= 1') 168 | 169 | def setUp(self): 170 | super().setUp() 171 | if self.test_timeout > 0: 172 | self.useFixture(fixtures.Timeout(self.test_timeout, gentle=True)) 173 | 174 | 175 | class WarningsFixture(fixtures.Fixture): 176 | """Filters out warnings during test runs.""" 177 | 178 | def setUp(self): 179 | super().setUp() 180 | # NOTE(sdague): Make deprecation warnings only happen once. Otherwise 181 | # this gets kind of crazy given the way that upstream python libs use 182 | # this. 183 | warnings.simplefilter("once", DeprecationWarning) 184 | warnings.filterwarnings('ignore', 185 | message='With-statements now directly support' 186 | ' multiple context managers') 187 | 188 | self.addCleanup(warnings.resetwarnings) 189 | -------------------------------------------------------------------------------- /oslo_versionedobjects/tests/test_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Justin Santa Barbara 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | from unittest import mock 15 | 16 | from oslo_versionedobjects import exception 17 | from oslo_versionedobjects import test 18 | 19 | notifier = mock.Mock() 20 | 21 | 22 | class TestWrapper: 23 | @exception.wrap_exception(notifier=notifier) 24 | def raise_exc(self, context, exc, admin_password): 25 | raise exc 26 | 27 | 28 | class ExceptionTestCase(test.TestCase): 29 | def test_wrap_exception_wrapped(self): 30 | test = TestWrapper() 31 | # Ensure that the original function is available in 32 | # the __wrapped__ attribute 33 | self.assertTrue(hasattr(test.raise_exc, '__wrapped__')) 34 | 35 | def test_wrap_exception(self): 36 | context = "context" 37 | exc = ValueError() 38 | 39 | test = TestWrapper() 40 | notifier.reset_mock() 41 | 42 | # wrap_exception() must reraise the exception 43 | self.assertRaises(ValueError, # nosec 44 | test.raise_exc, context, exc, admin_password="xxx") 45 | 46 | # wrap_exception() strips admin_password from args 47 | payload = {'args': {'self': test, 'context': context, 'exc': exc}, 48 | 'exception': exc} 49 | notifier.error.assert_called_once_with(context, 'raise_exc', payload) 50 | 51 | def test_vo_exception(self): 52 | exc = exception.VersionedObjectsException() 53 | self.assertEqual('An unknown exception occurred.', str(exc)) 54 | self.assertEqual({'code': 500}, exc.kwargs) 55 | 56 | def test_object_action_error(self): 57 | exc = exception.ObjectActionError(action='ACTION', reason='REASON', 58 | code=123) 59 | self.assertEqual('Object action ACTION failed because: REASON', 60 | str(exc)) 61 | self.assertEqual({'code': 123, 'action': 'ACTION', 'reason': 'REASON'}, 62 | exc.kwargs) 63 | 64 | def test_constructor_format_error(self): 65 | # Test error handling on formatting exception message in the 66 | # VersionedObjectsException constructor 67 | with mock.patch.object(exception, 'LOG') as log: 68 | exc = exception.ObjectActionError() 69 | 70 | log.error.assert_called_with('code: 500') 71 | 72 | # Formatting failed: the message is the original format string 73 | self.assertEqual(exception.ObjectActionError.msg_fmt, str(exc)) 74 | -------------------------------------------------------------------------------- /oslo_versionedobjects/tests/test_fixture.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 IBM Corp. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import copy 16 | import datetime 17 | import hashlib 18 | import inspect 19 | from unittest import mock 20 | 21 | from oslo_versionedobjects import base 22 | from oslo_versionedobjects import exception 23 | from oslo_versionedobjects import fields 24 | from oslo_versionedobjects import fixture 25 | from oslo_versionedobjects import test 26 | 27 | 28 | class MyObject(base.VersionedObject): 29 | fields = {'diglett': fields.IntegerField()} 30 | 31 | @base.remotable 32 | def remotable_method(self): 33 | pass 34 | 35 | @classmethod 36 | @base.remotable 37 | def remotable_classmethod(cls): 38 | pass 39 | 40 | def non_remotable_method(self): 41 | pass 42 | 43 | @classmethod 44 | def non_remotable_classmethod(cls): 45 | pass 46 | 47 | 48 | class MyObject2(base.VersionedObject): 49 | pass 50 | 51 | 52 | class MyExtraObject(base.VersionedObject): 53 | pass 54 | 55 | 56 | class TestObjectComparators(test.TestCase): 57 | @base.VersionedObjectRegistry.register_if(False) 58 | class MyComparedObject(base.VersionedObject): 59 | fields = {'foo': fields.IntegerField(), 60 | 'bar': fields.IntegerField()} 61 | 62 | @base.VersionedObjectRegistry.register_if(False) 63 | class MyComparedObjectWithTZ(base.VersionedObject): 64 | fields = {'tzfield': fields.DateTimeField()} 65 | 66 | def test_compare_obj(self): 67 | mock_test = mock.Mock() 68 | mock_test.assertEqual = mock.Mock() 69 | my_obj = self.MyComparedObject(foo=1, bar=2) 70 | my_db_obj = {'foo': 1, 'bar': 2} 71 | 72 | fixture.compare_obj(mock_test, my_obj, my_db_obj) 73 | 74 | expected_calls = [(1, 1), (2, 2)] 75 | actual_calls = [c[0] for c in mock_test.assertEqual.call_args_list] 76 | for call in expected_calls: 77 | self.assertIn(call, actual_calls) 78 | 79 | def test_compare_obj_with_unset(self): 80 | # If the object has nothing set, and also the db object has the same 81 | # thing not set, it's OK. 82 | mock_test = mock.Mock() 83 | mock_test.assertEqual = mock.Mock() 84 | my_obj = self.MyComparedObject() 85 | my_db_obj = {} 86 | 87 | fixture.compare_obj(mock_test, my_obj, my_db_obj) 88 | 89 | self.assertFalse(mock_test.assertEqual.called, "assertEqual should " 90 | "not have been called, there is nothing to compare.") 91 | 92 | def test_compare_obj_with_unset_in_obj(self): 93 | # If the db dict has something set, but the object doesn't, that's != 94 | mock_test = mock.Mock() 95 | mock_test.assertEqual = mock.Mock() 96 | my_obj = self.MyComparedObject(foo=1) 97 | my_db_obj = {'foo': 1, 'bar': 2} 98 | 99 | self.assertRaises(AssertionError, fixture.compare_obj, mock_test, 100 | my_obj, my_db_obj) 101 | 102 | def test_compare_obj_with_unset_in_db_dict(self): 103 | # If the object has something set, but the db dict doesn't, that's != 104 | mock_test = mock.Mock() 105 | mock_test.assertEqual = mock.Mock() 106 | my_obj = self.MyComparedObject(foo=1, bar=2) 107 | my_db_obj = {'foo': 1} 108 | 109 | self.assertRaises(AssertionError, fixture.compare_obj, mock_test, 110 | my_obj, my_db_obj) 111 | 112 | def test_compare_obj_with_unset_in_obj_ignored(self): 113 | # If the db dict has something set, but the object doesn't, but we 114 | # ignore that key, we are equal 115 | my_obj = self.MyComparedObject(foo=1) 116 | my_db_obj = {'foo': 1, 'bar': 2} 117 | ignore = ['bar'] 118 | 119 | fixture.compare_obj(self, my_obj, my_db_obj, allow_missing=ignore) 120 | 121 | def test_compare_obj_with_unset_in_db_dict_ignored(self): 122 | # If the object has something set, but the db dict doesn't, but we 123 | # ignore that key, we are equal 124 | my_obj = self.MyComparedObject(foo=1, bar=2) 125 | my_db_obj = {'foo': 1} 126 | ignore = ['bar'] 127 | 128 | fixture.compare_obj(self, my_obj, my_db_obj, allow_missing=ignore) 129 | 130 | def test_compare_obj_with_allow_missing_unequal(self): 131 | # If the tested key is in allow_missing, but both the obj and db_obj 132 | # have the value set, we should still check it for equality 133 | mock_test = mock.Mock() 134 | mock_test.assertEqual = mock.Mock() 135 | my_obj = self.MyComparedObject(foo=1, bar=2) 136 | my_db_obj = {'foo': 1, 'bar': 1} 137 | ignore = ['bar'] 138 | 139 | fixture.compare_obj(mock_test, my_obj, my_db_obj, 140 | allow_missing=ignore) 141 | 142 | expected_calls = [(1, 1), (1, 2)] 143 | actual_calls = [c[0] for c in mock_test.assertEqual.call_args_list] 144 | for call in expected_calls: 145 | self.assertIn(call, actual_calls) 146 | 147 | def test_compare_obj_with_subs(self): 148 | mock_test = mock.Mock() 149 | mock_test.assertEqual = mock.Mock() 150 | my_obj = self.MyComparedObject(foo=1, bar=2) 151 | my_db_obj = {'doo': 1, 'bar': 2} 152 | subs = {'foo': 'doo'} 153 | 154 | fixture.compare_obj(mock_test, my_obj, my_db_obj, subs=subs) 155 | 156 | expected_calls = [(1, 1), (2, 2)] 157 | actual_calls = [c[0] for c in mock_test.assertEqual.call_args_list] 158 | for call in expected_calls: 159 | self.assertIn(call, actual_calls) 160 | 161 | def test_compare_obj_with_allow_missing(self): 162 | mock_test = mock.Mock() 163 | mock_test.assertEqual = mock.Mock() 164 | my_obj = self.MyComparedObject(foo=1) 165 | my_db_obj = {'foo': 1, 'bar': 2} 166 | ignores = ['bar'] 167 | 168 | fixture.compare_obj(mock_test, my_obj, my_db_obj, 169 | allow_missing=ignores) 170 | 171 | mock_test.assertEqual.assert_called_once_with(1, 1) 172 | 173 | def test_compare_obj_with_comparators(self): 174 | mock_test = mock.Mock() 175 | mock_test.assertEqual = mock.Mock() 176 | comparator = mock.Mock() 177 | comp_dict = {'foo': comparator} 178 | my_obj = self.MyComparedObject(foo=1, bar=2) 179 | my_db_obj = {'foo': 1, 'bar': 2} 180 | 181 | fixture.compare_obj(mock_test, my_obj, my_db_obj, 182 | comparators=comp_dict) 183 | 184 | comparator.assert_called_once_with(1, 1) 185 | mock_test.assertEqual.assert_called_once_with(2, 2) 186 | 187 | def test_compare_obj_with_dt(self): 188 | mock_test = mock.Mock() 189 | mock_test.assertEqual = mock.Mock() 190 | dt = datetime.datetime(1955, 11, 5, tzinfo=datetime.timezone.utc) 191 | replaced_dt = dt.replace(tzinfo=None) 192 | my_obj = self.MyComparedObjectWithTZ(tzfield=dt) 193 | my_db_obj = {'tzfield': replaced_dt} 194 | 195 | fixture.compare_obj(mock_test, my_obj, my_db_obj) 196 | 197 | mock_test.assertEqual.assert_called_once_with(replaced_dt, 198 | replaced_dt) 199 | 200 | 201 | class FakeResource(base.VersionedObject): 202 | # Version 1.0: Initial version 203 | VERSION = '1.0' 204 | 205 | fields = { 206 | 'identifier': fields.Field(fields.Integer(), default=123) 207 | } 208 | 209 | 210 | class TestObjectVersionChecker(test.TestCase): 211 | def setUp(self): 212 | super().setUp() 213 | objects = [MyObject, MyObject2, ] 214 | self.obj_classes = {obj.__name__: [obj] for obj in objects} 215 | self.ovc = fixture.ObjectVersionChecker(obj_classes=self.obj_classes) 216 | 217 | def test_get_hashes(self): 218 | # Make sure get_hashes retrieves the fingerprint of all objects 219 | fp = 'ashketchum' 220 | with mock.patch.object(self.ovc, '_get_fingerprint') as mock_gf: 221 | mock_gf.return_value = fp 222 | actual = self.ovc.get_hashes() 223 | 224 | expected = self._generate_hashes(self.obj_classes, fp) 225 | self.assertEqual(expected, actual, "ObjectVersionChecker is not " 226 | "getting the fingerprints of all registered " 227 | "objects.") 228 | 229 | def test_get_hashes_with_extra_data(self): 230 | # Make sure get_hashes uses the extra_data_func 231 | fp = 'garyoak' 232 | mock_func = mock.MagicMock() 233 | with mock.patch.object(self.ovc, '_get_fingerprint') as mock_gf: 234 | mock_gf.return_value = fp 235 | actual = self.ovc.get_hashes(extra_data_func=mock_func) 236 | 237 | expected = self._generate_hashes(self.obj_classes, fp) 238 | expected_calls = [((name,), {'extra_data_func': mock_func}) 239 | for name in self.obj_classes.keys()] 240 | 241 | self.assertEqual(expected, actual, "ObjectVersionChecker is not " 242 | "getting the fingerprints of all registered " 243 | "objects.") 244 | 245 | self.assertEqual(len(expected_calls), len(mock_gf.call_args_list), 246 | "get_hashes() did not call get the fingerprints of " 247 | "all objects in the registry.") 248 | for call in expected_calls: 249 | self.assertIn(call, mock_gf.call_args_list, 250 | "get_hashes() did not call _get_fingerprint()" 251 | "correctly.") 252 | 253 | def test_test_hashes_none_changed(self): 254 | # Make sure test_hashes() generates an empty dictionary when 255 | # there are no objects that have changed 256 | fp = 'pikachu' 257 | hashes = self._generate_hashes(self.obj_classes, fp) 258 | 259 | with mock.patch.object(self.ovc, 'get_hashes') as mock_gh: 260 | mock_gh.return_value = hashes 261 | # I'm so sorry, but they have to be named this way 262 | actual_expected, actual_actual = self.ovc.test_hashes(hashes) 263 | 264 | expected_expected = expected_actual = {} 265 | 266 | self.assertEqual(expected_expected, actual_expected, "There are no " 267 | "objects changed, so the 'expected' return value " 268 | "should contain no objects.") 269 | self.assertEqual(expected_actual, actual_actual, "There are no " 270 | "objects changed, so the 'actual' return value " 271 | "should contain no objects.") 272 | 273 | def test_test_hashes_class_not_added(self): 274 | # Make sure the expected and actual values differ when a class 275 | # was added to the registry, but not the static dictionary 276 | fp = 'gyrados' 277 | new_classes = copy.copy(self.obj_classes) 278 | self._add_class(new_classes, MyExtraObject) 279 | 280 | expected_hashes = self._generate_hashes(self.obj_classes, fp) 281 | actual_hashes = self._generate_hashes(new_classes, fp) 282 | 283 | with mock.patch.object(self.ovc, 'get_hashes') as mock_gh: 284 | mock_gh.return_value = actual_hashes 285 | actual_exp, actual_act = self.ovc.test_hashes(expected_hashes) 286 | 287 | expected_expected = {MyExtraObject.__name__: None} 288 | expected_actual = {MyExtraObject.__name__: fp} 289 | 290 | self.assertEqual(expected_expected, actual_exp, "Expected hashes " 291 | "should not contain the fingerprint of the class " 292 | "that has not been added to the expected hash " 293 | "dictionary.") 294 | self.assertEqual(expected_actual, actual_act, "The actual hash " 295 | "should contain the class that was added to the " 296 | "registry.") 297 | 298 | def test_test_hashes_new_fp_incorrect(self): 299 | # Make sure the expected and actual values differ when a fingerprint 300 | # was changed, but the static dictionary was not updated 301 | fp1 = 'beedrill' 302 | fp2 = 'snorlax' 303 | expected_hashes = self._generate_hashes(self.obj_classes, fp1) 304 | actual_hashes = copy.copy(expected_hashes) 305 | 306 | actual_hashes[MyObject.__name__] = fp2 307 | 308 | with mock.patch.object(self.ovc, 'get_hashes') as mock_gh: 309 | mock_gh.return_value = actual_hashes 310 | actual_exp, actual_act = self.ovc.test_hashes(expected_hashes) 311 | 312 | expected_expected = {MyObject.__name__: fp1} 313 | expected_actual = {MyObject.__name__: fp2} 314 | 315 | self.assertEqual(expected_expected, actual_exp, "Expected hashes " 316 | "should contain the updated object with the old " 317 | "hash.") 318 | self.assertEqual(expected_actual, actual_act, "Actual hashes " 319 | "should contain the updated object with the new " 320 | "hash.") 321 | 322 | def test_test_hashes_passes_extra_func(self): 323 | # Make sure that test_hashes passes the extra_func to get_hashes 324 | mock_extra_func = mock.Mock() 325 | 326 | with mock.patch.object(self.ovc, 'get_hashes') as mock_get_hashes: 327 | self.ovc.test_hashes({}, extra_data_func=mock_extra_func) 328 | 329 | mock_get_hashes.assert_called_once_with( 330 | extra_data_func=mock_extra_func) 331 | 332 | def test_get_dependency_tree(self): 333 | # Make sure get_dependency_tree() gets the dependencies of all 334 | # objects in the registry 335 | with mock.patch.object(self.ovc, '_get_dependencies') as mock_gd: 336 | self.ovc.get_dependency_tree() 337 | 338 | expected_calls = [(({}, MyObject),), (({}, MyObject2),)] 339 | 340 | self.assertEqual(2, len(mock_gd.call_args_list), 341 | "get_dependency_tree() tried to get the dependencies" 342 | " too many times.") 343 | 344 | for call in expected_calls: 345 | self.assertIn(call, mock_gd.call_args_list, 346 | "get_dependency_tree() did not get the dependencies " 347 | "of the objects correctly.") 348 | 349 | def test_test_relationships_none_changed(self): 350 | # Make sure test_relationships() generates an empty dictionary when 351 | # no relationships have been changed 352 | dep_tree = {} 353 | # tree will be {'MyObject': {'MyObject2': '1.0'}} 354 | self._add_dependency(MyObject, MyObject2, dep_tree) 355 | 356 | with mock.patch.object(self.ovc, 'get_dependency_tree') as mock_gdt: 357 | mock_gdt.return_value = dep_tree 358 | actual_exp, actual_act = self.ovc.test_relationships(dep_tree) 359 | 360 | expected_expected = expected_actual = {} 361 | 362 | self.assertEqual(expected_expected, actual_exp, "There are no " 363 | "objects changed, so the 'expected' return value " 364 | "should contain no objects.") 365 | self.assertEqual(expected_actual, actual_act, "There are no " 366 | "objects changed, so the 'actual' return value " 367 | "should contain no objects.") 368 | 369 | def test_test_relationships_rel_added(self): 370 | # Make sure expected and actual relationships differ if a 371 | # relationship is added to a class 372 | exp_tree = {} 373 | actual_tree = {} 374 | self._add_dependency(MyObject, MyObject2, exp_tree) 375 | self._add_dependency(MyObject, MyObject2, actual_tree) 376 | self._add_dependency(MyObject, MyExtraObject, actual_tree) 377 | 378 | with mock.patch.object(self.ovc, 'get_dependency_tree') as mock_gdt: 379 | mock_gdt.return_value = actual_tree 380 | actual_exp, actual_act = self.ovc.test_relationships(exp_tree) 381 | 382 | expected_expected = {'MyObject': {'MyObject2': '1.0'}} 383 | expected_actual = {'MyObject': {'MyObject2': '1.0', 384 | 'MyExtraObject': '1.0'}} 385 | 386 | self.assertEqual(expected_expected, actual_exp, "The expected " 387 | "relationship tree is not being built from changes " 388 | "correctly.") 389 | self.assertEqual(expected_actual, actual_act, "The actual " 390 | "relationship tree is not being built from changes " 391 | "correctly.") 392 | 393 | def test_test_relationships_class_added(self): 394 | # Make sure expected and actual relationships differ if a new 395 | # class is added to the relationship tree 396 | exp_tree = {} 397 | actual_tree = {} 398 | self._add_dependency(MyObject, MyObject2, exp_tree) 399 | self._add_dependency(MyObject, MyObject2, actual_tree) 400 | self._add_dependency(MyObject2, MyExtraObject, actual_tree) 401 | 402 | with mock.patch.object(self.ovc, 'get_dependency_tree') as mock_gdt: 403 | mock_gdt.return_value = actual_tree 404 | actual_exp, actual_act = self.ovc.test_relationships(exp_tree) 405 | 406 | expected_expected = {'MyObject2': None} 407 | expected_actual = {'MyObject2': {'MyExtraObject': '1.0'}} 408 | 409 | self.assertEqual(expected_expected, actual_exp, "The expected " 410 | "relationship tree is not being built from changes " 411 | "correctly.") 412 | self.assertEqual(expected_actual, actual_act, "The actual " 413 | "relationship tree is not being built from changes " 414 | "correctly.") 415 | 416 | def test_test_compatibility_routines(self): 417 | # Make sure test_compatibility_routines() checks the object 418 | # compatibility of all objects in the registry 419 | del self.ovc.obj_classes[MyObject2.__name__] 420 | 421 | with mock.patch.object(self.ovc, '_test_object_compatibility') as toc: 422 | self.ovc.test_compatibility_routines() 423 | 424 | toc.assert_called_once_with(MyObject, manifest=None, init_args=[], 425 | init_kwargs={}) 426 | 427 | def test_test_compatibility_routines_with_manifest(self): 428 | # Make sure test_compatibility_routines() uses the version manifest 429 | del self.ovc.obj_classes[MyObject2.__name__] 430 | man = {'who': 'cares'} 431 | 432 | with mock.patch.object(self.ovc, '_test_object_compatibility') as toc: 433 | with mock.patch('oslo_versionedobjects.base' 434 | '.obj_tree_get_versions') as otgv: 435 | otgv.return_value = man 436 | self.ovc.test_compatibility_routines(use_manifest=True) 437 | 438 | otgv.assert_called_once_with(MyObject.__name__) 439 | toc.assert_called_once_with(MyObject, manifest=man, init_args=[], 440 | init_kwargs={}) 441 | 442 | def test_test_compatibility_routines_with_args_kwargs(self): 443 | # Make sure test_compatibility_routines() uses init args/kwargs 444 | del self.ovc.obj_classes[MyObject2.__name__] 445 | init_args = {MyObject: [1]} 446 | init_kwargs = {MyObject: {'foo': 'bar'}} 447 | 448 | with mock.patch.object(self.ovc, '_test_object_compatibility') as toc: 449 | self.ovc.test_compatibility_routines(init_args=init_args, 450 | init_kwargs=init_kwargs) 451 | 452 | toc.assert_called_once_with(MyObject, manifest=None, init_args=[1], 453 | init_kwargs={'foo': 'bar'}) 454 | 455 | def test_test_relationships_in_order(self): 456 | # Make sure test_relationships_in_order() tests the relationships 457 | # of all objects in the registry 458 | with mock.patch.object(self.ovc, 459 | '_test_relationships_in_order') as mock_tr: 460 | self.ovc.test_relationships_in_order() 461 | 462 | expected_calls = [((MyObject,),), ((MyObject2,),)] 463 | 464 | self.assertEqual(2, len(mock_tr.call_args_list), 465 | "test_relationships_in_order() tested too many " 466 | "relationships.") 467 | for call in expected_calls: 468 | self.assertIn(call, mock_tr.call_args_list, 469 | "test_relationships_in_order() did not test the " 470 | "relationships of the individual objects " 471 | "correctly.") 472 | 473 | def test_test_relationships_in_order_positive(self): 474 | # Make sure a correct relationship ordering doesn't blow up 475 | rels = {'bellsprout': [('1.0', '1.0'), ('1.1', '1.2'), 476 | ('1.3', '1.3')]} 477 | MyObject.obj_relationships = rels 478 | 479 | self.ovc._test_relationships_in_order(MyObject) 480 | 481 | def test_test_relationships_in_order_negative(self): 482 | # Make sure an out-of-order relationship does blow up 483 | rels = {'rattata': [('1.0', '1.0'), ('1.1', '1.2'), 484 | ('1.3', '1.1')]} 485 | MyObject.obj_relationships = rels 486 | 487 | self.assertRaises(AssertionError, 488 | self.ovc._test_relationships_in_order, MyObject) 489 | 490 | def test_find_remotable_method(self): 491 | # Make sure we can find a remotable method on an object 492 | method = self.ovc._find_remotable_method(MyObject, 493 | MyObject.remotable_method) 494 | 495 | self.assertEqual(MyObject.remotable_method.original_fn, 496 | method, 497 | "_find_remotable_method() did not find the remotable" 498 | " method of MyObject.") 499 | 500 | def test_find_remotable_method_classmethod(self): 501 | # Make sure we can find a remotable classmethod on an object 502 | rcm = MyObject.remotable_classmethod 503 | method = self.ovc._find_remotable_method(MyObject, rcm) 504 | 505 | expected = rcm.__get__(None, MyObject).original_fn 506 | self.assertEqual(expected, method, "_find_remotable_method() did not " 507 | "find the remotable classmethod.") 508 | 509 | def test_find_remotable_method_non_remotable_method(self): 510 | # Make sure nothing is found when we have only a non-remotable method 511 | nrm = MyObject.non_remotable_method 512 | method = self.ovc._find_remotable_method(MyObject, nrm) 513 | 514 | self.assertIsNone(method, "_find_remotable_method() found a method " 515 | "that isn't remotable.") 516 | 517 | def test_find_remotable_method_non_remotable_classmethod(self): 518 | # Make sure we don't find a non-remotable classmethod 519 | nrcm = MyObject.non_remotable_classmethod 520 | method = self.ovc._find_remotable_method(MyObject, nrcm) 521 | 522 | self.assertIsNone(method, "_find_remotable_method() found a method " 523 | "that isn't remotable.") 524 | 525 | def test_get_fingerprint(self): 526 | # Make sure _get_fingerprint() generates a consistent fingerprint 527 | MyObject.VERSION = '1.1' 528 | argspec = 'vulpix' 529 | 530 | with mock.patch.object(fixture, 'get_method_spec') as mock_gas: 531 | mock_gas.return_value = argspec 532 | fp = self.ovc._get_fingerprint(MyObject.__name__) 533 | 534 | exp_fields = sorted(list(MyObject.fields.items())) 535 | exp_methods = sorted([('remotable_method', argspec), 536 | ('remotable_classmethod', argspec)]) 537 | expected_relevant_data = (exp_fields, exp_methods) 538 | # NOTE(hberaud) the following hashlib usage will emit a bandit 539 | # warning. It can be solved by passing `usedforsecurity=False` to 540 | # the md5 function, however, this parameter was introduced with py39 541 | # so passing it will break py38 unittest. I'd suggest to ignore this 542 | # bandit rule while py38 is in our supported runtimes. 543 | expected_hash = hashlib.md5(bytes(repr( 544 | expected_relevant_data).encode())).hexdigest() # nosec 545 | expected_fp = '{}-{}'.format(MyObject.VERSION, expected_hash) 546 | 547 | self.assertEqual(expected_fp, fp, "_get_fingerprint() did not " 548 | "generate a correct fingerprint.") 549 | 550 | def test_get_fingerprint_with_child_versions(self): 551 | # Make sure _get_fingerprint() generates a consistent fingerprint 552 | # when child_versions are present 553 | child_versions = {'1.0': '1.0', '1.1': '1.1'} 554 | MyObject.VERSION = '1.1' 555 | MyObject.child_versions = child_versions 556 | argspec = 'onix' 557 | 558 | with mock.patch.object(fixture, 'get_method_spec') as mock_gas: 559 | mock_gas.return_value = argspec 560 | fp = self.ovc._get_fingerprint(MyObject.__name__) 561 | 562 | exp_fields = sorted(list(MyObject.fields.items())) 563 | exp_methods = sorted([('remotable_method', argspec), 564 | ('remotable_classmethod', argspec)]) 565 | exp_child_versions = fixture.OsloOrderedDict( 566 | sorted(child_versions.items()) 567 | ) 568 | exp_relevant_data = (exp_fields, exp_methods, exp_child_versions) 569 | 570 | # NOTE(hberaud) the following hashlib usage will emit a bandit 571 | # warning. It can be solved by passing `usedforsecurity=False` to 572 | # the md5 function, however, this parameter was introduced with py39 573 | # so passing it will break py38 unittest. I'd suggest to ignore this 574 | # bandit rule while py38 is in our supported runtimes. 575 | expected_hash = hashlib.md5(bytes(repr( 576 | exp_relevant_data).encode())).hexdigest() # nosec 577 | expected_fp = '{}-{}'.format(MyObject.VERSION, expected_hash) 578 | 579 | self.assertEqual(expected_fp, fp, "_get_fingerprint() did not " 580 | "generate a correct fingerprint.") 581 | 582 | def test_get_fingerprint_with_extra_data(self): 583 | # Make sure _get_fingerprint() uses extra_data_func when it is 584 | # supplied 585 | class ExtraDataObj(base.VersionedObject): 586 | pass 587 | 588 | def get_data(obj_class): 589 | return (obj_class,) 590 | 591 | ExtraDataObj.VERSION = '1.1' 592 | argspec = 'cubone' 593 | self._add_class(self.obj_classes, ExtraDataObj) 594 | 595 | with mock.patch.object(fixture, 'get_method_spec') as mock_gas: 596 | mock_gas.return_value = argspec 597 | fp = self.ovc._get_fingerprint(ExtraDataObj.__name__, 598 | extra_data_func=get_data) 599 | 600 | exp_fields = [] 601 | exp_methods = [] 602 | exp_extra_data = ExtraDataObj 603 | exp_relevant_data = (exp_fields, exp_methods, exp_extra_data) 604 | 605 | # NOTE(hberaud) the following hashlib usage will emit a bandit 606 | # warning. It can be solved by passing `usedforsecurity=False` to 607 | # the md5 function, however, this parameter was introduced with py39 608 | # so passing it will break py38 unittest. I'd suggest to ignore this 609 | # bandit rule while py38 is in our supported runtimes. 610 | expected_hash = hashlib.md5(bytes(repr( 611 | exp_relevant_data).encode())).hexdigest() # nosec 612 | expected_fp = '{}-{}'.format(ExtraDataObj.VERSION, expected_hash) 613 | 614 | self.assertEqual(expected_fp, fp, "_get_fingerprint() did not " 615 | "generate a correct fingerprint.") 616 | 617 | def test_get_fingerprint_with_defaulted_set(self): 618 | class ClassWithDefaultedSetField(base.VersionedObject): 619 | VERSION = 1.0 620 | fields = { 621 | 'empty_default': fields.SetOfIntegersField(default=set()), 622 | 'non_empty_default': fields.SetOfIntegersField(default={1, 2}) 623 | } 624 | self._add_class(self.obj_classes, ClassWithDefaultedSetField) 625 | 626 | # it is expected that this hash is stable across python versions 627 | expected = '1.0-bcc44920f2f727eca463c6eb4fe8445b' 628 | actual = self.ovc._get_fingerprint(ClassWithDefaultedSetField.__name__) 629 | self.assertEqual(expected, actual) 630 | 631 | def test_get_dependencies(self): 632 | # Make sure _get_dependencies() generates a correct tree when parsing 633 | # an object 634 | self._add_class(self.obj_classes, MyExtraObject) 635 | MyObject.fields['subob'] = fields.ObjectField('MyExtraObject') 636 | MyExtraObject.VERSION = '1.0' 637 | tree = {} 638 | 639 | self.ovc._get_dependencies(tree, MyObject) 640 | 641 | expected_tree = {'MyObject': {'MyExtraObject': '1.0'}} 642 | 643 | self.assertEqual(expected_tree, tree, "_get_dependencies() did " 644 | "not generate a correct dependency tree.") 645 | 646 | def test_test_object_compatibility(self): 647 | # Make sure _test_object_compatibility() tests obj_to_primitive() 648 | # on each prior version to the current version 649 | to_prim = mock.MagicMock(spec=callable) 650 | MyObject.VERSION = '1.1' 651 | MyObject.obj_to_primitive = to_prim 652 | 653 | self.ovc._test_object_compatibility(MyObject) 654 | 655 | expected_calls = [((), {'target_version': '1.0'}), 656 | ((), {'target_version': '1.1'})] 657 | 658 | self.assertEqual(expected_calls, to_prim.call_args_list, 659 | "_test_object_compatibility() did not test " 660 | "obj_to_primitive() on the correct target versions") 661 | 662 | def test_test_object_compatibility_args_kwargs(self): 663 | # Make sure _test_object_compatibility() tests obj_to_primitive() 664 | # with the correct args and kwargs to init 665 | to_prim = mock.MagicMock(spec=callable) 666 | MyObject.obj_to_primitive = to_prim 667 | MyObject.VERSION = '1.1' 668 | args = [1] 669 | kwargs = {'foo': 'bar'} 670 | 671 | with mock.patch.object(MyObject, '__init__', 672 | return_value=None) as mock_init: 673 | self.ovc._test_object_compatibility(MyObject, init_args=args, 674 | init_kwargs=kwargs) 675 | 676 | expected_init = ((1,), {'foo': 'bar'}) 677 | expected_init_calls = [expected_init, expected_init] 678 | self.assertEqual(expected_init_calls, mock_init.call_args_list, 679 | "_test_object_compatibility() did not call " 680 | "__init__() properly on the object") 681 | 682 | expected_to_prim = [((), {'target_version': '1.0'}), 683 | ((), {'target_version': '1.1'})] 684 | self.assertEqual(expected_to_prim, to_prim.call_args_list, 685 | "_test_object_compatibility() did not test " 686 | "obj_to_primitive() on the correct target versions") 687 | 688 | def _add_class(self, obj_classes, cls): 689 | obj_classes[cls.__name__] = [cls] 690 | 691 | def _generate_hashes(self, classes, fp): 692 | # Generate hashes for classes, giving fp as the fingerprint 693 | # for all classes 694 | return {cls: fp for cls in classes.keys()} 695 | 696 | def _add_dependency(self, parent_cls, child_cls, tree): 697 | # Add a dependency to the tree with the parent class holding 698 | # version 1.0 of the given child class 699 | deps = tree.get(parent_cls.__name__, {}) 700 | deps[child_cls.__name__] = '1.0' 701 | tree[parent_cls.__name__] = deps 702 | 703 | 704 | class TestVersionedObjectRegistryFixture(test.TestCase): 705 | 706 | primitive = {'versioned_object.name': 'FakeResource', 707 | 'versioned_object.namespace': 'versionedobjects', 708 | 'versioned_object.version': '1.0', 709 | 'versioned_object.data': {'identifier': 123}} 710 | 711 | def test_object_registered_temporarily(self): 712 | # Test object that has not been registered 713 | self.assertRaises( 714 | exception.UnsupportedObjectError, 715 | FakeResource.obj_from_primitive, 716 | self.primitive) 717 | 718 | with fixture.VersionedObjectRegistryFixture() as obj_registry: 719 | # Register object locally 720 | obj_registry.setUp() 721 | obj_registry.register(FakeResource) 722 | 723 | # Test object has now been registered 724 | obj = FakeResource.obj_from_primitive( 725 | self.primitive) 726 | self.assertEqual(obj.identifier, 123) 727 | self.assertEqual('1.0', obj.VERSION) 728 | 729 | # Test object that is no longer registered 730 | self.assertRaises( 731 | exception.UnsupportedObjectError, 732 | FakeResource.obj_from_primitive, 733 | self.primitive) 734 | 735 | 736 | class TestStableObjectJsonFixture(test.TestCase): 737 | def test_changes_sort(self): 738 | @base.VersionedObjectRegistry.register_if(False) 739 | class TestObject(base.VersionedObject): 740 | fields = {'z': fields.StringField(), 741 | 'a': fields.StringField()} 742 | 743 | def obj_what_changed(self): 744 | return ['z', 'a'] 745 | 746 | obj = TestObject(a='foo', z='bar') 747 | self.assertEqual(['z', 'a'], 748 | obj.obj_to_primitive()['versioned_object.changes']) 749 | with fixture.StableObjectJsonFixture(): 750 | self.assertEqual( 751 | ['a', 'z'], 752 | obj.obj_to_primitive()['versioned_object.changes']) 753 | 754 | 755 | class TestMethodSpec(test.TestCase): 756 | def setUp(self): 757 | super().setUp() 758 | 759 | def test_method1(a, b, kw1=123, **kwargs): 760 | pass 761 | 762 | def test_method2(a, b, *args): 763 | pass 764 | 765 | def test_method3(a, b, *args, kw1=123, **kwargs): 766 | pass 767 | 768 | self._test_method1 = test_method1 769 | self._test_method2 = test_method2 770 | self._test_method3 = test_method3 771 | 772 | def test_method_spec_compat(self): 773 | self.assertEqual(fixture.CompatArgSpec(args=['a', 'b', 'kw1'], 774 | varargs=None, 775 | keywords='kwargs', 776 | defaults=(123,)), 777 | fixture.get_method_spec(self._test_method1)) 778 | self.assertEqual(fixture.CompatArgSpec(args=['a', 'b'], 779 | varargs='args', 780 | keywords=None, 781 | defaults=None), 782 | fixture.get_method_spec(self._test_method2)) 783 | self.assertEqual(inspect.getfullargspec(self._test_method3), 784 | fixture.get_method_spec(self._test_method3)) 785 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pbr>=6.1.1"] 3 | build-backend = "pbr.build" 4 | -------------------------------------------------------------------------------- /releasenotes/notes/add-reno-996dd44974d53238.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | other: 3 | - Introduce reno for deployer release notes. 4 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-python27-support-b3e377b0dcfa4f5c.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Support for Python 2.7 has been dropped. The minimum version of Python now 5 | supported is Python 3.6. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/remove-py38-f4e8c7ce18a5914b.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Support for Python 3.8 has been removed. Now the minimum python version 5 | supported is 3.9 . 6 | -------------------------------------------------------------------------------- /releasenotes/notes/update_md5_for_fips-e5a8f8f438ac81fb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - Updated _get_fingerprint to use new oslo.utils encapsulation of md5 to 4 | allow md5 hashes to be returned on a FIPS enabled system. 5 | -------------------------------------------------------------------------------- /releasenotes/source/2023.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2023.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/2023.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/2023.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2023.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2023.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/2024.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2024.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2024.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/2024.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2024.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2024.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/2025.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2025.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2025.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/_static/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/oslo.versionedobjects/5c2d02ef021d16da8e14799bde5c7a8e7c811756/releasenotes/source/_static/.placeholder -------------------------------------------------------------------------------- /releasenotes/source/_templates/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/oslo.versionedobjects/5c2d02ef021d16da8e14799bde5c7a8e7c811756/releasenotes/source/_templates/.placeholder -------------------------------------------------------------------------------- /releasenotes/source/conf.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # This file is execfile()d with the current directory set to its 15 | # containing dir. 16 | # 17 | # Note that not all possible configuration values are present in this 18 | # autogenerated file. 19 | # 20 | # All configuration values have a default; values that are commented out 21 | # serve to show the default. 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | # sys.path.insert(0, os.path.abspath('.')) 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'openstackdocstheme', 38 | 'reno.sphinxext', 39 | ] 40 | 41 | # openstackdocstheme options 42 | openstackdocs_repo_name = 'openstack/oslo.versionedobjects' 43 | openstackdocs_bug_project = 'oslo.versionedobjects' 44 | openstackdocs_bug_tag = '' 45 | openstackdocs_auto_name = False 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix of source filenames. 51 | source_suffix = '.rst' 52 | 53 | # The encoding of source files. 54 | # source_encoding = 'utf-8-sig' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'oslo.versionedobjects Release Notes' 61 | copyright = '2016, oslo.versionedobjects Developers' 62 | 63 | # Release notes do not need a version in the title, they span 64 | # multiple versions. 65 | # The full version, including alpha/beta/rc tags. 66 | release = '' 67 | # The short X.Y version. 68 | version = '' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | # today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | # today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = [] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | # default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | # add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | # add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | # show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'native' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | # modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | # keep_warnings = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'openstackdocs' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | # html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | # html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | # html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | # html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | # html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | # html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | # html_extra_path = [] 148 | 149 | # If true, SmartyPants will be used to convert quotes and dashes to 150 | # typographically correct entities. 151 | # html_use_smartypants = True 152 | 153 | # Custom sidebar templates, maps document names to template names. 154 | # html_sidebars = {} 155 | 156 | # Additional templates that should be rendered to pages, maps page names to 157 | # template names. 158 | # html_additional_pages = {} 159 | 160 | # If false, no module index is generated. 161 | # html_domain_indices = True 162 | 163 | # If false, no index is generated. 164 | # html_use_index = True 165 | 166 | # If true, the index is split into individual pages for each letter. 167 | # html_split_index = False 168 | 169 | # If true, links to the reST sources are added to the pages. 170 | # html_show_sourcelink = True 171 | 172 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 173 | # html_show_sphinx = True 174 | 175 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 176 | # html_show_copyright = True 177 | 178 | # If true, an OpenSearch description file will be output, and all pages will 179 | # contain a tag referring to it. The value of this option must be the 180 | # base URL from which the finished HTML is served. 181 | # html_use_opensearch = '' 182 | 183 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 184 | # html_file_suffix = None 185 | 186 | # Output file base name for HTML help builder. 187 | htmlhelp_basename = 'oslo.versionedobjectsReleaseNotesDoc' 188 | 189 | 190 | # -- Options for LaTeX output --------------------------------------------- 191 | 192 | latex_elements = { 193 | # The paper size ('letterpaper' or 'a4paper'). 194 | # 'papersize': 'letterpaper', 195 | 196 | # The font size ('10pt', '11pt' or '12pt'). 197 | # 'pointsize': '10pt', 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | # 'preamble': '', 201 | } 202 | 203 | # Grouping the document tree into LaTeX files. List of tuples 204 | # (source start file, target name, title, 205 | # author, documentclass [howto, manual, or own class]). 206 | latex_documents = [ 207 | ('index', 'oslo.versionedobjectsReleaseNotes.tex', 208 | 'oslo.versionedobjects Release Notes Documentation', 209 | 'oslo.versionedobjects Developers', 'manual'), 210 | ] 211 | 212 | # The name of an image file (relative to this directory) to place at the top of 213 | # the title page. 214 | # latex_logo = None 215 | 216 | # For "manual" documents, if this is true, then toplevel headings are parts, 217 | # not chapters. 218 | # latex_use_parts = False 219 | 220 | # If true, show page references after internal links. 221 | # latex_show_pagerefs = False 222 | 223 | # If true, show URL addresses after external links. 224 | # latex_show_urls = False 225 | 226 | # Documents to append as an appendix to all manuals. 227 | # latex_appendices = [] 228 | 229 | # If false, no module index is generated. 230 | # latex_domain_indices = True 231 | 232 | 233 | # -- Options for manual page output --------------------------------------- 234 | 235 | # One entry per manual page. List of tuples 236 | # (source start file, name, description, authors, manual section). 237 | man_pages = [ 238 | ('index', 'oslo.versionedobjectsReleaseNotes', 239 | 'oslo.versionedobjects Release Notes Documentation', 240 | ['oslo.versionedobjects Developers'], 1) 241 | ] 242 | 243 | # If true, show URL addresses after external links. 244 | # man_show_urls = False 245 | 246 | 247 | # -- Options for Texinfo output ------------------------------------------- 248 | 249 | # Grouping the document tree into Texinfo files. List of tuples 250 | # (source start file, target name, title, author, 251 | # dir menu entry, description, category) 252 | texinfo_documents = [ 253 | ('index', 'oslo.versionedobjectsReleaseNotes', 254 | 'oslo.versionedobjects Release Notes Documentation', 255 | 'oslo.versionedobjects Developers', 'oslo.versionedobjectsReleaseNotes', 256 | 'One line description of project.', 257 | 'Miscellaneous'), 258 | ] 259 | 260 | # Documents to append as an appendix to all manuals. 261 | # texinfo_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | # texinfo_domain_indices = True 265 | 266 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 267 | # texinfo_show_urls = 'footnote' 268 | 269 | # If true, do not generate a @detailmenu in the "Top" node's menu. 270 | # texinfo_no_detailmenu = False 271 | 272 | # -- Options for Internationalization output ------------------------------ 273 | locale_dirs = ['locale/'] 274 | -------------------------------------------------------------------------------- /releasenotes/source/index.rst: -------------------------------------------------------------------------------- 1 | ===================================== 2 | oslo.versionedobjects Release Notes 3 | ===================================== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | unreleased 9 | 2025.1 10 | 2024.2 11 | 2024.1 12 | 2023.2 13 | 2023.1 14 | zed 15 | yoga 16 | xena 17 | wallaby 18 | victoria 19 | ussuri 20 | train 21 | stein 22 | rocky 23 | queens 24 | pike 25 | ocata 26 | -------------------------------------------------------------------------------- /releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po: -------------------------------------------------------------------------------- 1 | # Andi Chandler , 2017. #zanata 2 | # Andi Chandler , 2018. #zanata 3 | # Andi Chandler , 2020. #zanata 4 | # Andi Chandler , 2022. #zanata 5 | # Andi Chandler , 2023. #zanata 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: oslo.versionedobjects Release Notes\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-05-08 11:19+0000\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "PO-Revision-Date: 2023-06-21 08:08+0000\n" 15 | "Last-Translator: Andi Chandler \n" 16 | "Language-Team: English (United Kingdom)\n" 17 | "Language: en_GB\n" 18 | "X-Generator: Zanata 4.3.3\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 20 | 21 | msgid "1.19.0" 22 | msgstr "1.19.0" 23 | 24 | msgid "2.0.0" 25 | msgstr "2.0.0" 26 | 27 | msgid "2.4.0" 28 | msgstr "2.4.0" 29 | 30 | msgid "2023.1 Series Release Notes" 31 | msgstr "2023.1 Series Release Notes" 32 | 33 | msgid "Current Series Release Notes" 34 | msgstr "Current Series Release Notes" 35 | 36 | msgid "Introduce reno for deployer release notes." 37 | msgstr "Introduce Reno for deployer release notes." 38 | 39 | msgid "New Features" 40 | msgstr "New Features" 41 | 42 | msgid "Ocata Series Release Notes" 43 | msgstr "Ocata Series Release Notes" 44 | 45 | msgid "Other Notes" 46 | msgstr "Other Notes" 47 | 48 | msgid "Pike Series Release Notes" 49 | msgstr "Pike Series Release Notes" 50 | 51 | msgid "Queens Series Release Notes" 52 | msgstr "Queens Series Release Notes" 53 | 54 | msgid "Rocky Series Release Notes" 55 | msgstr "Rocky Series Release Notes" 56 | 57 | msgid "Stein Series Release Notes" 58 | msgstr "Stein Series Release Notes" 59 | 60 | msgid "" 61 | "Support for Python 2.7 has been dropped. The minimum version of Python now " 62 | "supported is Python 3.6." 63 | msgstr "" 64 | "Support for Python 2.7 has been dropped. The minimum version of Python now " 65 | "supported is Python 3.6." 66 | 67 | msgid "Train Series Release Notes" 68 | msgstr "Train Series Release Notes" 69 | 70 | msgid "" 71 | "Updated _get_fingerprint to use new oslo.utils encapsulation of md5 to allow " 72 | "md5 hashes to be returned on a FIPS enabled system." 73 | msgstr "" 74 | "Updated _get_fingerprint to use new oslo.utils encapsulation of MD5 to allow " 75 | "MD5 hashes to be returned on a FIPS-enabled system." 76 | 77 | msgid "Upgrade Notes" 78 | msgstr "Upgrade Notes" 79 | 80 | msgid "Ussuri Series Release Notes" 81 | msgstr "Ussuri Series Release Notes" 82 | 83 | msgid "Victoria Series Release Notes" 84 | msgstr "Victoria Series Release Notes" 85 | 86 | msgid "Wallaby Series Release Notes" 87 | msgstr "Wallaby Series Release Notes" 88 | 89 | msgid "Xena Series Release Notes" 90 | msgstr "Xena Series Release Notes" 91 | 92 | msgid "Yoga Series Release Notes" 93 | msgstr "Yoga Series Release Notes" 94 | 95 | msgid "Zed Series Release Notes" 96 | msgstr "Zed Series Release Notes" 97 | 98 | msgid "oslo.versionedobjects Release Notes" 99 | msgstr "oslo.versionedobjects Release Notes" 100 | -------------------------------------------------------------------------------- /releasenotes/source/ocata.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Ocata Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: origin/stable/ocata 7 | -------------------------------------------------------------------------------- /releasenotes/source/pike.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Pike Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/pike 7 | -------------------------------------------------------------------------------- /releasenotes/source/queens.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Queens Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/queens 7 | -------------------------------------------------------------------------------- /releasenotes/source/rocky.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Rocky Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/rocky 7 | -------------------------------------------------------------------------------- /releasenotes/source/stein.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Stein Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/stein 7 | -------------------------------------------------------------------------------- /releasenotes/source/train.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Train Series Release Notes 3 | ========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/train 7 | -------------------------------------------------------------------------------- /releasenotes/source/unreleased.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Current Series Release Notes 3 | ============================== 4 | 5 | .. release-notes:: 6 | -------------------------------------------------------------------------------- /releasenotes/source/ussuri.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Ussuri Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/ussuri 7 | -------------------------------------------------------------------------------- /releasenotes/source/victoria.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Victoria Series Release Notes 3 | ============================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/victoria 7 | -------------------------------------------------------------------------------- /releasenotes/source/wallaby.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Wallaby Series Release Notes 3 | ============================ 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/wallaby 7 | -------------------------------------------------------------------------------- /releasenotes/source/xena.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Xena Series Release Notes 3 | ========================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/xena 7 | -------------------------------------------------------------------------------- /releasenotes/source/yoga.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Yoga Series Release Notes 3 | ========================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/yoga 7 | -------------------------------------------------------------------------------- /releasenotes/source/zed.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Zed Series Release Notes 3 | ======================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/zed 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements lower bounds listed here are our best effort to keep them up to 2 | # date but we do not test them so no guarantee of having them all correct. If 3 | # you find any incorrect lower bounds, let us know or propose a fix. 4 | 5 | oslo.concurrency>=3.26.0 # Apache-2.0 6 | oslo.config>=5.2.0 # Apache-2.0 7 | oslo.context>=2.19.2 # Apache-2.0 8 | oslo.messaging>=5.29.0 # Apache-2.0 9 | oslo.serialization>=2.18.0 # Apache-2.0 10 | oslo.utils>=7.4.0 # Apache-2.0 11 | oslo.log>=3.36.0 # Apache-2.0 12 | oslo.i18n>=3.15.3 # Apache-2.0 13 | WebOb>=1.7.1 # MIT 14 | netaddr>=0.7.18 # BSD 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = oslo.versionedobjects 3 | summary = Oslo Versioned Objects library 4 | description_file = 5 | README.rst 6 | author = OpenStack 7 | author_email = openstack-discuss@lists.openstack.org 8 | home_page = https://docs.openstack.org/oslo.versionedobjects/latest/ 9 | python_requires = >=3.9 10 | classifier = 11 | Environment :: OpenStack 12 | Intended Audience :: Information Technology 13 | Intended Audience :: System Administrators 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: POSIX :: Linux 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.9 19 | Programming Language :: Python :: 3.10 20 | Programming Language :: Python :: 3.11 21 | Programming Language :: Python :: 3.12 22 | Programming Language :: Python :: 3 :: Only 23 | Programming Language :: Python :: Implementation :: CPython 24 | 25 | [files] 26 | packages = 27 | oslo_versionedobjects 28 | 29 | [entry_points] 30 | oslo.config.opts = 31 | oslo.versionedobjects = oslo_versionedobjects._options:list_opts 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import setuptools 17 | 18 | setuptools.setup( 19 | setup_requires=['pbr>=2.0.0'], 20 | pbr=True) 21 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | oslotest>=3.2.0 # Apache-2.0 2 | testtools>=2.2.0 # MIT 3 | coverage>=4.0 # Apache-2.0 4 | jsonschema>=3.2.0 # MIT 5 | stestr>=2.0.0 # Apache-2.0 6 | 7 | fixtures>=3.0.0 # Apache-2.0/BSD 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.18.0 3 | envlist = py3,pep8 4 | 5 | [testenv] 6 | deps = 7 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 8 | -r{toxinidir}/test-requirements.txt 9 | commands = stestr run --slowest {posargs} 10 | 11 | [testenv:pep8] 12 | skip_install = true 13 | deps = 14 | pre-commit>=2.6.0 # MIT 15 | commands = 16 | pre-commit run -a 17 | 18 | [testenv:venv] 19 | commands = {posargs} 20 | 21 | [testenv:cover] 22 | commands = python setup.py test --coverage --coverage-package-name=oslo_versionedobjects --testr-args='{posargs}' 23 | 24 | [testenv:docs] 25 | allowlist_externals = rm 26 | deps = 27 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 28 | -r{toxinidir}/doc/requirements.txt 29 | commands = 30 | rm -fr doc/build 31 | sphinx-build -W --keep-going -b html doc/source doc/build/html 32 | 33 | [testenv:releasenotes] 34 | allowlist_externals = rm 35 | deps = {[testenv:docs]deps} 36 | commands = 37 | rm -rf releasenotes/build 38 | sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html 39 | 40 | [flake8] 41 | # E123, E125 skipped as they are invalid PEP-8. 42 | # W504 skipped as you must choose this or W503 43 | show-source = True 44 | ignore = E123,E125,W504 45 | builtins = _ 46 | exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build 47 | 48 | [hacking] 49 | import_exceptions = oslo_versionedobjects._i18n 50 | --------------------------------------------------------------------------------