├── .github ├── FUNDING.yml ├── SECURITY.md └── workflows │ ├── main.yml │ └── pypi-package.yml ├── .gitignore ├── CHANGELOG.md ├── Justfile ├── LICENSE ├── NOTICE ├── README.md ├── pyproject.toml ├── src └── aiofiles │ ├── __init__.py │ ├── base.py │ ├── os.py │ ├── ospath.py │ ├── tempfile │ ├── __init__.py │ └── temptypes.py │ └── threadpool │ ├── __init__.py │ ├── binary.py │ ├── text.py │ └── utils.py ├── tests ├── resources │ ├── multiline_file.txt │ └── test_file1.txt ├── test_os.py ├── test_simple.py ├── test_stdio.py ├── test_tempfile.py └── threadpool │ ├── test_binary.py │ ├── test_concurrency.py │ ├── test_open.py │ ├── test_text.py │ └── test_wrap.py ├── tox.ini └── uv.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tidelift: "pypi/aiofiles" 3 | github: Tinche 4 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: ["main"] 7 | pull_request: 8 | branches: ["main"] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | tests: 13 | name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}" 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.9"] 19 | 20 | steps: 21 | - uses: "actions/checkout@v4" 22 | 23 | - uses: hynek/setup-cached-uv@v2 24 | - uses: extractions/setup-just@v3 25 | 26 | - name: "Run tox targets for ${{ matrix.python-version }}" 27 | env: 28 | V: ${{ matrix.python-version }} 29 | run: "uv run --group tox tox -e py$(echo $V | tr -d . | sed 's/^py//')" 30 | 31 | - name: "Lint" 32 | if: matrix.python-version == '3.13' && runner.os == 'Linux' 33 | run: "uv run --group tox tox -e lint" 34 | 35 | - name: "Upload coverage data" 36 | uses: "actions/upload-artifact@v4" 37 | with: 38 | name: coverage-data-${{ matrix.python-version }} 39 | path: ".coverage.*" 40 | if-no-files-found: "ignore" 41 | include-hidden-files: true 42 | if: runner.os == 'Linux' 43 | 44 | coverage: 45 | name: "Combine & check coverage." 46 | needs: "tests" 47 | runs-on: "ubuntu-latest" 48 | 49 | steps: 50 | - uses: "actions/checkout@v4" 51 | - uses: hynek/setup-cached-uv@v2 52 | 53 | - uses: "actions/download-artifact@v4" 54 | with: 55 | pattern: "coverage-data-*" 56 | merge-multiple: true 57 | 58 | - name: "Combine coverage" 59 | run: | 60 | uv sync --group test --python 3.13 61 | uv run coverage combine 62 | uv run coverage html --skip-covered --skip-empty 63 | uv run coverage json 64 | 65 | # Report and write to summary. 66 | uv run coverage report | sed 's/^/ /' >> $GITHUB_STEP_SUMMARY 67 | 68 | export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") 69 | echo "total=$TOTAL" >> $GITHUB_ENV 70 | 71 | - name: "Upload HTML report." 72 | uses: "actions/upload-artifact@v4" 73 | with: 74 | name: "html-report" 75 | path: "htmlcov" 76 | 77 | - name: "Make badge" 78 | if: github.ref == 'refs/heads/main' 79 | uses: "schneegans/dynamic-badges-action@v1.4.0" 80 | with: 81 | # GIST_TOKEN is a GitHub personal access token with scope "gist". 82 | auth: ${{ secrets.GIST_TOKEN }} 83 | gistID: 882f02e3df32136c847ba90d2688f06e 84 | filename: covbadge.json 85 | label: Coverage 86 | message: ${{ env.total }}% 87 | minColorRange: 50 88 | maxColorRange: 90 89 | valColorRange: ${{ env.total }} 90 | 91 | package: 92 | name: "Build & verify package" 93 | runs-on: "ubuntu-latest" 94 | 95 | steps: 96 | - uses: "actions/checkout@v4" 97 | - uses: hynek/setup-cached-uv@v2 98 | 99 | - name: "Build package" 100 | run: "uv build" 101 | - name: "List result" 102 | run: "ls -l dist" 103 | - name: "Check wheel contents" 104 | run: "uvx check-wheel-contents dist/*.whl" 105 | - name: "Check long_description" 106 | run: "uvx twine check dist/*" 107 | -------------------------------------------------------------------------------- /.github/workflows/pypi-package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build & maybe upload PyPI package 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | tags: ["*"] 8 | release: 9 | types: 10 | - published 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | id-token: write 16 | 17 | jobs: 18 | build-package: 19 | name: Build & verify package 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - uses: hynek/build-and-inspect-python-package@v2 28 | 29 | # Upload to Test PyPI on every commit on main. 30 | release-test-pypi: 31 | name: Publish in-dev package to test.pypi.org 32 | environment: release-test-pypi 33 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 34 | runs-on: ubuntu-latest 35 | needs: build-package 36 | 37 | steps: 38 | - name: Download packages built by build-and-inspect-python-package 39 | uses: actions/download-artifact@v4 40 | with: 41 | name: Packages 42 | path: dist 43 | 44 | - name: Upload package to Test PyPI 45 | uses: pypa/gh-action-pypi-publish@release/v1 46 | with: 47 | repository-url: https://test.pypi.org/legacy/ 48 | 49 | # Upload to real PyPI on GitHub Releases. 50 | release-pypi: 51 | name: Publish released package to pypi.org 52 | environment: release-pypi 53 | if: github.event.action == 'published' 54 | runs-on: ubuntu-latest 55 | needs: build-package 56 | 57 | steps: 58 | - name: Download packages built by build-and-inspect-python-package 59 | uses: actions/download-artifact@v4 60 | with: 61 | name: Packages 62 | path: dist 63 | 64 | - name: Upload package to PyPI 65 | uses: pypa/gh-action-pypi-publish@release/v1 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | # pyenv 64 | # For a library or package, you might want to ignore these files since the code is 65 | # intended to run in multiple environments; otherwise, check them in: 66 | # .python-version 67 | 68 | # UV 69 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 70 | # This is especially recommended for binary packages to ensure reproducibility, and is more 71 | # commonly ignored for libraries. 72 | # uv.lock 73 | 74 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 75 | __pypackages__/ 76 | 77 | # Environments 78 | .env 79 | .venv 80 | env/ 81 | venv/ 82 | ENV/ 83 | env.bak/ 84 | venv.bak/ 85 | 86 | # mypy 87 | .mypy_cache/ 88 | .dmypy.json 89 | dmypy.json 90 | 91 | # PyCharm 92 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 93 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 94 | # and can be added to the global gitignore or merged into this file. For a more nuclear 95 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 96 | .idea/ 97 | 98 | # Visual Studio Code 99 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 100 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 101 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 102 | # you could uncomment the following to ignore the entire vscode folder 103 | .vscode/ 104 | 105 | # Ruff stuff: 106 | .ruff_cache/ 107 | 108 | # PyPI configuration file 109 | .pypirc 110 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## 25.1.0 (UNRELEASED) 4 | 5 | - Switch to [uv](https://docs.astral.sh/uv/) + add Python v3.14 support. 6 | ([#219](https://github.com/Tinche/aiofiles/pull/219)) 7 | - Add `ruff` formatter and linter. 8 | [#216](https://github.com/Tinche/aiofiles/pull/216) 9 | - Drop Python 3.8 support. If you require it, use version 24.1.0. 10 | [#204](https://github.com/Tinche/aiofiles/pull/204) 11 | 12 | ## 24.1.0 (2024-06-24) 13 | 14 | - Import `os.link` conditionally to fix importing on android. 15 | [#175](https://github.com/Tinche/aiofiles/issues/175) 16 | - Remove spurious items from `aiofiles.os.__all__` when running on Windows. 17 | - Switch to more modern async idioms: Remove types.coroutine and make AiofilesContextManager an awaitable instead a coroutine. 18 | - Add `aiofiles.os.path.abspath` and `aiofiles.os.getcwd`. 19 | [#174](https://github.com/Tinche/aiofiles/issues/181) 20 | - _aiofiles_ is now tested on Python 3.13 too. 21 | [#184](https://github.com/Tinche/aiofiles/pull/184) 22 | - Drop Python 3.7 support. If you require it, use version 23.2.1. 23 | 24 | ## 23.2.1 (2023-08-09) 25 | 26 | - Import `os.statvfs` conditionally to fix importing on non-UNIX systems. 27 | [#171](https://github.com/Tinche/aiofiles/issues/171) [#172](https://github.com/Tinche/aiofiles/pull/172) 28 | - aiofiles is now also tested on Windows. 29 | 30 | ## 23.2.0 (2023-08-09) 31 | 32 | - aiofiles is now tested on Python 3.12 too. 33 | [#166](https://github.com/Tinche/aiofiles/issues/166) [#168](https://github.com/Tinche/aiofiles/pull/168) 34 | - On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` now accepts a `delete_on_close` argument, just like the stdlib version. 35 | - On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` no longer exposes a `delete` attribute, just like the stdlib version. 36 | - Added `aiofiles.os.statvfs` and `aiofiles.os.path.ismount`. 37 | [#162](https://github.com/Tinche/aiofiles/pull/162) 38 | - Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry. 39 | [#169](https://github.com/Tinche/aiofiles/pull/169) 40 | 41 | ## 23.1.0 (2023-02-09) 42 | 43 | - Added `aiofiles.os.access`. 44 | [#146](https://github.com/Tinche/aiofiles/pull/146) 45 | - Removed `aiofiles.tempfile.temptypes.AsyncSpooledTemporaryFile.softspace`. 46 | [#151](https://github.com/Tinche/aiofiles/pull/151) 47 | - Added `aiofiles.stdin`, `aiofiles.stdin_bytes`, and other stdio streams. 48 | [#154](https://github.com/Tinche/aiofiles/pull/154) 49 | - Transition to `asyncio.get_running_loop` (vs `asyncio.get_event_loop`) internally. 50 | 51 | ## 22.1.0 (2022-09-04) 52 | 53 | - Added `aiofiles.os.path.islink`. 54 | [#126](https://github.com/Tinche/aiofiles/pull/126) 55 | - Added `aiofiles.os.readlink`. 56 | [#125](https://github.com/Tinche/aiofiles/pull/125) 57 | - Added `aiofiles.os.symlink`. 58 | [#124](https://github.com/Tinche/aiofiles/pull/124) 59 | - Added `aiofiles.os.unlink`. 60 | [#123](https://github.com/Tinche/aiofiles/pull/123) 61 | - Added `aiofiles.os.link`. 62 | [#121](https://github.com/Tinche/aiofiles/pull/121) 63 | - Added `aiofiles.os.renames`. 64 | [#120](https://github.com/Tinche/aiofiles/pull/120) 65 | - Added `aiofiles.os.{listdir, scandir}`. 66 | [#143](https://github.com/Tinche/aiofiles/pull/143) 67 | - Switched to CalVer. 68 | - Dropped Python 3.6 support. If you require it, use version 0.8.0. 69 | - aiofiles is now tested on Python 3.11. 70 | 71 | ## 0.8.0 (2021-11-27) 72 | 73 | - aiofiles is now tested on Python 3.10. 74 | - Added `aiofiles.os.replace`. 75 | [#107](https://github.com/Tinche/aiofiles/pull/107) 76 | - Added `aiofiles.os.{makedirs, removedirs}`. 77 | - Added `aiofiles.os.path.{exists, isfile, isdir, getsize, getatime, getctime, samefile, sameopenfile}`. 78 | [#63](https://github.com/Tinche/aiofiles/pull/63) 79 | - Added `suffix`, `prefix`, `dir` args to `aiofiles.tempfile.TemporaryDirectory`. 80 | [#116](https://github.com/Tinche/aiofiles/pull/116) 81 | 82 | ## 0.7.0 (2021-05-17) 83 | 84 | - Added the `aiofiles.tempfile` module for async temporary files. 85 | [#56](https://github.com/Tinche/aiofiles/pull/56) 86 | - Switched to Poetry and GitHub actions. 87 | - Dropped 3.5 support. 88 | 89 | ## 0.6.0 (2020-10-27) 90 | 91 | - `aiofiles` is now tested on ppc64le. 92 | - Added `name` and `mode` properties to async file objects. 93 | [#82](https://github.com/Tinche/aiofiles/pull/82) 94 | - Fixed a DeprecationWarning internally. 95 | [#75](https://github.com/Tinche/aiofiles/pull/75) 96 | - Python 3.9 support and tests. 97 | 98 | ## 0.5.0 (2020-04-12) 99 | 100 | - Python 3.8 support. Code base modernization (using `async/await` instead of `asyncio.coroutine`/`yield from`). 101 | - Added `aiofiles.os.remove`, `aiofiles.os.rename`, `aiofiles.os.mkdir`, `aiofiles.os.rmdir`. 102 | [#62](https://github.com/Tinche/aiofiles/pull/62) 103 | 104 | ## 0.4.0 (2018-08-11) 105 | 106 | - Python 3.7 support. 107 | - Removed Python 3.3/3.4 support. If you use these versions, stick to aiofiles 0.3.x. 108 | 109 | ## 0.3.2 (2017-09-23) 110 | 111 | - The LICENSE is now included in the sdist. 112 | [#31](https://github.com/Tinche/aiofiles/pull/31) 113 | 114 | ## 0.3.1 (2017-03-10) 115 | 116 | - Introduced a changelog. 117 | - `aiofiles.os.sendfile` will now work if the standard `os` module contains a `sendfile` function. 118 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | tests_dir := "tests" 2 | code_dirs := "src" + " " + tests_dir 3 | run_prefix := if env_var_or_default("VIRTUAL_ENV", "") == "" { "uv run " } else { "" } 4 | 5 | check: 6 | {{ run_prefix }}ruff format --check {{ code_dirs }} 7 | {{ run_prefix }}ruff check {{ code_dirs }} 8 | 9 | coverage: 10 | {{ run_prefix }}coverage run -m pytest {{ tests_dir }} 11 | 12 | format: 13 | {{ run_prefix }}ruff format {{ code_dirs }} 14 | 15 | lint: format 16 | {{ run_prefix }}ruff check --fix {{ code_dirs }} 17 | 18 | test: 19 | {{ run_prefix }}pytest -x --ff {{ tests_dir }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Asyncio support for files 2 | Copyright 2016 Tin Tvrtkovic 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aiofiles: file support for asyncio 2 | 3 | [](https://pypi.python.org/pypi/aiofiles) 4 | [](https://github.com/Tinche/aiofiles/actions) 5 | [](https://github.com/Tinche/aiofiles/actions/workflows/main.yml) 6 | [](https://github.com/Tinche/aiofiles) 7 | [](https://github.com/psf/black) 8 | 9 | **aiofiles** is an Apache2 licensed library, written in Python, for handling local 10 | disk files in asyncio applications. 11 | 12 | Ordinary local file IO is blocking, and cannot easily and portably be made 13 | asynchronous. This means doing file IO may interfere with asyncio applications, 14 | which shouldn't block the executing thread. aiofiles helps with this by 15 | introducing asynchronous versions of files that support delegating operations to 16 | a separate thread pool. 17 | 18 | ```python 19 | async with aiofiles.open('filename', mode='r') as f: 20 | contents = await f.read() 21 | print(contents) 22 | 'My file contents' 23 | ``` 24 | 25 | Asynchronous iteration is also supported. 26 | 27 | ```python 28 | async with aiofiles.open('filename') as f: 29 | async for line in f: 30 | ... 31 | ``` 32 | 33 | Asynchronous interface to tempfile module. 34 | 35 | ```python 36 | async with aiofiles.tempfile.TemporaryFile('wb') as f: 37 | await f.write(b'Hello, World!') 38 | ``` 39 | 40 | ## Features 41 | 42 | - a file API very similar to Python's standard, blocking API 43 | - support for buffered and unbuffered binary files, and buffered text files 44 | - support for `async`/`await` ([PEP 492](https://peps.python.org/pep-0492/)) constructs 45 | - async interface to tempfile module 46 | 47 | ## Installation 48 | 49 | To install aiofiles, simply: 50 | 51 | ```shell 52 | pip install aiofiles 53 | ``` 54 | 55 | ## Usage 56 | 57 | Files are opened using the `aiofiles.open()` coroutine, which in addition to 58 | mirroring the builtin `open` accepts optional `loop` and `executor` 59 | arguments. If `loop` is absent, the default loop will be used, as per the 60 | set asyncio policy. If `executor` is not specified, the default event loop 61 | executor will be used. 62 | 63 | In case of success, an asynchronous file object is returned with an 64 | API identical to an ordinary file, except the following methods are coroutines 65 | and delegate to an executor: 66 | 67 | - `close` 68 | - `flush` 69 | - `isatty` 70 | - `read` 71 | - `readall` 72 | - `read1` 73 | - `readinto` 74 | - `readline` 75 | - `readlines` 76 | - `seek` 77 | - `seekable` 78 | - `tell` 79 | - `truncate` 80 | - `writable` 81 | - `write` 82 | - `writelines` 83 | 84 | In case of failure, one of the usual exceptions will be raised. 85 | 86 | `aiofiles.stdin`, `aiofiles.stdout`, `aiofiles.stderr`, 87 | `aiofiles.stdin_bytes`, `aiofiles.stdout_bytes`, and 88 | `aiofiles.stderr_bytes` provide async access to `sys.stdin`, 89 | `sys.stdout`, `sys.stderr`, and their corresponding `.buffer` properties. 90 | 91 | The `aiofiles.os` module contains executor-enabled coroutine versions of 92 | several useful `os` functions that deal with files: 93 | 94 | - `stat` 95 | - `statvfs` 96 | - `sendfile` 97 | - `rename` 98 | - `renames` 99 | - `replace` 100 | - `remove` 101 | - `unlink` 102 | - `mkdir` 103 | - `makedirs` 104 | - `rmdir` 105 | - `removedirs` 106 | - `link` 107 | - `symlink` 108 | - `readlink` 109 | - `listdir` 110 | - `scandir` 111 | - `access` 112 | - `getcwd` 113 | - `path.abspath` 114 | - `path.exists` 115 | - `path.isfile` 116 | - `path.isdir` 117 | - `path.islink` 118 | - `path.ismount` 119 | - `path.getsize` 120 | - `path.getatime` 121 | - `path.getctime` 122 | - `path.samefile` 123 | - `path.sameopenfile` 124 | 125 | ### Tempfile 126 | 127 | **aiofiles.tempfile** implements the following interfaces: 128 | 129 | - TemporaryFile 130 | - NamedTemporaryFile 131 | - SpooledTemporaryFile 132 | - TemporaryDirectory 133 | 134 | Results return wrapped with a context manager allowing use with async with and async for. 135 | 136 | ```python 137 | async with aiofiles.tempfile.NamedTemporaryFile('wb+') as f: 138 | await f.write(b'Line1\n Line2') 139 | await f.seek(0) 140 | async for line in f: 141 | print(line) 142 | 143 | async with aiofiles.tempfile.TemporaryDirectory() as d: 144 | filename = os.path.join(d, "file.ext") 145 | ``` 146 | 147 | ### Writing tests for aiofiles 148 | 149 | Real file IO can be mocked by patching `aiofiles.threadpool.sync_open` 150 | as desired. The return type also needs to be registered with the 151 | `aiofiles.threadpool.wrap` dispatcher: 152 | 153 | ```python 154 | aiofiles.threadpool.wrap.register(mock.MagicMock)( 155 | lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) 156 | ) 157 | 158 | async def test_stuff(): 159 | write_data = 'data' 160 | read_file_chunks = [ 161 | b'file chunks 1', 162 | b'file chunks 2', 163 | b'file chunks 3', 164 | b'', 165 | ] 166 | file_chunks_iter = iter(read_file_chunks) 167 | 168 | mock_file_stream = mock.MagicMock( 169 | read=lambda *args, **kwargs: next(file_chunks_iter) 170 | ) 171 | 172 | with mock.patch('aiofiles.threadpool.sync_open', return_value=mock_file_stream) as mock_open: 173 | async with aiofiles.open('filename', 'w') as f: 174 | await f.write(write_data) 175 | assert await f.read() == b'file chunks 1' 176 | 177 | mock_file_stream.write.assert_called_once_with(write_data) 178 | ``` 179 | 180 | ### Contributing 181 | 182 | Contributions are very welcome. Tests can be run with `tox`, please ensure 183 | the coverage at least stays the same before you submit a pull request. 184 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "aiofiles" 7 | description = "File support for asyncio." 8 | authors = [ 9 | {name = "Tin Tvrtkovic", email = "tinchester@gmail.com"}, 10 | ] 11 | requires-python = ">=3.9" 12 | readme = "README.md" 13 | license = {text = "Apache-2.0"} 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "License :: OSI Approved :: Apache Software License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Programming Language :: Python :: 3.14", 24 | "Programming Language :: Python :: Implementation :: CPython", 25 | "Programming Language :: Python :: Implementation :: PyPy", 26 | "Framework :: AsyncIO", 27 | ] 28 | dynamic = ["version"] 29 | dependencies = [] 30 | 31 | [project.urls] 32 | Changelog = "https://github.com/Tinche/aiofiles#history" 33 | "Bug Tracker" = "https://github.com/Tinche/aiofiles/issues" 34 | Repository = "https://github.com/Tinche/aiofiles" 35 | 36 | [dependency-groups] 37 | lint = [ 38 | "mypy>=1.16.0", 39 | "ruff>=0.11.12", 40 | ] 41 | test = [ 42 | "coverage>=7.8.2", 43 | "pytest>=8.3.5", 44 | "pytest-asyncio>=1.0.0", 45 | ] 46 | tox = [ 47 | "tox>=4.26.0", 48 | "tox-uv>=1.26.0", 49 | ] 50 | 51 | [tool.uv.build-backend] 52 | source-include = ["LICENSE"] 53 | 54 | [tool.hatch.version] 55 | source = "vcs" 56 | raw-options = { local_scheme = "no-local-version" } 57 | 58 | [tool.coverage.run] 59 | parallel = true 60 | source_pkgs = ["aiofiles"] 61 | 62 | [tool.coverage.paths] 63 | source = [ 64 | "src", 65 | ".tox/*/lib/python*/site-packages", 66 | ".tox/*/lib/pypy*/site-packages", 67 | ] 68 | 69 | [tool.pytest.ini_options] 70 | minversion = "8.3" 71 | asyncio_mode = "auto" 72 | asyncio_default_fixture_loop_scope = "function" 73 | 74 | [tool.ruff] 75 | indent-width = 4 76 | line-length = 88 77 | target-version = "py39" 78 | 79 | [tool.ruff.format] 80 | docstring-code-format = false 81 | docstring-code-line-length = "dynamic" 82 | indent-style = "space" 83 | line-ending = "auto" 84 | quote-style = "double" 85 | skip-magic-trailing-comma = false 86 | 87 | [tool.ruff.lint] 88 | select = [ 89 | # "A", # flake8-builtins (A) 90 | # "ANN", # flake8-annotations (ANN) 91 | # "ARG", # flake8-unused-arguments (ARG) 92 | "ASYNC", # flake8-async (ASYNC) 93 | "B", # flake8-bugbear (B) 94 | "BLE", # flake8-blind-except (BLE) 95 | "C4", # flake8-comprehensions (C4) 96 | # "COM", # flake8-commas (COM) 97 | # "D", # pydocstyle (D) 98 | "E", # Error (E) 99 | "EM", # flake8-errmsg (EM) 100 | "ERA", # eradicate (ERA) 101 | "F", # Pyflakes (F) 102 | # "FBT", # flake8-boolean-trap (FBT) 103 | "I", # isort (I) 104 | "ICN", # flake8-import-conventions (ICN) 105 | "ISC", # flake8-implicit-str-concat (ISC) 106 | # "N", # pep8-naming (N) 107 | "PIE", # flake8-pie (PIE) 108 | "PLE", # Error (PLE) 109 | # "PLR", # Refactor (PLR) 110 | "PLW", # Warning (PLW) 111 | # "PT", # flake8-pytest-style (PT) 112 | "PTH", # flake8-use-pathlib (PTH) 113 | "PYI", # flake8-pyi (PYI) 114 | "Q", # flake8-quotes (Q) 115 | "RET", # flake8-return (RET) 116 | "RSE", # flake8-raise (RSE) 117 | "S", # flake8-bandit (S) 118 | "SIM", # flake8-simplify (SIM) 119 | "T10", # flake8-debugger (T10) 120 | "T20", # flake8-print (T20) 121 | "TC", # flake8-type-checking (TC) 122 | "TID", # flake8-tidy-imports (TID) 123 | "TRY", # tryceratops (TRY) 124 | "UP", # pyupgrade (UP) 125 | "W", # Warning (W) 126 | "YTT" # flake8-2020 (YTT) 127 | ] 128 | ignore = [ 129 | "COM812", 130 | "ISC001", 131 | ] 132 | fixable = [ 133 | "COM", 134 | "I" 135 | ] 136 | 137 | [tool.ruff.lint.per-file-ignores] 138 | "__init__.py" = ["F401"] 139 | "src/**/*.py" = [ 140 | "TID252", # https://docs.astral.sh/ruff/rules/relative-imports/ 141 | ] 142 | "tests/**/*.py" = [ 143 | "ARG", 144 | "ASYNC", 145 | "BLE", 146 | "PTH", 147 | "S", 148 | "SIM", 149 | "T20" 150 | ] 151 | -------------------------------------------------------------------------------- /src/aiofiles/__init__.py: -------------------------------------------------------------------------------- 1 | """Utilities for asyncio-friendly file handling.""" 2 | 3 | from . import tempfile 4 | from .threadpool import ( 5 | open, 6 | stderr, 7 | stderr_bytes, 8 | stdin, 9 | stdin_bytes, 10 | stdout, 11 | stdout_bytes, 12 | ) 13 | 14 | __all__ = [ 15 | "open", 16 | "tempfile", 17 | "stdin", 18 | "stdout", 19 | "stderr", 20 | "stdin_bytes", 21 | "stdout_bytes", 22 | "stderr_bytes", 23 | ] 24 | -------------------------------------------------------------------------------- /src/aiofiles/base.py: -------------------------------------------------------------------------------- 1 | from asyncio import get_running_loop 2 | from collections.abc import Awaitable 3 | from contextlib import AbstractAsyncContextManager 4 | from functools import partial, wraps 5 | 6 | 7 | def wrap(func): 8 | @wraps(func) 9 | async def run(*args, loop=None, executor=None, **kwargs): 10 | if loop is None: 11 | loop = get_running_loop() 12 | pfunc = partial(func, *args, **kwargs) 13 | return await loop.run_in_executor(executor, pfunc) 14 | 15 | return run 16 | 17 | 18 | class AsyncBase: 19 | def __init__(self, file, loop, executor): 20 | self._file = file 21 | self._executor = executor 22 | self._ref_loop = loop 23 | 24 | @property 25 | def _loop(self): 26 | return self._ref_loop or get_running_loop() 27 | 28 | def __aiter__(self): 29 | """We are our own iterator.""" 30 | return self 31 | 32 | def __repr__(self): 33 | return super().__repr__() + " wrapping " + repr(self._file) 34 | 35 | async def __anext__(self): 36 | """Simulate normal file iteration.""" 37 | 38 | if line := await self.readline(): 39 | return line 40 | raise StopAsyncIteration 41 | 42 | 43 | class AsyncIndirectBase(AsyncBase): 44 | def __init__(self, name, loop, executor, indirect): 45 | self._indirect = indirect 46 | self._name = name 47 | super().__init__(None, loop, executor) 48 | 49 | @property 50 | def _file(self): 51 | return self._indirect() 52 | 53 | @_file.setter 54 | def _file(self, v): 55 | pass # discard writes 56 | 57 | 58 | class AiofilesContextManager(Awaitable, AbstractAsyncContextManager): 59 | """An adjusted async context manager for aiofiles.""" 60 | 61 | __slots__ = ("_coro", "_obj") 62 | 63 | def __init__(self, coro): 64 | self._coro = coro 65 | self._obj = None 66 | 67 | def __await__(self): 68 | if self._obj is None: 69 | self._obj = yield from self._coro.__await__() 70 | return self._obj 71 | 72 | async def __aenter__(self): 73 | return await self 74 | 75 | async def __aexit__(self, exc_type, exc_val, exc_tb): 76 | await get_running_loop().run_in_executor( 77 | None, self._obj._file.__exit__, exc_type, exc_val, exc_tb 78 | ) 79 | self._obj = None 80 | -------------------------------------------------------------------------------- /src/aiofiles/os.py: -------------------------------------------------------------------------------- 1 | """Async executor versions of file functions from the os module.""" 2 | 3 | import os 4 | 5 | from . import ospath as path 6 | from .base import wrap 7 | 8 | __all__ = [ 9 | "path", 10 | "stat", 11 | "rename", 12 | "renames", 13 | "replace", 14 | "remove", 15 | "unlink", 16 | "mkdir", 17 | "makedirs", 18 | "rmdir", 19 | "removedirs", 20 | "symlink", 21 | "readlink", 22 | "listdir", 23 | "scandir", 24 | "access", 25 | "wrap", 26 | "getcwd", 27 | ] 28 | 29 | access = wrap(os.access) 30 | 31 | getcwd = wrap(os.getcwd) 32 | 33 | listdir = wrap(os.listdir) 34 | 35 | makedirs = wrap(os.makedirs) 36 | mkdir = wrap(os.mkdir) 37 | 38 | readlink = wrap(os.readlink) 39 | remove = wrap(os.remove) 40 | removedirs = wrap(os.removedirs) 41 | rename = wrap(os.rename) 42 | renames = wrap(os.renames) 43 | replace = wrap(os.replace) 44 | rmdir = wrap(os.rmdir) 45 | 46 | scandir = wrap(os.scandir) 47 | stat = wrap(os.stat) 48 | symlink = wrap(os.symlink) 49 | 50 | unlink = wrap(os.unlink) 51 | 52 | 53 | if hasattr(os, "link"): 54 | __all__ += ["link"] 55 | link = wrap(os.link) 56 | if hasattr(os, "sendfile"): 57 | __all__ += ["sendfile"] 58 | sendfile = wrap(os.sendfile) 59 | if hasattr(os, "statvfs"): 60 | __all__ += ["statvfs"] 61 | statvfs = wrap(os.statvfs) 62 | -------------------------------------------------------------------------------- /src/aiofiles/ospath.py: -------------------------------------------------------------------------------- 1 | """Async executor versions of file functions from the os.path module.""" 2 | 3 | from os import path 4 | 5 | from .base import wrap 6 | 7 | __all__ = [ 8 | "abspath", 9 | "getatime", 10 | "getctime", 11 | "getmtime", 12 | "getsize", 13 | "exists", 14 | "isdir", 15 | "isfile", 16 | "islink", 17 | "ismount", 18 | "samefile", 19 | "sameopenfile", 20 | ] 21 | 22 | abspath = wrap(path.abspath) 23 | 24 | getatime = wrap(path.getatime) 25 | getctime = wrap(path.getctime) 26 | getmtime = wrap(path.getmtime) 27 | getsize = wrap(path.getsize) 28 | 29 | exists = wrap(path.exists) 30 | 31 | isdir = wrap(path.isdir) 32 | isfile = wrap(path.isfile) 33 | islink = wrap(path.islink) 34 | ismount = wrap(path.ismount) 35 | 36 | samefile = wrap(path.samefile) 37 | sameopenfile = wrap(path.sameopenfile) 38 | -------------------------------------------------------------------------------- /src/aiofiles/tempfile/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from functools import partial, singledispatch 4 | from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOBase 5 | from tempfile import NamedTemporaryFile as syncNamedTemporaryFile 6 | from tempfile import SpooledTemporaryFile as syncSpooledTemporaryFile 7 | from tempfile import TemporaryDirectory as syncTemporaryDirectory 8 | from tempfile import TemporaryFile as syncTemporaryFile 9 | from tempfile import _TemporaryFileWrapper as syncTemporaryFileWrapper 10 | 11 | from ..base import AiofilesContextManager 12 | from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO 13 | from ..threadpool.text import AsyncTextIOWrapper 14 | from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory 15 | 16 | __all__ = [ 17 | "NamedTemporaryFile", 18 | "TemporaryFile", 19 | "SpooledTemporaryFile", 20 | "TemporaryDirectory", 21 | ] 22 | 23 | 24 | # ================================================================ 25 | # Public methods for async open and return of temp file/directory 26 | # objects with async interface 27 | # ================================================================ 28 | if sys.version_info >= (3, 12): 29 | 30 | def NamedTemporaryFile( 31 | mode="w+b", 32 | buffering=-1, 33 | encoding=None, 34 | newline=None, 35 | suffix=None, 36 | prefix=None, 37 | dir=None, 38 | delete=True, 39 | delete_on_close=True, 40 | loop=None, 41 | executor=None, 42 | ): 43 | """Async open a named temporary file""" 44 | return AiofilesContextManager( 45 | _temporary_file( 46 | named=True, 47 | mode=mode, 48 | buffering=buffering, 49 | encoding=encoding, 50 | newline=newline, 51 | suffix=suffix, 52 | prefix=prefix, 53 | dir=dir, 54 | delete=delete, 55 | delete_on_close=delete_on_close, 56 | loop=loop, 57 | executor=executor, 58 | ) 59 | ) 60 | 61 | else: 62 | 63 | def NamedTemporaryFile( 64 | mode="w+b", 65 | buffering=-1, 66 | encoding=None, 67 | newline=None, 68 | suffix=None, 69 | prefix=None, 70 | dir=None, 71 | delete=True, 72 | loop=None, 73 | executor=None, 74 | ): 75 | """Async open a named temporary file""" 76 | return AiofilesContextManager( 77 | _temporary_file( 78 | named=True, 79 | mode=mode, 80 | buffering=buffering, 81 | encoding=encoding, 82 | newline=newline, 83 | suffix=suffix, 84 | prefix=prefix, 85 | dir=dir, 86 | delete=delete, 87 | loop=loop, 88 | executor=executor, 89 | ) 90 | ) 91 | 92 | 93 | def TemporaryFile( 94 | mode="w+b", 95 | buffering=-1, 96 | encoding=None, 97 | newline=None, 98 | suffix=None, 99 | prefix=None, 100 | dir=None, 101 | loop=None, 102 | executor=None, 103 | ): 104 | """Async open an unnamed temporary file""" 105 | return AiofilesContextManager( 106 | _temporary_file( 107 | named=False, 108 | mode=mode, 109 | buffering=buffering, 110 | encoding=encoding, 111 | newline=newline, 112 | suffix=suffix, 113 | prefix=prefix, 114 | dir=dir, 115 | loop=loop, 116 | executor=executor, 117 | ) 118 | ) 119 | 120 | 121 | def SpooledTemporaryFile( 122 | max_size=0, 123 | mode="w+b", 124 | buffering=-1, 125 | encoding=None, 126 | newline=None, 127 | suffix=None, 128 | prefix=None, 129 | dir=None, 130 | loop=None, 131 | executor=None, 132 | ): 133 | """Async open a spooled temporary file""" 134 | return AiofilesContextManager( 135 | _spooled_temporary_file( 136 | max_size=max_size, 137 | mode=mode, 138 | buffering=buffering, 139 | encoding=encoding, 140 | newline=newline, 141 | suffix=suffix, 142 | prefix=prefix, 143 | dir=dir, 144 | loop=loop, 145 | executor=executor, 146 | ) 147 | ) 148 | 149 | 150 | def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None): 151 | """Async open a temporary directory""" 152 | return AiofilesContextManagerTempDir( 153 | _temporary_directory( 154 | suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor 155 | ) 156 | ) 157 | 158 | 159 | # ========================================================= 160 | # Internal coroutines to open new temp files/directories 161 | # ========================================================= 162 | if sys.version_info >= (3, 12): 163 | 164 | async def _temporary_file( 165 | named=True, 166 | mode="w+b", 167 | buffering=-1, 168 | encoding=None, 169 | newline=None, 170 | suffix=None, 171 | prefix=None, 172 | dir=None, 173 | delete=True, 174 | delete_on_close=True, 175 | loop=None, 176 | executor=None, 177 | max_size=0, 178 | ): 179 | """Async method to open a temporary file with async interface""" 180 | if loop is None: 181 | loop = asyncio.get_running_loop() 182 | 183 | if named: 184 | cb = partial( 185 | syncNamedTemporaryFile, 186 | mode=mode, 187 | buffering=buffering, 188 | encoding=encoding, 189 | newline=newline, 190 | suffix=suffix, 191 | prefix=prefix, 192 | dir=dir, 193 | delete=delete, 194 | delete_on_close=delete_on_close, 195 | ) 196 | else: 197 | cb = partial( 198 | syncTemporaryFile, 199 | mode=mode, 200 | buffering=buffering, 201 | encoding=encoding, 202 | newline=newline, 203 | suffix=suffix, 204 | prefix=prefix, 205 | dir=dir, 206 | ) 207 | 208 | f = await loop.run_in_executor(executor, cb) 209 | 210 | # Wrap based on type of underlying IO object 211 | if type(f) is syncTemporaryFileWrapper: 212 | # _TemporaryFileWrapper was used (named files) 213 | result = wrap(f.file, f, loop=loop, executor=executor) 214 | result._closer = f._closer 215 | return result 216 | # IO object was returned directly without wrapper 217 | return wrap(f, f, loop=loop, executor=executor) 218 | 219 | else: 220 | 221 | async def _temporary_file( 222 | named=True, 223 | mode="w+b", 224 | buffering=-1, 225 | encoding=None, 226 | newline=None, 227 | suffix=None, 228 | prefix=None, 229 | dir=None, 230 | delete=True, 231 | loop=None, 232 | executor=None, 233 | max_size=0, 234 | ): 235 | """Async method to open a temporary file with async interface""" 236 | if loop is None: 237 | loop = asyncio.get_running_loop() 238 | 239 | if named: 240 | cb = partial( 241 | syncNamedTemporaryFile, 242 | mode=mode, 243 | buffering=buffering, 244 | encoding=encoding, 245 | newline=newline, 246 | suffix=suffix, 247 | prefix=prefix, 248 | dir=dir, 249 | delete=delete, 250 | ) 251 | else: 252 | cb = partial( 253 | syncTemporaryFile, 254 | mode=mode, 255 | buffering=buffering, 256 | encoding=encoding, 257 | newline=newline, 258 | suffix=suffix, 259 | prefix=prefix, 260 | dir=dir, 261 | ) 262 | 263 | f = await loop.run_in_executor(executor, cb) 264 | 265 | # Wrap based on type of underlying IO object 266 | if type(f) is syncTemporaryFileWrapper: 267 | # _TemporaryFileWrapper was used (named files) 268 | result = wrap(f.file, f, loop=loop, executor=executor) 269 | # add delete property 270 | result.delete = f.delete 271 | return result 272 | # IO object was returned directly without wrapper 273 | return wrap(f, f, loop=loop, executor=executor) 274 | 275 | 276 | async def _spooled_temporary_file( 277 | max_size=0, 278 | mode="w+b", 279 | buffering=-1, 280 | encoding=None, 281 | newline=None, 282 | suffix=None, 283 | prefix=None, 284 | dir=None, 285 | loop=None, 286 | executor=None, 287 | ): 288 | """Open a spooled temporary file with async interface""" 289 | if loop is None: 290 | loop = asyncio.get_running_loop() 291 | 292 | cb = partial( 293 | syncSpooledTemporaryFile, 294 | max_size=max_size, 295 | mode=mode, 296 | buffering=buffering, 297 | encoding=encoding, 298 | newline=newline, 299 | suffix=suffix, 300 | prefix=prefix, 301 | dir=dir, 302 | ) 303 | 304 | f = await loop.run_in_executor(executor, cb) 305 | 306 | # Single interface provided by SpooledTemporaryFile for all modes 307 | return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor) 308 | 309 | 310 | async def _temporary_directory( 311 | suffix=None, prefix=None, dir=None, loop=None, executor=None 312 | ): 313 | """Async method to open a temporary directory with async interface""" 314 | if loop is None: 315 | loop = asyncio.get_running_loop() 316 | 317 | cb = partial(syncTemporaryDirectory, suffix, prefix, dir) 318 | f = await loop.run_in_executor(executor, cb) 319 | 320 | return AsyncTemporaryDirectory(f, loop=loop, executor=executor) 321 | 322 | 323 | class AiofilesContextManagerTempDir(AiofilesContextManager): 324 | """With returns the directory location, not the object (matching sync lib)""" 325 | 326 | async def __aenter__(self): 327 | self._obj = await self._coro 328 | return self._obj.name 329 | 330 | 331 | @singledispatch 332 | def wrap(base_io_obj, file, *, loop=None, executor=None): 333 | """Wrap the object with interface based on type of underlying IO""" 334 | 335 | msg = f"Unsupported IO type: {base_io_obj}" 336 | raise TypeError(msg) 337 | 338 | 339 | @wrap.register(TextIOBase) 340 | def _(base_io_obj, file, *, loop=None, executor=None): 341 | return AsyncTextIOWrapper(file, loop=loop, executor=executor) 342 | 343 | 344 | @wrap.register(BufferedWriter) 345 | def _(base_io_obj, file, *, loop=None, executor=None): 346 | return AsyncBufferedIOBase(file, loop=loop, executor=executor) 347 | 348 | 349 | @wrap.register(BufferedReader) 350 | @wrap.register(BufferedRandom) 351 | def _(base_io_obj, file, *, loop=None, executor=None): 352 | return AsyncBufferedReader(file, loop=loop, executor=executor) 353 | 354 | 355 | @wrap.register(FileIO) 356 | def _(base_io_obj, file, *, loop=None, executor=None): 357 | return AsyncFileIO(file, loop=loop, executor=executor) 358 | -------------------------------------------------------------------------------- /src/aiofiles/tempfile/temptypes.py: -------------------------------------------------------------------------------- 1 | """Async wrappers for spooled temp files and temp directory objects""" 2 | 3 | from functools import partial 4 | 5 | from ..base import AsyncBase 6 | from ..threadpool.utils import ( 7 | cond_delegate_to_executor, 8 | delegate_to_executor, 9 | proxy_property_directly, 10 | ) 11 | 12 | 13 | @delegate_to_executor("fileno", "rollover") 14 | @cond_delegate_to_executor( 15 | "close", 16 | "flush", 17 | "isatty", 18 | "read", 19 | "readline", 20 | "readlines", 21 | "seek", 22 | "tell", 23 | "truncate", 24 | ) 25 | @proxy_property_directly("closed", "encoding", "mode", "name", "newlines") 26 | class AsyncSpooledTemporaryFile(AsyncBase): 27 | """Async wrapper for SpooledTemporaryFile class""" 28 | 29 | async def _check(self): 30 | if self._file._rolled: 31 | return 32 | max_size = self._file._max_size 33 | if max_size and self._file.tell() > max_size: 34 | await self.rollover() 35 | 36 | async def write(self, s): 37 | """Implementation to anticipate rollover""" 38 | if self._file._rolled: 39 | cb = partial(self._file.write, s) 40 | return await self._loop.run_in_executor(self._executor, cb) 41 | 42 | file = self._file._file # reference underlying base IO object 43 | rv = file.write(s) 44 | await self._check() 45 | return rv 46 | 47 | async def writelines(self, iterable): 48 | """Implementation to anticipate rollover""" 49 | if self._file._rolled: 50 | cb = partial(self._file.writelines, iterable) 51 | return await self._loop.run_in_executor(self._executor, cb) 52 | 53 | file = self._file._file # reference underlying base IO object 54 | rv = file.writelines(iterable) 55 | await self._check() 56 | return rv 57 | 58 | 59 | @delegate_to_executor("cleanup") 60 | @proxy_property_directly("name") 61 | class AsyncTemporaryDirectory: 62 | """Async wrapper for TemporaryDirectory class""" 63 | 64 | def __init__(self, file, loop, executor): 65 | self._file = file 66 | self._loop = loop 67 | self._executor = executor 68 | 69 | async def close(self): 70 | await self.cleanup() 71 | -------------------------------------------------------------------------------- /src/aiofiles/threadpool/__init__.py: -------------------------------------------------------------------------------- 1 | """Handle files using a thread pool executor.""" 2 | 3 | import asyncio 4 | import sys 5 | from functools import partial, singledispatch 6 | from io import ( 7 | BufferedIOBase, 8 | BufferedRandom, 9 | BufferedReader, 10 | BufferedWriter, 11 | FileIO, 12 | TextIOBase, 13 | ) 14 | 15 | from ..base import AiofilesContextManager 16 | from .binary import ( 17 | AsyncBufferedIOBase, 18 | AsyncBufferedReader, 19 | AsyncFileIO, 20 | AsyncIndirectBufferedIOBase, 21 | ) 22 | from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper 23 | 24 | sync_open = open 25 | 26 | __all__ = ( 27 | "open", 28 | "stdin", 29 | "stdout", 30 | "stderr", 31 | "stdin_bytes", 32 | "stdout_bytes", 33 | "stderr_bytes", 34 | ) 35 | 36 | 37 | def open( 38 | file, 39 | mode="r", 40 | buffering=-1, 41 | encoding=None, 42 | errors=None, 43 | newline=None, 44 | closefd=True, 45 | opener=None, 46 | *, 47 | loop=None, 48 | executor=None, 49 | ): 50 | return AiofilesContextManager( 51 | _open( 52 | file, 53 | mode=mode, 54 | buffering=buffering, 55 | encoding=encoding, 56 | errors=errors, 57 | newline=newline, 58 | closefd=closefd, 59 | opener=opener, 60 | loop=loop, 61 | executor=executor, 62 | ) 63 | ) 64 | 65 | 66 | async def _open( 67 | file, 68 | mode="r", 69 | buffering=-1, 70 | encoding=None, 71 | errors=None, 72 | newline=None, 73 | closefd=True, 74 | opener=None, 75 | *, 76 | loop=None, 77 | executor=None, 78 | ): 79 | """Open an asyncio file.""" 80 | if loop is None: 81 | loop = asyncio.get_running_loop() 82 | cb = partial( 83 | sync_open, 84 | file, 85 | mode=mode, 86 | buffering=buffering, 87 | encoding=encoding, 88 | errors=errors, 89 | newline=newline, 90 | closefd=closefd, 91 | opener=opener, 92 | ) 93 | f = await loop.run_in_executor(executor, cb) 94 | 95 | return wrap(f, loop=loop, executor=executor) 96 | 97 | 98 | @singledispatch 99 | def wrap(file, *, loop=None, executor=None): 100 | msg = f"Unsupported io type: {file}." 101 | raise TypeError(msg) 102 | 103 | 104 | @wrap.register(TextIOBase) 105 | def _(file, *, loop=None, executor=None): 106 | return AsyncTextIOWrapper(file, loop=loop, executor=executor) 107 | 108 | 109 | @wrap.register(BufferedWriter) 110 | @wrap.register(BufferedIOBase) 111 | def _(file, *, loop=None, executor=None): 112 | return AsyncBufferedIOBase(file, loop=loop, executor=executor) 113 | 114 | 115 | @wrap.register(BufferedReader) 116 | @wrap.register(BufferedRandom) 117 | def _(file, *, loop=None, executor=None): 118 | return AsyncBufferedReader(file, loop=loop, executor=executor) 119 | 120 | 121 | @wrap.register(FileIO) 122 | def _(file, *, loop=None, executor=None): 123 | return AsyncFileIO(file, loop=loop, executor=executor) 124 | 125 | 126 | stdin = AsyncTextIndirectIOWrapper("sys.stdin", None, None, indirect=lambda: sys.stdin) 127 | stdout = AsyncTextIndirectIOWrapper( 128 | "sys.stdout", None, None, indirect=lambda: sys.stdout 129 | ) 130 | stderr = AsyncTextIndirectIOWrapper( 131 | "sys.stderr", None, None, indirect=lambda: sys.stderr 132 | ) 133 | stdin_bytes = AsyncIndirectBufferedIOBase( 134 | "sys.stdin.buffer", None, None, indirect=lambda: sys.stdin.buffer 135 | ) 136 | stdout_bytes = AsyncIndirectBufferedIOBase( 137 | "sys.stdout.buffer", None, None, indirect=lambda: sys.stdout.buffer 138 | ) 139 | stderr_bytes = AsyncIndirectBufferedIOBase( 140 | "sys.stderr.buffer", None, None, indirect=lambda: sys.stderr.buffer 141 | ) 142 | -------------------------------------------------------------------------------- /src/aiofiles/threadpool/binary.py: -------------------------------------------------------------------------------- 1 | from ..base import AsyncBase, AsyncIndirectBase 2 | from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly 3 | 4 | 5 | @delegate_to_executor( 6 | "close", 7 | "flush", 8 | "isatty", 9 | "read", 10 | "read1", 11 | "readinto", 12 | "readline", 13 | "readlines", 14 | "seek", 15 | "seekable", 16 | "tell", 17 | "truncate", 18 | "writable", 19 | "write", 20 | "writelines", 21 | ) 22 | @proxy_method_directly("detach", "fileno", "readable") 23 | @proxy_property_directly("closed", "raw", "name", "mode") 24 | class AsyncBufferedIOBase(AsyncBase): 25 | """The asyncio executor version of io.BufferedWriter and BufferedIOBase.""" 26 | 27 | 28 | @delegate_to_executor("peek") 29 | class AsyncBufferedReader(AsyncBufferedIOBase): 30 | """The asyncio executor version of io.BufferedReader and Random.""" 31 | 32 | 33 | @delegate_to_executor( 34 | "close", 35 | "flush", 36 | "isatty", 37 | "read", 38 | "readall", 39 | "readinto", 40 | "readline", 41 | "readlines", 42 | "seek", 43 | "seekable", 44 | "tell", 45 | "truncate", 46 | "writable", 47 | "write", 48 | "writelines", 49 | ) 50 | @proxy_method_directly("fileno", "readable") 51 | @proxy_property_directly("closed", "name", "mode") 52 | class AsyncFileIO(AsyncBase): 53 | """The asyncio executor version of io.FileIO.""" 54 | 55 | 56 | @delegate_to_executor( 57 | "close", 58 | "flush", 59 | "isatty", 60 | "read", 61 | "read1", 62 | "readinto", 63 | "readline", 64 | "readlines", 65 | "seek", 66 | "seekable", 67 | "tell", 68 | "truncate", 69 | "writable", 70 | "write", 71 | "writelines", 72 | ) 73 | @proxy_method_directly("detach", "fileno", "readable") 74 | @proxy_property_directly("closed", "raw", "name", "mode") 75 | class AsyncIndirectBufferedIOBase(AsyncIndirectBase): 76 | """The indirect asyncio executor version of io.BufferedWriter and BufferedIOBase.""" 77 | 78 | 79 | @delegate_to_executor("peek") 80 | class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase): 81 | """The indirect asyncio executor version of io.BufferedReader and Random.""" 82 | 83 | 84 | @delegate_to_executor( 85 | "close", 86 | "flush", 87 | "isatty", 88 | "read", 89 | "readall", 90 | "readinto", 91 | "readline", 92 | "readlines", 93 | "seek", 94 | "seekable", 95 | "tell", 96 | "truncate", 97 | "writable", 98 | "write", 99 | "writelines", 100 | ) 101 | @proxy_method_directly("fileno", "readable") 102 | @proxy_property_directly("closed", "name", "mode") 103 | class AsyncIndirectFileIO(AsyncIndirectBase): 104 | """The indirect asyncio executor version of io.FileIO.""" 105 | -------------------------------------------------------------------------------- /src/aiofiles/threadpool/text.py: -------------------------------------------------------------------------------- 1 | from ..base import AsyncBase, AsyncIndirectBase 2 | from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly 3 | 4 | 5 | @delegate_to_executor( 6 | "close", 7 | "flush", 8 | "isatty", 9 | "read", 10 | "readable", 11 | "readline", 12 | "readlines", 13 | "seek", 14 | "seekable", 15 | "tell", 16 | "truncate", 17 | "write", 18 | "writable", 19 | "writelines", 20 | ) 21 | @proxy_method_directly("detach", "fileno", "readable") 22 | @proxy_property_directly( 23 | "buffer", 24 | "closed", 25 | "encoding", 26 | "errors", 27 | "line_buffering", 28 | "newlines", 29 | "name", 30 | "mode", 31 | ) 32 | class AsyncTextIOWrapper(AsyncBase): 33 | """The asyncio executor version of io.TextIOWrapper.""" 34 | 35 | 36 | @delegate_to_executor( 37 | "close", 38 | "flush", 39 | "isatty", 40 | "read", 41 | "readable", 42 | "readline", 43 | "readlines", 44 | "seek", 45 | "seekable", 46 | "tell", 47 | "truncate", 48 | "write", 49 | "writable", 50 | "writelines", 51 | ) 52 | @proxy_method_directly("detach", "fileno", "readable") 53 | @proxy_property_directly( 54 | "buffer", 55 | "closed", 56 | "encoding", 57 | "errors", 58 | "line_buffering", 59 | "newlines", 60 | "name", 61 | "mode", 62 | ) 63 | class AsyncTextIndirectIOWrapper(AsyncIndirectBase): 64 | """The indirect asyncio executor version of io.TextIOWrapper.""" 65 | -------------------------------------------------------------------------------- /src/aiofiles/threadpool/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | def delegate_to_executor(*attrs): 5 | def cls_builder(cls): 6 | for attr_name in attrs: 7 | setattr(cls, attr_name, _make_delegate_method(attr_name)) 8 | return cls 9 | 10 | return cls_builder 11 | 12 | 13 | def proxy_method_directly(*attrs): 14 | def cls_builder(cls): 15 | for attr_name in attrs: 16 | setattr(cls, attr_name, _make_proxy_method(attr_name)) 17 | return cls 18 | 19 | return cls_builder 20 | 21 | 22 | def proxy_property_directly(*attrs): 23 | def cls_builder(cls): 24 | for attr_name in attrs: 25 | setattr(cls, attr_name, _make_proxy_property(attr_name)) 26 | return cls 27 | 28 | return cls_builder 29 | 30 | 31 | def cond_delegate_to_executor(*attrs): 32 | def cls_builder(cls): 33 | for attr_name in attrs: 34 | setattr(cls, attr_name, _make_cond_delegate_method(attr_name)) 35 | return cls 36 | 37 | return cls_builder 38 | 39 | 40 | def _make_delegate_method(attr_name): 41 | async def method(self, *args, **kwargs): 42 | cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs) 43 | return await self._loop.run_in_executor(self._executor, cb) 44 | 45 | return method 46 | 47 | 48 | def _make_proxy_method(attr_name): 49 | def method(self, *args, **kwargs): 50 | return getattr(self._file, attr_name)(*args, **kwargs) 51 | 52 | return method 53 | 54 | 55 | def _make_proxy_property(attr_name): 56 | def proxy_property(self): 57 | return getattr(self._file, attr_name) 58 | 59 | return property(proxy_property) 60 | 61 | 62 | def _make_cond_delegate_method(attr_name): 63 | """For spooled temp files, delegate only if rolled to file object""" 64 | 65 | async def method(self, *args, **kwargs): 66 | if self._file._rolled: 67 | cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs) 68 | return await self._loop.run_in_executor(self._executor, cb) 69 | return getattr(self._file, attr_name)(*args, **kwargs) 70 | 71 | return method 72 | -------------------------------------------------------------------------------- /tests/resources/multiline_file.txt: -------------------------------------------------------------------------------- 1 | line 1 2 | line 2 3 | line 3 4 | line 4 -------------------------------------------------------------------------------- /tests/resources/test_file1.txt: -------------------------------------------------------------------------------- 1 | 0123456789 -------------------------------------------------------------------------------- /tests/test_os.py: -------------------------------------------------------------------------------- 1 | """Tests for asyncio's os module.""" 2 | 3 | import asyncio 4 | import os 5 | import platform 6 | from os import stat 7 | from os.path import dirname, exists, isdir, join 8 | from pathlib import Path 9 | 10 | import pytest 11 | 12 | import aiofiles.os 13 | 14 | 15 | async def test_stat(): 16 | """Test the stat call.""" 17 | filename = join(dirname(__file__), "resources", "test_file1.txt") 18 | 19 | stat_res = await aiofiles.os.stat(filename) 20 | 21 | assert stat_res.st_size == 10 22 | 23 | 24 | @pytest.mark.skipif(platform.system() == "Windows", reason="No statvfs on Windows") 25 | async def test_statvfs(): 26 | """Test the statvfs call.""" 27 | 28 | statvfs_res = await aiofiles.os.statvfs("/") 29 | 30 | assert statvfs_res.f_bsize == os.statvfs("/").f_bsize 31 | 32 | 33 | async def test_remove(): 34 | """Test the remove call.""" 35 | filename = join(dirname(__file__), "resources", "test_file2.txt") 36 | with open(filename, "w") as f: 37 | f.write("Test file for remove call") 38 | 39 | assert exists(filename) 40 | await aiofiles.os.remove(filename) 41 | assert exists(filename) is False 42 | 43 | 44 | async def test_unlink(): 45 | """Test the unlink call.""" 46 | filename = join(dirname(__file__), "resources", "test_file2.txt") 47 | with open(filename, "w") as f: 48 | f.write("Test file for unlink call") 49 | 50 | assert exists(filename) 51 | await aiofiles.os.unlink(filename) 52 | assert exists(filename) is False 53 | 54 | 55 | async def test_mkdir_and_rmdir(): 56 | """Test the mkdir and rmdir call.""" 57 | directory = join(dirname(__file__), "resources", "test_dir") 58 | await aiofiles.os.mkdir(directory) 59 | assert isdir(directory) 60 | await aiofiles.os.rmdir(directory) 61 | assert exists(directory) is False 62 | 63 | 64 | async def test_rename(): 65 | """Test the rename call.""" 66 | old_filename = join(dirname(__file__), "resources", "test_file1.txt") 67 | new_filename = join(dirname(__file__), "resources", "test_file2.txt") 68 | await aiofiles.os.rename(old_filename, new_filename) 69 | assert exists(old_filename) is False and exists(new_filename) 70 | await aiofiles.os.rename(new_filename, old_filename) 71 | assert exists(old_filename) and exists(new_filename) is False 72 | 73 | 74 | async def test_renames(): 75 | """Test the renames call.""" 76 | old_filename = join(dirname(__file__), "resources", "test_file1.txt") 77 | new_filename = join( 78 | dirname(__file__), "resources", "subdirectory", "test_file2.txt" 79 | ) 80 | await aiofiles.os.renames(old_filename, new_filename) 81 | assert exists(old_filename) is False and exists(new_filename) 82 | await aiofiles.os.renames(new_filename, old_filename) 83 | assert ( 84 | exists(old_filename) 85 | and exists(new_filename) is False 86 | and exists(dirname(new_filename)) is False 87 | ) 88 | 89 | 90 | async def test_replace(): 91 | """Test the replace call.""" 92 | old_filename = join(dirname(__file__), "resources", "test_file1.txt") 93 | new_filename = join(dirname(__file__), "resources", "test_file2.txt") 94 | 95 | await aiofiles.os.replace(old_filename, new_filename) 96 | assert exists(old_filename) is False and exists(new_filename) 97 | await aiofiles.os.replace(new_filename, old_filename) 98 | assert exists(old_filename) and exists(new_filename) is False 99 | 100 | with open(new_filename, "w") as f: 101 | f.write("Test file") 102 | assert exists(old_filename) and exists(new_filename) 103 | 104 | await aiofiles.os.replace(old_filename, new_filename) 105 | assert exists(old_filename) is False and exists(new_filename) 106 | await aiofiles.os.replace(new_filename, old_filename) 107 | assert exists(old_filename) and exists(new_filename) is False 108 | 109 | 110 | @pytest.mark.skipif( 111 | "2.4" < platform.release() < "2.6.33", 112 | reason="sendfile() syscall doesn't allow file->file", 113 | ) 114 | @pytest.mark.skipif( 115 | platform.system() in ("Darwin", "Windows"), 116 | reason="sendfile() doesn't work on mac and Win", 117 | ) 118 | async def test_sendfile_file(tmpdir): 119 | """Test the sendfile functionality, file-to-file.""" 120 | filename = join(dirname(__file__), "resources", "test_file1.txt") 121 | tmp_filename = tmpdir.join("tmp.bin") 122 | 123 | with open(filename) as f: 124 | contents = f.read() 125 | 126 | input_file = await aiofiles.open(filename) 127 | output_file = await aiofiles.open(str(tmp_filename), mode="w+") 128 | 129 | size = (await aiofiles.os.stat(filename)).st_size 130 | 131 | input_fd = input_file.fileno() 132 | output_fd = output_file.fileno() 133 | 134 | await aiofiles.os.sendfile(output_fd, input_fd, 0, size) 135 | 136 | await output_file.seek(0) 137 | 138 | actual_contents = await output_file.read() 139 | actual_size = (await aiofiles.os.stat(str(tmp_filename))).st_size 140 | 141 | assert contents == actual_contents 142 | assert size == actual_size 143 | 144 | 145 | @pytest.mark.skipif( 146 | platform.system() in ("Windows"), reason="sendfile() doesn't work on Win" 147 | ) 148 | async def test_sendfile_socket(unused_tcp_port): 149 | """Test the sendfile functionality, file-to-socket.""" 150 | filename = join(dirname(__file__), "resources", "test_file1.txt") 151 | 152 | with open(filename, mode="rb") as f: 153 | contents = f.read() 154 | 155 | async def serve_file(_, writer): 156 | out_fd = writer.transport.get_extra_info("socket").fileno() 157 | size = (await aiofiles.os.stat(filename)).st_size 158 | in_file = await aiofiles.open(filename) 159 | try: 160 | in_fd = in_file.fileno() 161 | await aiofiles.os.sendfile(out_fd, in_fd, 0, size) 162 | finally: 163 | await in_file.close() 164 | await writer.drain() 165 | writer.close() 166 | 167 | server = await asyncio.start_server(serve_file, port=unused_tcp_port) 168 | 169 | reader, writer = await asyncio.open_connection("127.0.0.1", unused_tcp_port) 170 | actual_contents = await reader.read() 171 | writer.close() 172 | 173 | assert contents == actual_contents 174 | server.close() 175 | 176 | await server.wait_closed() 177 | 178 | 179 | async def test_exists(): 180 | """Test path.exists call.""" 181 | filename = join(dirname(__file__), "resources", "test_file1.txt") 182 | result = await aiofiles.os.path.exists(filename) 183 | assert result 184 | 185 | 186 | async def test_isfile(): 187 | """Test path.isfile call.""" 188 | filename = join(dirname(__file__), "resources", "test_file1.txt") 189 | result = await aiofiles.os.path.isfile(filename) 190 | assert result 191 | 192 | 193 | async def test_isdir(): 194 | """Test path.isdir call.""" 195 | filename = join(dirname(__file__), "resources") 196 | result = await aiofiles.os.path.isdir(filename) 197 | assert result 198 | 199 | 200 | async def test_islink(): 201 | """Test the path.islink call.""" 202 | src_filename = join(dirname(__file__), "resources", "test_file1.txt") 203 | dst_filename = join(dirname(__file__), "resources", "test_file2.txt") 204 | await aiofiles.os.symlink(src_filename, dst_filename) 205 | assert await aiofiles.os.path.islink(dst_filename) 206 | await aiofiles.os.remove(dst_filename) 207 | 208 | 209 | async def test_ismount(): 210 | """Test the path.ismount call.""" 211 | filename = join(dirname(__file__), "resources") 212 | assert not await aiofiles.os.path.ismount(filename) 213 | assert await aiofiles.os.path.ismount("/") 214 | 215 | 216 | async def test_getsize(): 217 | """Test path.getsize call.""" 218 | filename = join(dirname(__file__), "resources", "test_file1.txt") 219 | result = await aiofiles.os.path.getsize(filename) 220 | assert result == 10 221 | 222 | 223 | async def test_samefile(): 224 | """Test path.samefile call.""" 225 | filename = join(dirname(__file__), "resources", "test_file1.txt") 226 | result = await aiofiles.os.path.samefile(filename, filename) 227 | assert result 228 | 229 | 230 | async def test_sameopenfile(): 231 | """Test path.samefile call.""" 232 | filename = join(dirname(__file__), "resources", "test_file1.txt") 233 | result = await aiofiles.os.path.samefile(filename, filename) 234 | assert result 235 | 236 | 237 | async def test_getmtime(): 238 | """Test path.getmtime call.""" 239 | filename = join(dirname(__file__), "resources", "test_file1.txt") 240 | result = await aiofiles.os.path.getmtime(filename) 241 | assert result 242 | 243 | 244 | async def test_getatime(): 245 | """Test path.getatime call.""" 246 | filename = join(dirname(__file__), "resources", "test_file1.txt") 247 | result = await aiofiles.os.path.getatime(filename) 248 | assert result 249 | 250 | 251 | async def test_getctime(): 252 | """Test path. call.""" 253 | filename = join(dirname(__file__), "resources", "test_file1.txt") 254 | result = await aiofiles.os.path.getctime(filename) 255 | assert result 256 | 257 | 258 | async def test_link(): 259 | """Test the link call.""" 260 | src_filename = join(dirname(__file__), "resources", "test_file1.txt") 261 | dst_filename = join(dirname(__file__), "resources", "test_file2.txt") 262 | initial_src_nlink = stat(src_filename).st_nlink 263 | await aiofiles.os.link(src_filename, dst_filename) 264 | assert ( 265 | exists(src_filename) 266 | and exists(dst_filename) 267 | and (stat(src_filename).st_ino == stat(dst_filename).st_ino) 268 | and (stat(src_filename).st_nlink == initial_src_nlink + 1) 269 | and (stat(dst_filename).st_nlink == 2) 270 | ) 271 | await aiofiles.os.remove(dst_filename) 272 | assert ( 273 | exists(src_filename) 274 | and exists(dst_filename) is False 275 | and (stat(src_filename).st_nlink == initial_src_nlink) 276 | ) 277 | 278 | 279 | async def test_symlink(): 280 | """Test the symlink call.""" 281 | src_filename = join(dirname(__file__), "resources", "test_file1.txt") 282 | dst_filename = join(dirname(__file__), "resources", "test_file2.txt") 283 | await aiofiles.os.symlink(src_filename, dst_filename) 284 | assert ( 285 | exists(src_filename) 286 | and exists(dst_filename) 287 | and stat(src_filename).st_ino == stat(dst_filename).st_ino 288 | ) 289 | await aiofiles.os.remove(dst_filename) 290 | assert exists(src_filename) and exists(dst_filename) is False 291 | 292 | 293 | @pytest.mark.skipif( 294 | platform.system() == "Windows", reason="Doesn't work on Win properly" 295 | ) 296 | async def test_readlink(): 297 | """Test the readlink call.""" 298 | src_filename = join(dirname(__file__), "resources", "test_file1.txt") 299 | dst_filename = join(dirname(__file__), "resources", "test_file2.txt") 300 | await aiofiles.os.symlink(src_filename, dst_filename) 301 | symlinked_path = await aiofiles.os.readlink(dst_filename) 302 | assert src_filename == symlinked_path 303 | await aiofiles.os.remove(dst_filename) 304 | 305 | 306 | async def test_listdir_empty_dir(): 307 | """Test the listdir call when the dir is empty.""" 308 | directory = join(dirname(__file__), "resources", "empty_dir") 309 | await aiofiles.os.mkdir(directory) 310 | dir_list = await aiofiles.os.listdir(directory) 311 | assert dir_list == [] 312 | await aiofiles.os.rmdir(directory) 313 | 314 | 315 | async def test_listdir_dir_with_only_one_file(): 316 | """Test the listdir call when the dir has one file.""" 317 | some_dir = join(dirname(__file__), "resources", "some_dir") 318 | some_file = join(some_dir, "some_file.txt") 319 | await aiofiles.os.mkdir(some_dir) 320 | with open(some_file, "w") as f: 321 | f.write("Test file") 322 | dir_list = await aiofiles.os.listdir(some_dir) 323 | assert "some_file.txt" in dir_list 324 | await aiofiles.os.remove(some_file) 325 | await aiofiles.os.rmdir(some_dir) 326 | 327 | 328 | async def test_listdir_dir_with_only_one_dir(): 329 | """Test the listdir call when the dir has one dir.""" 330 | some_dir = join(dirname(__file__), "resources", "some_dir") 331 | other_dir = join(some_dir, "other_dir") 332 | await aiofiles.os.mkdir(some_dir) 333 | await aiofiles.os.mkdir(other_dir) 334 | dir_list = await aiofiles.os.listdir(some_dir) 335 | assert "other_dir" in dir_list 336 | await aiofiles.os.rmdir(other_dir) 337 | await aiofiles.os.rmdir(some_dir) 338 | 339 | 340 | async def test_listdir_dir_with_multiple_files(): 341 | """Test the listdir call when the dir has multiple files.""" 342 | some_dir = join(dirname(__file__), "resources", "some_dir") 343 | some_file = join(some_dir, "some_file.txt") 344 | other_file = join(some_dir, "other_file.txt") 345 | await aiofiles.os.mkdir(some_dir) 346 | with open(some_file, "w") as f: 347 | f.write("Test file") 348 | with open(other_file, "w") as f: 349 | f.write("Test file") 350 | dir_list = await aiofiles.os.listdir(some_dir) 351 | assert "some_file.txt" in dir_list 352 | assert "other_file.txt" in dir_list 353 | await aiofiles.os.remove(some_file) 354 | await aiofiles.os.remove(other_file) 355 | await aiofiles.os.rmdir(some_dir) 356 | 357 | 358 | async def test_listdir_dir_with_a_file_and_a_dir(): 359 | """Test the listdir call when the dir has files and other dirs.""" 360 | some_dir = join(dirname(__file__), "resources", "some_dir") 361 | other_dir = join(some_dir, "other_dir") 362 | some_file = join(some_dir, "some_file.txt") 363 | await aiofiles.os.mkdir(some_dir) 364 | await aiofiles.os.mkdir(other_dir) 365 | with open(some_file, "w") as f: 366 | f.write("Test file") 367 | dir_list = await aiofiles.os.listdir(some_dir) 368 | assert "some_file.txt" in dir_list 369 | assert "other_dir" in dir_list 370 | await aiofiles.os.remove(some_file) 371 | await aiofiles.os.rmdir(other_dir) 372 | await aiofiles.os.rmdir(some_dir) 373 | 374 | 375 | async def test_listdir_non_existing_dir(): 376 | """Test the listdir call when the dir doesn't exist.""" 377 | some_dir = join(dirname(__file__), "resources", "some_dir") 378 | with pytest.raises(FileNotFoundError): 379 | await aiofiles.os.listdir(some_dir) 380 | 381 | 382 | async def test_scantdir_empty_dir(): 383 | """Test the scandir call when the dir is empty.""" 384 | empty_dir = join(dirname(__file__), "resources", "empty_dir") 385 | await aiofiles.os.mkdir(empty_dir) 386 | dir_iterator = await aiofiles.os.scandir(empty_dir) 387 | dir_list = [] 388 | for dir_entity in dir_iterator: 389 | dir_list.append(dir_entity) 390 | assert dir_list == [] 391 | await aiofiles.os.rmdir(empty_dir) 392 | 393 | 394 | async def test_scandir_dir_with_only_one_file(): 395 | """Test the scandir call when the dir has one file.""" 396 | some_dir = join(dirname(__file__), "resources", "some_dir") 397 | some_file = join(some_dir, "some_file.txt") 398 | await aiofiles.os.mkdir(some_dir) 399 | with open(some_file, "w") as f: 400 | f.write("Test file") 401 | dir_iterator = await aiofiles.os.scandir(some_dir) 402 | some_file_entity = next(dir_iterator) 403 | assert some_file_entity.name == "some_file.txt" 404 | await aiofiles.os.remove(some_file) 405 | await aiofiles.os.rmdir(some_dir) 406 | 407 | 408 | async def test_scandir_dir_with_only_one_dir(): 409 | """Test the scandir call when the dir has one dir.""" 410 | some_dir = join(dirname(__file__), "resources", "some_dir") 411 | other_dir = join(some_dir, "other_dir") 412 | await aiofiles.os.mkdir(some_dir) 413 | await aiofiles.os.mkdir(other_dir) 414 | dir_iterator = await aiofiles.os.scandir(some_dir) 415 | other_dir_entity = next(dir_iterator) 416 | assert other_dir_entity.name == "other_dir" 417 | await aiofiles.os.rmdir(other_dir) 418 | await aiofiles.os.rmdir(some_dir) 419 | 420 | 421 | async def test_scandir_non_existing_dir(): 422 | """Test the scandir call when the dir doesn't exist.""" 423 | some_dir = join(dirname(__file__), "resources", "some_dir") 424 | with pytest.raises(FileNotFoundError): 425 | await aiofiles.os.scandir(some_dir) 426 | 427 | 428 | @pytest.mark.skipif(platform.system() == "Windows", reason="Doesn't work on Win") 429 | async def test_access(): 430 | temp_file = Path(__file__).parent.joinpath("resources", "os_access_temp.txt") 431 | temp_dir = Path(__file__).parent.joinpath("resources", "os_access_temp") 432 | 433 | # prepare 434 | if temp_file.exists(): 435 | os.remove(temp_file) 436 | assert not temp_file.exists() 437 | temp_file.touch() 438 | 439 | if temp_dir.exists(): 440 | os.rmdir(temp_dir) 441 | assert not temp_dir.exists() 442 | os.mkdir(temp_dir) 443 | 444 | data = [ 445 | # full access 446 | [0o777, os.F_OK, True], 447 | [0o777, os.R_OK, True], 448 | [0o777, os.W_OK, True], 449 | [0o777, os.X_OK, True], 450 | # chmod -x 451 | [0o666, os.F_OK, True], 452 | [0o666, os.R_OK, True], 453 | [0o666, os.W_OK, True], 454 | [0o666, os.X_OK, False], 455 | # chmod -w 456 | [0o444, os.F_OK, True], 457 | [0o444, os.R_OK, True], 458 | [0o444, os.W_OK, False], 459 | [0o444, os.X_OK, False], 460 | # chmod -r 461 | [0o000, os.F_OK, True], 462 | [0o000, os.R_OK, False], 463 | [0o000, os.W_OK, False], 464 | [0o000, os.X_OK, False], 465 | ] 466 | for ch, mode, access in data: 467 | print(f"mode:{mode}, access:{access}") 468 | temp_file.chmod(ch) 469 | temp_dir.chmod(ch) 470 | assert await aiofiles.os.access(temp_file, mode) == access 471 | assert await aiofiles.os.access(temp_dir, mode) == access 472 | 473 | # not exists 474 | os.remove(temp_file) 475 | os.rmdir(temp_dir) 476 | for mode in [os.F_OK, os.R_OK, os.W_OK, os.X_OK]: 477 | print(f"mode:{mode}") 478 | assert not await aiofiles.os.access(temp_file, mode) 479 | assert not await aiofiles.os.access(temp_dir, mode) 480 | 481 | 482 | async def test_getcwd(): 483 | """Test the getcwd call.""" 484 | cwd = await aiofiles.os.getcwd() 485 | assert cwd == os.getcwd() 486 | 487 | 488 | async def test_abspath_given_abspath(): 489 | """Test the abspath call with an absolute path.""" 490 | filename = join(dirname(__file__), "resources", "test_file1.txt") 491 | file_abs_path = await aiofiles.os.path.abspath(filename) 492 | assert file_abs_path == filename 493 | 494 | 495 | async def test_abspath(): 496 | """Test the abspath call.""" 497 | relative_filename = "./tests/resources/test_file1.txt" 498 | abs_filename = join(dirname(__file__), "resources", "test_file1.txt") 499 | result = await aiofiles.os.path.abspath(relative_filename) 500 | assert result == abs_filename 501 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | """Simple tests verifying basic functionality.""" 2 | 3 | import asyncio 4 | 5 | from aiofiles import threadpool 6 | 7 | 8 | async def test_serve_small_bin_file_sync(tmpdir, unused_tcp_port): 9 | """Fire up a small simple file server, and fetch a file. 10 | 11 | The file is read into memory synchronously, so this test doesn't actually 12 | test anything except the general test concept. 13 | """ 14 | # First we'll write a small file. 15 | filename = "test.bin" 16 | file_content = b"0123456789" 17 | file = tmpdir.join(filename) 18 | file.write_binary(file_content) 19 | 20 | async def serve_file(reader, writer): 21 | full_filename = str(file) 22 | with open(full_filename, "rb") as f: 23 | writer.write(f.read()) 24 | writer.close() 25 | 26 | server = await asyncio.start_server(serve_file, port=unused_tcp_port) 27 | 28 | reader, _ = await asyncio.open_connection(host="localhost", port=unused_tcp_port) 29 | payload = await reader.read() 30 | 31 | assert payload == file_content 32 | 33 | server.close() 34 | await server.wait_closed() 35 | 36 | 37 | async def test_serve_small_bin_file(tmpdir, unused_tcp_port): 38 | """Fire up a small simple file server, and fetch a file.""" 39 | # First we'll write a small file. 40 | filename = "test.bin" 41 | file_content = b"0123456789" 42 | file = tmpdir.join(filename) 43 | file.write_binary(file_content) 44 | 45 | async def serve_file(reader, writer): 46 | full_filename = str(file) 47 | f = await threadpool.open(full_filename, mode="rb") 48 | writer.write(await f.read()) 49 | await f.close() 50 | writer.close() 51 | 52 | server = await asyncio.start_server(serve_file, port=unused_tcp_port) 53 | 54 | reader, _ = await asyncio.open_connection(host="localhost", port=unused_tcp_port) 55 | payload = await reader.read() 56 | 57 | assert payload == file_content 58 | 59 | server.close() 60 | await server.wait_closed() 61 | -------------------------------------------------------------------------------- /tests/test_stdio.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aiofiles import stderr, stderr_bytes, stdin, stdin_bytes, stdout, stdout_bytes 4 | 5 | 6 | async def test_stdio(capsys): 7 | await stdout.write("hello") 8 | await stderr.write("world") 9 | out, err = capsys.readouterr() 10 | assert out == "hello" 11 | assert err == "world" 12 | with pytest.raises(OSError): 13 | await stdin.read() 14 | 15 | 16 | async def test_stdio_bytes(capsysbinary): 17 | await stdout_bytes.write(b"hello") 18 | await stderr_bytes.write(b"world") 19 | out, err = capsysbinary.readouterr() 20 | assert out == b"hello" 21 | assert err == b"world" 22 | with pytest.raises(OSError): 23 | await stdin_bytes.read() 24 | -------------------------------------------------------------------------------- /tests/test_tempfile.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import platform 4 | import sys 5 | 6 | import pytest 7 | 8 | from aiofiles import tempfile 9 | 10 | 11 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"]) 12 | async def test_temporary_file(mode): 13 | """Test temporary file.""" 14 | data = b"Hello World!\n" if "b" in mode else "Hello World!\n" 15 | 16 | async with tempfile.TemporaryFile(mode=mode) as f: 17 | for _ in range(3): 18 | await f.write(data) 19 | 20 | await f.flush() 21 | await f.seek(0) 22 | 23 | async for line in f: 24 | assert line == data 25 | 26 | 27 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"]) 28 | @pytest.mark.skipif( 29 | sys.version_info >= (3, 12), 30 | reason=("3.12+ doesn't support tempfile.NamedTemporaryFile.delete"), 31 | ) 32 | async def test_named_temporary_file(mode): 33 | data = b"Hello World!" if "b" in mode else "Hello World!" 34 | filename = None 35 | 36 | async with tempfile.NamedTemporaryFile(mode=mode) as f: 37 | await f.write(data) 38 | await f.flush() 39 | await f.seek(0) 40 | assert await f.read() == data 41 | 42 | filename = f.name 43 | assert os.path.exists(filename) 44 | assert os.path.isfile(filename) 45 | assert f.delete 46 | 47 | assert not os.path.exists(filename) 48 | 49 | 50 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"]) 51 | @pytest.mark.skipif( 52 | sys.version_info < (3, 12), 53 | reason=("3.12+ doesn't support tempfile.NamedTemporaryFile.delete"), 54 | ) 55 | async def test_named_temporary_file_312(mode): 56 | data = b"Hello World!" if "b" in mode else "Hello World!" 57 | filename = None 58 | 59 | async with tempfile.NamedTemporaryFile(mode=mode) as f: 60 | await f.write(data) 61 | await f.flush() 62 | await f.seek(0) 63 | assert await f.read() == data 64 | 65 | filename = f.name 66 | assert os.path.exists(filename) 67 | assert os.path.isfile(filename) 68 | 69 | assert not os.path.exists(filename) 70 | 71 | 72 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"]) 73 | @pytest.mark.skipif( 74 | sys.version_info < (3, 12), reason=("3.12+ supports delete_on_close") 75 | ) 76 | async def test_named_temporary_delete_on_close(mode): 77 | data = b"Hello World!" if "b" in mode else "Hello World!" 78 | filename = None 79 | 80 | async with tempfile.NamedTemporaryFile(mode=mode, delete_on_close=True) as f: 81 | await f.write(data) 82 | await f.flush() 83 | await f.close() 84 | 85 | filename = f.name 86 | assert not os.path.exists(filename) 87 | 88 | async with tempfile.NamedTemporaryFile(mode=mode, delete_on_close=False) as f: 89 | await f.write(data) 90 | await f.flush() 91 | await f.close() 92 | 93 | filename = f.name 94 | assert os.path.exists(filename) 95 | 96 | assert not os.path.exists(filename) 97 | 98 | 99 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"]) 100 | async def test_spooled_temporary_file(mode): 101 | """Test spooled temporary file.""" 102 | data = b"Hello World!" if "b" in mode else "Hello World!" 103 | 104 | async with tempfile.SpooledTemporaryFile(max_size=len(data) + 1, mode=mode) as f: 105 | await f.write(data) 106 | await f.flush() 107 | if "b" in mode: 108 | assert type(f._file._file) is io.BytesIO 109 | 110 | await f.write(data) 111 | await f.flush() 112 | if "b" in mode: 113 | assert type(f._file._file) is not io.BytesIO 114 | 115 | await f.seek(0) 116 | assert await f.read() == data + data 117 | 118 | 119 | @pytest.mark.skipif( 120 | platform.system() == "Windows", reason="Doesn't work on Win properly" 121 | ) 122 | @pytest.mark.parametrize( 123 | "test_string, newlines", [("LF\n", "\n"), ("CRLF\r\n", "\r\n")] 124 | ) 125 | async def test_spooled_temporary_file_newlines(test_string, newlines): 126 | """ 127 | Test `newlines` property in spooled temporary file. 128 | issue https://github.com/Tinche/aiofiles/issues/118 129 | """ 130 | 131 | async with tempfile.SpooledTemporaryFile(mode="w+") as f: 132 | await f.write(test_string) 133 | await f.flush() 134 | await f.seek(0) 135 | 136 | assert f.newlines is None 137 | 138 | await f.read() 139 | 140 | assert f.newlines == newlines 141 | 142 | 143 | @pytest.mark.parametrize("prefix, suffix", [("a", "b"), ("c", "d"), ("e", "f")]) 144 | async def test_temporary_directory(prefix, suffix, tmp_path): 145 | """Test temporary directory.""" 146 | dir_path = None 147 | 148 | async with tempfile.TemporaryDirectory( 149 | suffix=suffix, prefix=prefix, dir=tmp_path 150 | ) as d: 151 | dir_path = d 152 | assert os.path.exists(dir_path) 153 | assert os.path.isdir(dir_path) 154 | assert d[-1] == suffix 155 | assert d.split(os.sep)[-1][0] == prefix 156 | assert not os.path.exists(dir_path) 157 | -------------------------------------------------------------------------------- /tests/threadpool/test_binary.py: -------------------------------------------------------------------------------- 1 | """PEP 0492/Python 3.5+ tests for binary files.""" 2 | 3 | import io 4 | from os.path import dirname, join 5 | 6 | import pytest 7 | 8 | from aiofiles.threadpool import open as aioopen 9 | 10 | 11 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 12 | @pytest.mark.parametrize("buffering", [-1, 0]) 13 | async def test_simple_iteration(mode, buffering): 14 | """Test iterating over lines from a file.""" 15 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 16 | 17 | async with aioopen(filename, mode=mode, buffering=buffering) as file: 18 | # Append mode needs us to seek. 19 | await file.seek(0) 20 | 21 | counter = 1 22 | # The old iteration pattern: 23 | while True: 24 | line = await file.readline() 25 | if not line: 26 | break 27 | assert line.strip() == b"line " + str(counter).encode() 28 | counter += 1 29 | 30 | counter = 1 31 | await file.seek(0) 32 | # The new iteration pattern: 33 | async for line in file: 34 | assert line.strip() == b"line " + str(counter).encode() 35 | counter += 1 36 | 37 | assert file.closed 38 | 39 | 40 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 41 | @pytest.mark.parametrize("buffering", [-1, 0]) 42 | async def test_simple_readlines(mode, buffering): 43 | """Test the readlines functionality.""" 44 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 45 | 46 | with open(filename, mode="rb") as f: 47 | expected = f.readlines() 48 | 49 | async with aioopen(str(filename), mode=mode) as file: 50 | # Append mode needs us to seek. 51 | await file.seek(0) 52 | 53 | actual = await file.readlines() 54 | 55 | assert actual == expected 56 | 57 | 58 | @pytest.mark.parametrize("mode", ["rb+", "wb", "ab"]) 59 | @pytest.mark.parametrize("buffering", [-1, 0]) 60 | async def test_simple_flush(mode, buffering, tmpdir): 61 | """Test flushing to a file.""" 62 | filename = "file.bin" 63 | 64 | full_file = tmpdir.join(filename) 65 | 66 | if "r" in mode: 67 | full_file.ensure() # Read modes want it to already exist. 68 | 69 | async with aioopen(str(full_file), mode=mode, buffering=buffering) as file: 70 | await file.write(b"0") # Shouldn't flush. 71 | 72 | if buffering == -1: 73 | assert b"" == full_file.read_binary() 74 | else: 75 | assert b"0" == full_file.read_binary() 76 | 77 | await file.flush() 78 | 79 | assert b"0" == full_file.read_binary() 80 | 81 | 82 | @pytest.mark.parametrize("mode", ["rb+", "wb+", "ab+"]) 83 | async def test_simple_peek(mode, tmpdir): 84 | """Test flushing to a file.""" 85 | filename = "file.bin" 86 | 87 | full_file = tmpdir.join(filename) 88 | full_file.write_binary(b"0123456789") 89 | 90 | async with aioopen(str(full_file), mode=mode) as file: 91 | if "a" in mode: 92 | await file.seek(0) # Rewind for append modes. 93 | 94 | peeked = await file.peek(1) 95 | 96 | # Technically it's OK for the peek to return less bytes than requested. 97 | if peeked: 98 | assert peeked.startswith(b"0") 99 | 100 | read = await file.read(1) 101 | 102 | assert peeked.startswith(read) 103 | 104 | 105 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 106 | @pytest.mark.parametrize("buffering", [-1, 0]) 107 | async def test_simple_read(mode, buffering): 108 | """Just read some bytes from a test file.""" 109 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 110 | async with aioopen(filename, mode=mode, buffering=buffering) as file: 111 | await file.seek(0) # Needed for the append mode. 112 | 113 | actual = await file.read() 114 | 115 | assert b"" == (await file.read()) 116 | assert actual == open(filename, mode="rb").read() 117 | 118 | 119 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 120 | @pytest.mark.parametrize("buffering", [-1, 0]) 121 | async def test_staggered_read(mode, buffering): 122 | """Read bytes repeatedly.""" 123 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 124 | async with aioopen(filename, mode=mode, buffering=buffering) as file: 125 | await file.seek(0) # Needed for the append mode. 126 | 127 | actual = [] 128 | while True: 129 | byte = await file.read(1) 130 | if byte: 131 | actual.append(byte) 132 | else: 133 | break 134 | 135 | assert b"" == (await file.read()) 136 | 137 | expected = [] 138 | with open(filename, mode="rb") as f: 139 | while True: 140 | byte = f.read(1) 141 | if byte: 142 | expected.append(byte) 143 | else: 144 | break 145 | 146 | assert actual == expected 147 | 148 | 149 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 150 | @pytest.mark.parametrize("buffering", [-1, 0]) 151 | async def test_simple_seek(mode, buffering, tmpdir): 152 | """Test seeking and then reading.""" 153 | filename = "bigfile.bin" 154 | content = b"0123456789" * 4 * io.DEFAULT_BUFFER_SIZE 155 | 156 | full_file = tmpdir.join(filename) 157 | full_file.write_binary(content) 158 | 159 | async with aioopen(str(full_file), mode=mode, buffering=buffering) as file: 160 | await file.seek(4) 161 | 162 | assert b"4" == (await file.read(1)) 163 | 164 | 165 | @pytest.mark.parametrize("mode", ["wb", "rb", "rb+", "wb+", "ab", "ab+"]) 166 | @pytest.mark.parametrize("buffering", [-1, 0]) 167 | async def test_simple_close_ctx_mgr_iter(mode, buffering, tmpdir): 168 | """Open a file, read a byte, and close it.""" 169 | filename = "bigfile.bin" 170 | content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE 171 | 172 | full_file = tmpdir.join(filename) 173 | full_file.write_binary(content) 174 | 175 | async with aioopen(str(full_file), mode=mode, buffering=buffering) as file: 176 | assert not file.closed 177 | assert not file._file.closed 178 | 179 | assert file.closed 180 | assert file._file.closed 181 | 182 | 183 | @pytest.mark.parametrize("mode", ["wb", "rb", "rb+", "wb+", "ab", "ab+"]) 184 | @pytest.mark.parametrize("buffering", [-1, 0]) 185 | async def test_simple_close_ctx_mgr(mode, buffering, tmpdir): 186 | """Open a file, read a byte, and close it.""" 187 | filename = "bigfile.bin" 188 | content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE 189 | 190 | full_file = tmpdir.join(filename) 191 | full_file.write_binary(content) 192 | 193 | file = await aioopen(str(full_file), mode=mode, buffering=buffering) 194 | assert not file.closed 195 | assert not file._file.closed 196 | 197 | await file.close() 198 | 199 | assert file.closed 200 | assert file._file.closed 201 | 202 | 203 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 204 | @pytest.mark.parametrize("buffering", [-1, 0]) 205 | async def test_simple_readinto(mode, buffering): 206 | """Test the readinto functionality.""" 207 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 208 | async with aioopen(filename, mode=mode, buffering=buffering) as file: 209 | await file.seek(0) # Needed for the append mode. 210 | 211 | array = bytearray(4) 212 | bytes_read = await file.readinto(array) 213 | 214 | assert bytes_read == 4 215 | assert array == open(filename, mode="rb").read(4) 216 | 217 | 218 | @pytest.mark.parametrize("mode", ["rb+", "wb", "ab+"]) 219 | @pytest.mark.parametrize("buffering", [-1, 0]) 220 | async def test_simple_truncate(mode, buffering, tmpdir): 221 | """Test truncating files.""" 222 | filename = "bigfile.bin" 223 | content = b"0123456789" * 4 * io.DEFAULT_BUFFER_SIZE 224 | 225 | full_file = tmpdir.join(filename) 226 | full_file.write_binary(content) 227 | 228 | async with aioopen(str(full_file), mode=mode, buffering=buffering) as file: 229 | # The append modes want us to seek first. 230 | await file.seek(0) 231 | 232 | if "w" in mode: 233 | # We've just erased the entire file. 234 | await file.write(content) 235 | await file.flush() 236 | await file.seek(0) 237 | 238 | await file.truncate() 239 | 240 | assert b"" == full_file.read_binary() 241 | 242 | 243 | @pytest.mark.parametrize("mode", ["wb", "rb+", "wb+", "ab", "ab+"]) 244 | @pytest.mark.parametrize("buffering", [-1, 0]) 245 | async def test_simple_write(mode, buffering, tmpdir): 246 | """Test writing into a file.""" 247 | filename = "bigfile.bin" 248 | content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE 249 | 250 | full_file = tmpdir.join(filename) 251 | 252 | if "r" in mode: 253 | full_file.ensure() # Read modes want it to already exist. 254 | 255 | async with aioopen(str(full_file), mode=mode, buffering=buffering) as file: 256 | bytes_written = await file.write(content) 257 | 258 | assert bytes_written == len(content) 259 | assert content == full_file.read_binary() 260 | 261 | 262 | async def test_simple_detach(tmpdir): 263 | """Test detaching for buffered streams.""" 264 | filename = "file.bin" 265 | 266 | full_file = tmpdir.join(filename) 267 | full_file.write_binary(b"0123456789") 268 | 269 | with pytest.raises(ValueError): 270 | async with aioopen(str(full_file), mode="rb") as file: 271 | raw_file = file.detach() 272 | 273 | assert raw_file 274 | 275 | with pytest.raises(ValueError): 276 | await file.read() 277 | 278 | assert b"0123456789" == raw_file.read(10) 279 | 280 | 281 | async def test_simple_readall(tmpdir): 282 | """Test the readall function by reading a large file in. 283 | 284 | Only RawIOBase supports readall(). 285 | """ 286 | filename = "bigfile.bin" 287 | content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE # Hopefully several reads. 288 | 289 | sync_file = tmpdir.join(filename) 290 | sync_file.write_binary(content) 291 | 292 | file = await aioopen(str(sync_file), mode="rb", buffering=0) 293 | 294 | actual = await file.readall() 295 | 296 | assert actual == content 297 | 298 | await file.close() 299 | assert file.closed 300 | 301 | 302 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 303 | @pytest.mark.parametrize("buffering", [-1, 0]) 304 | async def test_name_property(mode, buffering): 305 | """Test iterating over lines from a file.""" 306 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 307 | 308 | async with aioopen(filename, mode=mode, buffering=buffering) as file: 309 | assert file.name == filename 310 | 311 | assert file.closed 312 | 313 | 314 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"]) 315 | @pytest.mark.parametrize("buffering", [-1, 0]) 316 | async def test_mode_property(mode, buffering): 317 | """Test iterating over lines from a file.""" 318 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 319 | 320 | async with aioopen(filename, mode=mode, buffering=buffering) as file: 321 | assert file.mode == mode 322 | 323 | assert file.closed 324 | -------------------------------------------------------------------------------- /tests/threadpool/test_concurrency.py: -------------------------------------------------------------------------------- 1 | """Test concurrency properties of the implementation.""" 2 | 3 | import asyncio 4 | import time 5 | from os.path import dirname, join 6 | 7 | import aiofiles.threadpool 8 | 9 | 10 | async def test_slow_file(monkeypatch, unused_tcp_port): 11 | """Monkey patch open and file.read(), and assert the loop still works.""" 12 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 13 | 14 | with open(filename, mode="rb") as f: 15 | contents = f.read() 16 | 17 | def new_open(*args, **kwargs): 18 | time.sleep(1) 19 | return open(*args, **kwargs) 20 | 21 | monkeypatch.setattr(aiofiles.threadpool, "sync_open", value=new_open) 22 | 23 | async def serve_file(_, writer): 24 | file = await aiofiles.threadpool.open(filename, mode="rb") 25 | try: 26 | while True: 27 | data = await file.read(1) 28 | if not data: 29 | break 30 | writer.write(data) 31 | await writer.drain() 32 | await writer.drain() 33 | finally: 34 | writer.close() 35 | await file.close() 36 | 37 | async def return_one(_, writer): 38 | writer.write(b"1") 39 | await writer.drain() 40 | writer.close() 41 | 42 | counter = 0 43 | 44 | async def spam_client(): 45 | nonlocal counter 46 | while True: 47 | r, w = await asyncio.open_connection("127.0.0.1", port=30001) 48 | assert (await r.read()) == b"1" 49 | counter += 1 50 | w.close() 51 | await asyncio.sleep(0.01) 52 | 53 | file_server = await asyncio.start_server(serve_file, port=unused_tcp_port) 54 | spam_server = await asyncio.start_server(return_one, port=30001) 55 | 56 | spam_task = asyncio.ensure_future(spam_client()) 57 | 58 | reader, writer = await asyncio.open_connection("127.0.0.1", port=unused_tcp_port) 59 | 60 | actual_contents = await reader.read() 61 | writer.close() 62 | 63 | await asyncio.sleep(0) 64 | 65 | file_server.close() 66 | spam_server.close() 67 | 68 | await file_server.wait_closed() 69 | await spam_server.wait_closed() 70 | 71 | spam_task.cancel() 72 | 73 | assert actual_contents == contents 74 | assert counter > 30 75 | -------------------------------------------------------------------------------- /tests/threadpool/test_open.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from aiofiles.threadpool import open as aioopen 7 | 8 | RESOURCES_DIR = Path(__file__).parent.parent / "resources" 9 | TEST_FILE = RESOURCES_DIR / "test_file1.txt" 10 | TEST_FILE_CONTENTS = "0123456789" 11 | 12 | 13 | @pytest.mark.parametrize("mode", ["r", "rb"]) 14 | async def test_file_not_found(mode): 15 | filename = "non_existent" 16 | 17 | try: 18 | open(filename, mode=mode) 19 | except Exception as e: 20 | expected = e 21 | 22 | assert expected 23 | 24 | try: 25 | await aioopen(filename, mode=mode) 26 | except Exception as e: 27 | actual = e 28 | 29 | assert actual 30 | 31 | assert actual.errno == expected.errno 32 | assert str(actual) == str(expected) 33 | 34 | 35 | async def test_file_async_context_aexit(): 36 | async with aioopen(TEST_FILE) as fp: 37 | pass 38 | 39 | with pytest.raises(ValueError): 40 | line = await fp.read() 41 | 42 | async with aioopen(TEST_FILE) as fp: 43 | line = await fp.read() 44 | assert line == TEST_FILE_CONTENTS 45 | 46 | 47 | async def test_filetask_async_context_aexit(): 48 | async def _process_test_file(file_ctx, sleep_time: float = 1.0): 49 | nonlocal file_ref 50 | async with file_ctx as fp: 51 | file_ref = file_ctx._obj 52 | await asyncio.sleep(sleep_time) 53 | await fp.read() 54 | 55 | cancel_time, sleep_time = 0.1, 10 56 | assert cancel_time <= (sleep_time / 10) 57 | 58 | file_ref = None 59 | file_ctx = aioopen(TEST_FILE) 60 | 61 | task = asyncio.create_task( 62 | _process_test_file(file_ctx=file_ctx, sleep_time=sleep_time) 63 | ) 64 | try: 65 | await asyncio.wait_for(task, timeout=cancel_time) 66 | except asyncio.TimeoutError: 67 | assert task.cancelled 68 | 69 | assert file_ref.closed 70 | -------------------------------------------------------------------------------- /tests/threadpool/test_text.py: -------------------------------------------------------------------------------- 1 | """PEP 0492/Python 3.5+ tests for text files.""" 2 | 3 | import io 4 | from os.path import dirname, join 5 | 6 | import pytest 7 | 8 | from aiofiles.threadpool import open as aioopen 9 | 10 | 11 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 12 | async def test_simple_iteration(mode): 13 | """Test iterating over lines from a file.""" 14 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 15 | 16 | async with aioopen(filename, mode=mode) as file: 17 | # Append mode needs us to seek. 18 | await file.seek(0) 19 | 20 | counter = 1 21 | 22 | # The old iteration pattern: 23 | while True: 24 | line = await file.readline() 25 | if not line: 26 | break 27 | assert line.strip() == "line " + str(counter) 28 | counter += 1 29 | 30 | await file.seek(0) 31 | counter = 1 32 | 33 | # The new iteration pattern: 34 | async for line in file: 35 | assert line.strip() == "line " + str(counter) 36 | counter += 1 37 | 38 | assert file.closed 39 | 40 | 41 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 42 | async def test_simple_readlines(mode): 43 | """Test the readlines functionality.""" 44 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 45 | 46 | with open(filename) as f: 47 | expected = f.readlines() 48 | 49 | async with aioopen(filename, mode=mode) as file: 50 | # Append mode needs us to seek. 51 | await file.seek(0) 52 | 53 | actual = await file.readlines() 54 | 55 | assert file.closed 56 | 57 | assert actual == expected 58 | 59 | 60 | @pytest.mark.parametrize("mode", ["r+", "w", "a"]) 61 | async def test_simple_flush(mode, tmpdir): 62 | """Test flushing to a file.""" 63 | filename = "file.bin" 64 | 65 | full_file = tmpdir.join(filename) 66 | 67 | if "r" in mode: 68 | full_file.ensure() # Read modes want it to already exist. 69 | 70 | async with aioopen(str(full_file), mode=mode) as file: 71 | await file.write("0") # Shouldn't flush. 72 | 73 | assert "" == full_file.read_text(encoding="utf8") 74 | 75 | await file.flush() 76 | 77 | assert "0" == full_file.read_text(encoding="utf8") 78 | 79 | assert file.closed 80 | 81 | 82 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 83 | async def test_simple_read(mode): 84 | """Just read some bytes from a test file.""" 85 | filename = join(dirname(__file__), "..", "resources", "test_file1.txt") 86 | async with aioopen(filename, mode=mode) as file: 87 | await file.seek(0) # Needed for the append mode. 88 | 89 | actual = await file.read() 90 | 91 | assert "" == (await file.read()) 92 | assert actual == open(filename).read() 93 | 94 | assert file.closed 95 | 96 | 97 | @pytest.mark.parametrize("mode", ["w", "a"]) 98 | async def test_simple_read_fail(mode, tmpdir): 99 | """Try reading some bytes and fail.""" 100 | filename = "bigfile.bin" 101 | content = "0123456789" * 4 * io.DEFAULT_BUFFER_SIZE 102 | 103 | full_file = tmpdir.join(filename) 104 | full_file.write(content) 105 | with pytest.raises(ValueError): 106 | async with aioopen(str(full_file), mode=mode) as file: 107 | await file.seek(0) # Needed for the append mode. 108 | 109 | await file.read() 110 | 111 | assert file.closed 112 | 113 | 114 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 115 | async def test_staggered_read(mode): 116 | """Read bytes repeatedly.""" 117 | filename = join(dirname(__file__), "..", "resources", "test_file1.txt") 118 | async with aioopen(filename, mode=mode) as file: 119 | await file.seek(0) # Needed for the append mode. 120 | 121 | actual = [] 122 | while True: 123 | char = await file.read(1) 124 | if char: 125 | actual.append(char) 126 | else: 127 | break 128 | 129 | assert "" == (await file.read()) 130 | 131 | expected = [] 132 | with open(filename) as f: 133 | while True: 134 | char = f.read(1) 135 | if char: 136 | expected.append(char) 137 | else: 138 | break 139 | 140 | assert actual == expected 141 | 142 | assert file.closed 143 | 144 | 145 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 146 | async def test_simple_seek(mode, tmpdir): 147 | """Test seeking and then reading.""" 148 | filename = "bigfile.bin" 149 | content = "0123456789" * 4 * io.DEFAULT_BUFFER_SIZE 150 | 151 | full_file = tmpdir.join(filename) 152 | full_file.write(content) 153 | 154 | async with aioopen(str(full_file), mode=mode) as file: 155 | await file.seek(4) 156 | assert "4" == (await file.read(1)) 157 | 158 | assert file.closed 159 | 160 | 161 | @pytest.mark.parametrize("mode", ["w", "r", "r+", "w+", "a", "a+"]) 162 | async def test_simple_close(mode, tmpdir): 163 | """Open a file, read a byte, and close it.""" 164 | filename = "bigfile.bin" 165 | content = "0" * 4 * io.DEFAULT_BUFFER_SIZE 166 | 167 | full_file = tmpdir.join(filename) 168 | full_file.write(content) 169 | 170 | async with aioopen(str(full_file), mode=mode) as file: 171 | assert not file.closed 172 | assert not file._file.closed 173 | 174 | assert file.closed 175 | assert file._file.closed 176 | 177 | 178 | @pytest.mark.parametrize("mode", ["r+", "w", "a+"]) 179 | async def test_simple_truncate(mode, tmpdir): 180 | """Test truncating files.""" 181 | filename = "bigfile.bin" 182 | content = "0123456789" * 4 * io.DEFAULT_BUFFER_SIZE 183 | 184 | full_file = tmpdir.join(filename) 185 | full_file.write(content) 186 | 187 | async with aioopen(str(full_file), mode=mode) as file: 188 | # The append modes want us to seek first. 189 | await file.seek(0) 190 | 191 | if "w" in mode: 192 | # We've just erased the entire file. 193 | await file.write(content) 194 | await file.flush() 195 | await file.seek(0) 196 | 197 | await file.truncate() 198 | 199 | assert "" == full_file.read() 200 | 201 | 202 | @pytest.mark.parametrize("mode", ["w", "r+", "w+", "a", "a+"]) 203 | async def test_simple_write(mode, tmpdir): 204 | """Test writing into a file.""" 205 | filename = "bigfile.bin" 206 | content = "0" * 4 * io.DEFAULT_BUFFER_SIZE 207 | 208 | full_file = tmpdir.join(filename) 209 | 210 | if "r" in mode: 211 | full_file.ensure() # Read modes want it to already exist. 212 | 213 | async with aioopen(str(full_file), mode=mode) as file: 214 | bytes_written = await file.write(content) 215 | 216 | assert bytes_written == len(content) 217 | assert content == full_file.read() 218 | assert file.closed 219 | 220 | 221 | async def test_simple_detach(tmpdir): 222 | """Test detaching for buffered streams.""" 223 | filename = "file.bin" 224 | 225 | full_file = tmpdir.join(filename) 226 | full_file.write("0123456789") 227 | 228 | with pytest.raises(ValueError): # Close will error out. 229 | async with aioopen(str(full_file), mode="r") as file: 230 | raw_file = file.detach() 231 | 232 | assert raw_file 233 | 234 | with pytest.raises(ValueError): 235 | await file.read() 236 | 237 | assert b"0123456789" == raw_file.read(10) 238 | 239 | 240 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 241 | async def test_simple_iteration_ctx_mgr(mode): 242 | """Test iterating over lines from a file.""" 243 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 244 | 245 | async with aioopen(filename, mode=mode) as file: 246 | assert not file.closed 247 | await file.seek(0) 248 | 249 | counter = 1 250 | 251 | async for line in file: 252 | assert line.strip() == "line " + str(counter) 253 | counter += 1 254 | 255 | assert file.closed 256 | 257 | 258 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 259 | async def test_name_property(mode): 260 | """Test iterating over lines from a file.""" 261 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 262 | 263 | async with aioopen(filename, mode=mode) as file: 264 | assert file.name == filename 265 | 266 | assert file.closed 267 | 268 | 269 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"]) 270 | async def test_mode_property(mode): 271 | """Test iterating over lines from a file.""" 272 | filename = join(dirname(__file__), "..", "resources", "multiline_file.txt") 273 | 274 | async with aioopen(filename, mode=mode) as file: 275 | assert file.mode == mode 276 | 277 | assert file.closed 278 | -------------------------------------------------------------------------------- /tests/threadpool/test_wrap.py: -------------------------------------------------------------------------------- 1 | from io import FileIO 2 | 3 | import pytest 4 | 5 | from aiofiles.threadpool import wrap 6 | 7 | 8 | @pytest.mark.parametrize("entity", [int, [1, 2, 3], lambda x: x**x, FileIO]) 9 | def test_threadpool_wrapper_negative(entity): 10 | """Raising TypeError when wrapping unsupported entities.""" 11 | 12 | with pytest.raises(TypeError): 13 | wrap(entity) 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.26 4 | min_version = 4.26 5 | env_list = py39, py31{0,1,2,3,4}, pypy39, lint 6 | no_package = false 7 | 8 | [testenv:lint] 9 | skip_install = true 10 | basepython = python3.13 11 | allowlist_externals = just 12 | commands = 13 | just check 14 | dependency_groups = 15 | lint 16 | 17 | [testenv] 18 | runner = uv-venv-lock-runner 19 | allowlist_externals = just 20 | setenv = 21 | PYTHONHASHSEED=0 22 | commands = 23 | just coverage 24 | passenv = CI 25 | package = wheel 26 | wheel_build_env = .pkg 27 | uv_python_preference = only-managed 28 | dependency_groups = 29 | test -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.9" 4 | 5 | [[package]] 6 | name = "aiofiles" 7 | source = { editable = "." } 8 | 9 | [package.dev-dependencies] 10 | lint = [ 11 | { name = "mypy" }, 12 | { name = "ruff" }, 13 | ] 14 | test = [ 15 | { name = "coverage" }, 16 | { name = "pytest" }, 17 | { name = "pytest-asyncio" }, 18 | ] 19 | tox = [ 20 | { name = "tox" }, 21 | { name = "tox-uv" }, 22 | ] 23 | 24 | [package.metadata] 25 | 26 | [package.metadata.requires-dev] 27 | lint = [ 28 | { name = "mypy", specifier = ">=1.16.0" }, 29 | { name = "ruff", specifier = ">=0.11.12" }, 30 | ] 31 | test = [ 32 | { name = "coverage", specifier = ">=7.8.2" }, 33 | { name = "pytest", specifier = ">=8.3.5" }, 34 | { name = "pytest-asyncio", specifier = ">=1.0.0" }, 35 | ] 36 | tox = [ 37 | { name = "tox", specifier = ">=4.26.0" }, 38 | { name = "tox-uv", specifier = ">=1.26.0" }, 39 | ] 40 | 41 | [[package]] 42 | name = "cachetools" 43 | version = "6.0.0" 44 | source = { registry = "https://pypi.org/simple" } 45 | sdist = { url = "https://files.pythonhosted.org/packages/c0/b0/f539a1ddff36644c28a61490056e5bae43bd7386d9f9c69beae2d7e7d6d1/cachetools-6.0.0.tar.gz", hash = "sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf", size = 30160, upload-time = "2025-05-23T20:01:13.076Z" } 46 | wheels = [ 47 | { url = "https://files.pythonhosted.org/packages/6a/c3/8bb087c903c95a570015ce84e0c23ae1d79f528c349cbc141b5c4e250293/cachetools-6.0.0-py3-none-any.whl", hash = "sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e", size = 10964, upload-time = "2025-05-23T20:01:11.323Z" }, 48 | ] 49 | 50 | [[package]] 51 | name = "chardet" 52 | version = "5.2.0" 53 | source = { registry = "https://pypi.org/simple" } 54 | sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } 55 | wheels = [ 56 | { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, 57 | ] 58 | 59 | [[package]] 60 | name = "colorama" 61 | version = "0.4.6" 62 | source = { registry = "https://pypi.org/simple" } 63 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 64 | wheels = [ 65 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 66 | ] 67 | 68 | [[package]] 69 | name = "coverage" 70 | version = "7.8.2" 71 | source = { registry = "https://pypi.org/simple" } 72 | sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } 73 | wheels = [ 74 | { url = "https://files.pythonhosted.org/packages/26/6b/7dd06399a5c0b81007e3a6af0395cd60e6a30f959f8d407d3ee04642e896/coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a", size = 211573, upload-time = "2025-05-23T11:37:47.207Z" }, 75 | { url = "https://files.pythonhosted.org/packages/f0/df/2b24090820a0bac1412955fb1a4dade6bc3b8dcef7b899c277ffaf16916d/coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be", size = 212006, upload-time = "2025-05-23T11:37:50.289Z" }, 76 | { url = "https://files.pythonhosted.org/packages/c5/c4/e4e3b998e116625562a872a342419652fa6ca73f464d9faf9f52f1aff427/coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3", size = 241128, upload-time = "2025-05-23T11:37:52.229Z" }, 77 | { url = "https://files.pythonhosted.org/packages/b1/67/b28904afea3e87a895da850ba587439a61699bf4b73d04d0dfd99bbd33b4/coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6", size = 239026, upload-time = "2025-05-23T11:37:53.846Z" }, 78 | { url = "https://files.pythonhosted.org/packages/8c/0f/47bf7c5630d81bc2cd52b9e13043685dbb7c79372a7f5857279cc442b37c/coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622", size = 240172, upload-time = "2025-05-23T11:37:55.711Z" }, 79 | { url = "https://files.pythonhosted.org/packages/ba/38/af3eb9d36d85abc881f5aaecf8209383dbe0fa4cac2d804c55d05c51cb04/coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c", size = 240086, upload-time = "2025-05-23T11:37:57.724Z" }, 80 | { url = "https://files.pythonhosted.org/packages/9e/64/c40c27c2573adeba0fe16faf39a8aa57368a1f2148865d6bb24c67eadb41/coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3", size = 238792, upload-time = "2025-05-23T11:37:59.737Z" }, 81 | { url = "https://files.pythonhosted.org/packages/8e/ab/b7c85146f15457671c1412afca7c25a5696d7625e7158002aa017e2d7e3c/coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404", size = 239096, upload-time = "2025-05-23T11:38:01.693Z" }, 82 | { url = "https://files.pythonhosted.org/packages/d3/50/9446dad1310905fb1dc284d60d4320a5b25d4e3e33f9ea08b8d36e244e23/coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7", size = 214144, upload-time = "2025-05-23T11:38:03.68Z" }, 83 | { url = "https://files.pythonhosted.org/packages/23/ed/792e66ad7b8b0df757db8d47af0c23659cdb5a65ef7ace8b111cacdbee89/coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347", size = 215043, upload-time = "2025-05-23T11:38:05.217Z" }, 84 | { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" }, 85 | { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" }, 86 | { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" }, 87 | { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" }, 88 | { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" }, 89 | { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" }, 90 | { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" }, 91 | { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" }, 92 | { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" }, 93 | { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" }, 94 | { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" }, 95 | { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, 96 | { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, 97 | { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, 98 | { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, 99 | { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, 100 | { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, 101 | { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, 102 | { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, 103 | { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, 104 | { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, 105 | { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, 106 | { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, 107 | { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, 108 | { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, 109 | { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, 110 | { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, 111 | { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, 112 | { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, 113 | { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, 114 | { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, 115 | { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, 116 | { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, 117 | { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, 118 | { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, 119 | { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, 120 | { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, 121 | { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, 122 | { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, 123 | { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, 124 | { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, 125 | { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, 126 | { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, 127 | { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, 128 | { url = "https://files.pythonhosted.org/packages/71/1e/388267ad9c6aa126438acc1ceafede3bb746afa9872e3ec5f0691b7d5efa/coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a", size = 211566, upload-time = "2025-05-23T11:39:32.333Z" }, 129 | { url = "https://files.pythonhosted.org/packages/8f/a5/acc03e5cf0bba6357f5e7c676343de40fbf431bb1e115fbebf24b2f7f65e/coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d", size = 211996, upload-time = "2025-05-23T11:39:34.512Z" }, 130 | { url = "https://files.pythonhosted.org/packages/5b/a2/0fc0a9f6b7c24fa4f1d7210d782c38cb0d5e692666c36eaeae9a441b6755/coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca", size = 240741, upload-time = "2025-05-23T11:39:36.252Z" }, 131 | { url = "https://files.pythonhosted.org/packages/e6/da/1c6ba2cf259710eed8916d4fd201dccc6be7380ad2b3b9f63ece3285d809/coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d", size = 238672, upload-time = "2025-05-23T11:39:38.03Z" }, 132 | { url = "https://files.pythonhosted.org/packages/ac/51/c8fae0dc3ca421e6e2509503696f910ff333258db672800c3bdef256265a/coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787", size = 239769, upload-time = "2025-05-23T11:39:40.24Z" }, 133 | { url = "https://files.pythonhosted.org/packages/59/8e/b97042ae92c59f40be0c989df090027377ba53f2d6cef73c9ca7685c26a6/coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7", size = 239555, upload-time = "2025-05-23T11:39:42.3Z" }, 134 | { url = "https://files.pythonhosted.org/packages/47/35/b8893e682d6e96b1db2af5997fc13ef62219426fb17259d6844c693c5e00/coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3", size = 237768, upload-time = "2025-05-23T11:39:44.069Z" }, 135 | { url = "https://files.pythonhosted.org/packages/03/6c/023b0b9a764cb52d6243a4591dcb53c4caf4d7340445113a1f452bb80591/coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7", size = 238757, upload-time = "2025-05-23T11:39:46.195Z" }, 136 | { url = "https://files.pythonhosted.org/packages/03/ed/3af7e4d721bd61a8df7de6de9e8a4271e67f3d9e086454558fd9f48eb4f6/coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a", size = 214166, upload-time = "2025-05-23T11:39:47.934Z" }, 137 | { url = "https://files.pythonhosted.org/packages/9d/30/ee774b626773750dc6128354884652507df3c59d6aa8431526107e595227/coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e", size = 215050, upload-time = "2025-05-23T11:39:50.252Z" }, 138 | { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, 139 | { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, 140 | ] 141 | 142 | [[package]] 143 | name = "distlib" 144 | version = "0.3.9" 145 | source = { registry = "https://pypi.org/simple" } 146 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } 147 | wheels = [ 148 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, 149 | ] 150 | 151 | [[package]] 152 | name = "exceptiongroup" 153 | version = "1.3.0" 154 | source = { registry = "https://pypi.org/simple" } 155 | dependencies = [ 156 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 157 | ] 158 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 159 | wheels = [ 160 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 161 | ] 162 | 163 | [[package]] 164 | name = "filelock" 165 | version = "3.18.0" 166 | source = { registry = "https://pypi.org/simple" } 167 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } 168 | wheels = [ 169 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, 170 | ] 171 | 172 | [[package]] 173 | name = "iniconfig" 174 | version = "2.1.0" 175 | source = { registry = "https://pypi.org/simple" } 176 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 177 | wheels = [ 178 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 179 | ] 180 | 181 | [[package]] 182 | name = "mypy" 183 | version = "1.16.0" 184 | source = { registry = "https://pypi.org/simple" } 185 | dependencies = [ 186 | { name = "mypy-extensions" }, 187 | { name = "pathspec" }, 188 | { name = "tomli", marker = "python_full_version < '3.11'" }, 189 | { name = "typing-extensions" }, 190 | ] 191 | sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } 192 | wheels = [ 193 | { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" }, 194 | { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" }, 195 | { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" }, 196 | { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" }, 197 | { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" }, 198 | { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" }, 199 | { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" }, 200 | { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" }, 201 | { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" }, 202 | { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" }, 203 | { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" }, 204 | { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" }, 205 | { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, 206 | { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, 207 | { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, 208 | { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, 209 | { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, 210 | { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, 211 | { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, 212 | { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, 213 | { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, 214 | { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, 215 | { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, 216 | { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, 217 | { url = "https://files.pythonhosted.org/packages/bd/eb/c0759617fe2159aee7a653f13cceafbf7f0b6323b4197403f2e587ca947d/mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3", size = 10956081, upload-time = "2025-05-29T13:19:32.264Z" }, 218 | { url = "https://files.pythonhosted.org/packages/70/35/df3c74a2967bdf86edea58b265feeec181d693432faed1c3b688b7c231e3/mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92", size = 10084422, upload-time = "2025-05-29T13:18:01.437Z" }, 219 | { url = "https://files.pythonhosted.org/packages/b3/07/145ffe29f4b577219943b7b1dc0a71df7ead3c5bed4898686bd87c5b5cc2/mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436", size = 11879670, upload-time = "2025-05-29T13:17:45.971Z" }, 220 | { url = "https://files.pythonhosted.org/packages/c6/94/0421562d6b046e22986758c9ae31865d10ea0ba607ae99b32c9d18b16f66/mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2", size = 12610528, upload-time = "2025-05-29T13:34:36.983Z" }, 221 | { url = "https://files.pythonhosted.org/packages/1a/f1/39a22985b78c766a594ae1e0bbb6f8bdf5f31ea8d0c52291a3c211fd3cd5/mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20", size = 12871923, upload-time = "2025-05-29T13:32:21.823Z" }, 222 | { url = "https://files.pythonhosted.org/packages/f3/8e/84db4fb0d01f43d2c82fa9072ca72a42c49e52d58f44307bbd747c977bc2/mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21", size = 9482931, upload-time = "2025-05-29T13:21:32.326Z" }, 223 | { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, 224 | ] 225 | 226 | [[package]] 227 | name = "mypy-extensions" 228 | version = "1.1.0" 229 | source = { registry = "https://pypi.org/simple" } 230 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } 231 | wheels = [ 232 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, 233 | ] 234 | 235 | [[package]] 236 | name = "packaging" 237 | version = "25.0" 238 | source = { registry = "https://pypi.org/simple" } 239 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 240 | wheels = [ 241 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 242 | ] 243 | 244 | [[package]] 245 | name = "pathspec" 246 | version = "0.12.1" 247 | source = { registry = "https://pypi.org/simple" } 248 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 249 | wheels = [ 250 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 251 | ] 252 | 253 | [[package]] 254 | name = "platformdirs" 255 | version = "4.3.8" 256 | source = { registry = "https://pypi.org/simple" } 257 | sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } 258 | wheels = [ 259 | { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, 260 | ] 261 | 262 | [[package]] 263 | name = "pluggy" 264 | version = "1.6.0" 265 | source = { registry = "https://pypi.org/simple" } 266 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 267 | wheels = [ 268 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 269 | ] 270 | 271 | [[package]] 272 | name = "pyproject-api" 273 | version = "1.9.1" 274 | source = { registry = "https://pypi.org/simple" } 275 | dependencies = [ 276 | { name = "packaging" }, 277 | { name = "tomli", marker = "python_full_version < '3.11'" }, 278 | ] 279 | sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } 280 | wheels = [ 281 | { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, 282 | ] 283 | 284 | [[package]] 285 | name = "pytest" 286 | version = "8.3.5" 287 | source = { registry = "https://pypi.org/simple" } 288 | dependencies = [ 289 | { name = "colorama", marker = "sys_platform == 'win32'" }, 290 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 291 | { name = "iniconfig" }, 292 | { name = "packaging" }, 293 | { name = "pluggy" }, 294 | { name = "tomli", marker = "python_full_version < '3.11'" }, 295 | ] 296 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 297 | wheels = [ 298 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 299 | ] 300 | 301 | [[package]] 302 | name = "pytest-asyncio" 303 | version = "1.0.0" 304 | source = { registry = "https://pypi.org/simple" } 305 | dependencies = [ 306 | { name = "pytest" }, 307 | { name = "typing-extensions", marker = "python_full_version < '3.10'" }, 308 | ] 309 | sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } 310 | wheels = [ 311 | { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, 312 | ] 313 | 314 | [[package]] 315 | name = "ruff" 316 | version = "0.11.12" 317 | source = { registry = "https://pypi.org/simple" } 318 | sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" } 319 | wheels = [ 320 | { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" }, 321 | { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" }, 322 | { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" }, 323 | { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" }, 324 | { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" }, 325 | { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" }, 326 | { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" }, 327 | { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" }, 328 | { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" }, 329 | { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" }, 330 | { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" }, 331 | { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" }, 332 | { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" }, 333 | { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" }, 334 | { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" }, 335 | { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" }, 336 | { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, 337 | ] 338 | 339 | [[package]] 340 | name = "tomli" 341 | version = "2.2.1" 342 | source = { registry = "https://pypi.org/simple" } 343 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 344 | wheels = [ 345 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 346 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 347 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 348 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 349 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 350 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 351 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 352 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 353 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 354 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 355 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 356 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 357 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 358 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 359 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 360 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 361 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 362 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 363 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 364 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 365 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 366 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 367 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 368 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 369 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 370 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 371 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 372 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 373 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 374 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 375 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 376 | ] 377 | 378 | [[package]] 379 | name = "tox" 380 | version = "4.26.0" 381 | source = { registry = "https://pypi.org/simple" } 382 | dependencies = [ 383 | { name = "cachetools" }, 384 | { name = "chardet" }, 385 | { name = "colorama" }, 386 | { name = "filelock" }, 387 | { name = "packaging" }, 388 | { name = "platformdirs" }, 389 | { name = "pluggy" }, 390 | { name = "pyproject-api" }, 391 | { name = "tomli", marker = "python_full_version < '3.11'" }, 392 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 393 | { name = "virtualenv" }, 394 | ] 395 | sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/dcec0c00321a107f7f697fd00754c5112572ea6dcacb40b16d8c3eea7c37/tox-4.26.0.tar.gz", hash = "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca", size = 197260, upload-time = "2025-05-13T15:04:28.481Z" } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/de/14/f58b4087cf248b18c795b5c838c7a8d1428dfb07cb468dad3ec7f54041ab/tox-4.26.0-py3-none-any.whl", hash = "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", size = 172761, upload-time = "2025-05-13T15:04:26.207Z" }, 398 | ] 399 | 400 | [[package]] 401 | name = "tox-uv" 402 | version = "1.26.0" 403 | source = { registry = "https://pypi.org/simple" } 404 | dependencies = [ 405 | { name = "packaging" }, 406 | { name = "tox" }, 407 | { name = "typing-extensions", marker = "python_full_version < '3.10'" }, 408 | { name = "uv" }, 409 | ] 410 | sdist = { url = "https://files.pythonhosted.org/packages/7e/da/37790b4a176f05b0ec7a699f54979078fc726f743640aa5c10c551c27edb/tox_uv-1.26.0.tar.gz", hash = "sha256:5045880c467eed58a98f7eaa7fe286b7ef688e2c56f2123d53e275011495c381", size = 21523, upload-time = "2025-05-27T14:51:42.702Z" } 411 | wheels = [ 412 | { url = "https://files.pythonhosted.org/packages/46/b8/04c5cb83da072a3f96d357d68a551f5e97e162573c2011a09437df995811/tox_uv-1.26.0-py3-none-any.whl", hash = "sha256:894b2e7274fd6131c3bd1012813edc858753cad67727050c21cd973a08e691c8", size = 16562, upload-time = "2025-05-27T14:51:40.803Z" }, 413 | ] 414 | 415 | [[package]] 416 | name = "typing-extensions" 417 | version = "4.13.2" 418 | source = { registry = "https://pypi.org/simple" } 419 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 420 | wheels = [ 421 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 422 | ] 423 | 424 | [[package]] 425 | name = "uv" 426 | version = "0.7.9" 427 | source = { registry = "https://pypi.org/simple" } 428 | sdist = { url = "https://files.pythonhosted.org/packages/9a/7c/8621d5928111f985196dc75c50a64147b3bad39f36164686f24d45581367/uv-0.7.9.tar.gz", hash = "sha256:baac54e49f3b0d05ee83f534fdcb27b91d2923c585bf349a1532ca25d62c216f", size = 3272882, upload-time = "2025-05-30T19:54:33.003Z" } 429 | wheels = [ 430 | { url = "https://files.pythonhosted.org/packages/c5/7a/e4d12029e16f30279ef48f387545f8f3974dc3c4c9d8ef59c381ae7e6a7d/uv-0.7.9-py3-none-linux_armv6l.whl", hash = "sha256:0f8c53d411f95cec2fa19471c23b41ec456fc0d5f2efca96480d94e0c34026c2", size = 16746809, upload-time = "2025-05-30T19:53:35.447Z" }, 431 | { url = "https://files.pythonhosted.org/packages/fc/85/8df3ca683e1a260117efa31373e91e1c03a4862b7add865662f60a967fdf/uv-0.7.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:85c1a63669e49b825923fc876b7467cc3c20d4aa010f522c0ac8b0f30ce2b18e", size = 16821006, upload-time = "2025-05-30T19:53:40.102Z" }, 432 | { url = "https://files.pythonhosted.org/packages/77/d4/c40502ec8f5575798b7ec13ac38c0d5ded84cc32129c1d74a47f8cb7bc0a/uv-0.7.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa10c61668f94515acf93f31dbb8de41b1f2e7a9c41db828f2448cef786498ff", size = 15600148, upload-time = "2025-05-30T19:53:43.513Z" }, 433 | { url = "https://files.pythonhosted.org/packages/4f/dd/4deec6d5b556f4033d6bcc35d6aad70c08acea3f5da749cb34112dced5da/uv-0.7.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9de67ca9ea97db71e5697c1320508e25679fb68d4ee2cea27bbeac499a6bad56", size = 16038119, upload-time = "2025-05-30T19:53:46.504Z" }, 434 | { url = "https://files.pythonhosted.org/packages/cb/c5/2c23763e18566a9a7767738714791203cc97a7530979f61e0fd32d8473a2/uv-0.7.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13ce63524f88228152edf8a9c1c07cecc07d69a2853b32ecc02ac73538aaa5c1", size = 16467257, upload-time = "2025-05-30T19:53:49.592Z" }, 435 | { url = "https://files.pythonhosted.org/packages/da/94/f452d0093f466f9f81a2ede3ea2d48632237b79eb1dc595c7c91be309de5/uv-0.7.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3453b7bb65eaea87c9129e27bff701007a8bd1a563249982a1ede7ec4357ced6", size = 17170719, upload-time = "2025-05-30T19:53:52.828Z" }, 436 | { url = "https://files.pythonhosted.org/packages/69/bf/e15ef77520e9bbf00d29a3b639dfaf4fe63996863d6db00c53eba19535c7/uv-0.7.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d7b1e36a8b39600d0f0333bf50c966e83beeaaee1a38380ccb0f16ab45f351c3", size = 18052903, upload-time = "2025-05-30T19:53:56.237Z" }, 437 | { url = "https://files.pythonhosted.org/packages/32/9f/ebf3f9910121ef037c0fe9e7e7fb5f1c25b77d41a65a029d5cbcd85cc886/uv-0.7.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab412ed3d415f07192805788669c8a89755086cdd6fe9f021e1ba21781728031", size = 17771828, upload-time = "2025-05-30T19:53:59.561Z" }, 438 | { url = "https://files.pythonhosted.org/packages/fe/6c/82b4cd471432e721c239ddde2ebee2e674238f3bd88e279e6c71f3cbc775/uv-0.7.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbeb229ee86f69913f5f9236ff1b8ccbae212f559d7f029f8432fa8d9abcc7e0", size = 17886161, upload-time = "2025-05-30T19:54:02.865Z" }, 439 | { url = "https://files.pythonhosted.org/packages/63/e2/922d2eed25647b50a7257a7bfea10c36d9ff910d1451f9a1ba5e31766f41/uv-0.7.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d654a14d632ecb078969ae7252d89dd98c89205df567a1eff18b5f078a6d00", size = 17442630, upload-time = "2025-05-30T19:54:06.519Z" }, 440 | { url = "https://files.pythonhosted.org/packages/96/b8/45a5598cc8d466bb1669ccf0fc4f556719babfdb7a1983edc24967cb3845/uv-0.7.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f5f47e93a5f948f431ca55d765af6e818c925500807539b976bfda7f94369aa9", size = 16299207, upload-time = "2025-05-30T19:54:09.713Z" }, 441 | { url = "https://files.pythonhosted.org/packages/14/35/7e70639cd175f340138c88290c819214a496dfc52461f30f71e51e776293/uv-0.7.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:267fe25ad3adf024e13617be9fc99bedebf96bf726c6140e48d856e844f21af4", size = 16427594, upload-time = "2025-05-30T19:54:13.318Z" }, 442 | { url = "https://files.pythonhosted.org/packages/5e/f6/90fe538370bc60509cca942b703bca06c06c160ec09816ea6946882278d1/uv-0.7.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:473d3c6ee07588cff8319079d9225fb393ed177d8d57186fce0d7c1aebff79c0", size = 16751451, upload-time = "2025-05-30T19:54:16.833Z" }, 443 | { url = "https://files.pythonhosted.org/packages/09/cb/c099aba21fb22e50713b42e874075a5b60c6b4d141cc3868ae22f505baa7/uv-0.7.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:19792c88058894c10f0370a5e5492bb4a7e6302c439fed9882e73ba2e4b231ef", size = 17594496, upload-time = "2025-05-30T19:54:20.383Z" }, 444 | { url = "https://files.pythonhosted.org/packages/c0/2e/e35b2c5669533075987e1d74da45af891890ae5faee031f90997ed81cada/uv-0.7.9-py3-none-win32.whl", hash = "sha256:298e9b3c65742edcb3097c2cf3f62ec847df174a7c62c85fe139dddaa1b9ab65", size = 17121149, upload-time = "2025-05-30T19:54:23.608Z" }, 445 | { url = "https://files.pythonhosted.org/packages/33/8e/d10425711156d0d5d9a28299950acb3ab4a3987b3150a3c871ac95ce2fdd/uv-0.7.9-py3-none-win_amd64.whl", hash = "sha256:82d76ea988ff1347158c6de46a571b1db7d344219e452bd7b3339c21ec37cfd8", size = 18622895, upload-time = "2025-05-30T19:54:27.427Z" }, 446 | { url = "https://files.pythonhosted.org/packages/72/77/cac29a8fb608b5613b7a0863ec6bd7c2517f3a80b94c419e9d890c12257e/uv-0.7.9-py3-none-win_arm64.whl", hash = "sha256:4d419bcc3138fd787ce77305f1a09e2a984766e0804c6e5a2b54adfa55d2439a", size = 17316542, upload-time = "2025-05-30T19:54:30.697Z" }, 447 | ] 448 | 449 | [[package]] 450 | name = "virtualenv" 451 | version = "20.31.2" 452 | source = { registry = "https://pypi.org/simple" } 453 | dependencies = [ 454 | { name = "distlib" }, 455 | { name = "filelock" }, 456 | { name = "platformdirs" }, 457 | ] 458 | sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } 459 | wheels = [ 460 | { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, 461 | ] 462 | --------------------------------------------------------------------------------