diff options
Diffstat (limited to 'contrib/python/propcache')
19 files changed, 1796 insertions, 0 deletions
diff --git a/contrib/python/propcache/.dist-info/METADATA b/contrib/python/propcache/.dist-info/METADATA new file mode 100644 index 00000000000..37cb00053d8 --- /dev/null +++ b/contrib/python/propcache/.dist-info/METADATA @@ -0,0 +1,539 @@ +Metadata-Version: 2.4 +Name: propcache +Version: 0.5.2 +Summary: Accelerated property cache +Home-page: https://github.com/aio-libs/propcache +Author: Andrew Svetlov +Author-email: [email protected] +Maintainer: aiohttp team <[email protected]> +Maintainer-email: [email protected] +License: Apache-2.0 +Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org +Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org +Project-URL: CI: GitHub Workflows, https://github.com/aio-libs/propcache/actions?query=branch:master +Project-URL: Code of Conduct, https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md +Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/propcache +Project-URL: Docs: Changelog, https://propcache.readthedocs.io/en/latest/changes/ +Project-URL: Docs: RTD, https://propcache.readthedocs.io +Project-URL: GitHub: issues, https://github.com/aio-libs/propcache/issues +Project-URL: GitHub: repo, https://github.com/aio-libs/propcache +Keywords: cython,cext,propcache +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: NOTICE +Dynamic: license-file + +propcache +========= + +The module provides a fast implementation of cached properties for Python 3.10+. + +.. image:: https://github.com/aio-libs/propcache/actions/workflows/ci-cd.yml/badge.svg + :target: https://github.com/aio-libs/propcache/actions?query=workflow%3ACI + :align: right + +.. image:: https://codecov.io/gh/aio-libs/propcache/branch/master/graph/badge.svg + :target: https://codecov.io/gh/aio-libs/propcache + +.. image:: https://badge.fury.io/py/propcache.svg + :target: https://badge.fury.io/py/propcache + + +.. image:: https://readthedocs.org/projects/propcache/badge/?version=latest + :target: https://propcache.readthedocs.io + + +.. image:: https://img.shields.io/pypi/pyversions/propcache.svg + :target: https://pypi.python.org/pypi/propcache + +.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs:matrix.org + :alt: Matrix Room — #aio-libs:matrix.org + +.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs-space:matrix.org + :alt: Matrix Space — #aio-libs-space:matrix.org + +Introduction +------------ + +The API is designed to be nearly identical to the built-in ``functools.cached_property`` class, +except for the additional ``under_cached_property`` class which uses ``self._cache`` +instead of ``self.__dict__`` to store the cached values and prevents ``__set__`` from being called. + +For full documentation please read https://propcache.readthedocs.io. + +Installation +------------ + +:: + + $ pip install propcache + +The library is Python 3 only! + +PyPI contains binary wheels for Linux, Windows and MacOS. If you want to install +``propcache`` on another operating system where wheels are not provided, +the the tarball will be used to compile the library from +the source code. It requires a C compiler and and Python headers installed. + +To skip the compilation you must explicitly opt-in by using a PEP 517 +configuration setting ``pure-python``, or setting the ``PROPCACHE_NO_EXTENSIONS`` +environment variable to a non-empty value, e.g.: + +.. code-block:: console + + $ pip install propcache --config-settings=pure-python=false + +Please note that the pure-Python (uncompiled) version is much slower. However, +PyPy always uses a pure-Python implementation, and, as such, it is unaffected +by this variable. + + +API documentation +------------------ + +The documentation is located at https://propcache.readthedocs.io. + +Source code +----------- + +The project is hosted on GitHub_ + +Please file an issue on the `bug tracker +<https://github.com/aio-libs/propcache/issues>`_ if you have found a bug +or have some suggestion in order to improve the library. + +Discussion list +--------------- + +*aio-libs* google group: https://groups.google.com/forum/#!forum/aio-libs + +Feel free to post your questions and ideas here. + + +Authors and License +------------------- + +The ``propcache`` package is derived from ``yarl`` which is written by Andrew Svetlov. + +It's *Apache 2* licensed and freely available. + + +.. _GitHub: https://github.com/aio-libs/propcache + +========= +Changelog +========= + +.. + You should *NOT* be adding new change log entries to this file, this + file is managed by towncrier. You *may* edit previous change logs to + fix problems like typo corrections or such. + To add a new change log entry, please see + https://pip.pypa.io/en/latest/development/#adding-a-news-entry + we named the news folder "changes". + + WARNING: Don't drop the next directive! + +.. towncrier release notes start + +0.5.2 +===== + +*(2026-05-08)* + + +No significant changes. + + +---- + + +0.5.1 +===== + +*(2026-05-08)* + + +No significant changes. + + +---- + + +0.5.0 +===== + +*(2026-05-08)* + + +Features +-------- + +- Added support for newer type hints and remove ``Optional`` and ``Union`` from all annotations + -- by `@Vizonex <https://github.com/sponsors/Vizonex>`__ + + *Related issues and pull requests on GitHub:* + `#193 <https://github.com/aio-libs/propcache/issues/193>`__. + + +Removals and backward incompatible breaking changes +--------------------------------------------------- + +- Dropped support for Python 3.9 as it has reached end of life. + + *Related issues and pull requests on GitHub:* + `#216 <https://github.com/aio-libs/propcache/issues/216>`__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Changed the Cython build dependency from ``~= 3.1.0`` to ``>= 3.2.0``, + removing the upper version bound to avoid conflicts for downstream packagers + -- by `@jameshilliard <https://github.com/sponsors/jameshilliard>`__ and `@gundalow <https://github.com/sponsors/gundalow>`__. + + The upstream Cython version is pinned to 3.2.4 in the CI/CD environment. + + *Related issues and pull requests on GitHub:* + `#184 <https://github.com/aio-libs/propcache/issues/184>`__, `#188 <https://github.com/aio-libs/propcache/issues/188>`__, `#214 <https://github.com/aio-libs/propcache/issues/214>`__. + +- Start building and shipping riscv64 wheels + -- by `@justeph <https://github.com/sponsors/justeph>`__. + + *Related issues and pull requests on GitHub:* + `#194 <https://github.com/aio-libs/propcache/issues/194>`__. + +- The `PEP 517 <https://peps.python.org/pep-517>`__ build backend now supports a new ``build-inplace`` + config setting (and ``PROPCACHE_BUILD_INPLACE`` environment variable) + for controlling whether to build the project in-tree or in a + temporary directory. It only affects wheels and is set up to build + in a temporary directory by default. It does not affect editable + wheel builds; they will keep being built in-tree regardless. + + Here's an example of using this setting: + + .. code-block:: console + + $ python -m build --config-setting=build-inplace=true + + Additionally, when building wheels in an automatically created + temporary directory, the build backend now normalizes the + respective file system path to a deterministic source checkout + directory by injecting the ``-ffile-prefix-map`` compiler option + into the ``CFLAGS`` environment variable, as suggested by known + `reproducible build practices + <https://reproducible-builds.org/docs/build-path/>`__. + + The effect is that downstreams will get reproducible build results. + + *Related issues and pull requests on GitHub:* + `#218 <https://github.com/aio-libs/propcache/issues/218>`__. + + +---- + + +0.4.1 +===== + +*(2025-10-08)* + + +Bug fixes +--------- + +- Fixed reference leak caused by ``Py_INCREF`` because Cython has its own reference counter systems -- by `@Vizonex <https://github.com/sponsors/Vizonex>`__. + + *Related issues and pull requests on GitHub:* + `#162 <https://github.com/aio-libs/propcache/issues/162>`__. + + +Contributor-facing changes +-------------------------- + +- Fixes the default value for the ``os`` + parameter in ``reusable-build-wheel.yml`` + to be ``ubuntu-latest`` instead of + ``ubuntu``. + + *Related issues and pull requests on GitHub:* + `#155 <https://github.com/aio-libs/propcache/issues/155>`__. + + +---- + + +0.4.0 +===== + +*(2025-10-04)* + + +Features +-------- + +- Optimized propcache by replacing sentinel ``object`` for checking if + the ``object`` is ``NULL`` and changed ``dict`` API for + Python C-API -- by `@Vizonex <https://github.com/sponsors/Vizonex>`__. + + *Related issues and pull requests on GitHub:* + `#121 <https://github.com/aio-libs/propcache/issues/121>`__. + + +Contributor-facing changes +-------------------------- + +- Builds have been added for arm64 Windows + wheels and the ``reusable-build-wheel.yml`` + workflow has been modified to allow for + an OS value (``windows-11-arm``) which + does not include the ``-latest`` postfix + -- by `@finnagin <https://github.com/sponsors/finnagin>`__. + + *Related issues and pull requests on GitHub:* + `#133 <https://github.com/aio-libs/propcache/issues/133>`__. + +- Added CI for CPython 3.14 -- by `@kumaraditya303 <https://github.com/sponsors/kumaraditya303>`__. + + *Related issues and pull requests on GitHub:* + `#140 <https://github.com/aio-libs/propcache/issues/140>`__. + + +---- + + +0.3.2 +===== + +*(2025-06-09)* + + +Improved documentation +---------------------- + +- Fixed incorrect decorator usage in the ``~propcache.api.under_cached_property`` example code -- by `@meanmail <https://github.com/sponsors/meanmail>`__. + + *Related issues and pull requests on GitHub:* + `#109 <https://github.com/aio-libs/propcache/issues/109>`__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Updated to use Cython 3.1 universally across the build path -- by `@lysnikolaou <https://github.com/sponsors/lysnikolaou>`__. + + *Related issues and pull requests on GitHub:* + `#117 <https://github.com/aio-libs/propcache/issues/117>`__. + +- Made Cython line tracing opt-in via the ``with-cython-tracing`` build config setting -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + Previously, line tracing was enabled by default in ``pyproject.toml``, which caused build issues for some users and made wheels nearly twice as slow. + + Now line tracing is only enabled when explicitly requested via ``pip install . --config-setting=with-cython-tracing=true`` or by setting the ``PROPCACHE_CYTHON_TRACING`` environment variable. + + *Related issues and pull requests on GitHub:* + `#118 <https://github.com/aio-libs/propcache/issues/118>`__. + + +---- + + +0.3.1 +===== + +*(2025-03-25)* + + +Bug fixes +--------- + +- Improved typing annotations, fixing some type errors under correct usage + and improving typing robustness generally -- by `@Dreamsorcerer <https://github.com/sponsors/Dreamsorcerer>`__. + + *Related issues and pull requests on GitHub:* + `#103 <https://github.com/aio-libs/propcache/issues/103>`__. + + +---- + + +0.3.0 +===== + +*(2025-02-20)* + + +Features +-------- + +- Implemented support for the free-threaded build of CPython 3.13 -- by `@lysnikolaou <https://github.com/sponsors/lysnikolaou>`__. + + *Related issues and pull requests on GitHub:* + `#84 <https://github.com/aio-libs/propcache/issues/84>`__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Started building wheels for the free-threaded build of CPython 3.13 -- by `@lysnikolaou <https://github.com/sponsors/lysnikolaou>`__. + + *Related issues and pull requests on GitHub:* + `#84 <https://github.com/aio-libs/propcache/issues/84>`__. + + +Contributor-facing changes +-------------------------- + +- GitHub Actions CI/CD is now configured to manage caching pip-ecosystem + dependencies using `re-actors/cache-python-deps`_ -- an action by + `@webknjaz <https://github.com/sponsors/webknjaz>`__ that takes into account ABI stability and the exact + version of Python runtime. + + .. _`re-actors/cache-python-deps`: + https://github.com/marketplace/actions/cache-python-deps + + *Related issues and pull requests on GitHub:* + `#93 <https://github.com/aio-libs/propcache/issues/93>`__. + + +---- + + +0.2.1 +===== + +*(2024-12-01)* + + +Bug fixes +--------- + +- Stopped implicitly allowing the use of Cython pre-release versions when + building the distribution package -- by `@ajsanchezsanz <https://github.com/sponsors/ajsanchezsanz>`__ and + `@markgreene74 <https://github.com/sponsors/markgreene74>`__. + + *Related commits on GitHub:* + `64df0a6 <https://github.com/aio-libs/propcache/commit/64df0a6>`__. + +- Fixed ``wrapped`` and ``func`` not being accessible in the Cython versions of ``propcache.api.cached_property`` and ``propcache.api.under_cached_property`` decorators -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + *Related issues and pull requests on GitHub:* + `#72 <https://github.com/aio-libs/propcache/issues/72>`__. + + +Removals and backward incompatible breaking changes +--------------------------------------------------- + +- Removed support for Python 3.8 as it has reached end of life -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + *Related issues and pull requests on GitHub:* + `#57 <https://github.com/aio-libs/propcache/issues/57>`__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Stopped implicitly allowing the use of Cython pre-release versions when + building the distribution package -- by `@ajsanchezsanz <https://github.com/sponsors/ajsanchezsanz>`__ and + `@markgreene74 <https://github.com/sponsors/markgreene74>`__. + + *Related commits on GitHub:* + `64df0a6 <https://github.com/aio-libs/propcache/commit/64df0a6>`__. + + +---- + + +0.2.0 +===== + +*(2024-10-07)* + + +Bug fixes +--------- + +- Fixed loading the C-extensions on Python 3.8 -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + *Related issues and pull requests on GitHub:* + `#26 <https://github.com/aio-libs/propcache/issues/26>`__. + + +Features +-------- + +- Improved typing for the ``propcache.api.under_cached_property`` decorator -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + *Related issues and pull requests on GitHub:* + `#38 <https://github.com/aio-libs/propcache/issues/38>`__. + + +Improved documentation +---------------------- + +- Added API documentation for the ``propcache.api.cached_property`` and ``propcache.api.under_cached_property`` decorators -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + *Related issues and pull requests on GitHub:* + `#16 <https://github.com/aio-libs/propcache/issues/16>`__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Moved ``propcache.api.under_cached_property`` and ``propcache.api.cached_property`` to `propcache.api` -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + Both decorators remain importable from the top-level package, however importing from `propcache.api` is now the recommended way to use them. + + *Related issues and pull requests on GitHub:* + `#19 <https://github.com/aio-libs/propcache/issues/19>`__, `#24 <https://github.com/aio-libs/propcache/issues/24>`__, `#32 <https://github.com/aio-libs/propcache/issues/32>`__. + +- Converted project to use a src layout -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + *Related issues and pull requests on GitHub:* + `#22 <https://github.com/aio-libs/propcache/issues/22>`__, `#29 <https://github.com/aio-libs/propcache/issues/29>`__, `#37 <https://github.com/aio-libs/propcache/issues/37>`__. + + +---- + + +0.1.0 +===== + +*(2024-10-03)* + + +Features +-------- + +- Added ``armv7l`` wheels -- by `@bdraco <https://github.com/sponsors/bdraco>`__. + + *Related issues and pull requests on GitHub:* + `#5 <https://github.com/aio-libs/propcache/issues/5>`__. + + +---- + + +0.0.0 +===== + +*(2024-10-02)* + + +- Initial release. diff --git a/contrib/python/propcache/.dist-info/top_level.txt b/contrib/python/propcache/.dist-info/top_level.txt new file mode 100644 index 00000000000..8c9accf6226 --- /dev/null +++ b/contrib/python/propcache/.dist-info/top_level.txt @@ -0,0 +1 @@ +propcache diff --git a/contrib/python/propcache/.yandex_meta/yamaker.yaml b/contrib/python/propcache/.yandex_meta/yamaker.yaml new file mode 100644 index 00000000000..4daee40a45c --- /dev/null +++ b/contrib/python/propcache/.yandex_meta/yamaker.yaml @@ -0,0 +1,4 @@ +mark_as_cython_c: + - propcache/_helpers_c.pyx +exclude: + - tests/test_benchmarks.py diff --git a/contrib/python/propcache/LICENSE b/contrib/python/propcache/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/contrib/python/propcache/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/contrib/python/propcache/NOTICE b/contrib/python/propcache/NOTICE new file mode 100644 index 00000000000..fa53b2b138d --- /dev/null +++ b/contrib/python/propcache/NOTICE @@ -0,0 +1,13 @@ + Copyright 2016-2021, Andrew Svetlov and aio-libs team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/contrib/python/propcache/README.rst b/contrib/python/propcache/README.rst new file mode 100644 index 00000000000..ce10fbf8037 --- /dev/null +++ b/contrib/python/propcache/README.rst @@ -0,0 +1,98 @@ +propcache +========= + +The module provides a fast implementation of cached properties for Python 3.10+. + +.. image:: https://github.com/aio-libs/propcache/actions/workflows/ci-cd.yml/badge.svg + :target: https://github.com/aio-libs/propcache/actions?query=workflow%3ACI + :align: right + +.. image:: https://codecov.io/gh/aio-libs/propcache/branch/master/graph/badge.svg + :target: https://codecov.io/gh/aio-libs/propcache + +.. image:: https://badge.fury.io/py/propcache.svg + :target: https://badge.fury.io/py/propcache + + +.. image:: https://readthedocs.org/projects/propcache/badge/?version=latest + :target: https://propcache.readthedocs.io + + +.. image:: https://img.shields.io/pypi/pyversions/propcache.svg + :target: https://pypi.python.org/pypi/propcache + +.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs:matrix.org + :alt: Matrix Room — #aio-libs:matrix.org + +.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs-space:matrix.org + :alt: Matrix Space — #aio-libs-space:matrix.org + +Introduction +------------ + +The API is designed to be nearly identical to the built-in ``functools.cached_property`` class, +except for the additional ``under_cached_property`` class which uses ``self._cache`` +instead of ``self.__dict__`` to store the cached values and prevents ``__set__`` from being called. + +For full documentation please read https://propcache.readthedocs.io. + +Installation +------------ + +:: + + $ pip install propcache + +The library is Python 3 only! + +PyPI contains binary wheels for Linux, Windows and MacOS. If you want to install +``propcache`` on another operating system where wheels are not provided, +the the tarball will be used to compile the library from +the source code. It requires a C compiler and and Python headers installed. + +To skip the compilation you must explicitly opt-in by using a PEP 517 +configuration setting ``pure-python``, or setting the ``PROPCACHE_NO_EXTENSIONS`` +environment variable to a non-empty value, e.g.: + +.. code-block:: console + + $ pip install propcache --config-settings=pure-python=false + +Please note that the pure-Python (uncompiled) version is much slower. However, +PyPy always uses a pure-Python implementation, and, as such, it is unaffected +by this variable. + + +API documentation +------------------ + +The documentation is located at https://propcache.readthedocs.io. + +Source code +----------- + +The project is hosted on GitHub_ + +Please file an issue on the `bug tracker +<https://github.com/aio-libs/propcache/issues>`_ if you have found a bug +or have some suggestion in order to improve the library. + +Discussion list +--------------- + +*aio-libs* google group: https://groups.google.com/forum/#!forum/aio-libs + +Feel free to post your questions and ideas here. + + +Authors and License +------------------- + +The ``propcache`` package is derived from ``yarl`` which is written by Andrew Svetlov. + +It's *Apache 2* licensed and freely available. + + +.. _GitHub: https://github.com/aio-libs/propcache diff --git a/contrib/python/propcache/propcache/__init__.py b/contrib/python/propcache/propcache/__init__.py new file mode 100644 index 00000000000..bbfd78a3628 --- /dev/null +++ b/contrib/python/propcache/propcache/__init__.py @@ -0,0 +1,32 @@ +"""propcache: An accelerated property cache for Python classes.""" + +from typing import TYPE_CHECKING + +_PUBLIC_API = ("cached_property", "under_cached_property") + +__version__ = "0.5.2" +__all__ = () + +# Imports have moved to `propcache.api` in 0.2.0+. +# This module is now a facade for the API. +if TYPE_CHECKING: + from .api import cached_property as cached_property # noqa: F401 + from .api import under_cached_property as under_cached_property # noqa: F401 + + +def _import_facade(attr: str) -> object: + """Import the public API from the `api` module.""" + if attr in _PUBLIC_API: + from . import api # pylint: disable=import-outside-toplevel + + return getattr(api, attr) + raise AttributeError(f"module '{__package__}' has no attribute '{attr}'") + + +def _dir_facade() -> list[str]: + """Include the public API in the module's dir() output.""" + return [*_PUBLIC_API, *globals().keys()] + + +__getattr__ = _import_facade +__dir__ = _dir_facade diff --git a/contrib/python/propcache/propcache/_helpers.py b/contrib/python/propcache/propcache/_helpers.py new file mode 100644 index 00000000000..1e52895c151 --- /dev/null +++ b/contrib/python/propcache/propcache/_helpers.py @@ -0,0 +1,39 @@ +import os +import sys +from typing import TYPE_CHECKING + +__all__ = ("cached_property", "under_cached_property") + + +NO_EXTENSIONS = bool(os.environ.get("PROPCACHE_NO_EXTENSIONS")) # type: bool +if sys.implementation.name != "cpython": + NO_EXTENSIONS = True + + +# isort: off +if TYPE_CHECKING: + from ._helpers_py import cached_property as cached_property_py + from ._helpers_py import under_cached_property as under_cached_property_py + + cached_property = cached_property_py + under_cached_property = under_cached_property_py +elif not NO_EXTENSIONS: # pragma: no branch + try: + from ._helpers_c import cached_property as cached_property_c # type: ignore[attr-defined, unused-ignore] + from ._helpers_c import under_cached_property as under_cached_property_c # type: ignore[attr-defined, unused-ignore] + + cached_property = cached_property_c + under_cached_property = under_cached_property_c + except ImportError: # pragma: no cover + from ._helpers_py import cached_property as cached_property_py + from ._helpers_py import under_cached_property as under_cached_property_py + + cached_property = cached_property_py # type: ignore[assignment, misc] + under_cached_property = under_cached_property_py +else: + from ._helpers_py import cached_property as cached_property_py + from ._helpers_py import under_cached_property as under_cached_property_py + + cached_property = cached_property_py # type: ignore[assignment, misc] + under_cached_property = under_cached_property_py +# isort: on diff --git a/contrib/python/propcache/propcache/_helpers_c.pyx b/contrib/python/propcache/propcache/_helpers_c.pyx new file mode 100644 index 00000000000..9e9e5589b95 --- /dev/null +++ b/contrib/python/propcache/propcache/_helpers_c.pyx @@ -0,0 +1,103 @@ +# cython: language_level=3, freethreading_compatible=True +from types import GenericAlias + +from cpython.dict cimport PyDict_GetItem +from cpython.object cimport PyObject + + +cdef extern from "Python.h": + # Call a callable Python object callable with exactly + # 1 positional argument arg and no keyword arguments. + # Return the result of the call on success, or raise + # an exception and return NULL on failure. + PyObject* PyObject_CallOneArg( + object callable, object arg + ) except NULL + int PyDict_SetItem( + object dict, object key, PyObject* value + ) except -1 + void Py_DECREF(PyObject*) + + +cdef class under_cached_property: + """Use as a class method decorator. It operates almost exactly like + the Python `@property` decorator, but it puts the result of the + method it decorates into the instance dict after the first call, + effectively replacing the function it decorates with an instance + variable. It is, in Python parlance, a data descriptor. + + """ + + cdef readonly object wrapped + cdef object name + + def __init__(self, object wrapped): + self.wrapped = wrapped + self.name = wrapped.__name__ + + @property + def __doc__(self): + return self.wrapped.__doc__ + + def __get__(self, object inst, owner): + if inst is None: + return self + cdef dict cache = inst._cache + cdef PyObject* val = PyDict_GetItem(cache, self.name) + if val == NULL: + val = PyObject_CallOneArg(self.wrapped, inst) + PyDict_SetItem(cache, self.name, val) + Py_DECREF(val) + return <object>val + + def __set__(self, inst, value): + raise AttributeError("cached property is read-only") + + __class_getitem__ = classmethod(GenericAlias) + + +cdef class cached_property: + """Use as a class method decorator. It operates almost exactly like + the Python `@property` decorator, but it puts the result of the + method it decorates into the instance dict after the first call, + effectively replacing the function it decorates with an instance + variable. It is, in Python parlance, a data descriptor. + + """ + + cdef readonly object func + cdef object name + + def __init__(self, func): + self.func = func + self.name = None + + @property + def __doc__(self): + return self.func.__doc__ + + def __set_name__(self, owner, object name): + if self.name is None: + self.name = name + elif name != self.name: + raise TypeError( + "Cannot assign the same cached_property to two different names " + f"({self.name!r} and {name!r})." + ) + + def __get__(self, inst, owner): + if inst is None: + return self + if self.name is None: + raise TypeError( + "Cannot use cached_property instance" + " without calling __set_name__ on it.") + cdef dict cache = inst.__dict__ + cdef PyObject* val = PyDict_GetItem(cache, self.name) + if val is NULL: + val = PyObject_CallOneArg(self.func, inst) + PyDict_SetItem(cache, self.name, val) + Py_DECREF(val) + return <object>val + + __class_getitem__ = classmethod(GenericAlias) diff --git a/contrib/python/propcache/propcache/_helpers_py.py b/contrib/python/propcache/propcache/_helpers_py.py new file mode 100644 index 00000000000..db5b8d6378f --- /dev/null +++ b/contrib/python/propcache/propcache/_helpers_py.py @@ -0,0 +1,64 @@ +"""Various helper functions.""" + +from __future__ import annotations + +import sys +from collections.abc import Callable, Mapping +from functools import cached_property +from typing import Any, Generic, Protocol, TypeVar, overload + +__all__ = ("under_cached_property", "cached_property") + + +if sys.version_info >= (3, 11): + from typing import Self +else: + Self = Any + +_T = TypeVar("_T") +# We use Mapping to make it possible to use TypedDict, but this isn't +# technically type safe as we need to assign into the dict. +_Cache = TypeVar("_Cache", bound=Mapping[str, Any]) + + +class _CacheImpl(Protocol[_Cache]): + _cache: _Cache + + +class under_cached_property(Generic[_T]): + """Use as a class method decorator. + + It operates almost exactly like + the Python `@property` decorator, but it puts the result of the + method it decorates into the instance dict after the first call, + effectively replacing the function it decorates with an instance + variable. It is, in Python parlance, a data descriptor. + """ + + def __init__(self, wrapped: Callable[[Any], _T]) -> None: + self.wrapped = wrapped + self.__doc__ = wrapped.__doc__ + self.name = wrapped.__name__ + + @overload + def __get__(self, inst: None, owner: type[object] | None = None) -> Self: ... + + @overload + def __get__( + self, inst: _CacheImpl[Any], owner: type[object] | None = None + ) -> _T: ... + + def __get__( + self, inst: _CacheImpl[Any] | None, owner: type[object] | None = None + ) -> _T | Self: + if inst is None: + return self + try: + return inst._cache[self.name] # type: ignore[no-any-return] + except KeyError: + val = self.wrapped(inst) + inst._cache[self.name] = val + return val + + def __set__(self, inst: _CacheImpl[Any], value: _T) -> None: + raise AttributeError("cached property is read-only") diff --git a/contrib/python/propcache/propcache/api.py b/contrib/python/propcache/propcache/api.py new file mode 100644 index 00000000000..22389e6337f --- /dev/null +++ b/contrib/python/propcache/propcache/api.py @@ -0,0 +1,8 @@ +"""Public API of the property caching library.""" + +from ._helpers import cached_property, under_cached_property + +__all__ = ( + "cached_property", + "under_cached_property", +) diff --git a/contrib/python/propcache/propcache/py.typed b/contrib/python/propcache/propcache/py.typed new file mode 100644 index 00000000000..dcf2c804da5 --- /dev/null +++ b/contrib/python/propcache/propcache/py.typed @@ -0,0 +1 @@ +# Placeholder diff --git a/contrib/python/propcache/tests/conftest.py b/contrib/python/propcache/tests/conftest.py new file mode 100644 index 00000000000..ee43f339231 --- /dev/null +++ b/contrib/python/propcache/tests/conftest.py @@ -0,0 +1,115 @@ +import argparse +from dataclasses import dataclass +from functools import cached_property +from importlib import import_module +from types import ModuleType + +import pytest + +C_EXT_MARK = pytest.mark.c_extension + + +@dataclass(frozen=True) +class PropcacheImplementation: + """A facade for accessing importable propcache module variants. + + An instance essentially represents a c-extension or a pure-python module. + The actual underlying module is accessed dynamically through a property and + is cached. + + It also has a text tag depending on what variant it is, and a string + representation suitable for use in Pytest's test IDs via parametrization. + """ + + is_pure_python: bool + """A flag showing whether this is a pure-python module or a C-extension.""" + + @cached_property + def tag(self) -> str: + """Return a text representation of the pure-python attribute.""" + return "pure-python" if self.is_pure_python else "c-extension" + + @cached_property + def imported_module(self) -> ModuleType: + """Return a loaded importable containing a propcache variant.""" + importable_module = "_helpers_py" if self.is_pure_python else "_helpers_c" + return import_module(f"propcache.{importable_module}") + + def __str__(self) -> str: + """Render the implementation facade instance as a string.""" + return f"{self.tag}-module" + + + scope="session", + params=( + pytest.param( + PropcacheImplementation(is_pure_python=False), + marks=C_EXT_MARK, + ), + PropcacheImplementation(is_pure_python=True), + ), + ids=str, +) +def propcache_implementation(request: pytest.FixtureRequest) -> PropcacheImplementation: + """Return a propcache variant facade.""" + return request.param # type: ignore[no-any-return] + + [email protected](scope="session") +def propcache_module( + propcache_implementation: PropcacheImplementation, +) -> ModuleType: + """Return a pre-imported module containing a propcache variant.""" + return propcache_implementation.imported_module + + +def pytest_addoption( + parser: pytest.Parser, + pluginmanager: pytest.PytestPluginManager, +) -> None: + """Define a new ``--c-extensions`` flag. + + This lets the callers deselect tests executed against the C-extension + version of the ``propcache`` implementation. + """ + del pluginmanager + parser.addoption( + "--c-extensions", # disabled with `--no-c-extensions` + action=argparse.BooleanOptionalAction, + default=True, + dest="c_extensions", + help="Test C-extensions (on by default)", + ) + + +def pytest_collection_modifyitems( + session: pytest.Session, + config: pytest.Config, + items: list[pytest.Item], +) -> None: + """Deselect tests against C-extensions when requested via CLI.""" + test_c_extensions = config.getoption("--c-extensions") is True + + if test_c_extensions: + return + + selected_tests: list[pytest.Item] = [] + deselected_tests: list[pytest.Item] = [] + + for item in items: + c_ext = item.get_closest_marker(C_EXT_MARK.name) is not None + + target_items_list = deselected_tests if c_ext else selected_tests + target_items_list.append(item) + + config.hook.pytest_deselected(items=deselected_tests) + items[:] = selected_tests + + +def pytest_configure(config: pytest.Config) -> None: + """Declare the C-extension marker in config.""" + config.addinivalue_line( + "markers", + f"{C_EXT_MARK.name}: tests running against the C-extension implementation.", + ) diff --git a/contrib/python/propcache/tests/test_api.py b/contrib/python/propcache/tests/test_api.py new file mode 100644 index 00000000000..85beec3606a --- /dev/null +++ b/contrib/python/propcache/tests/test_api.py @@ -0,0 +1,11 @@ +"""Test we do not break the public API.""" + +from propcache import _helpers, api + + +def test_api() -> None: + """Verify the public API is accessible.""" + assert api.cached_property is not None + assert api.under_cached_property is not None + assert api.cached_property is _helpers.cached_property + assert api.under_cached_property is _helpers.under_cached_property diff --git a/contrib/python/propcache/tests/test_cached_property.py b/contrib/python/propcache/tests/test_cached_property.py new file mode 100644 index 00000000000..a46c715c31b --- /dev/null +++ b/contrib/python/propcache/tests/test_cached_property.py @@ -0,0 +1,226 @@ +import gc +import sys +from collections.abc import Callable +from operator import not_ +from typing import TYPE_CHECKING, Any, Protocol, TypeVar + +import pytest + +from propcache.api import cached_property + +IS_PYPY = hasattr(sys, "pypy_version_info") + +if sys.version_info >= (3, 11): + from typing import assert_type + +_T_co = TypeVar("_T_co", covariant=True) + + +class APIProtocol(Protocol): + def cached_property( + self, func: Callable[[Any], _T_co] + ) -> cached_property[_T_co]: ... + + +def test_cached_property(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + """Init.""" + + @propcache_module.cached_property + def prop(self) -> int: + return 1 + + a = A() + if sys.version_info >= (3, 11): + assert_type(a.prop, int) + assert a.prop == 1 + + +def test_cached_property_without_cache(propcache_module: APIProtocol) -> None: + class A: + + __slots__ = () + + def __init__(self) -> None: + pass + + @propcache_module.cached_property + def prop(self) -> None: + """Mock property.""" + + a = A() + + with pytest.raises(AttributeError): + a.prop = 123 # type: ignore[assignment] + + +def test_cached_property_check_without_cache(propcache_module: APIProtocol) -> None: + class A: + + __slots__ = () + + def __init__(self) -> None: + """Init.""" + + @propcache_module.cached_property + def prop(self) -> None: + """Mock property.""" + + a = A() + with pytest.raises((TypeError, AttributeError)): + assert a.prop == 1 + + +def test_cached_property_caching(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + """Init.""" + + @propcache_module.cached_property + def prop(self) -> int: + """Docstring.""" + return 1 + + a = A() + assert a.prop == 1 + + +def test_cached_property_class_docstring(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + """Init.""" + + @propcache_module.cached_property + def prop(self) -> None: + """Docstring.""" + + if TYPE_CHECKING: + assert isinstance(A.prop, cached_property) + else: + assert isinstance(A.prop, propcache_module.cached_property) + assert "Docstring." == A.prop.__doc__ + + +def test_set_name(propcache_module: APIProtocol) -> None: + """Test that the __set_name__ method is called and checked.""" + + class A: + + @propcache_module.cached_property + def prop(self) -> None: + """Docstring.""" + + A.prop.__set_name__(A, "prop") + + match = r"Cannot assign the same cached_property to two " + with pytest.raises(TypeError, match=match): + A.prop.__set_name__(A, "something_else") + + +def test_get_without_set_name(propcache_module: APIProtocol) -> None: + """Test that get without __set_name__ fails.""" + cp = propcache_module.cached_property(not_) + + class A: + """A class.""" + + A.cp = cp # type: ignore[attr-defined] + match = r"Cannot use cached_property instance " + with pytest.raises(TypeError, match=match): + _ = A().cp # type: ignore[attr-defined] + + +def test_ensured_wrapped_function_is_accessible(propcache_module: APIProtocol) -> None: + """Test that the wrapped function can be accessed from python.""" + + class A: + def __init__(self) -> None: + """Init.""" + + @propcache_module.cached_property + def prop(self) -> int: + """Docstring.""" + return 1 + + a = A() + assert A.prop.func(a) == 1 + + [email protected]_extension [email protected](IS_PYPY, reason="PyPy has no C extension") +def test_cached_property_no_refcount_leak(propcache_module: APIProtocol) -> None: + """Test that cached_property does not leak references.""" + + class CachedPropertySentinel: + """A unique object we can track.""" + + def count_sentinels() -> int: + """Count the number of CachedPropertySentinel instances in gc.""" + gc.collect() + return sum( + 1 for obj in gc.get_objects() if isinstance(obj, CachedPropertySentinel) + ) + + class A: + def __init__(self) -> None: + """Init.""" + + @propcache_module.cached_property + def prop(self) -> CachedPropertySentinel: + """Return a sentinel object.""" + return CachedPropertySentinel() + + initial_sentinel_count = count_sentinels() + + a = A() + + # First access - creates and caches the object + result = a.prop + # sys.getrefcount returns 1 higher than actual (the temp ref from the call) + # After first access: result owns 1, __dict__ owns 1, getrefcount call owns 1 = 3 + initial_refcount = sys.getrefcount(result) + + # Should have exactly 1 Sentinel instance now + assert count_sentinels() == initial_sentinel_count + 1 + + # Second access - should return the cached object without creating new refs + result2 = a.prop + assert result is result2 + # After second access: result owns 1, result2 owns 1, __dict__ owns 1, getrefcount call owns 1 = 4 + # Only result2 should add 1 + second_refcount = sys.getrefcount(result) + assert second_refcount == initial_refcount + 1 + + # Still should have exactly 1 Sentinel instance + assert count_sentinels() == initial_sentinel_count + 1 + + # Third access + result3 = a.prop + assert result is result3 + # result2 and result3 each add 1 + third_refcount = sys.getrefcount(result) + assert third_refcount == initial_refcount + 2 + + # Clean up local refs - should be back to just result and __dict__ + del result2 + del result3 + after_cleanup_refcount = sys.getrefcount(result) + assert after_cleanup_refcount == initial_refcount + + # Clear the cache and verify no leak when re-fetching + # After clearing: only result owns it + del a.__dict__["prop"] + cleared_refcount = sys.getrefcount(result) + assert cleared_refcount == initial_refcount - 1 # No longer in __dict__ + + # Re-fetch - this should create a new object, not affect old one + result4 = a.prop + assert result4 is not result # Should be a new object + refetch_refcount = sys.getrefcount(result) + assert refetch_refcount == cleared_refcount # Original object refcount unchanged + + # Now we should have 2 Sentinel instances: + # - original in `result` + # - new one in `result4` + assert count_sentinels() == initial_sentinel_count + 2 diff --git a/contrib/python/propcache/tests/test_init.py b/contrib/python/propcache/tests/test_init.py new file mode 100644 index 00000000000..6bcffe7faa6 --- /dev/null +++ b/contrib/python/propcache/tests/test_init.py @@ -0,0 +1,43 @@ +"""Test imports can happen from top-level.""" + +import pytest + +import propcache +from propcache import _helpers + + +def test_api_at_top_level() -> None: + """Verify the public API is accessible at top-level.""" + assert propcache.cached_property is not None + assert propcache.under_cached_property is not None + assert propcache.cached_property is _helpers.cached_property + assert propcache.under_cached_property is _helpers.under_cached_property + + + "prop_name", + ("cached_property", "under_cached_property"), +) +def test_public_api_is_discoverable_in_dir(prop_name: str) -> None: + """Verify the public API is discoverable programmatically.""" + assert prop_name in dir(propcache) + + +def test_importing_invalid_attr_raises() -> None: + """Verify importing an invalid attribute raises an AttributeError.""" + match = r"^module 'propcache' has no attribute 'invalid_attr'$" + with pytest.raises(AttributeError, match=match): + propcache.invalid_attr + + +def test_import_error_invalid_attr() -> None: + """Verify importing an invalid attribute raises an ImportError.""" + # No match here because the error is raised by the import system + # and may vary between Python versions. + with pytest.raises(ImportError): + from propcache import invalid_attr # noqa: F401 + + +def test_no_wildcard_imports() -> None: + """Verify wildcard imports are prohibited.""" + assert not propcache.__all__ diff --git a/contrib/python/propcache/tests/test_under_cached_property.py b/contrib/python/propcache/tests/test_under_cached_property.py new file mode 100644 index 00000000000..9c0dc304f65 --- /dev/null +++ b/contrib/python/propcache/tests/test_under_cached_property.py @@ -0,0 +1,252 @@ +import gc +import sys +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Protocol, TypedDict, TypeVar + +import pytest + +from propcache.api import under_cached_property + +IS_PYPY = hasattr(sys, "pypy_version_info") + +if sys.version_info >= (3, 11): + from typing import assert_type + +_T_co = TypeVar("_T_co", covariant=True) + + +class APIProtocol(Protocol): + def under_cached_property( + self, func: Callable[[Any], _T_co] + ) -> under_cached_property[_T_co]: ... + + +def test_under_cached_property(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + self._cache: dict[str, int] = {} + + @propcache_module.under_cached_property + def prop(self) -> int: + return 1 + + @propcache_module.under_cached_property + def prop2(self) -> str: + return "foo" + + a = A() + if sys.version_info >= (3, 11): + assert_type(a.prop, int) + assert a.prop == 1 + if sys.version_info >= (3, 11): + assert_type(a.prop2, str) + assert a.prop2 == "foo" + + +def test_under_cached_property_typeddict(propcache_module: APIProtocol) -> None: + """Test static typing passes with TypedDict.""" + + class _Cache(TypedDict, total=False): + prop: int + prop2: str + + class A: + def __init__(self) -> None: + self._cache: _Cache = {} + + @propcache_module.under_cached_property + def prop(self) -> int: + return 1 + + @propcache_module.under_cached_property + def prop2(self) -> str: + return "foo" + + a = A() + if sys.version_info >= (3, 11): + assert_type(a.prop, int) + assert a.prop == 1 + if sys.version_info >= (3, 11): + assert_type(a.prop2, str) + assert a.prop2 == "foo" + + +def test_under_cached_property_assignment(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + self._cache: dict[str, Any] = {} + + @propcache_module.under_cached_property + def prop(self) -> None: + """Mock property.""" + + a = A() + + with pytest.raises(AttributeError): + a.prop = 123 # type: ignore[assignment] + + +def test_under_cached_property_without_cache(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + """Init.""" + self._cache: dict[str, int] = {} + + @propcache_module.under_cached_property + def prop(self) -> None: + """Mock property.""" + + a = A() + + with pytest.raises(AttributeError): + a.prop = 123 # type: ignore[assignment] + + +def test_under_cached_property_check_without_cache( + propcache_module: APIProtocol, +) -> None: + class A: + def __init__(self) -> None: + """Init.""" + # Note that self._cache is intentionally missing + # here to verify AttributeError + + @propcache_module.under_cached_property + def prop(self) -> None: + """Mock property.""" + + a = A() + with pytest.raises(AttributeError): + _ = a.prop # type: ignore[call-overload] + + +def test_under_cached_property_caching(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + self._cache: dict[str, int] = {} + + @propcache_module.under_cached_property + def prop(self) -> int: + """Docstring.""" + return 1 + + a = A() + assert a.prop == 1 + + +def test_under_cached_property_class_docstring(propcache_module: APIProtocol) -> None: + class A: + def __init__(self) -> None: + """Init.""" + + @propcache_module.under_cached_property + def prop(self) -> None: + """Docstring.""" + + if TYPE_CHECKING: + # At type checking, the fixture doesn't represent the real module, so + # we use the global-level imported module to verify the isinstance() check here + # matches the behaviour users would see in real code. + assert isinstance(A.prop, under_cached_property) + else: + assert isinstance(A.prop, propcache_module.under_cached_property) + assert "Docstring." == A.prop.__doc__ + + +def test_ensured_wrapped_function_is_accessible(propcache_module: APIProtocol) -> None: + """Test that the wrapped function can be accessed from python.""" + + class A: + def __init__(self) -> None: + """Init.""" + self._cache: dict[str, int] = {} + + @propcache_module.under_cached_property + def prop(self) -> int: + """Docstring.""" + return 1 + + a = A() + assert A.prop.wrapped(a) == 1 + + [email protected]_extension [email protected](IS_PYPY, reason="PyPy has no C extension") +def test_under_cached_property_no_refcount_leak(propcache_module: APIProtocol) -> None: + """Test that under_cached_property does not leak references.""" + + class UnderCachedPropertySentinel: + """A unique object we can track.""" + + def count_sentinels() -> int: + """Count the number of UnderCachedPropertySentinel instances in gc.""" + gc.collect() + return sum( + 1 + for obj in gc.get_objects() + if isinstance(obj, UnderCachedPropertySentinel) + ) + + class A: + def __init__(self) -> None: + """Init.""" + self._cache: dict[str, Any] = {} + + @propcache_module.under_cached_property + def prop(self) -> UnderCachedPropertySentinel: + """Return a sentinel object.""" + return UnderCachedPropertySentinel() + + initial_sentinel_count = count_sentinels() + + a = A() + + # First access - creates and caches the object + result = a.prop + # sys.getrefcount returns 1 higher than actual (the temp ref from the call) + # After first access: result owns 1, _cache owns 1, getrefcount call owns 1 = 3 + initial_refcount = sys.getrefcount(result) + + # Should have exactly 1 Sentinel instance now + assert count_sentinels() == initial_sentinel_count + 1 + + # Second access - should return the cached object without creating new refs + result2 = a.prop + assert result is result2 + # After second access: result owns 1, result2 owns 1, _cache owns 1, getrefcount call owns 1 = 4 + # Only result2 should add 1 + second_refcount = sys.getrefcount(result) + assert second_refcount == initial_refcount + 1 + + # Still should have exactly 1 Sentinel instance + assert count_sentinels() == initial_sentinel_count + 1 + + # Third access + result3 = a.prop + assert result is result3 + # result2 and result3 each add 1 + third_refcount = sys.getrefcount(result) + assert third_refcount == initial_refcount + 2 + + # Clean up local refs - should be back to just result and _cache + del result2 + del result3 + after_cleanup_refcount = sys.getrefcount(result) + assert after_cleanup_refcount == initial_refcount + + # Clear the cache and verify no leak when re-fetching + # After clearing: only result owns it + del a._cache["prop"] + cleared_refcount = sys.getrefcount(result) + assert cleared_refcount == initial_refcount - 1 # No longer in _cache + + # Re-fetch - this should create a new object, not affect old one + result4 = a.prop + assert result4 is not result # Should be a new object + refetch_refcount = sys.getrefcount(result) + assert refetch_refcount == cleared_refcount # Original object refcount unchanged + + # Now we should have 2 Sentinel instances: + # - original in `result` + # - new one in `result4` + assert count_sentinels() == initial_sentinel_count + 2 diff --git a/contrib/python/propcache/tests/ya.make b/contrib/python/propcache/tests/ya.make new file mode 100644 index 00000000000..53173b0a58f --- /dev/null +++ b/contrib/python/propcache/tests/ya.make @@ -0,0 +1,11 @@ +PY3TEST() + +PEERDIR( + contrib/python/propcache +) + +ALL_PYTEST_SRCS() + +NO_LINT() + +END() diff --git a/contrib/python/propcache/ya.make b/contrib/python/propcache/ya.make new file mode 100644 index 00000000000..dea81d7aa7f --- /dev/null +++ b/contrib/python/propcache/ya.make @@ -0,0 +1,34 @@ +# Generated by devtools/yamaker (pypi). + +PY3_LIBRARY() + +VERSION(0.5.2) + +LICENSE(Apache-2.0) + +NO_COMPILER_WARNINGS() + +NO_LINT() + +PY_SRCS( + TOP_LEVEL + propcache/__init__.py + propcache/_helpers.py + propcache/_helpers_py.py + propcache/api.py + CYTHON_C + propcache/_helpers_c.pyx +) + +RESOURCE_FILES( + PREFIX contrib/python/propcache/ + .dist-info/METADATA + .dist-info/top_level.txt + propcache/py.typed +) + +END() + +RECURSE_FOR_TESTS( + tests +) |
