phase 3 complete
authorSam Mirazi <sasan345@gmail.com>
Sat, 31 May 2025 06:05:11 +0000 (23:05 -0700)
committerSam Mirazi <sasan345@gmail.com>
Sat, 31 May 2025 06:05:11 +0000 (23:05 -0700)
25 files changed:
.venv/Lib/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc [new file with mode: 0644]
.venv/Lib/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-8.3.5.pyc [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/INSTALLER [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/METADATA [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/RECORD [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/REQUESTED [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/WHEEL [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/entry_points.txt [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/licenses/LICENSE [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/top_level.txt [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/__init__.py [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312.pyc [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312-pytest-8.3.5.pyc [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312.pyc [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312-pytest-8.3.5.pyc [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312.pyc [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/_version.py [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/plugin.py [new file with mode: 0644]
.venv/Lib/site-packages/pytest_asyncio/py.typed [new file with mode: 0644]
app_flask/flask_application.py
tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc [new file with mode: 0644]
tests/__pycache__/test_flask_route.cpython-312-pytest-8.3.5.pyc
tests/test_fastapi_route.py [new file with mode: 0644]
tests/test_flask_route.py

diff --git a/.venv/Lib/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc b/.venv/Lib/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc
new file mode 100644 (file)
index 0000000..c9d6137
Binary files /dev/null and b/.venv/Lib/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc differ
diff --git a/.venv/Lib/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-8.3.5.pyc b/.venv/Lib/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-8.3.5.pyc
new file mode 100644 (file)
index 0000000..21894a4
Binary files /dev/null and b/.venv/Lib/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-8.3.5.pyc differ
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/INSTALLER b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/INSTALLER
new file mode 100644 (file)
index 0000000..a1b589e
--- /dev/null
@@ -0,0 +1 @@
+pip
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/METADATA b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/METADATA
new file mode 100644 (file)
index 0000000..4e70c84
--- /dev/null
@@ -0,0 +1,89 @@
+Metadata-Version: 2.4
+Name: pytest-asyncio
+Version: 1.0.0
+Summary: Pytest support for asyncio
+Author-email: "Tin Tvrtković <tinchester@gmail.com>" <tinchester@gmail.com>
+License-Expression: Apache-2.0
+Project-URL: Bug Tracker, https://github.com/pytest-dev/pytest-asyncio/issues
+Project-URL: Changelog, https://pytest-asyncio.readthedocs.io/en/latest/reference/changelog.html
+Project-URL: Documentation, https://pytest-asyncio.readthedocs.io
+Project-URL: Homepage, https://github.com/pytest-dev/pytest-asyncio
+Project-URL: Source Code, https://github.com/pytest-dev/pytest-asyncio
+Classifier: Development Status :: 4 - Beta
+Classifier: Framework :: AsyncIO
+Classifier: Framework :: Pytest
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Topic :: Software Development :: Testing
+Classifier: Typing :: Typed
+Requires-Python: >=3.9
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Requires-Dist: pytest<9,>=8.2
+Requires-Dist: typing-extensions>=4.12; python_version < "3.10"
+Provides-Extra: docs
+Requires-Dist: sphinx>=5.3; extra == "docs"
+Requires-Dist: sphinx-rtd-theme>=1; extra == "docs"
+Provides-Extra: testing
+Requires-Dist: coverage>=6.2; extra == "testing"
+Requires-Dist: hypothesis>=5.7.1; extra == "testing"
+Dynamic: license-file
+
+pytest-asyncio
+==============
+
+.. image:: https://img.shields.io/pypi/v/pytest-asyncio.svg
+    :target: https://pypi.python.org/pypi/pytest-asyncio
+.. image:: https://github.com/pytest-dev/pytest-asyncio/workflows/CI/badge.svg
+    :target: https://github.com/pytest-dev/pytest-asyncio/actions?workflow=CI
+.. image:: https://codecov.io/gh/pytest-dev/pytest-asyncio/branch/main/graph/badge.svg
+    :target: https://codecov.io/gh/pytest-dev/pytest-asyncio
+.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio.svg
+    :target: https://github.com/pytest-dev/pytest-asyncio
+    :alt: Supported Python versions
+.. image:: https://img.shields.io/badge/Matrix-%23pytest--asyncio-brightgreen
+    :alt: Matrix chat room: #pytest-asyncio
+    :target: https://matrix.to/#/#pytest-asyncio:matrix.org
+
+`pytest-asyncio <https://pytest-asyncio.readthedocs.io/en/latest/>`_ is a `pytest <https://docs.pytest.org/en/latest/contents.html>`_ plugin. It facilitates testing of code that uses the `asyncio <https://docs.python.org/3/library/asyncio.html>`_ library.
+
+Specifically, pytest-asyncio provides support for coroutines as test functions. This allows users to *await* code inside their tests. For example, the following code is executed as a test item by pytest:
+
+.. code-block:: python
+
+    @pytest.mark.asyncio
+    async def test_some_asyncio_code():
+        res = await library.do_something()
+        assert b"expected result" == res
+
+More details can be found in the `documentation <https://pytest-asyncio.readthedocs.io/en/latest/>`_.
+
+Note that test classes subclassing the standard `unittest <https://docs.python.org/3/library/unittest.html>`__ library are not supported. Users
+are advised to use `unittest.IsolatedAsyncioTestCase <https://docs.python.org/3/library/unittest.html#unittest.IsolatedAsyncioTestCase>`__
+or an async framework such as `asynctest <https://asynctest.readthedocs.io/en/latest>`__.
+
+
+pytest-asyncio is available under the `Apache License 2.0 <https://github.com/pytest-dev/pytest-asyncio/blob/main/LICENSE>`_.
+
+
+Installation
+------------
+
+To install pytest-asyncio, simply:
+
+.. code-block:: bash
+
+    $ pip install pytest-asyncio
+
+This is enough for pytest to pick up pytest-asyncio.
+
+
+Contributing
+------------
+Contributions are very welcome. Tests can be run with ``tox``, please ensure
+the coverage at least stays the same before you submit a pull request.
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/RECORD b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/RECORD
new file mode 100644 (file)
index 0000000..ff8b08c
--- /dev/null
@@ -0,0 +1,15 @@
+pytest_asyncio-1.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+pytest_asyncio-1.0.0.dist-info/METADATA,sha256=COXTlvAPs6WJu76Pi1olsFlvFIhY7gHlEnhIi5tZKxM,3956
+pytest_asyncio-1.0.0.dist-info/RECORD,,
+pytest_asyncio-1.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+pytest_asyncio-1.0.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
+pytest_asyncio-1.0.0.dist-info/entry_points.txt,sha256=_5TsciE-7mIopUy1NrPCEjHVTDIwhNbyXvP-su1-O7w,43
+pytest_asyncio-1.0.0.dist-info/licenses/LICENSE,sha256=qK0xscP0DcpahBGTUbj6jdyGjt13-tio6_bY8tFvpK4,11324
+pytest_asyncio-1.0.0.dist-info/top_level.txt,sha256=J4BTi9IZbfghCsiVybot1y0AaLUgxp3NMaNpH9fghNI,15
+pytest_asyncio/__init__.py,sha256=EWf0KLgnvHEAZXcbCJ3BxAGeH1OLzMhyh-oCU0Idwew,236
+pytest_asyncio/__pycache__/__init__.cpython-312.pyc,,
+pytest_asyncio/__pycache__/_version.cpython-312.pyc,,
+pytest_asyncio/__pycache__/plugin.cpython-312.pyc,,
+pytest_asyncio/_version.py,sha256=fo5PXsZuloQZu3LdpIFTUAXvJmY2L9N5sNGe2tvdU98,511
+pytest_asyncio/plugin.py,sha256=iQBFi9d2REjOarR7EfvLogAdq6VvJ_aUL5CrUAYawrg,32098
+pytest_asyncio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/REQUESTED b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/REQUESTED
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/WHEEL b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/WHEEL
new file mode 100644 (file)
index 0000000..870aa26
--- /dev/null
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: setuptools (80.8.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/entry_points.txt b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/entry_points.txt
new file mode 100644 (file)
index 0000000..88db714
--- /dev/null
@@ -0,0 +1,2 @@
+[pytest11]
+asyncio = pytest_asyncio.plugin
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/licenses/LICENSE b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/licenses/LICENSE
new file mode 100644 (file)
index 0000000..5c304d1
--- /dev/null
@@ -0,0 +1,201 @@
+Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/top_level.txt b/.venv/Lib/site-packages/pytest_asyncio-1.0.0.dist-info/top_level.txt
new file mode 100644 (file)
index 0000000..08d05d1
--- /dev/null
@@ -0,0 +1 @@
+pytest_asyncio
diff --git a/.venv/Lib/site-packages/pytest_asyncio/__init__.py b/.venv/Lib/site-packages/pytest_asyncio/__init__.py
new file mode 100644 (file)
index 0000000..c25c1bf
--- /dev/null
@@ -0,0 +1,8 @@
+"""The main point for importing pytest-asyncio items."""
+
+from __future__ import annotations
+
+from ._version import version as __version__  # noqa: F401
+from .plugin import fixture, is_async_test
+
+__all__ = ("fixture", "is_async_test")
diff --git a/.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc
new file mode 100644 (file)
index 0000000..871a29a
Binary files /dev/null and b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312-pytest-8.3.5.pyc differ
diff --git a/.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312.pyc b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312.pyc
new file mode 100644 (file)
index 0000000..cdd90ac
Binary files /dev/null and b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/__init__.cpython-312.pyc differ
diff --git a/.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312-pytest-8.3.5.pyc b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312-pytest-8.3.5.pyc
new file mode 100644 (file)
index 0000000..2bb73bb
Binary files /dev/null and b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312-pytest-8.3.5.pyc differ
diff --git a/.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312.pyc b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312.pyc
new file mode 100644 (file)
index 0000000..791824e
Binary files /dev/null and b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/_version.cpython-312.pyc differ
diff --git a/.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312-pytest-8.3.5.pyc b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312-pytest-8.3.5.pyc
new file mode 100644 (file)
index 0000000..fae8c58
Binary files /dev/null and b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312-pytest-8.3.5.pyc differ
diff --git a/.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312.pyc b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312.pyc
new file mode 100644 (file)
index 0000000..9c9a607
Binary files /dev/null and b/.venv/Lib/site-packages/pytest_asyncio/__pycache__/plugin.cpython-312.pyc differ
diff --git a/.venv/Lib/site-packages/pytest_asyncio/_version.py b/.venv/Lib/site-packages/pytest_asyncio/_version.py
new file mode 100644 (file)
index 0000000..93f235a
--- /dev/null
@@ -0,0 +1,21 @@
+# file generated by setuptools-scm
+# don't change, don't track in version control
+
+__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+    from typing import Tuple
+    from typing import Union
+
+    VERSION_TUPLE = Tuple[Union[int, str], ...]
+else:
+    VERSION_TUPLE = object
+
+version: str
+__version__: str
+__version_tuple__: VERSION_TUPLE
+version_tuple: VERSION_TUPLE
+
+__version__ = version = '1.0.0'
+__version_tuple__ = version_tuple = (1, 0, 0)
diff --git a/.venv/Lib/site-packages/pytest_asyncio/plugin.py b/.venv/Lib/site-packages/pytest_asyncio/plugin.py
new file mode 100644 (file)
index 0000000..aecf6e9
--- /dev/null
@@ -0,0 +1,948 @@
+"""pytest-asyncio implementation."""
+
+from __future__ import annotations
+
+import asyncio
+import contextlib
+import contextvars
+import enum
+import functools
+import inspect
+import socket
+import sys
+import warnings
+from asyncio import AbstractEventLoop, AbstractEventLoopPolicy
+from collections.abc import (
+    AsyncIterator,
+    Awaitable,
+    Coroutine as AbstractCoroutine,
+    Generator,
+    Iterable,
+    Iterator,
+    Sequence,
+)
+from typing import (
+    Any,
+    Callable,
+    Literal,
+    TypeVar,
+    Union,
+    cast,
+    overload,
+)
+
+import pluggy
+import pytest
+from _pytest.scope import Scope
+from pytest import (
+    Collector,
+    Config,
+    FixtureDef,
+    FixtureRequest,
+    Function,
+    Item,
+    Mark,
+    Metafunc,
+    Parser,
+    PytestCollectionWarning,
+    PytestDeprecationWarning,
+    PytestPluginManager,
+)
+
+if sys.version_info >= (3, 10):
+    from typing import ParamSpec
+else:
+    from typing_extensions import ParamSpec
+
+
+_ScopeName = Literal["session", "package", "module", "class", "function"]
+_T = TypeVar("_T")
+_R = TypeVar("_R", bound=Union[Awaitable[Any], AsyncIterator[Any]])
+_P = ParamSpec("_P")
+FixtureFunction = Callable[_P, _R]
+
+
+class PytestAsyncioError(Exception):
+    """Base class for exceptions raised by pytest-asyncio"""
+
+
+class Mode(str, enum.Enum):
+    AUTO = "auto"
+    STRICT = "strict"
+
+
+ASYNCIO_MODE_HELP = """\
+'auto' - for automatically handling all async functions by the plugin
+'strict' - for autoprocessing disabling (useful if different async frameworks \
+should be tested together, e.g. \
+both pytest-asyncio and pytest-trio are used in the same project)
+"""
+
+
+def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None:
+    group = parser.getgroup("asyncio")
+    group.addoption(
+        "--asyncio-mode",
+        dest="asyncio_mode",
+        default=None,
+        metavar="MODE",
+        help=ASYNCIO_MODE_HELP,
+    )
+    parser.addini(
+        "asyncio_mode",
+        help="default value for --asyncio-mode",
+        default="strict",
+    )
+    parser.addini(
+        "asyncio_default_fixture_loop_scope",
+        type="string",
+        help="default scope of the asyncio event loop used to execute async fixtures",
+        default=None,
+    )
+    parser.addini(
+        "asyncio_default_test_loop_scope",
+        type="string",
+        help="default scope of the asyncio event loop used to execute tests",
+        default="function",
+    )
+
+
+@overload
+def fixture(
+    fixture_function: FixtureFunction[_P, _R],
+    *,
+    scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
+    loop_scope: _ScopeName | None = ...,
+    params: Iterable[object] | None = ...,
+    autouse: bool = ...,
+    ids: (
+        Iterable[str | float | int | bool | None]
+        | Callable[[Any], object | None]
+        | None
+    ) = ...,
+    name: str | None = ...,
+) -> FixtureFunction[_P, _R]: ...
+
+
+@overload
+def fixture(
+    fixture_function: None = ...,
+    *,
+    scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
+    loop_scope: _ScopeName | None = ...,
+    params: Iterable[object] | None = ...,
+    autouse: bool = ...,
+    ids: (
+        Iterable[str | float | int | bool | None]
+        | Callable[[Any], object | None]
+        | None
+    ) = ...,
+    name: str | None = None,
+) -> Callable[[FixtureFunction[_P, _R]], FixtureFunction[_P, _R]]: ...
+
+
+def fixture(
+    fixture_function: FixtureFunction[_P, _R] | None = None,
+    loop_scope: _ScopeName | None = None,
+    **kwargs: Any,
+) -> (
+    FixtureFunction[_P, _R]
+    | Callable[[FixtureFunction[_P, _R]], FixtureFunction[_P, _R]]
+):
+    if fixture_function is not None:
+        _make_asyncio_fixture_function(fixture_function, loop_scope)
+        return pytest.fixture(fixture_function, **kwargs)
+
+    else:
+
+        @functools.wraps(fixture)
+        def inner(fixture_function: FixtureFunction[_P, _R]) -> FixtureFunction[_P, _R]:
+            return fixture(fixture_function, loop_scope=loop_scope, **kwargs)
+
+        return inner
+
+
+def _is_asyncio_fixture_function(obj: Any) -> bool:
+    obj = getattr(obj, "__func__", obj)  # instance method maybe?
+    return getattr(obj, "_force_asyncio_fixture", False)
+
+
+def _make_asyncio_fixture_function(obj: Any, loop_scope: _ScopeName | None) -> None:
+    if hasattr(obj, "__func__"):
+        # instance method, check the function object
+        obj = obj.__func__
+    obj._force_asyncio_fixture = True
+    obj._loop_scope = loop_scope
+
+
+def _is_coroutine_or_asyncgen(obj: Any) -> bool:
+    return inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj)
+
+
+def _get_asyncio_mode(config: Config) -> Mode:
+    val = config.getoption("asyncio_mode")
+    if val is None:
+        val = config.getini("asyncio_mode")
+    try:
+        return Mode(val)
+    except ValueError as e:
+        modes = ", ".join(m.value for m in Mode)
+        raise pytest.UsageError(
+            f"{val!r} is not a valid asyncio_mode. Valid modes: {modes}."
+        ) from e
+
+
+_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET = """\
+The configuration option "asyncio_default_fixture_loop_scope" is unset.
+The event loop scope for asynchronous fixtures will default to the fixture caching \
+scope. Future versions of pytest-asyncio will default the loop scope for asynchronous \
+fixtures to function scope. Set the default fixture loop scope explicitly in order to \
+avoid unexpected behavior in the future. Valid fixture loop scopes are: \
+"function", "class", "module", "package", "session"
+"""
+
+
+def pytest_configure(config: Config) -> None:
+    default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
+    if not default_loop_scope:
+        warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))
+    config.addinivalue_line(
+        "markers",
+        "asyncio: "
+        "mark the test as a coroutine, it will be "
+        "run using an asyncio event loop",
+    )
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_report_header(config: Config) -> list[str]:
+    """Add asyncio config to pytest header."""
+    mode = _get_asyncio_mode(config)
+    default_fixture_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
+    default_test_loop_scope = _get_default_test_loop_scope(config)
+    header = [
+        f"mode={mode}",
+        f"asyncio_default_fixture_loop_scope={default_fixture_loop_scope}",
+        f"asyncio_default_test_loop_scope={default_test_loop_scope}",
+    ]
+    return [
+        "asyncio: " + ", ".join(header),
+    ]
+
+
+def _preprocess_async_fixtures(
+    collector: Collector,
+    processed_fixturedefs: set[FixtureDef],
+) -> None:
+    config = collector.config
+    default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
+    asyncio_mode = _get_asyncio_mode(config)
+    fixturemanager = config.pluginmanager.get_plugin("funcmanage")
+    assert fixturemanager is not None
+    for fixtures in fixturemanager._arg2fixturedefs.values():
+        for fixturedef in fixtures:
+            func = fixturedef.func
+            if fixturedef in processed_fixturedefs or not _is_coroutine_or_asyncgen(
+                func
+            ):
+                continue
+            if asyncio_mode == Mode.STRICT and not _is_asyncio_fixture_function(func):
+                # Ignore async fixtures without explicit asyncio mark in strict mode
+                # This applies to pytest_trio fixtures, for example
+                continue
+            loop_scope = (
+                getattr(func, "_loop_scope", None)
+                or default_loop_scope
+                or fixturedef.scope
+            )
+            _make_asyncio_fixture_function(func, loop_scope)
+            if "request" not in fixturedef.argnames:
+                fixturedef.argnames += ("request",)
+            _synchronize_async_fixture(fixturedef)
+            assert _is_asyncio_fixture_function(fixturedef.func)
+            processed_fixturedefs.add(fixturedef)
+
+
+def _synchronize_async_fixture(fixturedef: FixtureDef) -> None:
+    """Wraps the fixture function of an async fixture in a synchronous function."""
+    if inspect.isasyncgenfunction(fixturedef.func):
+        _wrap_asyncgen_fixture(fixturedef)
+    elif inspect.iscoroutinefunction(fixturedef.func):
+        _wrap_async_fixture(fixturedef)
+
+
+def _add_kwargs(
+    func: Callable[..., Any],
+    kwargs: dict[str, Any],
+    request: FixtureRequest,
+) -> dict[str, Any]:
+    sig = inspect.signature(func)
+    ret = kwargs.copy()
+    if "request" in sig.parameters:
+        ret["request"] = request
+    return ret
+
+
+def _perhaps_rebind_fixture_func(func: _T, instance: Any | None) -> _T:
+    if instance is not None:
+        # The fixture needs to be bound to the actual request.instance
+        # so it is bound to the same object as the test method.
+        unbound, cls = func, None
+        try:
+            unbound, cls = func.__func__, type(func.__self__)  # type: ignore
+        except AttributeError:
+            pass
+        # Only if the fixture was bound before to an instance of
+        # the same type.
+        if cls is not None and isinstance(instance, cls):
+            func = unbound.__get__(instance)  # type: ignore
+    return func
+
+
+def _wrap_asyncgen_fixture(fixturedef: FixtureDef) -> None:
+    fixture = fixturedef.func
+
+    @functools.wraps(fixture)
+    def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
+        func = _perhaps_rebind_fixture_func(fixture, request.instance)
+        event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
+            request, func
+        )
+        event_loop = request.getfixturevalue(event_loop_fixture_id)
+        kwargs.pop(event_loop_fixture_id, None)
+        gen_obj = func(**_add_kwargs(func, kwargs, request))
+
+        async def setup():
+            res = await gen_obj.__anext__()  # type: ignore[union-attr]
+            return res
+
+        context = contextvars.copy_context()
+        setup_task = _create_task_in_context(event_loop, setup(), context)
+        result = event_loop.run_until_complete(setup_task)
+
+        reset_contextvars = _apply_contextvar_changes(context)
+
+        def finalizer() -> None:
+            """Yield again, to finalize."""
+
+            async def async_finalizer() -> None:
+                try:
+                    await gen_obj.__anext__()  # type: ignore[union-attr]
+                except StopAsyncIteration:
+                    pass
+                else:
+                    msg = "Async generator fixture didn't stop."
+                    msg += "Yield only once."
+                    raise ValueError(msg)
+
+            task = _create_task_in_context(event_loop, async_finalizer(), context)
+            event_loop.run_until_complete(task)
+            if reset_contextvars is not None:
+                reset_contextvars()
+
+        request.addfinalizer(finalizer)
+        return result
+
+    fixturedef.func = _asyncgen_fixture_wrapper  # type: ignore[misc]
+
+
+def _wrap_async_fixture(fixturedef: FixtureDef) -> None:
+    fixture = fixturedef.func
+
+    @functools.wraps(fixture)
+    def _async_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
+        func = _perhaps_rebind_fixture_func(fixture, request.instance)
+        event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
+            request, func
+        )
+        event_loop = request.getfixturevalue(event_loop_fixture_id)
+        kwargs.pop(event_loop_fixture_id, None)
+
+        async def setup():
+            res = await func(**_add_kwargs(func, kwargs, request))
+            return res
+
+        context = contextvars.copy_context()
+        setup_task = _create_task_in_context(event_loop, setup(), context)
+        result = event_loop.run_until_complete(setup_task)
+
+        # Copy the context vars modified by the setup task into the current
+        # context, and (if needed) add a finalizer to reset them.
+        #
+        # Note that this is slightly different from the behavior of a non-async
+        # fixture, which would rely on the fixture author to add a finalizer
+        # to reset the variables. In this case, the author of the fixture can't
+        # write such a finalizer because they have no way to capture the Context
+        # in which the setup function was run, so we need to do it for them.
+        reset_contextvars = _apply_contextvar_changes(context)
+        if reset_contextvars is not None:
+            request.addfinalizer(reset_contextvars)
+
+        return result
+
+    fixturedef.func = _async_fixture_wrapper  # type: ignore[misc]
+
+
+def _get_event_loop_fixture_id_for_async_fixture(
+    request: FixtureRequest, func: Any
+) -> str:
+    default_loop_scope = cast(
+        _ScopeName, request.config.getini("asyncio_default_fixture_loop_scope")
+    )
+    loop_scope = (
+        getattr(func, "_loop_scope", None) or default_loop_scope or request.scope
+    )
+    return f"_{loop_scope}_event_loop"
+
+
+def _create_task_in_context(
+    loop: asyncio.AbstractEventLoop,
+    coro: AbstractCoroutine[Any, Any, _T],
+    context: contextvars.Context,
+) -> asyncio.Task[_T]:
+    """
+    Return an asyncio task that runs the coro in the specified context,
+    if possible.
+
+    This allows fixture setup and teardown to be run as separate asyncio tasks,
+    while still being able to use context-manager idioms to maintain context
+    variables and make those variables visible to test functions.
+
+    This is only fully supported on Python 3.11 and newer, as it requires
+    the API added for https://github.com/python/cpython/issues/91150.
+    On earlier versions, the returned task will use the default context instead.
+    """
+    try:
+        return loop.create_task(coro, context=context)
+    except TypeError:
+        return loop.create_task(coro)
+
+
+def _apply_contextvar_changes(
+    context: contextvars.Context,
+) -> Callable[[], None] | None:
+    """
+    Copy contextvar changes from the given context to the current context.
+
+    If any contextvars were modified by the fixture, return a finalizer that
+    will restore them.
+    """
+    context_tokens = []
+    for var in context:
+        try:
+            if var.get() is context.get(var):
+                # This variable is not modified, so leave it as-is.
+                continue
+        except LookupError:
+            # This variable isn't yet set in the current context at all.
+            pass
+        token = var.set(context.get(var))
+        context_tokens.append((var, token))
+
+    if not context_tokens:
+        return None
+
+    def restore_contextvars():
+        while context_tokens:
+            (var, token) = context_tokens.pop()
+            var.reset(token)
+
+    return restore_contextvars
+
+
+class PytestAsyncioFunction(Function):
+    """Base class for all test functions managed by pytest-asyncio."""
+
+    @classmethod
+    def item_subclass_for(cls, item: Function, /) -> type[PytestAsyncioFunction] | None:
+        """
+        Returns a subclass of PytestAsyncioFunction if there is a specialized subclass
+        for the specified function item.
+
+        Return None if no specialized subclass exists for the specified item.
+        """
+        for subclass in cls.__subclasses__():
+            if subclass._can_substitute(item):
+                return subclass
+        return None
+
+    @classmethod
+    def _from_function(cls, function: Function, /) -> Function:
+        """
+        Instantiates this specific PytestAsyncioFunction type from the specified
+        Function item.
+        """
+        assert function.get_closest_marker("asyncio")
+        subclass_instance = cls.from_parent(
+            function.parent,
+            name=function.name,
+            callspec=getattr(function, "callspec", None),
+            callobj=function.obj,
+            fixtureinfo=function._fixtureinfo,
+            keywords=function.keywords,
+            originalname=function.originalname,
+        )
+        subclass_instance.own_markers = function.own_markers
+        assert subclass_instance.own_markers == function.own_markers
+        return subclass_instance
+
+    @staticmethod
+    def _can_substitute(item: Function) -> bool:
+        """Returns whether the specified function can be replaced by this class"""
+        raise NotImplementedError()
+
+
+class Coroutine(PytestAsyncioFunction):
+    """Pytest item created by a coroutine"""
+
+    @staticmethod
+    def _can_substitute(item: Function) -> bool:
+        func = item.obj
+        return inspect.iscoroutinefunction(func)
+
+    def runtest(self) -> None:
+        self.obj = wrap_in_sync(
+            # https://github.com/pytest-dev/pytest-asyncio/issues/596
+            self.obj,  # type: ignore[has-type]
+        )
+        super().runtest()
+
+
+class AsyncGenerator(PytestAsyncioFunction):
+    """Pytest item created by an asynchronous generator"""
+
+    @staticmethod
+    def _can_substitute(item: Function) -> bool:
+        func = item.obj
+        return inspect.isasyncgenfunction(func)
+
+    @classmethod
+    def _from_function(cls, function: Function, /) -> Function:
+        async_gen_item = super()._from_function(function)
+        unsupported_item_type_message = (
+            f"Tests based on asynchronous generators are not supported. "
+            f"{function.name} will be ignored."
+        )
+        async_gen_item.warn(PytestCollectionWarning(unsupported_item_type_message))
+        async_gen_item.add_marker(
+            pytest.mark.xfail(run=False, reason=unsupported_item_type_message)
+        )
+        return async_gen_item
+
+
+class AsyncStaticMethod(PytestAsyncioFunction):
+    """
+    Pytest item that is a coroutine or an asynchronous generator
+    decorated with staticmethod
+    """
+
+    @staticmethod
+    def _can_substitute(item: Function) -> bool:
+        func = item.obj
+        return isinstance(func, staticmethod) and _is_coroutine_or_asyncgen(
+            func.__func__
+        )
+
+    def runtest(self) -> None:
+        self.obj = wrap_in_sync(
+            # https://github.com/pytest-dev/pytest-asyncio/issues/596
+            self.obj,  # type: ignore[has-type]
+        )
+        super().runtest()
+
+
+class AsyncHypothesisTest(PytestAsyncioFunction):
+    """
+    Pytest item that is coroutine or an asynchronous generator decorated by
+    @hypothesis.given.
+    """
+
+    @staticmethod
+    def _can_substitute(item: Function) -> bool:
+        func = item.obj
+        return (
+            getattr(func, "is_hypothesis_test", False)  # type: ignore[return-value]
+            and getattr(func, "hypothesis", None)
+            and inspect.iscoroutinefunction(func.hypothesis.inner_test)
+        )
+
+    def runtest(self) -> None:
+        self.obj.hypothesis.inner_test = wrap_in_sync(
+            self.obj.hypothesis.inner_test,
+        )
+        super().runtest()
+
+
+_HOLDER: set[FixtureDef] = set()
+
+
+# The function name needs to start with "pytest_"
+# see https://github.com/pytest-dev/pytest/issues/11307
+@pytest.hookimpl(specname="pytest_pycollect_makeitem", tryfirst=True)
+def pytest_pycollect_makeitem_preprocess_async_fixtures(
+    collector: pytest.Module | pytest.Class, name: str, obj: object
+) -> pytest.Item | pytest.Collector | list[pytest.Item | pytest.Collector] | None:
+    """A pytest hook to collect asyncio coroutines."""
+    if not collector.funcnamefilter(name):
+        return None
+    _preprocess_async_fixtures(collector, _HOLDER)
+    return None
+
+
+# The function name needs to start with "pytest_"
+# see https://github.com/pytest-dev/pytest/issues/11307
+@pytest.hookimpl(specname="pytest_pycollect_makeitem", hookwrapper=True)
+def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
+    collector: pytest.Module | pytest.Class, name: str, obj: object
+) -> Generator[None, pluggy.Result, None]:
+    """
+    Converts coroutines and async generators collected as pytest.Functions
+    to AsyncFunction items.
+    """
+    hook_result = yield
+    try:
+        node_or_list_of_nodes: (
+            pytest.Item | pytest.Collector | list[pytest.Item | pytest.Collector] | None
+        ) = hook_result.get_result()
+    except BaseException as e:
+        hook_result.force_exception(e)
+        return
+    if not node_or_list_of_nodes:
+        return
+    if isinstance(node_or_list_of_nodes, Sequence):
+        node_iterator = iter(node_or_list_of_nodes)
+    else:
+        # Treat single node as a single-element iterable
+        node_iterator = iter((node_or_list_of_nodes,))
+    updated_node_collection = []
+    for node in node_iterator:
+        updated_item = node
+        if isinstance(node, Function):
+            specialized_item_class = PytestAsyncioFunction.item_subclass_for(node)
+            if specialized_item_class:
+                if _get_asyncio_mode(
+                    node.config
+                ) == Mode.AUTO and not node.get_closest_marker("asyncio"):
+                    node.add_marker("asyncio")
+                if node.get_closest_marker("asyncio"):
+                    updated_item = specialized_item_class._from_function(node)
+        updated_node_collection.append(updated_item)
+    hook_result.force_result(updated_node_collection)
+
+
+@contextlib.contextmanager
+def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]:
+    old_loop_policy = _get_event_loop_policy()
+    try:
+        old_loop = _get_event_loop_no_warn()
+    except RuntimeError:
+        old_loop = None
+    _set_event_loop_policy(policy)
+    try:
+        yield
+    finally:
+        _set_event_loop_policy(old_loop_policy)
+        _set_event_loop(old_loop)
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_generate_tests(metafunc: Metafunc) -> None:
+    marker = metafunc.definition.get_closest_marker("asyncio")
+    if not marker:
+        return
+    default_loop_scope = _get_default_test_loop_scope(metafunc.config)
+    loop_scope = _get_marked_loop_scope(marker, default_loop_scope)
+    event_loop_fixture_id = f"_{loop_scope}_event_loop"
+    # This specific fixture name may already be in metafunc.argnames, if this
+    # test indirectly depends on the fixture. For example, this is the case
+    # when the test depends on an async fixture, both of which share the same
+    # event loop fixture mark.
+    if event_loop_fixture_id in metafunc.fixturenames:
+        return
+    fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage")
+    assert fixturemanager is not None
+    # Add the scoped event loop fixture to Metafunc's list of fixture names and
+    # fixturedefs and leave the actual parametrization to pytest
+    # The fixture needs to be appended to avoid messing up the fixture evaluation
+    # order
+    metafunc.fixturenames.append(event_loop_fixture_id)
+    metafunc._arg2fixturedefs[event_loop_fixture_id] = fixturemanager._arg2fixturedefs[
+        event_loop_fixture_id
+    ]
+
+
+def _get_event_loop_policy() -> AbstractEventLoopPolicy:
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", DeprecationWarning)
+        return asyncio.get_event_loop_policy()
+
+
+def _set_event_loop_policy(policy: AbstractEventLoopPolicy) -> None:
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", DeprecationWarning)
+        asyncio.set_event_loop_policy(policy)
+
+
+def _get_event_loop_no_warn(
+    policy: AbstractEventLoopPolicy | None = None,
+) -> asyncio.AbstractEventLoop:
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", DeprecationWarning)
+        if policy is not None:
+            return policy.get_event_loop()
+        else:
+            return asyncio.get_event_loop()
+
+
+def _set_event_loop(loop: AbstractEventLoop | None) -> None:
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", DeprecationWarning)
+        asyncio.set_event_loop(loop)
+
+
+@pytest.hookimpl(tryfirst=True, hookwrapper=True)
+def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
+    """
+    Pytest hook called before a test case is run.
+
+    Wraps marked tests in a synchronous function
+    where the wrapped test coroutine is executed in an event loop.
+    """
+    if pyfuncitem.get_closest_marker("asyncio") is not None:
+        if isinstance(pyfuncitem, PytestAsyncioFunction):
+            asyncio_mode = _get_asyncio_mode(pyfuncitem.config)
+            for fixname, fixtures in pyfuncitem._fixtureinfo.name2fixturedefs.items():
+                # name2fixturedefs is a dict between fixture name and a list of matching
+                # fixturedefs. The last entry in the list is closest and the one used.
+                func = fixtures[-1].func
+                if (
+                    asyncio_mode == Mode.STRICT
+                    and _is_coroutine_or_asyncgen(func)
+                    and not _is_asyncio_fixture_function(func)
+                ):
+                    warnings.warn(
+                        PytestDeprecationWarning(
+                            f"asyncio test {pyfuncitem.name!r} requested async "
+                            "@pytest.fixture "
+                            f"{fixname!r} in strict mode. "
+                            "You might want to use @pytest_asyncio.fixture or switch "
+                            "to auto mode. "
+                            "This will become an error in future versions of "
+                            "flake8-asyncio."
+                        ),
+                        stacklevel=1,
+                    )
+                    # no stacklevel points at the users code, so we set stacklevel=1
+                    # so it at least indicates that it's the plugin complaining.
+                    # Pytest gives the test file & name in the warnings summary at least
+
+        else:
+            pyfuncitem.warn(
+                pytest.PytestWarning(
+                    f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' "
+                    "but it is not an async function. "
+                    "Please remove the asyncio mark. "
+                    "If the test is not marked explicitly, "
+                    "check for global marks applied via 'pytestmark'."
+                )
+            )
+    yield
+    return None
+
+
+def wrap_in_sync(
+    func: Callable[..., Awaitable[Any]],
+):
+    """
+    Return a sync wrapper around an async function executing it in the
+    current event loop.
+    """
+    # if the function is already wrapped, we rewrap using the original one
+    # not using __wrapped__ because the original function may already be
+    # a wrapped one
+    raw_func = getattr(func, "_raw_test_func", None)
+    if raw_func is not None:
+        func = raw_func
+
+    @functools.wraps(func)
+    def inner(*args, **kwargs):
+        coro = func(*args, **kwargs)
+        _loop = _get_event_loop_no_warn()
+        task = asyncio.ensure_future(coro, loop=_loop)
+        try:
+            _loop.run_until_complete(task)
+        except BaseException:
+            # run_until_complete doesn't get the result from exceptions
+            # that are not subclasses of `Exception`. Consume all
+            # exceptions to prevent asyncio's warning from logging.
+            if task.done() and not task.cancelled():
+                task.exception()
+            raise
+
+    inner._raw_test_func = func  # type: ignore[attr-defined]
+    return inner
+
+
+def pytest_runtest_setup(item: pytest.Item) -> None:
+    marker = item.get_closest_marker("asyncio")
+    if marker is None:
+        return
+    default_loop_scope = _get_default_test_loop_scope(item.config)
+    loop_scope = _get_marked_loop_scope(marker, default_loop_scope)
+    event_loop_fixture_id = f"_{loop_scope}_event_loop"
+    fixturenames = item.fixturenames  # type: ignore[attr-defined]
+    if event_loop_fixture_id not in fixturenames:
+        fixturenames.append(event_loop_fixture_id)
+    obj = getattr(item, "obj", None)
+    if not getattr(obj, "hypothesis", False) and getattr(
+        obj, "is_hypothesis_test", False
+    ):
+        pytest.fail(
+            f"test function `{item!r}` is using Hypothesis, but pytest-asyncio "
+            "only works with Hypothesis 3.64.0 or later."
+        )
+
+
+_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR = """\
+An asyncio pytest marker defines both "scope" and "loop_scope", \
+but it should only use "loop_scope".
+"""
+
+_MARKER_SCOPE_KWARG_DEPRECATION_WARNING = """\
+The "scope" keyword argument to the asyncio marker has been deprecated. \
+Please use the "loop_scope" argument instead.
+"""
+
+
+def _get_marked_loop_scope(
+    asyncio_marker: Mark, default_loop_scope: _ScopeName
+) -> _ScopeName:
+    assert asyncio_marker.name == "asyncio"
+    if asyncio_marker.args or (
+        asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope"}
+    ):
+        raise ValueError("mark.asyncio accepts only a keyword argument 'loop_scope'.")
+    if "scope" in asyncio_marker.kwargs:
+        if "loop_scope" in asyncio_marker.kwargs:
+            raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR)
+        warnings.warn(PytestDeprecationWarning(_MARKER_SCOPE_KWARG_DEPRECATION_WARNING))
+    scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get(
+        "scope"
+    )
+    if scope is None:
+        scope = default_loop_scope
+    assert scope in {"function", "class", "module", "package", "session"}
+    return scope
+
+
+def _get_default_test_loop_scope(config: Config) -> _ScopeName:
+    return config.getini("asyncio_default_test_loop_scope")
+
+
+def _create_scoped_event_loop_fixture(scope: _ScopeName) -> Callable:
+    @pytest.fixture(
+        scope=scope,
+        name=f"_{scope}_event_loop",
+    )
+    def _scoped_event_loop(
+        *args,  # Function needs to accept "cls" when collected by pytest.Class
+        event_loop_policy,
+    ) -> Iterator[asyncio.AbstractEventLoop]:
+        new_loop_policy = event_loop_policy
+        with (
+            _temporary_event_loop_policy(new_loop_policy),
+            _provide_event_loop() as loop,
+        ):
+            _set_event_loop(loop)
+            yield loop
+
+    return _scoped_event_loop
+
+
+for scope in Scope:
+    globals()[f"_{scope.value}_event_loop"] = _create_scoped_event_loop_fixture(
+        scope.value
+    )
+
+
+@contextlib.contextmanager
+def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]:
+    policy = _get_event_loop_policy()
+    loop = policy.new_event_loop()
+    try:
+        yield loop
+    finally:
+        # cleanup the event loop if it hasn't been cleaned up already
+        if not loop.is_closed():
+            try:
+                loop.run_until_complete(loop.shutdown_asyncgens())
+            except Exception as e:
+                warnings.warn(f"Error cleaning up asyncio loop: {e}", RuntimeWarning)
+            finally:
+                loop.close()
+
+
+@pytest.fixture(scope="session", autouse=True)
+def event_loop_policy() -> AbstractEventLoopPolicy:
+    """Return an instance of the policy used to create asyncio event loops."""
+    return _get_event_loop_policy()
+
+
+def is_async_test(item: Item) -> bool:
+    """Returns whether a test item is a pytest-asyncio test"""
+    return isinstance(item, PytestAsyncioFunction)
+
+
+def _unused_port(socket_type: int) -> int:
+    """Find an unused localhost port from 1024-65535 and return it."""
+    with contextlib.closing(socket.socket(type=socket_type)) as sock:
+        sock.bind(("127.0.0.1", 0))
+        return sock.getsockname()[1]
+
+
+@pytest.fixture
+def unused_tcp_port() -> int:
+    return _unused_port(socket.SOCK_STREAM)
+
+
+@pytest.fixture
+def unused_udp_port() -> int:
+    return _unused_port(socket.SOCK_DGRAM)
+
+
+@pytest.fixture(scope="session")
+def unused_tcp_port_factory() -> Callable[[], int]:
+    """A factory function, producing different unused TCP ports."""
+    produced = set()
+
+    def factory():
+        """Return an unused port."""
+        port = _unused_port(socket.SOCK_STREAM)
+
+        while port in produced:
+            port = _unused_port(socket.SOCK_STREAM)
+
+        produced.add(port)
+
+        return port
+
+    return factory
+
+
+@pytest.fixture(scope="session")
+def unused_udp_port_factory() -> Callable[[], int]:
+    """A factory function, producing different unused UDP ports."""
+    produced = set()
+
+    def factory():
+        """Return an unused port."""
+        port = _unused_port(socket.SOCK_DGRAM)
+
+        while port in produced:
+            port = _unused_port(socket.SOCK_DGRAM)
+
+        produced.add(port)
+
+        return port
+
+    return factory
diff --git a/.venv/Lib/site-packages/pytest_asyncio/py.typed b/.venv/Lib/site-packages/pytest_asyncio/py.typed
new file mode 100644 (file)
index 0000000..e69de29
index bd4f688176c062b75342c122673873cb8f21ad9f..ca23d351b2c2832a07c53a2b4e05a85cce1be0d8 100644 (file)
@@ -7,7 +7,7 @@ app = Flask(__name__)
 @app.route("/")
 def home():
     time.sleep(3)                # simulate slow work
-    html = "Hello from Flask :)"
+    html = "<h1>Slow Flask Demo</h1>" # TDD Phase 2 content
     return Response(html, mimetype="text/html")
 
 if __name__ == "__main__":
@@ -18,4 +18,4 @@ if __name__ == "__main__":
     # Open the URL in a new browser tab
     webbrowser.open_new_tab(url)
     
-    app.run(host=host, port=port
\ No newline at end of file
+    app.run(host="0.0.0.0", port=3000
\ No newline at end of file
diff --git a/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc
new file mode 100644 (file)
index 0000000..bb35783
Binary files /dev/null and b/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc differ
index c1722ee5ccde3ef84ffba700013ff44010e3b294..25cc9664466b4d68d8f47d78e8bd164467b7b4ee 100644 (file)
Binary files a/tests/__pycache__/test_flask_route.cpython-312-pytest-8.3.5.pyc and b/tests/__pycache__/test_flask_route.cpython-312-pytest-8.3.5.pyc differ
diff --git a/tests/test_fastapi_route.py b/tests/test_fastapi_route.py
new file mode 100644 (file)
index 0000000..f873426
--- /dev/null
@@ -0,0 +1,41 @@
+# tests/test_fastapi_route.py
+import httpx
+import subprocess, asyncio, os, signal, time # signal and time may not be needed if Popen is handled well
+import pytest # For @pytest.mark.asyncio
+
+# Adjusted to be an async function and use uvicorn
+async def start_server_fastapi():
+    # Ensure CWD is project root
+    project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+    # Command to run FastAPI app with uvicorn
+    # Assuming app_fastapi/app.py and app instance is named 'app'
+    cmd = ["uvicorn", "app_fastapi.app:app", "--host", "0.0.0.0", "--port", "8000"]
+    
+    proc = subprocess.Popen(
+        cmd,
+        cwd=project_root,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE
+    )
+    await asyncio.sleep(1.5) # Allow uvicorn to start
+    return proc
+
+@pytest.mark.asyncio
+async def test_home_returns_html_fastapi():
+    proc = await start_server_fastapi()
+    try:
+        async with httpx.AsyncClient() as client:
+            r = await client.get("http://127.0.0.1:8000/") # Default FastAPI port
+            assert r.status_code == 200
+            assert "<h1>Slow FastAPI Demo</h1>" in r.text # Expected content
+    finally:
+        proc.terminate()
+        try:
+            # Use communicate to get output and ensure process is reaped
+            stdout, stderr = proc.communicate(timeout=5)
+            # print(f"FastAPI stdout:\n{stdout.decode(errors='replace')}") # Optional: for debugging
+            # print(f"FastAPI stderr:\n{stderr.decode(errors='replace')}") # Optional: for debugging
+        except subprocess.TimeoutExpired:
+            print("FastAPI server did not terminate/communicate gracefully, killing.")
+            proc.kill()
+            proc.wait() # Ensure kill completes 
\ No newline at end of file
index 0ab89a3f5444c408fc0bdcfc1d91e5408afe36bf..d9a7e617567ce6eed8e13a4ec5fa833b12f878c9 100644 (file)
@@ -2,25 +2,48 @@ import httpx
 import subprocess, time, os, signal
 
 def start_server():
-    # Ensure the Python interpreter can find the app_flask module.
-    # This might require adjusting PYTHONPATH or running pytest from the project root.
-    proc = subprocess.Popen(["python", "-m", "app_flask.flask_application"]) # Updated module name
-    time.sleep(1)  # Increased sleep to allow server startup, especially on slower systems
+    current_env = os.environ.copy()
+    project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+    script_path = os.path.join(project_root, "app_flask", "flask_application.py")
+    proc = subprocess.Popen(
+        ["python", script_path],
+        env=current_env,
+        cwd=project_root,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE
+    )
+    time.sleep(1.5)
     return proc
 
 def test_home_returns_html():
     proc = start_server()
+    server_stdout_data = b""
+    server_stderr_data = b""
     try:
-        r = httpx.get("http://127.0.0.1:3000/", timeout=10)
+        headers = {
+            "Cache-Control": "no-cache, no-store, must-revalidate",
+            "Pragma": "no-cache",
+            "Expires": "0"
+        }
+        r = httpx.get("http://127.0.0.1:3000/", timeout=10, headers=headers)
         assert r.status_code == 200
-        assert "Hello from Flask :)" in r.text  # Corrected assertion
+        assert "<h1>Slow Flask Demo</h1>" in r.text
     finally:
-        # It's important to ensure the server process is terminated.
-        # os.kill might not be cross-platform for SIGINT.
-        # proc.terminate() followed by proc.wait() is generally safer.
         proc.terminate()
         try:
-            proc.wait(timeout=5) # Wait for the process to terminate
+            stdout_bytes, stderr_bytes = proc.communicate(timeout=5)
+            server_stdout_data = stdout_bytes
+            server_stderr_data = stderr_bytes
         except subprocess.TimeoutExpired:
-            proc.kill() # Force kill if terminate doesn't work
-            proc.wait() # Wait for the kill to complete 
\ No newline at end of file
+            print("Server did not terminate/communicate gracefully, killing.")
+            proc.kill()
+            try:
+                stdout_bytes, stderr_bytes = proc.communicate(timeout=1)
+                server_stdout_data = stdout_bytes
+                server_stderr_data = stderr_bytes
+            except subprocess.TimeoutExpired:
+                print("Could not get output even after kill.")
+        
+        # Keep these for CI logs or if needed later, but they will be empty if flask app is quiet.
+        # print(f"Server stdout captured:\n{server_stdout_data.decode(errors='replace')}")
+        # print(f"Server stderr captured:\n{server_stderr_data.decode(errors='replace')}") 
\ No newline at end of file