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