diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..2cdfb9d
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,43 @@
+---
+
+codecov:
+ notify:
+ after_n_builds: 24 # The number of test matrix+lint jobs uploading coverage
+ wait_for_ci: false
+
+ require_ci_to_pass: false
+
+ token: 26f4a393-24a9-48d9-8fa4-f1344d930846 # repo-scoped
+
+comment:
+ require_changes: true
+
+coverage:
+ range: 99.34..100
+ status:
+ patch:
+ default:
+ target: 100%
+ flags:
+ - pytest
+ project:
+ default:
+ target: 100%
+ lib:
+ flags:
+ - pytest
+ paths:
+ - propcache/
+ target: 97.91%
+ tests:
+ flags:
+ - pytest
+ paths:
+ - tests/
+ target: 99.87% # 100%
+ typing:
+ flags:
+ - MyPy
+ target: 100%
+
+...
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..a66e3ce
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,33 @@
+[html]
+show_contexts = true
+skip_covered = false
+
+[paths]
+source =
+ propcache
+ */lib/pypy*/site-packages/propcache
+ */lib/python*/site-packages/propcache
+ */Lib/site-packages/propcache
+
+[report]
+fail_under = 98.95
+skip_covered = true
+skip_empty = true
+show_missing = true
+exclude_also =
+ ^\s*@pytest\.mark\.xfail
+
+[run]
+branch = true
+cover_pylib = false
+# https://coverage.rtfd.io/en/latest/contexts.html#dynamic-contexts
+# dynamic_context = test_function # conflicts with `pytest-cov` if set here
+parallel = true
+plugins =
+ covdefaults
+ Cython.Coverage
+relative_files = true
+source =
+ .
+source_pkgs =
+ propcache
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..50a6a22
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,143 @@
+---
+name: 🐞 Bug Report
+description: Create a report to help us improve.
+labels:
+- bug
+body:
+- type: markdown
+ attributes:
+ value: |
+ **Thanks for taking a minute to file a bug report!**
+
+ ⚠
+ Verify first that your issue is not [already reported on
+ GitHub][issue search].
+
+ _Please fill out the form below with as many precise
+ details as possible._
+
+ [issue search]: ../search?q=is%3Aissue&type=issues
+
+- type: checkboxes
+ id: terms
+ attributes:
+ label: Please confirm the following
+ description: |
+ Read the [aio-libs Code of Conduct][CoC] first. Check the existing issues
+ on the tracker. Take into account the possibility of your report
+ surfacing a security vulnerability.
+
+ [CoC]: ../../.github/blob/master/CODE_OF_CONDUCT.md
+ options:
+ - label: |
+ I agree to follow the [aio-libs Code of Conduct][CoC]
+
+ [CoC]: ../../.github/blob/master/CODE_OF_CONDUCT.md
+ required: true
+ - label: |
+ I have checked the [current issues][issue search] for duplicates.
+
+ [issue search]: ../search?q=is%3Aissue&type=issues
+ required: true
+ - label: >-
+ I understand this is open source software provided for free and
+ that I might not receive a timely response.
+ required: true
+ - label: |
+ I am positive I am **NOT** reporting a (potential) security
+ vulnerability, to the best of my knowledge. *(These must be shared by
+ submitting [this report form][vulnerability report form] instead, if
+ any hesitation exists.)*
+
+ [vulnerability report form]: ../security/advisories/new
+ required: true
+ - label: >-
+ I am willing to submit a pull request with reporoducers as xfailing test
+ cases or even entire fix. *(Assign this issue to me.)*
+ required: false
+
+- type: textarea
+ attributes:
+ label: Describe the bug
+ description: >-
+ A clear and concise description of what the bug is.
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: To Reproduce
+ description: >-
+ Describe the steps to reproduce this bug.
+ placeholder: |
+ 1. Have certain environment
+ 2. Run given code snippet in a certain way
+ 3. See some behavior described
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Expected behavior
+ description: >-
+ A clear and concise description of what you expected to happen.
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Logs/tracebacks
+ description: |
+ If applicable, add logs/tracebacks to help explain your problem.
+ Paste the output of the steps above, including the commands
+ themselves and their output/traceback etc.
+ render: python-traceback
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Python Version
+ description: Attach your version of Python.
+ render: console
+ value: |
+ $ python --version
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: multidict Version
+ description: Attach your version of multidict.
+ render: console
+ value: |
+ $ python -m pip show multidict
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: propcache Version
+ description: Attach your version of propcache.
+ render: console
+ value: |
+ $ python -m pip show propcache
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: OS
+ placeholder: >-
+ For example, Arch Linux, Windows, macOS, etc.
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Additional context
+ description: |
+ Add any other context about the problem here.
+
+ Describe the environment you have that lead to your issue.
+ This includes proxy server and other bits that are related to your case.
+
+...
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..4220673
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,37 @@
+---
+
+# yamllint disable rule:line-length
+# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
+# yamllint enable rule:line-length
+blank_issues_enabled: false # default: true
+contact_links:
+- name: 🔐 Security bug report 🔥
+ url: https://github.com/aio-libs/.github/security/policy
+ about: |
+ Please learn how to report security vulnerabilities here.
+
+ For all security related bugs, send an email
+ instead of using this issue tracker and you
+ will receive a prompt response.
+
+ For more information, see
+ https://github.com/aio-libs/.github/security/policy
+- name: >-
+ [🎉 NEW 🎉]
+ 🤷💻🤦 GitHub Discussions
+ url: https://github.com/aio-libs/propcache/discussions
+ about: >-
+ Please ask typical Q&A in the Discussions tab or on StackOverflow
+- name: 🤷💻🤦 StackOverflow
+ url: https://stackoverflow.com/questions/tagged/aiohttp
+ about: >-
+ Please ask typical Q&A here or in the
+ Discussions tab @ https://github.com/aio-libs/propcache/discussions
+- name: 💬 Gitter Chat
+ url: https://gitter.im/aio-libs/Lobby
+ about: Chat with devs and community
+- name: 📝 Code of Conduct
+ url: https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md
+ about: ❤ Be nice to other members of the community. ☮ Behave.
+
+...
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..2004256
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,62 @@
+---
+name: 🚀 Feature request
+description: Suggest an idea for this project.
+labels:
+- enhancement
+body:
+- type: markdown
+ attributes:
+ value: |
+ **Thanks for taking a minute to file a feature for multidict!**
+
+ ⚠
+ Verify first that your feature request is not [already reported on
+ GitHub][issue search].
+
+ _Please fill out the form below with as many precise
+ details as possible._
+
+ [issue search]: ../search?q=is%3Aissue&type=issues
+
+- type: textarea
+ attributes:
+ label: Is your feature request related to a problem?
+ description: >-
+ Please add a clear and concise description of what
+ the problem is. _Ex. I'm always frustrated when [...]_
+
+- type: textarea
+ attributes:
+ label: Describe the solution you'd like
+ description: >-
+ A clear and concise description of what you want to happen.
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Describe alternatives you've considered
+ description: >-
+ A clear and concise description of any alternative solutions
+ or features you've considered.
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Additional context
+ description: >-
+ Add any other context or screenshots about
+ the feature request here.
+
+- type: checkboxes
+ attributes:
+ label: Code of Conduct
+ description: |
+ Read the [aio-libs Code of Conduct][CoC] first.
+
+ [CoC]: https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md
+ options:
+ - label: I agree to follow the aio-libs Code of Conduct
+ required: true
+...
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..493e5f0
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,23 @@
+---
+
+version: 2
+updates:
+
+# Maintain dependencies for GitHub Actions
+- package-ecosystem: "github-actions"
+ directory: "/"
+ labels:
+ - dependencies
+ schedule:
+ interval: "daily"
+
+# Maintain dependencies for Python
+- package-ecosystem: "pip"
+ directory: "/"
+ labels:
+ - dependencies
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 10
+
+...
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
new file mode 100644
index 0000000..d8de5d7
--- /dev/null
+++ b/.github/workflows/auto-merge.yml
@@ -0,0 +1,27 @@
+---
+
+name: Dependabot auto-merge
+on: pull_request_target
+
+permissions:
+ pull-requests: write
+ contents: write
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ timeout-minutes: 1
+ if: ${{ github.actor == 'dependabot[bot]' }}
+ steps:
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v2
+ with:
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Enable auto-merge for Dependabot PRs
+ run: gh pr merge --auto --squash "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+
+...
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
new file mode 100644
index 0000000..ea82099
--- /dev/null
+++ b/.github/workflows/ci-cd.yml
@@ -0,0 +1,467 @@
+---
+
+name: CI/CD
+
+on:
+ merge_group:
+ push:
+ branches:
+ - master
+ - >-
+ [0-9].[0-9]+
+ tags:
+ - v*
+ pull_request:
+ branches:
+ - master
+ - >-
+ [0-9].[0-9]+
+ schedule:
+ - cron: 0 6 * * * # Daily 6AM UTC build
+
+
+concurrency:
+ group: ${{ github.ref }}-${{ github.workflow }}
+ cancel-in-progress: true
+
+
+env:
+ COLOR: >- # Supposedly, pytest or coveragepy use this
+ yes
+ FORCE_COLOR: 1 # Request colored output from CLI tools supporting it
+ MYPY_FORCE_COLOR: 1 # MyPy's color enforcement
+ PIP_DISABLE_PIP_VERSION_CHECK: 1
+ PIP_NO_PYTHON_VERSION_WARNING: 1
+ PIP_NO_WARN_SCRIPT_LOCATION: 1
+ PRE_COMMIT_COLOR: always
+ PROJECT_NAME: propcache
+ PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest`
+ PYTHONIOENCODING: utf-8
+ PYTHONUTF8: 1
+ PYTHON_LATEST: 3.12
+
+
+jobs:
+
+ pre-setup:
+ name: ⚙️ Pre-set global build settings
+ runs-on: ubuntu-latest
+ timeout-minutes: 1
+ defaults:
+ run:
+ shell: python
+ outputs:
+ # NOTE: These aren't env vars because the `${{ env }}` context is
+ # NOTE: inaccessible when passing inputs to reusable workflows.
+ dists-artifact-name: python-package-distributions
+ sdist-name: ${{ env.PROJECT_NAME }}-*.tar.gz
+ wheel-name: ${{ env.PROJECT_NAME }}-*.whl
+ steps:
+ - run: >-
+ print('No-op')
+
+
+ build-pure-python-dists:
+ name: 📦 Build distribution packages
+ needs:
+ - pre-setup
+ runs-on: ubuntu-latest
+ timeout-minutes: 1
+ outputs:
+ sdist-filename: >-
+ ${{ steps.dist-filenames-detection.outputs.sdist-filename }}
+ wheel-filename: >-
+ ${{ steps.dist-filenames-detection.outputs.wheel-filename }}
+ steps:
+ - name: Checkout project
+ uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_LATEST }}
+ cache: pip
+ cache-dependency-path:
+ requirements/*.txt
+ - name: Install core libraries for build
+ run: python -Im pip install build
+ - name: Build sdists and pure-python wheel
+ env:
+ PIP_CONSTRAINT: requirements/cython.txt
+ run: python -Im build --config-setting=pure-python=true
+ - name: Determine actual created filenames
+ id: dist-filenames-detection
+ run: >-
+ {
+ echo -n sdist-filename=
+ ;
+ basename "$(ls -1 dist/${{ needs.pre-setup.outputs.sdist-name }})"
+ ;
+ echo -n wheel-filename=
+ ;
+ basename "$(ls -1 dist/${{ needs.pre-setup.outputs.wheel-name }})"
+ ;
+ }
+ >> "${GITHUB_OUTPUT}"
+ - name: Upload built artifacts for testing
+ uses: actions/upload-artifact@v4
+ with:
+ if-no-files-found: error
+ name: ${{ needs.pre-setup.outputs.dists-artifact-name }}
+ # NOTE: Exact expected file names are specified here
+ # NOTE: as a safety measure — if anything weird ends
+ # NOTE: up being in this dir or not all dists will be
+ # NOTE: produced, this will fail the workflow.
+ path: |
+ dist/${{ steps.dist-filenames-detection.outputs.sdist-filename }}
+ dist/${{ steps.dist-filenames-detection.outputs.wheel-filename }}
+ retention-days: 15
+
+ lint:
+ uses: ./.github/workflows/reusable-linters.yml
+ secrets:
+ codecov-token: 26f4a393-24a9-48d9-8fa4-f1344d930846
+
+ build-wheels-for-tested-arches:
+ name: >- # ${{ '' } is a hack to nest jobs under the same sidebar category
+ 📦 Build wheels for tested arches${{ '' }}
+ needs:
+ - build-pure-python-dists
+ - pre-setup # transitive, for accessing settings
+ strategy:
+ matrix:
+ os:
+ - ubuntu
+ - windows
+ - macos
+ uses: ./.github/workflows/reusable-build-wheel.yml
+ with:
+ os: ${{ matrix.os }}
+ wheel-tags-to-skip: >-
+ ${{
+ (github.event_name != 'push' || !contains(github.ref, 'refs/tags/'))
+ && '*_i686
+ *-macosx_universal2
+ *-musllinux_*
+ *-win32
+ pp*'
+ || ''
+ }}
+ source-tarball-name: >-
+ ${{ needs.build-pure-python-dists.outputs.sdist-filename }}
+ dists-artifact-name: ${{ needs.pre-setup.outputs.dists-artifact-name }}
+ cython-tracing: >- # Cython line tracing for coverage collection
+ ${{
+ (
+ github.event_name == 'push'
+ && contains(github.ref, 'refs/tags/')
+ )
+ && 'false'
+ || 'true'
+ }}
+
+ test:
+ name: Test
+ needs:
+ - build-pure-python-dists # transitive, for accessing settings
+ - build-wheels-for-tested-arches
+ - pre-setup # transitive, for accessing settings
+ strategy:
+ matrix:
+ pyver:
+ - 3.13
+ - 3.12
+ - 3.11
+ - >-
+ 3.10
+ - 3.9
+ - 3.8
+ no-extensions: ['', 'Y']
+ os:
+ - ubuntu-latest
+ - macos-latest
+ - windows-latest
+ experimental: [false]
+ exclude:
+ - os: macos-latest
+ no-extensions: Y
+ - os: windows-latest
+ no-extensions: Y
+ include:
+ - pyver: pypy-3.10
+ no-extensions: Y
+ experimental: false
+ os: ubuntu-latest
+ - pyver: pypy-3.9
+ no-extensions: Y
+ experimental: false
+ os: ubuntu-latest
+ - pyver: pypy-3.8
+ no-extensions: Y
+ experimental: false
+ os: ubuntu-latest
+ fail-fast: false
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 5
+ continue-on-error: ${{ matrix.experimental }}
+ steps:
+ - name: Retrieve the project source from an sdist inside the GHA artifact
+ uses: re-actors/checkout-python-sdist@release/v2
+ with:
+ source-tarball-name: >-
+ ${{ needs.build-pure-python-dists.outputs.sdist-filename }}
+ workflow-artifact-name: >-
+ ${{ needs.pre-setup.outputs.dists-artifact-name }}
+ - name: Download distributions
+ uses: actions/download-artifact@v4
+ with:
+ path: dist
+ pattern: ${{ needs.pre-setup.outputs.dists-artifact-name }}*
+ merge-multiple: true
+
+ - name: Setup Python ${{ matrix.pyver }}
+ id: python-install
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.pyver }}
+ allow-prereleases: true
+ cache: pip
+ cache-dependency-path: requirements/*.txt
+ - name: Install dependencies
+ uses: py-actions/py-dependency-install@v4
+ with:
+ path: requirements/test.txt
+ - name: Determine pre-compiled compatible wheel
+ env:
+ # NOTE: When `pip` is forced to colorize output piped into `jq`,
+ # NOTE: the latter can't parse it. So we're overriding the color
+ # NOTE: preference here via https://no-color.org.
+ # NOTE: Setting `FORCE_COLOR` to any value (including 0, an empty
+ # NOTE: string, or a "YAML null" `~`) doesn't have any effect and
+ # NOTE: `pip` (through its verndored copy of `rich`) treats the
+ # NOTE: presence of the variable as "force-color" regardless.
+ #
+ # NOTE: This doesn't actually work either, so we'll resort to unsetting
+ # NOTE: in the Bash script.
+ # NOTE: Ref: https://github.com/Textualize/rich/issues/2622
+ NO_COLOR: 1
+ id: wheel-file
+ run: >
+ echo -n path= | tee -a "${GITHUB_OUTPUT}"
+
+
+ unset FORCE_COLOR
+
+
+ python
+ -X utf8
+ -u -I
+ -m pip install
+ --find-links=./dist
+ --no-index
+ '${{ env.PROJECT_NAME }}'
+ --force-reinstall
+ --no-color
+ --no-deps
+ --only-binary=:all:
+ --dry-run
+ --report=-
+ --quiet
+ | jq --raw-output .install[].download_info.url
+ | tee -a "${GITHUB_OUTPUT}"
+ shell: bash
+ - name: Self-install
+ run: python -Im pip install '${{ steps.wheel-file.outputs.path }}'
+ - name: Produce the C-files for the Coverage.py Cython plugin
+ if: >- # Only works if the dists were built with line tracing
+ !matrix.no-extensions
+ && (
+ github.event_name != 'push'
+ || !contains(github.ref, 'refs/tags/')
+ )
+ env:
+ PYTHONPATH: packaging/
+ run: |
+ set -eEuo pipefail
+
+ python -Im pip install expandvars
+ python -m pep517_backend.cli translate-cython
+ shell: bash
+ - name: Disable the Cython.Coverage Produce plugin
+ if: >- # Only works if the dists were built with line tracing
+ matrix.no-extensions
+ || (
+ github.event_name == 'push'
+ && contains(github.ref, 'refs/tags/')
+ )
+ run: |
+ set -eEuo pipefail
+ sed -i.bak 's/^\s\{2\}Cython\.Coverage$//g' .coveragerc
+ shell: bash
+ - name: Run unittests
+ env:
+ PROPCACHE_NO_EXTENSIONS: ${{ matrix.no-extensions }}
+ run: >-
+ python -Im
+ pytest
+ -v
+ --cov-report xml
+ --junitxml=.test-results/pytest/test.xml
+ - name: Produce markdown test summary from JUnit
+ if: >-
+ !cancelled()
+ uses: test-summary/action@v2.4
+ with:
+ paths: .test-results/pytest/test.xml
+ - name: Append coverage results to Job Summary
+ if: >-
+ !cancelled()
+ continue-on-error: true
+ run: >-
+ python -Im coverage report --format=markdown
+ >> "${GITHUB_STEP_SUMMARY}"
+ shell: bash
+ - name: Re-run the failing tests with maximum verbosity
+ if: >-
+ !cancelled()
+ && failure()
+ env:
+ PROPCACHE_NO_EXTENSIONS: ${{ matrix.no-extensions }}
+ run: >- # `exit 1` makes sure that the job remains red with flaky runs
+ python -Im
+ pytest --no-cov -vvvvv --lf -rA
+ && exit 1
+ shell: bash
+ - name: Send coverage data to Codecov
+ if: >-
+ !cancelled()
+ uses: codecov/codecov-action@v4
+ with:
+ token: 26f4a393-24a9-48d9-8fa4-f1344d930846
+ file: ./coverage.xml
+ flags: >-
+ CI-GHA,
+ pytest,
+ OS-${{ runner.os }},
+ VM-${{ matrix.os }},
+ Py-${{ steps.python-install.outputs.python-version }}
+ fail_ci_if_error: false
+
+ test-summary:
+ name: Test matrix status
+ if: always()
+ runs-on: ubuntu-latest
+ timeout-minutes: 1
+ needs: [lint, test]
+ steps:
+ - name: Decide whether the needed jobs succeeded or failed
+ uses: re-actors/alls-green@release/v1
+ with:
+ jobs: ${{ toJSON(needs) }}
+
+ pre-deploy:
+ name: Pre-Deploy
+ runs-on: ubuntu-latest
+ timeout-minutes: 1
+ needs: test-summary
+ # Run only on pushing a tag
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+ steps:
+ - name: Dummy
+ run: |
+ echo "Predeploy step"
+
+ build-wheels-for-odd-archs:
+ name: >- # ${{ '' } is a hack to nest jobs under the same sidebar category
+ 📦 Build wheels for odd arches${{ '' }}
+ needs:
+ - build-pure-python-dists
+ - pre-deploy
+ - pre-setup # transitive, for accessing settings
+ strategy:
+ matrix:
+ qemu:
+ - aarch64
+ - ppc64le
+ - s390x
+ uses: ./.github/workflows/reusable-build-wheel.yml
+ with:
+ qemu: ${{ matrix.qemu }}
+ source-tarball-name: >-
+ ${{ needs.build-pure-python-dists.outputs.sdist-filename }}
+ dists-artifact-name: ${{ needs.pre-setup.outputs.dists-artifact-name }}
+
+ deploy:
+ name: Deploy
+ needs:
+ - build-pure-python-dists
+ - build-wheels-for-odd-archs
+ - build-wheels-for-tested-arches
+ - pre-setup # transitive, for accessing settings
+ runs-on: ubuntu-latest
+ timeout-minutes: 14
+
+ permissions:
+ contents: write # IMPORTANT: mandatory for making GitHub Releases
+ id-token: write # IMPORTANT: mandatory for trusted publishing & sigstore
+
+ environment:
+ name: pypi
+ url: https://pypi.org/p/${{ env.PROJECT_NAME }}
+
+ steps:
+ - name: Retrieve the project source from an sdist inside the GHA artifact
+ uses: re-actors/checkout-python-sdist@release/v2
+ with:
+ source-tarball-name: >-
+ ${{ needs.build-pure-python-dists.outputs.sdist-filename }}
+ workflow-artifact-name: >-
+ ${{ needs.pre-setup.outputs.dists-artifact-name }}
+
+ - name: Download distributions
+ uses: actions/download-artifact@v4
+ with:
+ path: dist
+ pattern: ${{ needs.pre-setup.outputs.dists-artifact-name }}*
+ merge-multiple: true
+ - run: |
+ tree
+ - name: Login
+ run: |
+ echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
+ - name: Make Release
+ uses: aio-libs/create-release@v1.6.6
+ with:
+ changes_file: CHANGES.rst
+ version_file: ${{ env.PROJECT_NAME }}/__init__.py
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ head_line: >-
+ {version}\n=+\n\n\*\({date}\)\*\n
+ fix_issue_regex: >-
+ :issue:`(\d+)`
+ fix_issue_repl: >-
+ #\1
+
+ - name: >-
+ Publish 🐍📦 to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ attestations: true
+
+ - name: Sign the dists with Sigstore
+ uses: sigstore/gh-action-sigstore-python@v3.0.0
+ with:
+ inputs: >-
+ ./dist/${{ needs.build-pure-python-dists.outputs.sdist-filename }}
+ ./dist/*.whl
+
+ - name: Upload artifact signatures to GitHub Release
+ # Confusingly, this action also supports updating releases, not
+ # just creating them. This is what we want here, since we've manually
+ # created the release above.
+ uses: softprops/action-gh-release@v2
+ with:
+ # dist/ contains the built packages, which smoketest-artifacts/
+ # contains the signatures and certificates.
+ files: dist/**
+
+...
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..3ca4c6e
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,49 @@
+---
+
+name: "CodeQL"
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+ schedule:
+ - cron: "46 14 * * 0"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ timeout-minutes: 4
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language:
+ - python
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ queries: +security-and-quality
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{ matrix.language }}"
+
+...
diff --git a/.github/workflows/reusable-build-wheel.yml b/.github/workflows/reusable-build-wheel.yml
new file mode 100644
index 0000000..acc5f84
--- /dev/null
+++ b/.github/workflows/reusable-build-wheel.yml
@@ -0,0 +1,90 @@
+---
+
+name: Build wheel
+
+on:
+ workflow_call:
+ inputs:
+ dists-artifact-name:
+ description: Workflow artifact name containing dists
+ required: true
+ type: string
+ cython-tracing:
+ description: Whether to build Cython modules with line tracing
+ default: '0'
+ required: false
+ type: string
+ os:
+ description: VM OS to use, without version suffix
+ default: ubuntu
+ required: false
+ type: string
+ qemu:
+ description: Emulated QEMU architecture
+ default: ''
+ required: false
+ type: string
+ source-tarball-name:
+ description: Sdist filename wildcard
+ required: true
+ type: string
+ wheel-tags-to-skip:
+ description: Wheel tags to skip building
+ default: ''
+ required: false
+ type: string
+
+env:
+ FORCE_COLOR: "1" # Make tools pretty.
+ PIP_DISABLE_PIP_VERSION_CHECK: "1"
+ PIP_NO_PYTHON_VERSION_WARNING: "1"
+
+jobs:
+
+ build-wheel:
+ name: Build wheels on ${{ inputs.os }} ${{ inputs.qemu }}
+ runs-on: ${{ inputs.os }}-latest
+ timeout-minutes: ${{ inputs.qemu && 60 || 20 }}
+ steps:
+ - name: Retrieve the project source from an sdist inside the GHA artifact
+ uses: re-actors/checkout-python-sdist@release/v2
+ with:
+ source-tarball-name: ${{ inputs.source-tarball-name }}
+ workflow-artifact-name: ${{ inputs.dists-artifact-name }}
+
+ - name: Set up QEMU
+ if: inputs.qemu
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: all
+ id: qemu
+ - name: Prepare emulation
+ if: inputs.qemu
+ run: |
+ # Build emulated architectures only if QEMU is set,
+ # use default "auto" otherwise
+ echo "CIBW_ARCHS_LINUX=${{ inputs.qemu }}" >> "${GITHUB_ENV}"
+ shell: bash
+
+ - name: Skip building some wheel tags
+ if: inputs.wheel-tags-to-skip
+ run: |
+ echo "CIBW_SKIP=${{ inputs.wheel-tags-to-skip }}" >> "${GITHUB_ENV}"
+ shell: bash
+
+ - name: Build wheels
+ uses: pypa/cibuildwheel@v2.21.1
+ env:
+ CIBW_ARCHS_MACOS: x86_64 arm64 universal2
+ CIBW_CONFIG_SETTINGS: >- # Cython line tracing for coverage collection
+ pure-python=false
+ with-cython-tracing=${{ inputs.cython-tracing }}
+
+ - name: Upload built artifacts for testing and publishing
+ uses: actions/upload-artifact@v4
+ with:
+ name: >-
+ ${{ inputs.dists-artifact-name }}-${{ inputs.os }}-${{ inputs.qemu }}
+ path: ./wheelhouse/*.whl
+
+...
diff --git a/.github/workflows/reusable-linters.yml b/.github/workflows/reusable-linters.yml
new file mode 100644
index 0000000..1f51216
--- /dev/null
+++ b/.github/workflows/reusable-linters.yml
@@ -0,0 +1,88 @@
+---
+
+name: Linters
+
+on:
+ workflow_call:
+ secrets:
+ codecov-token:
+ description: Mandatory token for uploading to Codecov
+ required: true
+
+env:
+ COLOR: >- # Supposedly, pytest or coveragepy use this
+ yes
+ FORCE_COLOR: 1 # Request colored output from CLI tools supporting it
+ MYPY_FORCE_COLOR: 1 # MyPy's color enforcement
+ PIP_DISABLE_PIP_VERSION_CHECK: 1
+ PIP_NO_PYTHON_VERSION_WARNING: 1
+ PIP_NO_WARN_SCRIPT_LOCATION: 1
+ PRE_COMMIT_COLOR: always
+ PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest`
+ PYTHONIOENCODING: utf-8
+ PYTHONUTF8: 1
+ PYTHON_LATEST: 3.12
+
+jobs:
+
+ lint:
+ name: Linter
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Setup Python ${{ env.PYTHON_LATEST }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_LATEST }}
+ cache: pip
+ cache-dependency-path: requirements/*.txt
+ - name: Cache pre-commit.com virtualenvs
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pre-commit
+ key: >-
+ ${{
+ runner.os
+ }}-pre-commit-${{
+ hashFiles('.pre-commit-config.yaml')
+ }}
+ - name: Install dependencies
+ uses: py-actions/py-dependency-install@v4
+ with:
+ path: requirements/lint.txt
+ - name: Self-install
+ run: |
+ pip install .
+ - name: Run linters
+ run: |
+ make lint
+ - name: Send coverage data to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.codecov-token }}
+ files: >-
+ .tox/.tmp/.mypy/python-3.12/cobertura.xml,
+ .tox/.tmp/.mypy/python-3.10/cobertura.xml,
+ .tox/.tmp/.mypy/python-3.8/cobertura.xml
+ flags: >-
+ CI-GHA,
+ MyPy
+ fail_ci_if_error: false
+ - name: Install spell checker
+ run: |
+ sudo apt install libenchant-2-dev
+ pip install -r requirements/doc-spelling.txt
+ - name: Run docs spelling
+ run: |
+ make doc-spelling
+ - name: Prepare twine checker
+ run: |
+ pip install -U build twine
+ python -m build --config-setting=pure-python=true
+ - name: Run twine checker
+ run: |
+ twine check --strict dist/*
+
+...
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2da41fd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,107 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# IPython Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# dotenv
+.env
+
+# virtualenv
+.venv/
+venv/
+ENV/
+
+# Spyder project settings
+.spyderproject
+
+# Rope project settings
+.ropeproject
+
+coverage
+
+
+propcache/*.c
+propcache/*.html
+
+.develop
+
+# Idea
+.idea
+
+.mypy_cache
+.install-cython
+.install-deps
+.pytest_cache
+pip-wheel-metadata
diff --git a/.mypy.ini b/.mypy.ini
new file mode 100644
index 0000000..c6f5968
--- /dev/null
+++ b/.mypy.ini
@@ -0,0 +1,49 @@
+[mypy]
+python_version = 3.8
+color_output = true
+error_summary = true
+files =
+ packaging/,
+ tests/,
+ propcache/
+
+# check_untyped_defs = true
+
+# disallow_untyped_calls = true
+# disallow_untyped_defs = true
+# disallow_any_generics = true
+
+enable_error_code =
+ ignore-without-code
+
+follow_imports = normal
+
+ignore_missing_imports = false
+
+pretty = true
+
+show_column_numbers = true
+show_error_codes = true
+strict_optional = true
+
+warn_no_return = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+
+[mypy-Cython.*]
+ignore_missing_imports = true
+
+[mypy-distutils.*]
+ignore_missing_imports = true
+
+[mypy-expandvars]
+ignore_missing_imports = true
+
+[mypy-idna]
+ignore_missing_imports = true
+
+[mypy-pytest]
+ignore_missing_imports = true
+
+[mypy-tomllib]
+ignore_missing_imports = true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..64e9e35
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,191 @@
+---
+
+ci:
+ autoupdate_schedule: quarterly
+ skip:
+ - actionlint-docker
+
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: 'v4.6.0'
+ hooks:
+ - id: check-merge-conflict
+- repo: https://github.com/asottile/yesqa
+ rev: v1.5.0
+ hooks:
+ - id: yesqa
+ additional_dependencies:
+ - wemake-python-styleguide
+- repo: https://github.com/PyCQA/isort
+ rev: '5.13.2'
+ hooks:
+ - id: isort
+- repo: https://github.com/psf/black
+ rev: '24.8.0'
+ hooks:
+ - id: black
+ language_version: python3 # Should be a command that runs python
+
+- repo: https://github.com/python-jsonschema/check-jsonschema.git
+ rev: 0.28.6
+ hooks:
+ - id: check-github-workflows
+ files: ^\.github/workflows/[^/]+$
+ types:
+ - yaml
+ - id: check-jsonschema
+ alias: check-github-workflows-timeout
+ name: Check GitHub Workflows set timeout-minutes
+ args:
+ - --builtin-schema
+ - github-workflows-require-timeout
+ files: ^\.github/workflows/[^/]+$
+ types:
+ - yaml
+ - id: check-readthedocs
+
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: 'v4.6.0'
+ hooks:
+ - id: end-of-file-fixer
+ - id: requirements-txt-fixer
+ - id: trailing-whitespace
+ - id: file-contents-sorter
+ files: |
+ docs/spelling_wordlist.txt|
+ .gitignore|
+ .gitattributes
+ - id: check-case-conflict
+ - id: check-json
+ - id: check-xml
+ - id: check-executables-have-shebangs
+ - id: check-toml
+ - id: check-xml
+ - id: check-yaml
+ - id: debug-statements
+ - id: check-added-large-files
+ - id: check-symlinks
+ - id: debug-statements
+ - id: detect-aws-credentials
+ args: ['--allow-missing-credentials']
+ - id: detect-private-key
+ exclude: ^examples/
+- repo: https://github.com/asottile/pyupgrade
+ rev: 'v3.16.0'
+ hooks:
+ - id: pyupgrade
+ args: ['--py38-plus']
+- repo: https://github.com/PyCQA/flake8
+ rev: '7.1.0'
+ hooks:
+ - id: flake8
+ exclude: "^docs/"
+
+- repo: https://github.com/codespell-project/codespell.git
+ rev: v2.3.0
+ hooks:
+ - id: codespell
+
+- repo: https://github.com/adrienverge/yamllint.git
+ rev: v1.35.1
+ hooks:
+ - id: yamllint
+ args:
+ - --strict
+
+- repo: https://github.com/MarcoGorelli/cython-lint.git
+ rev: v0.16.2
+ hooks:
+ - id: cython-lint
+
+- repo: https://github.com/Lucas-C/pre-commit-hooks-markup
+ rev: v1.0.1
+ hooks:
+ - id: rst-linter
+ exclude: ^CHANGES\.rst$
+ files: >-
+ ^[^/]+[.]rst$
+
+- repo: https://github.com/pre-commit/mirrors-mypy.git
+ rev: v1.11.2
+ hooks:
+ - id: mypy
+ alias: mypy-py313
+ name: MyPy, for Python 3.13
+ additional_dependencies:
+ - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report`
+ - multidict
+ - pytest
+ - tomli # requirement of packaging/pep517_backend/
+ - types-setuptools # requirement of packaging/pep517_backend/
+ args:
+ - --python-version=3.13
+ - --txt-report=.tox/.tmp/.mypy/python-3.13
+ - --cobertura-xml-report=.tox/.tmp/.mypy/python-3.13
+ - --html-report=.tox/.tmp/.mypy/python-3.13
+ pass_filenames: false
+ - id: mypy
+ alias: mypy-py312
+ name: MyPy, for Python 3.12
+ additional_dependencies:
+ - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report`
+ - multidict
+ - pytest
+ - tomli # requirement of packaging/pep517_backend/
+ - types-setuptools # requirement of packaging/pep517_backend/
+ args:
+ - --python-version=3.12
+ - --txt-report=.tox/.tmp/.mypy/python-3.12
+ - --cobertura-xml-report=.tox/.tmp/.mypy/python-3.12
+ - --html-report=.tox/.tmp/.mypy/python-3.12
+ pass_filenames: false
+ - id: mypy
+ alias: mypy-py310
+ name: MyPy, for Python 3.10
+ additional_dependencies:
+ - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report`
+ - multidict
+ - pytest
+ - tomli # requirement of packaging/pep517_backend/
+ - types-setuptools # requirement of packaging/pep517_backend/
+ - types-Pygments
+ - types-colorama
+ args:
+ - --python-version=3.10
+ - --txt-report=.tox/.tmp/.mypy/python-3.10
+ - --cobertura-xml-report=.tox/.tmp/.mypy/python-3.10
+ - --html-report=.tox/.tmp/.mypy/python-3.10
+ pass_filenames: false
+ - id: mypy
+ alias: mypy-py38
+ name: MyPy, for Python 3.8
+ additional_dependencies:
+ - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report`
+ - multidict
+ - pytest
+ - tomli # requirement of packaging/pep517_backend/
+ - types-setuptools # requirement of packaging/pep517_backend/
+ - types-Pygments
+ - types-colorama
+ args:
+ - --python-version=3.8
+ - --txt-report=.tox/.tmp/.mypy/python-3.8
+ - --cobertura-xml-report=.tox/.tmp/.mypy/python-3.8
+ - --html-report=.tox/.tmp/.mypy/python-3.8
+ pass_filenames: false
+
+- repo: https://github.com/rhysd/actionlint.git
+ rev: v1.7.1
+ hooks:
+ - id: actionlint-docker
+ args:
+ - -ignore
+ - >- # https://github.com/rhysd/actionlint/issues/384
+ ^type of expression at "float number value" must be number
+ but found type string$
+ - -ignore
+ - >- # https://github.com/rhysd/actionlint/pull/380#issuecomment-2325391372
+ ^input "attestations" is not defined in action
+ "pypa/gh-action-pypi-publish@release/v1". available inputs are ".*"$
+
+...
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..d3c25a7
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,25 @@
+---
+
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+ jobs:
+ post_create_environment:
+ - >-
+ pip install .
+ --config-settings=pure-python=true
+
+python:
+ install:
+ - requirements: requirements/doc.txt
+
+sphinx:
+ builder: dirhtml
+ configuration: docs/conf.py
+ fail_on_warning: true
+
+...
diff --git a/.yamllint b/.yamllint
new file mode 100644
index 0000000..82cb77f
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,18 @@
+---
+
+extends: default
+
+rules:
+ indentation:
+ level: error
+ indent-sequences: false
+ truthy:
+ allowed-values:
+ - >-
+ false
+ - >-
+ true
+ - >- # Allow "on" key name in GHA CI/CD workflow definitions
+ on
+
+...
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..cfcf43a
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,20 @@
+=========
+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.0.0 (2024-01-02)
+==================
+
+* The first release.
diff --git a/CHANGES/.TEMPLATE.rst b/CHANGES/.TEMPLATE.rst
new file mode 100644
index 0000000..2879ab2
--- /dev/null
+++ b/CHANGES/.TEMPLATE.rst
@@ -0,0 +1,90 @@
+{# TOWNCRIER TEMPLATE #}
+
+*({{ versiondata.date }})*
+
+{% for section, _ in sections.items() %}
+{% set underline = underlines[0] %}{% if section %}{{section}}
+{{ underline * section|length }}{% set underline = underlines[1] %}
+
+{% endif %}
+
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+{{ definitions[category]['name'] }}
+{{ underline * definitions[category]['name']|length }}
+
+{% if definitions[category]['showcontent'] %}
+{% for text, change_note_refs in sections[section][category].items() %}
+- {{ text }}
+
+ {{- '\n' * 2 -}}
+
+ {#-
+ NOTE: Replacing 'e' with 'f' is a hack that prevents Jinja's `int`
+ NOTE: filter internal implementation from treating the input as an
+ NOTE: infinite float when it looks like a scientific notation (with a
+ NOTE: single 'e' char in between digits), raising an `OverflowError`,
+ NOTE: subsequently. 'f' is still a hex letter so it won't affect the
+ NOTE: check for whether it's a (short or long) commit hash or not.
+ Ref: https://github.com/pallets/jinja/issues/1921
+ -#}
+ {%-
+ set pr_issue_numbers = change_note_refs
+ | map('lower')
+ | map('replace', 'e', 'f')
+ | map('int', default=None)
+ | select('integer')
+ | map('string')
+ | list
+ -%}
+ {%- set arbitrary_refs = [] -%}
+ {%- set commit_refs = [] -%}
+ {%- with -%}
+ {%- set commit_ref_candidates = change_note_refs | reject('in', pr_issue_numbers) -%}
+ {%- for cf in commit_ref_candidates -%}
+ {%- if cf | length in (7, 8, 40) and cf | int(default=None, base=16) is not none -%}
+ {%- set _ = commit_refs.append(cf) -%}
+ {%- else -%}
+ {%- set _ = arbitrary_refs.append(cf) -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {%- endwith -%}
+
+ {% if pr_issue_numbers %}
+ *Related issues and pull requests on GitHub:*
+ :issue:`{{ pr_issue_numbers | join('`, :issue:`') }}`.
+ {{- '\n' * 2 -}}
+ {%- endif -%}
+
+ {% if commit_refs %}
+ *Related commits on GitHub:*
+ :commit:`{{ commit_refs | join('`, :commit:`') }}`.
+ {{- '\n' * 2 -}}
+ {%- endif -%}
+
+ {% if arbitrary_refs %}
+ *Unlinked references:*
+ {{ arbitrary_refs | join(', ') }}.
+ {{- '\n' * 2 -}}
+ {%- endif -%}
+
+{% endfor %}
+{% else %}
+- {{ sections[section][category]['']|join(', ') }}
+
+{% endif %}
+{% if sections[section][category]|length == 0 %}
+No significant changes.
+
+{% else %}
+{% endif %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+
+{% endif %}
+{% endfor %}
+----
+{{ '\n' * 2 }}
diff --git a/CHANGES/.gitignore b/CHANGES/.gitignore
new file mode 100644
index 0000000..d6409a0
--- /dev/null
+++ b/CHANGES/.gitignore
@@ -0,0 +1,28 @@
+*
+!.TEMPLATE.rst
+!.gitignore
+!README.rst
+!*.bugfix
+!*.bugfix.rst
+!*.bugfix.*.rst
+!*.breaking
+!*.breaking.rst
+!*.breaking.*.rst
+!*.contrib
+!*.contrib.rst
+!*.contrib.*.rst
+!*.deprecation
+!*.deprecation.rst
+!*.deprecation.*.rst
+!*.doc
+!*.doc.rst
+!*.doc.*.rst
+!*.feature
+!*.feature.rst
+!*.feature.*.rst
+!*.misc
+!*.misc.rst
+!*.misc.*.rst
+!*.packaging
+!*.packaging.rst
+!*.packaging.*.rst
diff --git a/CHANGES/README.rst b/CHANGES/README.rst
new file mode 100644
index 0000000..dc99d01
--- /dev/null
+++ b/CHANGES/README.rst
@@ -0,0 +1,109 @@
+.. _Adding change notes with your PRs:
+
+Adding change notes with your PRs
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+It is very important to maintain a log for news of how
+updating to the new version of the software will affect
+end-users. This is why we enforce collection of the change
+fragment files in pull requests as per `Towncrier philosophy`_.
+
+The idea is that when somebody makes a change, they must record
+the bits that would affect end-users only including information
+that would be useful to them. Then, when the maintainers publish
+a new release, they'll automatically use these records to compose
+a change log for the respective version. It is important to
+understand that including unnecessary low-level implementation
+related details generates noise that is not particularly useful
+to the end-users most of the time. And so such details should be
+recorded in the Git history rather than a changelog.
+
+Alright! So how to add a news fragment?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``propcache`` uses `towncrier `_
+for changelog management.
+To submit a change note about your PR, add a text file into the
+``CHANGES/`` folder. It should contain an
+explanation of what applying this PR will change in the way
+end-users interact with the project. One sentence is usually
+enough but feel free to add as many details as you feel necessary
+for the users to understand what it means.
+
+**Use the past tense** for the text in your fragment because,
+combined with others, it will be a part of the "news digest"
+telling the readers **what changed** in a specific version of
+the library *since the previous version*. You should also use
+reStructuredText syntax for highlighting code (inline or block),
+linking parts of the docs or external sites.
+If you wish to sign your change, feel free to add ``-- by
+:user:`github-username``` at the end (replace ``github-username``
+with your own!).
+
+Finally, name your file following the convention that Towncrier
+understands: it should start with the number of an issue or a
+PR followed by a dot, then add a patch type, like ``feature``,
+``doc``, ``contrib`` etc., and add ``.rst`` as a suffix. If you
+need to add more than one fragment, you may add an optional
+sequence number (delimited with another period) between the type
+and the suffix.
+
+In general the name will follow ``..rst`` pattern,
+where the categories are:
+
+- ``bugfix``: A bug fix for something we deemed an improper undesired
+ behavior that got corrected in the release to match pre-agreed
+ expectations.
+- ``feature``: A new behavior, public APIs. That sort of stuff.
+- ``deprecation``: A declaration of future API removals and breaking
+ changes in behavior.
+- ``breaking``: When something public gets removed in a breaking way.
+ Could be deprecated in an earlier release.
+- ``doc``: Notable updates to the documentation structure or build
+ process.
+- ``packaging``: Notes for downstreams about unobvious side effects
+ and tooling. Changes in the test invocation considerations and
+ runtime assumptions.
+- ``contrib``: Stuff that affects the contributor experience. e.g.
+ Running tests, building the docs, setting up the development
+ environment.
+- ``misc``: Changes that are hard to assign to any of the above
+ categories.
+
+A pull request may have more than one of these components, for example
+a code change may introduce a new feature that deprecates an old
+feature, in which case two fragments should be added. It is not
+necessary to make a separate documentation fragment for documentation
+changes accompanying the relevant code changes.
+
+Examples for adding changelog entries to your Pull Requests
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+File :file:`CHANGES/603.removal.1.rst`:
+
+.. code-block:: rst
+
+ Dropped Python 3.5 support; Python 3.6 is the minimal supported Python
+ version -- by :user:`webknjaz`.
+
+File :file:`CHANGES/550.bugfix.rst`:
+
+.. code-block:: rst
+
+ Started shipping Windows wheels for the x86 architecture
+ -- by :user:`Dreamsorcerer`.
+
+File :file:`CHANGES/553.feature.rst`:
+
+.. code-block:: rst
+
+ Added support for ``GenericAliases`` (``MultiDict[str]``) under Python 3.9
+ and higher -- by :user:`mjpieters`.
+
+.. tip::
+
+ See :file:`towncrier.toml` for all available categories
+ (``tool.towncrier.type``).
+
+.. _Towncrier philosophy:
+ https://towncrier.readthedocs.io/en/stable/#philosophy
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/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/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..d0b57c1
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,21 @@
+include .coveragerc
+include pyproject.toml
+include pytest.ini
+include towncrier.toml
+include LICENSE
+include NOTICE
+include CHANGES.rst
+include README.rst
+graft propcache
+graft packaging
+graft docs
+graft CHANGES
+graft requirements
+graft tests
+global-exclude *.pyc
+global-exclude *.cache
+exclude propcache/*.c
+exclude propcache/*.html
+exclude propcache/*.so
+exclude propcache/*.pyd
+prune docs/_build
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..27e2121
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,64 @@
+PYXS = $(wildcard propcache/*.pyx)
+SRC = propcache tests
+
+all: test
+
+
+.install-deps: $(shell find requirements -type f)
+ pip install -U -r requirements/dev.txt
+ pre-commit install
+ @touch .install-deps
+
+
+.install-cython: requirements/cython.txt
+ pip install -r requirements/cython.txt
+ touch .install-cython
+
+
+propcache/%.c: propcache/%.pyx
+ python -m cython -3 -o $@ $< -I propcache
+
+
+.cythonize: .install-cython $(PYXS:.pyx=.c)
+
+
+cythonize: .cythonize
+
+
+.develop: .install-deps $(shell find propcache -type f)
+ @pip install -e .
+ @touch .develop
+
+fmt:
+ifdef CI
+ pre-commit run --all-files --show-diff-on-failure
+else
+ pre-commit run --all-files
+endif
+
+lint: fmt
+
+test: lint .develop
+ pytest ./tests ./propcache
+
+
+vtest: lint .develop
+ pytest ./tests ./propcache -v
+
+
+cov: lint .develop
+ pytest --cov propcache --cov-report html --cov-report term ./tests/ ./propcache/
+ @echo "open file://`pwd`/htmlcov/index.html"
+
+
+doc: doctest doc-spelling
+ make -C docs html SPHINXOPTS="-W -E --keep-going -n"
+ @echo "open file://`pwd`/docs/_build/html/index.html"
+
+
+doctest: .develop
+ make -C docs doctest SPHINXOPTS="-W -E --keep-going -n"
+
+
+doc-spelling:
+ make -C docs spelling SPHINXOPTS="-W -E --keep-going -n"
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..fa53b2b
--- /dev/null
+++ b/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/README.rst b/README.rst
new file mode 100644
index 0000000..0b8886c
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,99 @@
+propcache
+=========
+
+The module provides a fast implementation of cached properties for Python 3.8+.
+
+.. image:: https://github.com/aio-libs/propcache/workflows/CI/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.aio-libs.org
+
+
+.. 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.aio-libs.org.
+
+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 (like *Alpine Linux*, which is not
+manylinux-compliant because of the missing glibc and therefore, cannot be
+used with our wheels) 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.aio-libs.org.
+
+Source code
+-----------
+
+The project is hosted on GitHub_
+
+Please file an issue on the `bug tracker
+`_ 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/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..f7c1b21
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,230 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+ @echo "Please use \`make ' where is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " epub3 to make an epub3"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+ @echo " dummy to check syntax errors of document sources"
+
+.PHONY: clean
+clean:
+ rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/propcache.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/propcache.qhc"
+
+.PHONY: applehelp
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+.PHONY: devhelp
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/propcache"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/propcache"
+ @echo "# devhelp"
+
+.PHONY: epub
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: epub3
+epub3:
+ $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
+ @echo
+ @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
+
+.PHONY: latex
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+
+.PHONY: dummy
+dummy:
+ $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
+ @echo
+ @echo "Build finished. Dummy builder generates no files."
+
+spelling:
+ $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling
+ @echo
+ @echo "Build finished."
diff --git a/docs/changes.rst b/docs/changes.rst
new file mode 100644
index 0000000..f4ad80d
--- /dev/null
+++ b/docs/changes.rst
@@ -0,0 +1,18 @@
+.. _propcache_changes:
+
+=========
+Changelog
+=========
+
+.. only:: not is_release
+
+ To be included in v\ |release| (if present)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ .. towncrier-draft-entries:: |release| [UNRELEASED DRAFT]
+
+ Released versions
+ ^^^^^^^^^^^^^^^^^
+
+.. include:: ../CHANGES.rst
+ :start-after: .. towncrier release notes start
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..7acfa5d
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,453 @@
+#!/usr/bin/env python3
+#
+# propcache documentation build configuration file, created by
+# sphinx-quickstart on Mon Aug 29 19:55:36 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+import os
+import re
+from pathlib import Path
+
+PROJECT_ROOT_DIR = Path(__file__).parents[1].resolve()
+IS_RELEASE_ON_RTD = (
+ os.getenv("READTHEDOCS", "False") == "True"
+ and os.environ["READTHEDOCS_VERSION_TYPE"] == "tag"
+)
+if IS_RELEASE_ON_RTD:
+ tags.add("is_release")
+
+
+_docs_path = Path(__file__).parent
+_version_path = _docs_path / "../propcache/__init__.py"
+
+
+with _version_path.open() as fp:
+ try:
+ _version_info = re.search(
+ r"^__version__ = \""
+ r"(?P\d+)"
+ r"\.(?P\d+)"
+ r"\.(?P\d+)"
+ r"(?P.*)?\"$",
+ fp.read(),
+ re.M,
+ ).groupdict()
+ except IndexError:
+ raise RuntimeError("Unable to determine version.")
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ # stdlib-party extensions:
+ "sphinx.ext.extlinks",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.coverage",
+ "sphinx.ext.doctest",
+ "sphinx.ext.viewcode",
+ # Third-party extensions:
+ "alabaster",
+ "sphinxcontrib.towncrier.ext", # provides `towncrier-draft-entries` directive
+ "myst_parser", # extended markdown; https://pypi.org/project/myst-parser/
+]
+
+
+try:
+ import sphinxcontrib.spelling # noqa
+
+ extensions.append("sphinxcontrib.spelling")
+except ImportError:
+ pass
+
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3", None),
+ "multidict": ("https://multidict.aio-libs.org/en/stable", None),
+}
+
+
+# Add any paths that contain templates here, relative to this directory.
+# templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+
+# The encoding of source files.
+#
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = "index"
+
+# -- Project information -----------------------------------------------------
+
+github_url = "https://github.com"
+github_repo_org = "aio-libs"
+github_repo_name = "propcache"
+github_repo_slug = f"{github_repo_org}/{github_repo_name}"
+github_repo_url = f"{github_url}/{github_repo_slug}"
+github_sponsors_url = f"{github_url}/sponsors"
+
+project = github_repo_name
+copyright = f"2016, Andrew Svetlov, {project} contributors and aio-libs team"
+author = "Andrew Svetlov and aio-libs team"
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = "{major}.{minor}".format(**_version_info)
+# The full version, including alpha/beta/rc tags.
+release = "{major}.{minor}.{patch}-{tag}".format(**_version_info)
+
+rst_epilog = f"""
+.. |project| replace:: {project}
+""" # pylint: disable=invalid-name
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = "en"
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#
+# today = ''
+#
+# Else, today_fmt is used as the format for a strftime call.
+#
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+# -- Extension configuration -------------------------------------------------
+
+# -- Options for extlinks extension ---------------------------------------
+extlinks = {
+ "issue": (f"{github_repo_url}/issues/%s", "#%s"),
+ "pr": (f"{github_repo_url}/pull/%s", "PR #%s"),
+ "commit": (f"{github_repo_url}/commit/%s", "%s"),
+ "gh": (f"{github_url}/%s", "GitHub: %s"),
+ "user": (f"{github_sponsors_url}/%s", "@%s"),
+}
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "alabaster"
+
+html_theme_options = {
+ "description": "propcache",
+ "github_user": "aio-libs",
+ "github_repo": "propcache",
+ "github_button": True,
+ "github_type": "star",
+ "github_banner": True,
+ "codecov_button": True,
+ "pre_bg": "#FFF6E5",
+ "note_bg": "#E5ECD1",
+ "note_border": "#BFCF8C",
+ "body_text": "#482C0A",
+ "sidebar_text": "#49443E",
+ "sidebar_header": "#4B4032",
+ "sidebar_collapse": False,
+}
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# " v documentation" by default.
+#
+# html_title = 'propcache v0.1.0'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#
+# html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#
+# html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#
+# html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#
+# html_sidebars = {}
+
+html_sidebars = {
+ "**": [
+ "about.html",
+ "navigation.html",
+ "searchbox.html",
+ ]
+}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+#
+# html_domain_indices = True
+
+# If false, no index is generated.
+#
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
+#
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#
+# html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "propcachedoc"
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, "propcache.tex", "propcache Documentation", "Andrew Svetlov", "manual"),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+#
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#
+# latex_appendices = []
+
+# It false, will not define \strong, \code, itleref, \crossref ... but only
+# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
+# packages.
+#
+# latex_keep_old_macro_names = True
+
+# If false, no module index is generated.
+#
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [(master_doc, "propcache", "propcache Documentation", [author], 1)]
+
+# If true, show URL addresses after external links.
+#
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ master_doc,
+ "propcache",
+ "propcache Documentation",
+ author,
+ "propcache",
+ "One line description of project.",
+ "Miscellaneous",
+ ),
+]
+
+# Documents to append as an appendix to all manuals.
+#
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+#
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#
+# texinfo_no_detailmenu = False
+
+default_role = "any"
+nitpicky = True
+nitpick_ignore = [
+ ("envvar", "TMPDIR"),
+]
+
+# -- Options for towncrier_draft extension -----------------------------------
+
+towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-version', 'sphinx-release'
+towncrier_draft_include_empty = True
+towncrier_draft_working_directory = PROJECT_ROOT_DIR
+# Not yet supported: towncrier_draft_config_path = 'pyproject.toml' # relative to cwd
diff --git a/docs/contributing/guidelines.rst b/docs/contributing/guidelines.rst
new file mode 100644
index 0000000..f0413b8
--- /dev/null
+++ b/docs/contributing/guidelines.rst
@@ -0,0 +1,28 @@
+-----------------
+Contributing docs
+-----------------
+
+We use Sphinx_ to generate our docs website. You can trigger
+the process locally by executing:
+
+ .. code-block:: shell-session
+
+ $ make doc
+
+It is also integrated with `Read The Docs`_ that builds and
+publishes each commit to the main branch and generates live
+docs previews for each pull request.
+
+The sources of the Sphinx_ documents use reStructuredText as a
+de-facto standard. But in order to make contributing docs more
+beginner-friendly, we've integrated `MyST parser`_ allowing us
+to also accept new documents written in an extended version of
+Markdown that supports using Sphinx directives and roles. `Read
+the docs `_ to learn more on how to use it.
+
+.. _MyST docs: https://myst-parser.readthedocs.io/en/latest/using/intro.html#writing-myst-in-sphinx
+.. _MyST parser: https://pypi.org/project/myst-parser/
+.. _Read The Docs: https://readthedocs.org
+.. _Sphinx: https://www.sphinx-doc.org
+
+.. include:: ../../CHANGES/README.rst
diff --git a/docs/contributing/release_guide.rst b/docs/contributing/release_guide.rst
new file mode 100644
index 0000000..e5c7e5d
--- /dev/null
+++ b/docs/contributing/release_guide.rst
@@ -0,0 +1,105 @@
+*************
+Release Guide
+*************
+
+Welcome to the |project| Release Guide!
+
+This page contains information on how to release a new version
+of |project| using the automated Continuous Delivery pipeline.
+
+.. tip::
+
+ The intended audience for this document is maintainers
+ and core contributors.
+
+
+Pre-release activities
+======================
+
+1. Check if there are any open Pull Requests that could be
+ desired in the upcoming release. If there are any — merge
+ them. If some are incomplete, try to get them ready.
+ Don't forget to review the enclosed change notes per our
+ guidelines.
+2. Visually inspect the draft section of the :ref:`Changelog`
+ page. Make sure the content looks consistent, uses the same
+ writing style, targets the end-users and adheres to our
+ documented guidelines.
+ Most of the changelog sections will typically use the past
+ tense or another way to relay the effect of the changes for
+ the users, since the previous release.
+ It should not target core contributors as the information
+ they are normally interested in is already present in the
+ Git history.
+ Update the changelog fragments if you see any problems with
+ this changelog section.
+3. Optionally, test the previously published nightlies, that are
+ available through GitHub Actions CI/CD artifacts, locally.
+4. If you are satisfied with the above, inspect the changelog
+ section categories in the draft. Presence of the breaking
+ changes or features will hint you what version number
+ segment to bump for the release.
+5. Update the hardcoded version string in :file:`propcache/__init__.py`.
+ Generate a new changelog from the fragments, and commit it
+ along with the fragments removal and the Python module changes.
+ Use the following commands, don't prepend a leading-``v`` before
+ the version number. Just use the raw version number as per
+ :pep:`440`.
+
+ .. code-block:: shell-session
+
+ [dir:propcache] $ propcache/__init__.py
+ [dir:propcache] $ python -m towncrier build \
+ -- --version 'VERSION_WITHOUT_LEADING_V'
+ [dir:propcache] $ git commit -v CHANGES{.rst,/} propcache/__init__.py
+
+.. seealso::
+
+ :ref:`Adding change notes with your PRs`
+ Writing beautiful changelogs for humans
+
+
+The release stage
+=================
+
+1. Tag the commit with version and changelog changes, created
+ during the preparation stage. If possible, make it GPG-signed.
+ Prepend a leading ``v`` before the version number for the tag
+ name. Add an extra sentence describing the release contents,
+ in a few words.
+
+ .. code-block:: shell-session
+
+ [dir:propcache] $ git tag \
+ -s 'VERSION_WITH_LEADING_V' \
+ -m 'VERSION_WITH_LEADING_V' \
+ -m 'This release does X and Y.'
+
+
+2. Push that tag to the upstream repository, which ``origin`` is
+ considered to be in the example below.
+
+ .. code-block:: shell-session
+
+ [dir:propcache] $ git push origin 'VERSION_WITH_LEADING_V'
+
+3. You can open the `GitHub Actions CI/CD workflow page `_ in your web browser to monitor the
+ progress. But generally, you don't need to babysit the CI.
+4. Check that web page or your email inbox for the notification
+ with an approval request. GitHub will send it when it reaches
+ the final "publishing" job.
+5. Approve the deployment and wait for the CD workflow to complete.
+6. Verify that the following things got created:
+ - a PyPI release
+ - a Git tag
+ - a GitHub Releases page
+7. Tell everyone you released a new version of |project| :)
+ Depending on your mental capacity and the burnout stage, you
+ are encouraged to post the updates in issues asking for the
+ next release, contributed PRs, Bluesky, Twitter etc. You can
+ also call out prominent contributors and thank them!
+
+
+.. _GitHub Actions CI/CD workflow:
+ https://github.com/aio-libs/propcache/actions/workflows/ci-cd.yml
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..24a7328
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,74 @@
+.. propcache documentation master file, created by
+ sphinx-quickstart on Mon Aug 29 19:55:36 2016.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+propcache
+====
+
+The module provides handy :class:`~propcache.URL` class for URL parsing and
+changing.
+
+Introduction
+------------
+
+Usage
+-----
+
+The API is designed to be nearly identical to the built-in :py:class:`functools.cached_property` class,
+except for the additional :class:`under_cached_property` class which uses ``self._cache``
+instead of ``self.__dict__`` to store the cached values and prevents ``__set__`` from being called.
+
+Source code
+-----------
+
+The project is hosted on GitHub_
+
+Please file an issue on the `bug tracker
+`_ 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.
+
+
+
+Contents:
+
+.. toctree::
+ :caption: What's new
+
+ changes
+
+.. toctree::
+ :caption: Contributing
+
+ contributing/guidelines
+
+.. toctree::
+ :caption: Maintenance
+
+ contributing/release_guide
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+
+.. _GitHub: https://github.com/aio-libs/propcache
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..517bc84
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,281 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^` where ^ is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. epub3 to make an epub3
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ echo. coverage to run coverage check of the documentation if enabled
+ echo. dummy to check syntax errors of document sources
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+REM Check if sphinx-build is available and fallback to Python version if any
+%SPHINXBUILD% 1>NUL 2>NUL
+if errorlevel 9009 goto sphinx_python
+goto sphinx_ok
+
+:sphinx_python
+
+set SPHINXBUILD=python -m sphinx.__init__
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+:sphinx_ok
+
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\propcache.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\propcache.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "epub3" (
+ %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "coverage" (
+ %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of coverage in the sources finished, look at the ^
+results in %BUILDDIR%/coverage/python.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+if "%1" == "dummy" (
+ %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. Dummy builder generates no files.
+ goto end
+)
+
+:end
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
new file mode 100644
index 0000000..752315c
--- /dev/null
+++ b/docs/spelling_wordlist.txt
@@ -0,0 +1,50 @@
+Bluesky
+Bugfixes
+Changelog
+Codecov
+Cython
+GPG
+IPv
+PRs
+PYX
+Towncrier
+Twitter
+UTF
+aiohttp
+backend
+boolean
+booleans
+bools
+changelog
+changelogs
+config
+de
+decodable
+dev
+dists
+downstreams
+facto
+glibc
+google
+hardcoded
+hostnames
+macOS
+mailto
+manylinux
+multi
+nightlies
+pre
+rc
+reStructuredText
+reencoding
+requote
+requoting
+runtimes
+sdist
+subclass
+subclasses
+subcomponent
+svetlov
+uncompiled
+v1
+propcache
diff --git a/packaging/README.md b/packaging/README.md
new file mode 100644
index 0000000..9940dc5
--- /dev/null
+++ b/packaging/README.md
@@ -0,0 +1,11 @@
+# `pep517_backend` in-tree build backend
+
+The `pep517_backend.hooks` importable exposes callables declared by PEP 517
+and PEP 660 and is integrated into `pyproject.toml`'s
+`[build-system].build-backend` through `[build-system].backend-path`.
+
+# Design considerations
+
+`__init__.py` is to remain empty, leaving `hooks.py` the only entrypoint
+exposing the callables. The logic is contained in private modules. This is
+to prevent import-time side effects.
diff --git a/packaging/pep517_backend/__init__.py b/packaging/pep517_backend/__init__.py
new file mode 100644
index 0000000..74ae436
--- /dev/null
+++ b/packaging/pep517_backend/__init__.py
@@ -0,0 +1 @@
+"""PEP 517 build backend for optionally pre-building Cython."""
diff --git a/packaging/pep517_backend/__main__.py b/packaging/pep517_backend/__main__.py
new file mode 100644
index 0000000..7ad33e7
--- /dev/null
+++ b/packaging/pep517_backend/__main__.py
@@ -0,0 +1,6 @@
+import sys
+
+from . import cli
+
+if __name__ == "__main__":
+ sys.exit(cli.run_main_program(argv=sys.argv))
diff --git a/packaging/pep517_backend/_backend.py b/packaging/pep517_backend/_backend.py
new file mode 100644
index 0000000..f9b81ae
--- /dev/null
+++ b/packaging/pep517_backend/_backend.py
@@ -0,0 +1,395 @@
+# fmt: off
+"""PEP 517 build backend wrapper for pre-building Cython for wheel."""
+
+from __future__ import annotations
+
+import os
+import typing as t
+from contextlib import contextmanager, nullcontext, suppress
+from functools import partial
+from pathlib import Path
+from shutil import copytree
+from sys import implementation as _system_implementation
+from sys import stderr as _standard_error_stream
+from sys import version_info as _python_version_tuple
+from tempfile import TemporaryDirectory
+from warnings import warn as _warn_that
+
+from setuptools.build_meta import build_sdist as _setuptools_build_sdist
+from setuptools.build_meta import build_wheel as _setuptools_build_wheel
+from setuptools.build_meta import (
+ get_requires_for_build_wheel as _setuptools_get_requires_for_build_wheel,
+)
+from setuptools.build_meta import (
+ prepare_metadata_for_build_wheel as _setuptools_prepare_metadata_for_build_wheel,
+)
+
+try:
+ from setuptools.build_meta import build_editable as _setuptools_build_editable
+except ImportError:
+ _setuptools_build_editable = None # type: ignore[assignment]
+
+
+# isort: split
+from distutils.command.install import install as _distutils_install_cmd
+from distutils.core import Distribution as _DistutilsDistribution
+from distutils.dist import DistributionMetadata as _DistutilsDistributionMetadata
+
+with suppress(ImportError):
+ # NOTE: Only available for wheel builds that bundle C-extensions. Declared
+ # NOTE: by `get_requires_for_build_wheel()` and
+ # NOTE: `get_requires_for_build_editable()`, when `pure-python`
+ # NOTE: is not passed.
+ from Cython.Build.Cythonize import main as _cythonize_cli_cmd
+
+from ._compat import chdir_cm
+from ._cython_configuration import ( # noqa: WPS436
+ get_local_cython_config as _get_local_cython_config,
+)
+from ._cython_configuration import (
+ make_cythonize_cli_args_from_config as _make_cythonize_cli_args_from_config,
+)
+from ._cython_configuration import patched_env as _patched_cython_env
+from ._transformers import sanitize_rst_roles # noqa: WPS436
+
+__all__ = ( # noqa: WPS410
+ 'build_sdist',
+ 'build_wheel',
+ 'get_requires_for_build_wheel',
+ 'prepare_metadata_for_build_wheel',
+ *(
+ () if _setuptools_build_editable is None
+ else (
+ 'build_editable',
+ 'get_requires_for_build_editable',
+ 'prepare_metadata_for_build_editable',
+ )
+ ),
+)
+
+_ConfigDict = t.Dict[str, t.Union[str, t.List[str], None]]
+
+
+CYTHON_TRACING_CONFIG_SETTING = 'with-cython-tracing'
+"""Config setting name toggle to include line tracing to C-exts."""
+
+CYTHON_TRACING_ENV_VAR = 'PROPCACHECYTHON_TRACING'
+"""Environment variable name toggle used to opt out of making C-exts."""
+
+PURE_PYTHON_CONFIG_SETTING = 'pure-python'
+"""Config setting name toggle that is used to opt out of making C-exts."""
+
+PURE_PYTHON_ENV_VAR = 'PROPCACHE_NO_EXTENSIONS'
+"""Environment variable name toggle used to opt out of making C-exts."""
+
+IS_PY3_12_PLUS = _python_version_tuple[:2] >= (3, 12)
+"""A flag meaning that the current runtime is Python 3.12 or higher."""
+
+IS_CPYTHON = _system_implementation.name == "cpython"
+"""A flag meaning that the current interpreter implementation is CPython."""
+
+PURE_PYTHON_MODE_CLI_FALLBACK = not IS_CPYTHON
+"""A fallback for ``pure-python`` is not set."""
+
+
+def _is_truthy_setting_value(setting_value) -> bool:
+ truthy_values = {'', None, 'true', '1', 'on'}
+ return setting_value.lower() in truthy_values
+
+
+def _get_setting_value(
+ config_settings: _ConfigDict | None = None,
+ config_setting_name: str | None = None,
+ env_var_name: str | None = None,
+ *,
+ default: bool = False,
+) -> bool:
+ user_provided_setting_sources = (
+ (config_settings, config_setting_name, (KeyError, TypeError)),
+ (os.environ, env_var_name, KeyError),
+ )
+ for src_mapping, src_key, lookup_errors in user_provided_setting_sources:
+ if src_key is None:
+ continue
+
+ with suppress(lookup_errors): # type: ignore[arg-type]
+ return _is_truthy_setting_value(src_mapping[src_key]) # type: ignore[index]
+
+ return default
+
+
+def _make_pure_python(config_settings: _ConfigDict | None = None) -> bool:
+ return _get_setting_value(
+ config_settings,
+ PURE_PYTHON_CONFIG_SETTING,
+ PURE_PYTHON_ENV_VAR,
+ default=PURE_PYTHON_MODE_CLI_FALLBACK,
+ )
+
+
+def _include_cython_line_tracing(
+ config_settings: _ConfigDict | None = None,
+ *,
+ default=False,
+) -> bool:
+ return _get_setting_value(
+ config_settings,
+ CYTHON_TRACING_CONFIG_SETTING,
+ CYTHON_TRACING_ENV_VAR,
+ default=default,
+ )
+
+
+@contextmanager
+def patched_distutils_cmd_install():
+ """Make `install_lib` of `install` cmd always use `platlib`.
+
+ :yields: None
+ """
+ # Without this, build_lib puts stuff under `*.data/purelib/` folder
+ orig_finalize = _distutils_install_cmd.finalize_options
+
+ def new_finalize_options(self): # noqa: WPS430
+ self.install_lib = self.install_platlib
+ orig_finalize(self)
+
+ _distutils_install_cmd.finalize_options = new_finalize_options
+ try:
+ yield
+ finally:
+ _distutils_install_cmd.finalize_options = orig_finalize
+
+
+@contextmanager
+def patched_dist_has_ext_modules():
+ """Make `has_ext_modules` of `Distribution` always return `True`.
+
+ :yields: None
+ """
+ # Without this, build_lib puts stuff under `*.data/platlib/` folder
+ orig_func = _DistutilsDistribution.has_ext_modules
+
+ _DistutilsDistribution.has_ext_modules = lambda *args, **kwargs: True
+ try:
+ yield
+ finally:
+ _DistutilsDistribution.has_ext_modules = orig_func
+
+
+@contextmanager
+def patched_dist_get_long_description():
+ """Make `has_ext_modules` of `Distribution` always return `True`.
+
+ :yields: None
+ """
+ # Without this, build_lib puts stuff under `*.data/platlib/` folder
+ _orig_func = _DistutilsDistributionMetadata.get_long_description
+
+ def _get_sanitized_long_description(self):
+ return sanitize_rst_roles(self.long_description)
+
+ _DistutilsDistributionMetadata.get_long_description = (
+ _get_sanitized_long_description
+ )
+ try:
+ yield
+ finally:
+ _DistutilsDistributionMetadata.get_long_description = _orig_func
+
+
+def _exclude_dir_path(
+ excluded_dir_path: Path,
+ visited_directory: str,
+ _visited_dir_contents: list[str],
+) -> list[str]:
+ """Prevent recursive directory traversal."""
+ # This stops the temporary directory from being copied
+ # into self recursively forever.
+ # Ref: https://github.com/aio-libs/propcache/issues/992
+ visited_directory_subdirs_to_ignore = [
+ subdir
+ for subdir in _visited_dir_contents
+ if excluded_dir_path == Path(visited_directory) / subdir
+ ]
+ if visited_directory_subdirs_to_ignore:
+ print(
+ f'Preventing `{excluded_dir_path !s}` from being '
+ 'copied into itself recursively...',
+ file=_standard_error_stream,
+ )
+ return visited_directory_subdirs_to_ignore
+
+
+@contextmanager
+def _in_temporary_directory(src_dir: Path) -> t.Iterator[None]:
+ with TemporaryDirectory(prefix='.tmp-propcache-pep517-') as tmp_dir:
+ tmp_dir_path = Path(tmp_dir)
+ root_tmp_dir_path = tmp_dir_path.parent
+ _exclude_tmpdir_parent = partial(_exclude_dir_path, root_tmp_dir_path)
+
+ with chdir_cm(tmp_dir):
+ tmp_src_dir = tmp_dir_path / 'src'
+ copytree(
+ src_dir,
+ tmp_src_dir,
+ ignore=_exclude_tmpdir_parent,
+ symlinks=True,
+ )
+ os.chdir(tmp_src_dir)
+ yield
+
+
+@contextmanager
+def maybe_prebuild_c_extensions(
+ line_trace_cython_when_unset: bool = False,
+ build_inplace: bool = False,
+ config_settings: _ConfigDict | None = None,
+) -> t.Generator[None, t.Any, t.Any]:
+ """Pre-build C-extensions in a temporary directory, when needed.
+
+ This context manager also patches metadata, setuptools and distutils.
+
+ :param build_inplace: Whether to copy and chdir to a temporary location.
+ :param config_settings: :pep:`517` config settings mapping.
+
+ """
+ cython_line_tracing_requested = _include_cython_line_tracing(
+ config_settings,
+ default=line_trace_cython_when_unset,
+ )
+ is_pure_python_build = _make_pure_python(config_settings)
+
+ if is_pure_python_build:
+ print("*********************", file=_standard_error_stream)
+ print("* Pure Python build *", file=_standard_error_stream)
+ print("*********************", file=_standard_error_stream)
+
+ if cython_line_tracing_requested:
+ _warn_that(
+ f'The `{CYTHON_TRACING_CONFIG_SETTING !s}` setting requesting '
+ 'Cython line tracing is set, but building C-extensions is not. '
+ 'This option will not have any effect for in the pure-python '
+ 'build mode.',
+ RuntimeWarning,
+ stacklevel=999,
+ )
+
+ yield
+ return
+
+ print("**********************", file=_standard_error_stream)
+ print("* Accelerated build *", file=_standard_error_stream)
+ print("**********************", file=_standard_error_stream)
+ if not IS_CPYTHON:
+ _warn_that(
+ 'Building C-extensions under the runtimes other than CPython is '
+ 'unsupported and will likely fail. Consider passing the '
+ f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.',
+ RuntimeWarning,
+ stacklevel=999,
+ )
+
+ build_dir_ctx = (
+ nullcontext() if build_inplace
+ else _in_temporary_directory(src_dir=Path.cwd().resolve())
+ )
+ with build_dir_ctx:
+ config = _get_local_cython_config()
+
+ cythonize_args = _make_cythonize_cli_args_from_config(config)
+ with _patched_cython_env(config['env'], cython_line_tracing_requested):
+ _cythonize_cli_cmd(cythonize_args)
+ with patched_distutils_cmd_install():
+ with patched_dist_has_ext_modules():
+ yield
+
+
+@patched_dist_get_long_description()
+def build_wheel(
+ wheel_directory: str,
+ config_settings: _ConfigDict | None = None,
+ metadata_directory: str | None = None,
+) -> str:
+ """Produce a built wheel.
+
+ This wraps the corresponding ``setuptools``' build backend hook.
+
+ :param wheel_directory: Directory to put the resulting wheel in.
+ :param config_settings: :pep:`517` config settings mapping.
+ :param metadata_directory: :file:`.dist-info` directory path.
+
+ """
+ with maybe_prebuild_c_extensions(
+ line_trace_cython_when_unset=False,
+ build_inplace=False,
+ config_settings=config_settings,
+ ):
+ return _setuptools_build_wheel(
+ wheel_directory=wheel_directory,
+ config_settings=config_settings,
+ metadata_directory=metadata_directory,
+ )
+
+
+@patched_dist_get_long_description()
+def build_editable(
+ wheel_directory: str,
+ config_settings: _ConfigDict | None = None,
+ metadata_directory: str | None = None,
+) -> str:
+ """Produce a built wheel for editable installs.
+
+ This wraps the corresponding ``setuptools``' build backend hook.
+
+ :param wheel_directory: Directory to put the resulting wheel in.
+ :param config_settings: :pep:`517` config settings mapping.
+ :param metadata_directory: :file:`.dist-info` directory path.
+
+ """
+ with maybe_prebuild_c_extensions(
+ line_trace_cython_when_unset=True,
+ build_inplace=True,
+ config_settings=config_settings,
+ ):
+ return _setuptools_build_editable(
+ wheel_directory=wheel_directory,
+ config_settings=config_settings,
+ metadata_directory=metadata_directory,
+ )
+
+
+def get_requires_for_build_wheel(
+ config_settings: _ConfigDict | None = None,
+) -> list[str]:
+ """Determine additional requirements for building wheels.
+
+ :param config_settings: :pep:`517` config settings mapping.
+
+ """
+ is_pure_python_build = _make_pure_python(config_settings)
+
+ if not is_pure_python_build and not IS_CPYTHON:
+ _warn_that(
+ 'Building C-extensions under the runtimes other than CPython is '
+ 'unsupported and will likely fail. Consider passing the '
+ f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.',
+ RuntimeWarning,
+ stacklevel=999,
+ )
+
+ c_ext_build_deps = [] if is_pure_python_build else [
+ 'Cython >= 3.0.0b3' if IS_PY3_12_PLUS # Only Cython 3+ is compatible
+ else 'Cython',
+ ]
+
+ return _setuptools_get_requires_for_build_wheel(
+ config_settings=config_settings,
+ ) + c_ext_build_deps
+
+
+build_sdist = patched_dist_get_long_description()(_setuptools_build_sdist)
+get_requires_for_build_editable = get_requires_for_build_wheel
+prepare_metadata_for_build_wheel = patched_dist_get_long_description()(
+ _setuptools_prepare_metadata_for_build_wheel,
+)
+prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel
diff --git a/packaging/pep517_backend/_compat.py b/packaging/pep517_backend/_compat.py
new file mode 100644
index 0000000..dccada6
--- /dev/null
+++ b/packaging/pep517_backend/_compat.py
@@ -0,0 +1,33 @@
+"""Cross-python stdlib shims."""
+
+import os
+import typing as t
+from contextlib import contextmanager
+from pathlib import Path
+
+# isort: off
+try:
+ from contextlib import chdir as chdir_cm # type: ignore[attr-defined, unused-ignore] # noqa: E501
+except ImportError:
+
+ @contextmanager # type: ignore[no-redef, unused-ignore]
+ def chdir_cm(path: os.PathLike) -> t.Iterator[None]:
+ """Temporarily change the current directory, recovering on exit."""
+ original_wd = Path.cwd()
+ os.chdir(path)
+ try:
+ yield
+ finally:
+ os.chdir(original_wd)
+
+
+# isort: on
+
+
+try:
+ from tomllib import loads as load_toml_from_string
+except ImportError:
+ from tomli import loads as load_toml_from_string
+
+
+__all__ = ("chdir_cm", "load_toml_from_string") # noqa: WPS410
diff --git a/packaging/pep517_backend/_cython_configuration.py b/packaging/pep517_backend/_cython_configuration.py
new file mode 100644
index 0000000..316b85f
--- /dev/null
+++ b/packaging/pep517_backend/_cython_configuration.py
@@ -0,0 +1,107 @@
+# fmt: off
+
+from __future__ import annotations
+
+import os
+from contextlib import contextmanager
+from pathlib import Path
+from sys import version_info as _python_version_tuple
+
+from expandvars import expandvars
+
+from ._compat import load_toml_from_string # noqa: WPS436
+from ._transformers import ( # noqa: WPS436
+ get_cli_kwargs_from_config,
+ get_enabled_cli_flags_from_config,
+)
+
+
+def get_local_cython_config() -> dict:
+ """Grab optional build dependencies from pyproject.toml config.
+
+ :returns: config section from ``pyproject.toml``
+ :rtype: dict
+
+ This basically reads entries from::
+
+ [tool.local.cythonize]
+ # Env vars provisioned during cythonize call
+ src = ["src/**/*.pyx"]
+
+ [tool.local.cythonize.env]
+ # Env vars provisioned during cythonize call
+ LDFLAGS = "-lssh"
+
+ [tool.local.cythonize.flags]
+ # This section can contain the following booleans:
+ # * annotate — generate annotated HTML page for source files
+ # * build — build extension modules using distutils
+ # * inplace — build extension modules in place using distutils (implies -b)
+ # * force — force recompilation
+ # * quiet — be less verbose during compilation
+ # * lenient — increase Python compat by ignoring some compile time errors
+ # * keep-going — compile as much as possible, ignore compilation failures
+ annotate = false
+ build = false
+ inplace = true
+ force = true
+ quiet = false
+ lenient = false
+ keep-going = false
+
+ [tool.local.cythonize.kwargs]
+ # This section can contain args that have values:
+ # * exclude=PATTERN exclude certain file patterns from the compilation
+ # * parallel=N run builds in N parallel jobs (default: calculated per system)
+ exclude = "**.py"
+ parallel = 12
+
+ [tool.local.cythonize.kwargs.directives]
+ # This section can contain compiler directives
+ # NAME = "VALUE"
+
+ [tool.local.cythonize.kwargs.compile-time-env]
+ # This section can contain compile time env vars
+ # NAME = "VALUE"
+
+ [tool.local.cythonize.kwargs.options]
+ # This section can contain cythonize options
+ # NAME = "VALUE"
+ """
+ config_toml_txt = (Path.cwd().resolve() / 'pyproject.toml').read_text()
+ config_mapping = load_toml_from_string(config_toml_txt)
+ return config_mapping['tool']['local']['cythonize']
+
+
+def make_cythonize_cli_args_from_config(config) -> list[str]:
+ py_ver_arg = f'-{_python_version_tuple.major!s}'
+
+ cli_flags = get_enabled_cli_flags_from_config(config['flags'])
+ cli_kwargs = get_cli_kwargs_from_config(config['kwargs'])
+
+ return cli_flags + [py_ver_arg] + cli_kwargs + ['--'] + config['src']
+
+
+@contextmanager
+def patched_env(env: dict[str, str], cython_line_tracing_requested: bool):
+ """Temporary set given env vars.
+
+ :param env: tmp env vars to set
+ :type env: dict
+
+ :yields: None
+ """
+ orig_env = os.environ.copy()
+ expanded_env = {name: expandvars(var_val) for name, var_val in env.items()}
+ os.environ.update(expanded_env)
+
+ if cython_line_tracing_requested:
+ os.environ['CFLAGS'] = ' '.join((
+ os.getenv('CFLAGS', ''),
+ '-DCYTHON_TRACE_NOGIL=1', # Implies CYTHON_TRACE=1
+ )).strip()
+ try:
+ yield
+ finally:
+ os.environ.clear()
+ os.environ.update(orig_env)
diff --git a/packaging/pep517_backend/_transformers.py b/packaging/pep517_backend/_transformers.py
new file mode 100644
index 0000000..39bfdf8
--- /dev/null
+++ b/packaging/pep517_backend/_transformers.py
@@ -0,0 +1,107 @@
+"""Data conversion helpers for the in-tree PEP 517 build backend."""
+
+from itertools import chain
+from re import sub as _substitute_with_regexp
+
+
+def _emit_opt_pairs(opt_pair):
+ flag, flag_value = opt_pair
+ flag_opt = f"--{flag!s}"
+ if isinstance(flag_value, dict):
+ sub_pairs = flag_value.items()
+ else:
+ sub_pairs = ((flag_value,),)
+
+ yield from ("=".join(map(str, (flag_opt,) + pair)) for pair in sub_pairs)
+
+
+def get_cli_kwargs_from_config(kwargs_map):
+ """Make a list of options with values from config."""
+ return list(chain.from_iterable(map(_emit_opt_pairs, kwargs_map.items())))
+
+
+def get_enabled_cli_flags_from_config(flags_map):
+ """Make a list of enabled boolean flags from config."""
+ return [f"--{flag}" for flag, is_enabled in flags_map.items() if is_enabled]
+
+
+def sanitize_rst_roles(rst_source_text: str) -> str:
+ """Replace RST roles with inline highlighting."""
+ pep_role_regex = r"""(?x)
+ :pep:`(?P\d+)`
+ """
+ pep_substitution_pattern = (
+ r"`PEP \g>`__"
+ )
+
+ user_role_regex = r"""(?x)
+ :user:`(?P[^`]+)(?:\s+(.*))?`
+ """
+ user_substitution_pattern = (
+ r"`@\g "
+ r">`__"
+ )
+
+ issue_role_regex = r"""(?x)
+ :issue:`(?P[^`]+)(?:\s+(.*))?`
+ """
+ issue_substitution_pattern = (
+ r"`#\g "
+ r">`__"
+ )
+
+ pr_role_regex = r"""(?x)
+ :pr:`(?P[^`]+)(?:\s+(.*))?`
+ """
+ pr_substitution_pattern = (
+ r"`PR #\g "
+ r">`__"
+ )
+
+ commit_role_regex = r"""(?x)
+ :commit:`(?P[^`]+)(?:\s+(.*))?`
+ """
+ commit_substitution_pattern = (
+ r"`\g "
+ r">`__"
+ )
+
+ gh_role_regex = r"""(?x)
+ :gh:`(?P[^`<]+)(?:\s+([^`]*))?`
+ """
+ gh_substitution_pattern = r"GitHub: ``\g``"
+
+ meth_role_regex = r"""(?x)
+ (?::py)?:meth:`~?(?P[^`<]+)(?:\s+([^`]*))?`
+ """
+ meth_substitution_pattern = r"``\g()``"
+
+ role_regex = r"""(?x)
+ (?::\w+)?:\w+:`(?P[^`<]+)(?:\s+([^`]*))?`
+ """
+ substitution_pattern = r"``\g``"
+
+ project_substitution_regex = r"\|project\|"
+ project_substitution_pattern = "propcache"
+
+ substitutions = (
+ (pep_role_regex, pep_substitution_pattern),
+ (user_role_regex, user_substitution_pattern),
+ (issue_role_regex, issue_substitution_pattern),
+ (pr_role_regex, pr_substitution_pattern),
+ (commit_role_regex, commit_substitution_pattern),
+ (gh_role_regex, gh_substitution_pattern),
+ (meth_role_regex, meth_substitution_pattern),
+ (role_regex, substitution_pattern),
+ (project_substitution_regex, project_substitution_pattern),
+ )
+
+ rst_source_normalized_text = rst_source_text
+ for regex, substitution in substitutions:
+ rst_source_normalized_text = _substitute_with_regexp(
+ regex,
+ substitution,
+ rst_source_normalized_text,
+ )
+
+ return rst_source_normalized_text
diff --git a/packaging/pep517_backend/cli.py b/packaging/pep517_backend/cli.py
new file mode 100644
index 0000000..f3a1c85
--- /dev/null
+++ b/packaging/pep517_backend/cli.py
@@ -0,0 +1,53 @@
+# fmt: off
+
+from __future__ import annotations
+
+import sys
+from itertools import chain
+from pathlib import Path
+
+from Cython.Compiler.Main import compile as _translate_cython_cli_cmd
+from Cython.Compiler.Main import parse_command_line as _split_cython_cli_args
+
+from ._cython_configuration import get_local_cython_config as _get_local_cython_config
+from ._cython_configuration import (
+ make_cythonize_cli_args_from_config as _make_cythonize_cli_args_from_config,
+)
+from ._cython_configuration import patched_env as _patched_cython_env
+
+_PROJECT_PATH = Path(__file__).parents[2]
+
+
+def run_main_program(argv) -> int | str:
+ """Invoke ``translate-cython`` or fail."""
+ if len(argv) != 2:
+ return 'This program only accepts one argument -- "translate-cython"'
+
+ if argv[1] != 'translate-cython':
+ return 'This program only implements the "translate-cython" subcommand'
+
+ config = _get_local_cython_config()
+ config['flags'] = {'keep-going': config['flags']['keep-going']}
+ config['src'] = list(
+ map(
+ str,
+ chain.from_iterable(
+ map(_PROJECT_PATH.glob, config['src']),
+ ),
+ ),
+ )
+ translate_cython_cli_args = _make_cythonize_cli_args_from_config(config)
+
+ cython_options, cython_sources = _split_cython_cli_args(
+ translate_cython_cli_args,
+ )
+
+ with _patched_cython_env(config['env'], cython_line_tracing_requested=True):
+ return _translate_cython_cli_cmd(
+ cython_sources,
+ cython_options,
+ ).num_errors
+
+
+if __name__ == '__main__':
+ sys.exit(run_main_program(argv=sys.argv))
diff --git a/packaging/pep517_backend/hooks.py b/packaging/pep517_backend/hooks.py
new file mode 100644
index 0000000..5fa77fe
--- /dev/null
+++ b/packaging/pep517_backend/hooks.py
@@ -0,0 +1,21 @@
+"""PEP 517 build backend for optionally pre-building Cython."""
+
+from contextlib import suppress as _suppress
+
+from setuptools.build_meta import * # Re-exporting PEP 517 hooks # pylint: disable=unused-wildcard-import,wildcard-import # noqa: E501, F401, F403
+
+# Re-exporting PEP 517 hooks
+from ._backend import ( # type: ignore[assignment] # noqa: WPS436
+ build_sdist,
+ build_wheel,
+ get_requires_for_build_wheel,
+ prepare_metadata_for_build_wheel,
+)
+
+with _suppress(ImportError): # Only succeeds w/ setuptools implementing PEP 660
+ # Re-exporting PEP 660 hooks
+ from ._backend import ( # type: ignore[assignment] # noqa: WPS436
+ build_editable,
+ get_requires_for_build_editable,
+ prepare_metadata_for_build_editable,
+ )
diff --git a/propcache/__init__.py b/propcache/__init__.py
new file mode 100644
index 0000000..553d1d8
--- /dev/null
+++ b/propcache/__init__.py
@@ -0,0 +1,8 @@
+from ._helpers import cached_property, under_cached_property
+
+__version__ = "0.0.0.dev0"
+
+__all__ = (
+ "cached_property",
+ "under_cached_property",
+)
diff --git a/propcache/_helpers.py b/propcache/_helpers.py
new file mode 100644
index 0000000..99cadfd
--- /dev/null
+++ b/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] # noqa: E501
+ from ._helpers_c import under_cached_property as under_cached_property_c # type: ignore[attr-defined, unused-ignore] # noqa: E501
+
+ 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/propcache/_helpers_c.pyx b/propcache/_helpers_c.pyx
new file mode 100644
index 0000000..235510b
--- /dev/null
+++ b/propcache/_helpers_c.pyx
@@ -0,0 +1,76 @@
+# cython: language_level=3
+from types import GenericAlias
+
+cdef _sentinel = object()
+
+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 object wrapped
+ cdef object name
+
+ def __init__(self, wrapped):
+ self.wrapped = wrapped
+ self.name = wrapped.__name__
+ self.__doc__ = func.__doc__
+
+ def __get__(self, inst, owner):
+ if inst is None:
+ return self
+ cdef dict cache = inst._cache
+ val = cache.get(self.name, _sentinel)
+ if val is _sentinel:
+ val = self.wrapped(inst)
+ cache[self.name] = val
+ return val
+
+ def __set__(self, inst, value):
+ raise AttributeError("cached property is read-only")
+
+
+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 object wrapped
+ cdef object name
+
+ def __init__(self, wrapped):
+ self.func = func
+ self.name = None
+ self.__doc__ = func.__doc__
+
+ def __set_name__(self, owner, 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__
+ val = cache.get(self.name, _sentinel)
+ if val is _sentinel:
+ val = self.wrapped(inst)
+ cache[self.name] = val
+ return val
+
+ __class_getitem__ = classmethod(GenericAlias)
\ No newline at end of file
diff --git a/propcache/_helpers_py.py b/propcache/_helpers_py.py
new file mode 100644
index 0000000..a3e71ea
--- /dev/null
+++ b/propcache/_helpers_py.py
@@ -0,0 +1,44 @@
+"""Various helper functions."""
+
+from typing import Any, Callable, Dict, Generic, Optional, Protocol, Type, TypeVar
+from functools import cached_property
+
+__all__ = ("under_cached_property","cached_property")
+
+_T = TypeVar("_T")
+
+
+class _TSelf(Protocol, Generic[_T]):
+ _cache: Dict[str, _T]
+
+
+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[..., _T]) -> None:
+ self.wrapped = wrapped
+ self.__doc__ = wrapped.__doc__
+ self.name = wrapped.__name__
+
+ def __get__(self, inst: _TSelf[_T], owner: Optional[Type[Any]] = None) -> _T:
+ try:
+ try:
+ return inst._cache[self.name]
+ except KeyError:
+ val = self.wrapped(inst)
+ inst._cache[self.name] = val
+ return val
+ except AttributeError:
+ if inst is None:
+ return self
+ raise
+
+ def __set__(self, inst: _TSelf[_T], value: _T) -> None:
+ raise AttributeError("cached property is read-only")
diff --git a/propcache/py.typed b/propcache/py.typed
new file mode 100644
index 0000000..dcf2c80
--- /dev/null
+++ b/propcache/py.typed
@@ -0,0 +1 @@
+# Placeholder
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..a1ba466
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,93 @@
+[build-system]
+requires = [
+ # NOTE: The following build dependencies are necessary for initial
+ # NOTE: provisioning of the in-tree build backend located under
+ # NOTE: `packaging/pep517_backend/`.
+ "expandvars",
+ "setuptools >= 47", # Minimum required for `version = attr:`
+ "tomli; python_version < '3.11'",
+]
+backend-path = ["packaging"] # requires `pip >= 20` or `pep517 >= 0.6.0`
+build-backend = "pep517_backend.hooks" # wraps `setuptools.build_meta`
+
+[tool.local.cythonize]
+# This attr can contain multiple globs
+src = ["propcache/*.pyx"]
+
+[tool.local.cythonize.env]
+# Env vars provisioned during cythonize call
+#CFLAGS = "-DCYTHON_TRACE=1 ${CFLAGS}"
+#LDFLAGS = "${LDFLAGS}"
+
+[tool.local.cythonize.flags]
+# This section can contain the following booleans:
+# * annotate — generate annotated HTML page for source files
+# * build — build extension modules using distutils
+# * inplace — build extension modules in place using distutils (implies -b)
+# * force — force recompilation
+# * quiet — be less verbose during compilation
+# * lenient — increase Python compat by ignoring some compile time errors
+# * keep-going — compile as much as possible, ignore compilation failures
+annotate = false
+build = false
+inplace = true
+force = true
+quiet = false
+lenient = false
+keep-going = false
+
+[tool.local.cythonize.kwargs]
+# This section can contain args that have values:
+# * exclude=PATTERN exclude certain file patterns from the compilation
+# * parallel=N run builds in N parallel jobs (default: calculated per system)
+# exclude = "**.py"
+# parallel = 12
+
+[tool.local.cythonize.kwargs.directive]
+# This section can contain compiler directives. Ref:
+# https://cython.rtfd.io/en/latest/src/userguide/source_files_and_compilation.html#compiler-directives
+embedsignature = "True"
+emit_code_comments = "True"
+linetrace = "True" # Implies `profile=True`
+
+[tool.local.cythonize.kwargs.compile-time-env]
+# This section can contain compile time env vars
+
+[tool.local.cythonize.kwargs.option]
+# This section can contain cythonize options
+# Ref: https://github.com/cython/cython/blob/d6e6de9/Cython/Compiler/Options.py#L694-L730
+#docstrings = "True"
+#embed_pos_in_docstring = "True"
+#warning_errors = "True"
+#error_on_unknown_names = "True"
+#error_on_uninitialized = "True"
+
+[tool.cibuildwheel]
+build-frontend = "build"
+before-test = [
+ # NOTE: Attempt to have pip pre-compile PyYAML wheel with our build
+ # NOTE: constraints unset. The hope is that pip will cache that wheel
+ # NOTE: and the test env provisioning stage will pick up PyYAML from
+ # NOTE: said cache rather than attempting to build it with a conflicting.
+ # NOTE: Version of Cython.
+ # Ref: https://github.com/pypa/cibuildwheel/issues/1666
+ "PIP_CONSTRAINT= pip install PyYAML",
+]
+test-requires = "-r requirements/test.txt"
+test-command = "pytest -v --no-cov {project}/tests"
+# don't build PyPy wheels, install from source instead
+skip = "pp*"
+
+[tool.cibuildwheel.environment]
+COLOR = "yes"
+FORCE_COLOR = "1"
+MYPY_FORCE_COLOR = "1"
+PIP_CONSTRAINT = "requirements/cython.txt"
+PRE_COMMIT_COLOR = "always"
+PY_COLORS = "1"
+
+[tool.cibuildwheel.config-settings]
+pure-python = "false"
+
+[tool.cibuildwheel.windows]
+before-test = [] # Windows cmd has different syntax and pip chooses wheels
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..dc242af
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,88 @@
+[pytest]
+addopts =
+ # `pytest-xdist`:
+ --numprocesses=auto
+
+ # Show 10 slowest invocations:
+ --durations=10
+
+ # Report all the things == -rxXs:
+ -ra
+
+ # Show values of the local vars in errors/tracebacks:
+ --showlocals
+
+ # Autocollect and invoke the doctests from all modules:
+ # https://docs.pytest.org/en/stable/doctest.html
+ --doctest-modules
+
+ # Dump the test results in junit format:
+ # --junitxml=.test-results/pytest/results.xml
+
+ # Pre-load the `pytest-cov` plugin early:
+ -p pytest_cov
+
+ # `pytest-cov`:
+ --cov
+ --cov-context=test
+ --cov-config=.coveragerc
+
+ # Fail on config parsing warnings:
+ # --strict-config
+
+ # Fail on non-existing markers:
+ # * Deprecated since v6.2.0 but may be reintroduced later covering a
+ # broader scope:
+ # --strict
+ # * Exists since v4.5.0 (advised to be used instead of `--strict`):
+ --strict-markers
+
+doctest_optionflags = ALLOW_UNICODE ELLIPSIS
+
+# Marks tests with an empty parameterset as xfail(run=False)
+empty_parameter_set_mark = xfail
+
+faulthandler_timeout = 30
+
+filterwarnings =
+ error
+
+ # FIXME: drop this once `pytest-cov` is updated.
+ # Ref: https://github.com/pytest-dev/pytest-cov/issues/557
+ ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning
+
+ # https://github.com/pytest-dev/pytest/issues/10977 and https://github.com/pytest-dev/pytest/pull/10894
+ ignore:ast\.(Num|NameConstant|Str) is deprecated and will be removed in Python 3\.14; use ast\.Constant instead:DeprecationWarning:_pytest
+ ignore:Attribute s is deprecated and will be removed in Python 3\.14; use value instead:DeprecationWarning:_pytest
+
+# https://docs.pytest.org/en/stable/usage.html#creating-junitxml-format-files
+junit_duration_report = call
+# xunit1 contains more metadata than xunit2 so it's better for CI UIs:
+junit_family = xunit1
+junit_logging = all
+junit_log_passing_tests = true
+junit_suite_name = propcache_test_suite
+
+# A mapping of markers to their descriptions allowed in strict mode:
+markers =
+
+minversion = 3.8.2
+
+# Optimize pytest's lookup by restricting potentially deep dir tree scan:
+norecursedirs =
+ build
+ dist
+ docs
+ venv
+ virtualenv
+ propcache.egg-info
+ .cache
+ .eggs
+ .git
+ .github
+ .tox
+ *.egg
+
+testpaths = tests/
+
+xfail_strict = true
diff --git a/requirements/cython.txt b/requirements/cython.txt
new file mode 100644
index 0000000..3eaca16
--- /dev/null
+++ b/requirements/cython.txt
@@ -0,0 +1 @@
+cython==3.0.11
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..2a4069d
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,2 @@
+-r test.txt
+-r towncrier.txt
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
new file mode 100644
index 0000000..c94dfb6
--- /dev/null
+++ b/requirements/doc-spelling.txt
@@ -0,0 +1,2 @@
+-r doc.txt
+sphinxcontrib-spelling==8.0.0; platform_system!="Windows" # We only use it in Azure CI
diff --git a/requirements/doc.txt b/requirements/doc.txt
new file mode 100644
index 0000000..ab21660
--- /dev/null
+++ b/requirements/doc.txt
@@ -0,0 +1,4 @@
+-r towncrier.txt
+myst-parser >= 0.10.0
+sphinx==8.0.2
+sphinxcontrib-towncrier
diff --git a/requirements/lint.txt b/requirements/lint.txt
new file mode 100644
index 0000000..1509fe7
--- /dev/null
+++ b/requirements/lint.txt
@@ -0,0 +1 @@
+pre-commit==3.8.0
diff --git a/requirements/test.txt b/requirements/test.txt
new file mode 100644
index 0000000..f23edfc
--- /dev/null
+++ b/requirements/test.txt
@@ -0,0 +1,7 @@
+-r cython.txt
+covdefaults
+idna==3.10
+multidict==6.1.0
+pytest==8.3.3
+pytest-cov>=2.3.1
+pytest-xdist
diff --git a/requirements/towncrier.txt b/requirements/towncrier.txt
new file mode 100644
index 0000000..409f3a3
--- /dev/null
+++ b/requirements/towncrier.txt
@@ -0,0 +1 @@
+towncrier==23.11.0
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..02785a7
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,98 @@
+[bdist_wheel]
+# wheels should be OS-specific:
+# their names must contain macOS/manulinux1/2010/2014/Windows identifiers
+universal = 0
+
+[metadata]
+name = propcache
+version = attr: propcache.__version__
+url = https://github.com/aio-libs/propcache
+project_urls =
+ Chat: Matrix = https://matrix.to/#/#aio-libs:matrix.org
+ Chat: Matrix Space = https://matrix.to/#/#aio-libs-space:matrix.org
+ CI: GitHub Workflows = https://github.com/aio-libs/propcache/actions?query=branch:master
+ Code of Conduct = https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md
+ Coverage: codecov = https://codecov.io/github/aio-libs/propcache
+ Docs: Changelog = https://propcache.aio-libs.org/en/latest/changes/
+ Docs: RTD = https://propcache.aio-libs.org
+ GitHub: issues = https://github.com/aio-libs/propcache/issues
+ GitHub: repo = https://github.com/aio-libs/propcache
+description = Yet another URL library
+long_description = file: README.rst, CHANGES.rst
+long_description_content_type = text/x-rst
+author = Andrew Svetlov
+author_email = andrew.svetlov@gmail.com
+maintainer = aiohttp team
+maintainer_email = team@aiohttp.org
+license = Apache-2.0
+license_files =
+ LICENSE
+ NOTICE
+classifiers =
+ Development Status :: 5 - Production/Stable
+
+ Intended Audience :: Developers
+
+ License :: OSI Approved :: Apache Software License
+
+ Programming Language :: Cython
+ Programming Language :: Python
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
+ Programming Language :: Python :: 3.13
+
+ Topic :: Internet :: WWW/HTTP
+ Topic :: Software Development :: Libraries :: Python Modules
+keywords =
+ cython
+ cext
+ propcache
+
+[options]
+python_requires = >=3.8
+# Ref:
+# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#using-a-src-layout
+# (`src/` layout)
+# package_dir =
+# = src
+packages =
+ propcache
+# https://setuptools.pypa.io/en/latest/deprecated/zip_safe.html
+zip_safe = False
+include_package_data = True
+
+install_requires =
+ idna >= 2.0
+ multidict >= 4.0
+
+[options.package_data]
+# Ref:
+# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
+# (see notes for the asterisk/`*` meaning)
+* =
+ *.so
+
+[options.exclude_package_data]
+* =
+ *.c
+ *.h
+
+[pep8]
+max-line-length=79
+
+[flake8]
+ignore = E203,E301,E302,E704,W503,W504,F811
+max-line-length = 88
+
+# Allow certain violations in certain files:
+per-file-ignores =
+
+ # F401 imported but unused
+ packaging/pep517_backend/hooks.py: F401
+
+[isort]
+profile=black
diff --git a/tests/test_cached_property.py b/tests/test_cached_property.py
new file mode 100644
index 0000000..2c13ea8
--- /dev/null
+++ b/tests/test_cached_property.py
@@ -0,0 +1,109 @@
+import pytest
+
+from propcache._helpers import cached_property
+
+import platform
+
+import pytest
+
+from propcache import _helpers, _helpers_py
+
+IS_PYPY = platform.python_implementation() == "PyPy"
+
+
+class CachedPropertyMixin:
+ cached_property = NotImplemented
+
+ def test_cached_property(self) -> None:
+ class A:
+ def __init__(self):
+ self._cache = {}
+
+ @self.cached_property # type: ignore[misc]
+ def prop(self):
+ return 1
+
+ a = A()
+ assert a.prop == 1
+
+ def test_cached_property_class(self) -> None:
+ class A:
+ def __init__(self):
+ """Init."""
+ # self._cache not set because its never accessed in this test
+
+ @self.cached_property # type: ignore[misc]
+ def prop(self):
+ """Docstring."""
+
+ assert isinstance(A.prop, self.cached_property)
+ assert A.prop.__doc__ == "Docstring."
+
+ def test_cached_property_without_cache(self) -> None:
+ class A:
+
+ __slots__ = ()
+
+ def __init__(self):
+ pass
+
+ @self.cached_property # type: ignore[misc]
+ def prop(self):
+ """Mock property."""
+
+ a = A()
+
+ with pytest.raises(AttributeError):
+ a.prop = 123
+
+ def test_cached_property_check_without_cache(self) -> None:
+ class A:
+
+ __slots__ = ()
+
+ def __init__(self):
+ pass
+
+ @self.cached_property # type: ignore[misc]
+ def prop(self):
+ """Mock property."""
+
+ a = A()
+ with pytest.raises(TypeError):
+ assert a.prop == 1
+
+
+class A:
+ def __init__(self):
+ self._cache = {}
+
+ @cached_property
+ def prop(self):
+ """Docstring."""
+ return 1
+
+
+def test_cached_property():
+ a = A()
+ assert 1 == a.prop
+
+
+def test_cached_property_class():
+ assert isinstance(A.prop, cached_property)
+ assert "Docstring." == A.prop.__doc__
+
+
+
+class TestPyCachedProperty(CachedPropertyMixin):
+ cached_property = _helpers_py.cached_property # type: ignore[assignment]
+
+
+if (
+ not _helpers.NO_EXTENSIONS
+ and not IS_PYPY
+ and hasattr(_helpers, "cached_property_c")
+):
+
+ class TestCCachedProperty(CachedPropertyMixin):
+ cached_property = _helpers.cached_property_c # type: ignore[assignment, attr-defined, unused-ignore] # noqa: E501
+
diff --git a/tests/test_under_cached_property.py b/tests/test_under_cached_property.py
new file mode 100644
index 0000000..3896fec
--- /dev/null
+++ b/tests/test_under_cached_property.py
@@ -0,0 +1,123 @@
+import pytest
+
+from propcache._helpers import under_cached_property
+
+import platform
+
+import pytest
+
+from propcache import _helpers, _helpers_py
+
+IS_PYPY = platform.python_implementation() == "PyPy"
+
+
+class CachedPropertyMixin:
+ under_cached_property = NotImplemented
+
+ def test_under_cached_property(self) -> None:
+ class A:
+ def __init__(self):
+ self._cache = {}
+
+ @self.under_cached_property # type: ignore[misc]
+ def prop(self):
+ return 1
+
+ a = A()
+ assert a.prop == 1
+
+ def test_under_cached_property_class(self) -> None:
+ class A:
+ def __init__(self):
+ """Init."""
+ # self._cache not set because its never accessed in this test
+
+ @self.under_cached_property # type: ignore[misc]
+ def prop(self):
+ """Docstring."""
+
+ assert isinstance(A.prop, self.under_cached_property)
+ assert A.prop.__doc__ == "Docstring."
+
+ def test_under_cached_property_assignment(self) -> None:
+ class A:
+ def __init__(self):
+ self._cache = {}
+
+ @self.under_cached_property # type: ignore[misc]
+ def prop(self):
+ """Mock property."""
+
+ a = A()
+
+ with pytest.raises(AttributeError):
+ a.prop = 123
+
+ def test_under_cached_property_without_cache(self) -> None:
+ class A:
+ def __init__(self):
+ pass
+
+ @self.under_cached_property # type: ignore[misc]
+ def prop(self):
+ """Mock property."""
+
+ a = A()
+
+ with pytest.raises(AttributeError):
+ a.prop = 123
+
+ def test_under_cached_property_check_without_cache(self) -> None:
+ class A:
+ def __init__(self):
+ pass
+
+ @self.under_cached_property # type: ignore[misc]
+ def prop(self):
+ """Mock property."""
+
+ a = A()
+ with pytest.raises(AttributeError):
+ assert a.prop == 1
+
+
+class A:
+ def __init__(self):
+ self._cache = {}
+
+ @under_cached_property
+ def prop(self):
+ """Docstring."""
+ return 1
+
+
+def test_under_cached_property():
+ a = A()
+ assert 1 == a.prop
+
+
+def test_under_cached_property_class():
+ assert isinstance(A.prop, under_cached_property)
+ assert "Docstring." == A.prop.__doc__
+
+
+def test_under_cached_property_assignment():
+ a = A()
+
+ with pytest.raises(AttributeError):
+ a.prop = 123
+
+
+class TestPyCachedProperty(CachedPropertyMixin):
+ under_cached_property = _helpers_py.under_cached_property # type: ignore[assignment]
+
+
+if (
+ not _helpers.NO_EXTENSIONS
+ and not IS_PYPY
+ and hasattr(_helpers, "under_cached_property_c")
+):
+
+ class TestCCachedProperty(CachedPropertyMixin):
+ under_cached_property = _helpers.under_cached_property_c # type: ignore[assignment, attr-defined, unused-ignore] # noqa: E501
+
diff --git a/towncrier.toml b/towncrier.toml
new file mode 100644
index 0000000..8b20677
--- /dev/null
+++ b/towncrier.toml
@@ -0,0 +1,68 @@
+[tool.towncrier]
+ package = "propcache"
+ filename = "CHANGES.rst"
+ directory = "CHANGES/"
+ title_format = "v{version}"
+ template = "CHANGES/.TEMPLATE.rst"
+ issue_format = "{issue}"
+
+ # NOTE: The types are declared because:
+ # NOTE: - there is no mechanism to override just the value of
+ # NOTE: `tool.towncrier.type.misc.showcontent`;
+ # NOTE: - and, we want to declare extra non-default types for
+ # NOTE: clarity and flexibility.
+
+ [[tool.towncrier.section]]
+ path = ""
+
+ [[tool.towncrier.type]]
+ # Something we deemed an improper undesired behavior that got corrected
+ # in the release to match pre-agreed expectations.
+ directory = "bugfix"
+ name = "Bug fixes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ # New behaviors, public APIs. That sort of stuff.
+ directory = "feature"
+ name = "Features"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ # Declarations of future API removals and breaking changes in behavior.
+ directory = "deprecation"
+ name = "Deprecations (removal in next major release)"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ # When something public gets removed in a breaking way. Could be
+ # deprecated in an earlier release.
+ directory = "breaking"
+ name = "Removals and backward incompatible breaking changes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ # Notable updates to the documentation structure or build process.
+ directory = "doc"
+ name = "Improved documentation"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ # Notes for downstreams about unobvious side effects and tooling. Changes
+ # in the test invocation considerations and runtime assumptions.
+ directory = "packaging"
+ name = "Packaging updates and notes for downstreams"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ # Stuff that affects the contributor experience. e.g. Running tests,
+ # building the docs, setting up the development environment.
+ directory = "contrib"
+ name = "Contributor-facing changes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ # Changes that are hard to assign to any of the above categories.
+ directory = "misc"
+ name = "Miscellaneous internal changes"
+ showcontent = true