diff --git a/.coveragerc b/.coveragerc index 659df7b6e9..73b1e9eda2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -36,7 +36,7 @@ ignore_errors = True omit = ./.venv/** - ./tests/* + noxfile.py [html] directory = coverage_html_report diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb89af7fab..8246e843b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,7 @@ jobs: - name: coverage xml run: coverage xml -i if: ${{ always() }} + - uses: codecov/codecov-action@v3 if: ${{ always() }} with: @@ -148,25 +149,29 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: wntrblm/nox@main + - run: pipx install poetry + - run: pipx install coverage + - uses: actions/setup-python@v4 + id: setup-python with: - python-versions: "3.7, 3.8, 3.9, 3.10, 3.11" + python-version: "3.11" + cache: "poetry" - - name: Pip and nox cache - id: cache - uses: actions/cache@v3 - with: - path: | - ~/.cache - ~/.nox - .nox - key: - ${{ runner.os }}-nox-windows-${{ hashFiles('**/poetry.lock') }}-${{ - hashFiles('**/noxfile.py') }} - restore-keys: | - ${{ runner.os }}-nox-windows - ${{ runner.os }}-nox- + - run: poetry install --with integrations + if: steps.setup-python.outputs.cache-hit != 'true' - - run: pipx install poetry - - run: pipx inject nox nox-poetry - - run: nox -r -s "Tests" -p 3.11 + # we use poetry directly instead of nox since we want to + # test all integrations at once on windows + - run: | + poetry run pytest --cov=. --cov-append --cov-report=xml -n auto --showlocals -vv + + - name: coverage xml + run: coverage xml -i + if: ${{ always() }} + + - uses: codecov/codecov-action@v3 + if: ${{ always() }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + verbose: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e235cfefd..e7d78805fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,7 +117,7 @@ Now, you will need to install the required dependencies for Strawberry and be su that the current tests are passing on your machine: ```shell -$ poetry install +$ poetry install --with integrations $ poetry run pytest tests -n auto $ poetry run mypy ``` diff --git a/federation-compatibility/Dockerfile b/federation-compatibility/Dockerfile index 2c22b87f4e..66d4e04ce8 100644 --- a/federation-compatibility/Dockerfile +++ b/federation-compatibility/Dockerfile @@ -9,7 +9,7 @@ COPY pyproject.toml ./ COPY poetry.lock ./ COPY README.md ./ -RUN poetry install +RUN poetry install --with integrations COPY federation-compatibility/schema.py ./ diff --git a/noxfile.py b/noxfile.py index 6630690efe..59e8d46404 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,32 +1,55 @@ import nox from nox_poetry import Session, session +nox.options.reuse_existing_virtualenvs = True +nox.options.error_on_external_run = True + PYTHON_VERSIONS = ["3.11", "3.10", "3.9", "3.8", "3.7"] +COMMON_PYTEST_OPTIONS = [ + "--cov=.", + "--cov-append", + "--cov-report=xml", + "-n", + "auto", + "--showlocals", + "-vv", + "--ignore=tests/mypy", + "--ignore=tests/pyright", + "--ignore=tests/cli", + # TODO: reintroduce this in its own test session + "--ignore=tests/experimental/pydantic", +] + +INTEGRATIONS = [ + "asgi", + "aiohttp", + "chalice", + "channels", + "django", + "fastapi", + "flask", + "sanic", + "starlite", + "pydantic", +] + + @session(python=PYTHON_VERSIONS, name="Tests", tags=["tests"]) def tests(session: Session) -> None: session.run_always("poetry", "install", external=True) + markers = ( + ["-m", f"not {integration}", f"--ignore=tests/{integration}"] + for integration in INTEGRATIONS + ) + markers = [item for sublist in markers for item in sublist] + session.run( "pytest", - "--cov=strawberry", - "--cov-append", - "--cov-report=xml", - "-n", - "auto", - "--showlocals", - "-vv", - "-m", - "not starlette", - "-m", - "not django", - "-m", - "not starlite", - "-m", - "not pydantic", - "--ignore=tests/mypy", - "--ignore=tests/pyright", + *COMMON_PYTEST_OPTIONS, + *markers, ) @@ -36,19 +59,9 @@ def tests_django(session: Session, django: str) -> None: session.run_always("poetry", "install", external=True) session._session.install(f"django~={django}") # type: ignore + session._session.install("pytest-django") # type: ignore - session.run( - "pytest", - "--cov=strawberry", - "--cov-append", - "--cov-report=xml", - "-n", - "auto", - "--showlocals", - "-vv", - "-m", - "django", - ) + session.run("pytest", *COMMON_PYTEST_OPTIONS, "-m", "django") @session(python=["3.11"], name="Starlette tests", tags=["tests"]) @@ -58,36 +71,38 @@ def tests_starlette(session: Session, starlette: str) -> None: session._session.install(f"starlette=={starlette}") # type: ignore - session.run( - "pytest", - "--cov=strawberry", - "--cov-append", - "--cov-report=xml", - "-n", - "auto", - "--showlocals", - "-vv", - "-m", - "starlette", - ) + session.run("pytest", *COMMON_PYTEST_OPTIONS, "-m", "asgi") -@session(python=["3.11"], name="Litestar tests", tags=["tests"]) -def tests_litestar(session: Session) -> None: +@session(python=["3.11"], name="Test integrations", tags=["tests"]) +@nox.parametrize( + "integration", + [ + "aiohttp", + "chalice", + "channels", + "fastapi", + "flask", + "sanic", + "starlite", + ], +) +def tests_integrations(session: Session, integration: str) -> None: session.run_always("poetry", "install", external=True) - session.run( - "pytest", - "--cov=strawberry", - "--cov-append", - "--cov-report=xml", - "-n", - "auto", - "--showlocals", - "-vv", - "-m", - "starlite", - ) + session._session.install(integration) # type: ignore + + if integration == "aiohttp": + session._session.install("pytest-aiohttp") # type: ignore + elif integration == "flask": + session._session.install("pytest-flask") # type: ignore + elif integration == "channels": + session._session.install("pytest-django") # type: ignore + session._session.install("daphne") # type: ignore + elif integration == "starlite": + session._session.install("pydantic<2.0") # type: ignore + + session.run("pytest", *COMMON_PYTEST_OPTIONS, "-m", integration) @session(python=["3.11"], name="Pydantic tests", tags=["tests"]) @@ -100,13 +115,9 @@ def test_pydantic(session: Session, pydantic: str) -> None: session.run( "pytest", - "--cov=strawberry", + "--cov=.", "--cov-append", "--cov-report=xml", - "-n", - "auto", - "--showlocals", - "-vv", "-m", "pydantic", ) @@ -114,11 +125,11 @@ def test_pydantic(session: Session, pydantic: str) -> None: @session(python=PYTHON_VERSIONS, name="Mypy tests") def tests_mypy(session: Session) -> None: - session.run_always("poetry", "install", external=True) + session.run_always("poetry", "install", "--with", "integrations", external=True) session.run( "pytest", - "--cov=strawberry", + "--cov=.", "--cov-append", "--cov-report=xml", "tests/mypy", @@ -133,7 +144,7 @@ def tests_pyright(session: Session) -> None: session.run( "pytest", - "--cov=strawberry", + "--cov=.", "--cov-append", "--cov-report=xml", "tests/pyright", @@ -143,6 +154,23 @@ def tests_pyright(session: Session) -> None: @session(name="Mypy", tags=["lint"]) def mypy(session: Session) -> None: - session.run_always("poetry", "install", external=True) + session.run_always("poetry", "install", "--with", "integrations", external=True) session.run("mypy", "--config-file", "mypy.ini") + + +@session(python=PYTHON_VERSIONS, name="CLI tests", tags=["tests"]) +def tests_cli(session: Session) -> None: + session.run_always("poetry", "install", external=True) + + session._session.install("uvicorn") # type: ignore + session._session.install("starlette") # type: ignore + + session.run( + "pytest", + "--cov=.", + "--cov-append", + "--cov-report=xml", + "tests/cli", + "-vv", + ) diff --git a/poetry.lock b/poetry.lock index c4480ae968..1bf22a0472 100644 --- a/poetry.lock +++ b/poetry.lock @@ -686,13 +686,13 @@ files = [ [[package]] name = "click" -version = "8.1.4" +version = "8.1.5" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.4-py3-none-any.whl", hash = "sha256:2739815aaa5d2c986a88f1e9230c55e17f0caad3d958a5e13ad0797c166db9e3"}, - {file = "click-8.1.4.tar.gz", hash = "sha256:b97d0c74955da062a7d4ef92fadb583806a585b2ea81958a81bd72726cbb8e37"}, + {file = "click-8.1.5-py3-none-any.whl", hash = "sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548"}, + {file = "click-8.1.5.tar.gz", hash = "sha256:4be4b1af8d665c6d942909916d31a213a106800c47d0eeba73d34da3cbc11367"}, ] [package.dependencies] @@ -1141,17 +1141,18 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "faker" -version = "18.13.0" +version = "19.1.0" description = "Faker is a Python package that generates fake data for you." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Faker-18.13.0-py3-none-any.whl", hash = "sha256:801d1a2d71f1fc54d332de2ab19de7452454309937233ea2f7485402882d67b3"}, - {file = "Faker-18.13.0.tar.gz", hash = "sha256:84bcf92bb725dd7341336eea4685df9a364f16f2470c4d29c1d7e6c5fd5a457d"}, + {file = "Faker-19.1.0-py3-none-any.whl", hash = "sha256:4b7d5cd0c898f0b64f88fbf0a35aac66762f2273446ba4a4e459985a2e5c8f8c"}, + {file = "Faker-19.1.0.tar.gz", hash = "sha256:d1eb772faf4a7c458c90b19d3626c40ae3460bd665ad7f5fb7b089e31d1a6dcf"}, ] [package.dependencies] python-dateutil = ">=2.4" +typing-extensions = {version = ">=3.10.0.1", markers = "python_version <= \"3.8\""} [[package]] name = "fast-query-parsers" @@ -1488,13 +1489,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.0.1" +version = "6.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.0.1-py3-none-any.whl", hash = "sha256:1543daade821c89b1c4a55986c326f36e54f2e6ca3bad96be4563d0acb74dcd4"}, - {file = "importlib_metadata-6.0.1.tar.gz", hash = "sha256:950127d57e35a806d520817d3e92eec3f19fdae9f0cd99da77a407c5aabefba3"}, + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, ] [package.dependencies] @@ -1504,7 +1505,7 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "incremental" @@ -1728,40 +1729,40 @@ files = [ [[package]] name = "msgspec" -version = "0.16.0" +version = "0.17.0" description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." optional = false python-versions = ">=3.8" files = [ - {file = "msgspec-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f06692e976bbb89d1c1eb95109679195a4ec172fbec73dee5027af1450f46b59"}, - {file = "msgspec-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:77b19814d7206542927c46e0c7807955739c181fef71f973e96c4e47a14c5893"}, - {file = "msgspec-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0cda74ffda2b2757eadf2259f8a68a5321f4fb8423bff26fa9e28eaaf8720d6"}, - {file = "msgspec-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddebe801459cd6f67e4279b3c679dc731729fabf64f42d7a4bd567ca3eb56377"}, - {file = "msgspec-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c0458cf8a44f630d372348d95b3b536a52412d4e61a53a3f3f31f070c95eb461"}, - {file = "msgspec-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ce21b56ecb462abb5291863c2e29dc58177da3c8f43f3d0edf69009daca05b66"}, - {file = "msgspec-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbd657cc2e2840f86a75c1fee265854835e2196d12502a64ce1390239cca58a9"}, - {file = "msgspec-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7cdfad50f388d1c1a933d9239913cb3bd993d4b631011df34d893fb3011971e0"}, - {file = "msgspec-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a4f641e10d4ef70a77184c002ec1512c0b83ddbb6c21314c85f9507c029b997"}, - {file = "msgspec-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9075d40d7739228b6158969239ad7708f483bbd4e8eb09c92c95c6062b470617"}, - {file = "msgspec-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f710d8b1992cf0690c9feeebd741d69c3627bace3f16e09e8556d65eb012fe"}, - {file = "msgspec-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1b6412c20bd687df6fb7c72a8a1bbc1a5da1be948bc01ce3d21e645263cddb6c"}, - {file = "msgspec-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e1a6708408cd5a44e39aa268086fe0992001e5881282c178a158af86727ddfa3"}, - {file = "msgspec-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:ccd842d25593fbff6505e77e0a3701c89bf3a1c260247e4e541e4e58dc81a6cc"}, - {file = "msgspec-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dbc137f037c2cb4ee731ef5066d3cb85a639b5d805df7f4c96aaefd914c7c5af"}, - {file = "msgspec-0.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:611c90eff0e2dd19b53e93bf8040450404f262aa05eee27089c8d29e92031db6"}, - {file = "msgspec-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e177b0f05f05321e415d42a30f854df47452c973e18957899410163da5c88c"}, - {file = "msgspec-0.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db3b2e32be73d155525e0886764963379eef8d6f7a16da6cd023516aed01ee"}, - {file = "msgspec-0.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa4bb83597ad8fce23b53ff16acd7931a55bf4ee2197c0282f077e5caacd5ee2"}, - {file = "msgspec-0.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:15c64bbefd34a5beb0da9eb22bff3ba0aab296f9828084998cd7716b5c1e2964"}, - {file = "msgspec-0.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:4111ab4373c185df543248d86eeb885c623319f82f4256164617beb8fbfa5071"}, - {file = "msgspec-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24809b66ef632f1ae91af7d281dd78eec2f516ad9963b3e9e61cb7b34495875d"}, - {file = "msgspec-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f14e3b1df80967aef772c9ac083df56ecf067f7cad7d291180f2733449e83a5"}, - {file = "msgspec-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78f3a914a356daf334f9dc7e72fb55025b39c65b6fcec507b18cdca7e65b97f6"}, - {file = "msgspec-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4830b073860e05d2cf1ef56d610035402f83b129a2742032ef2492d093385ef"}, - {file = "msgspec-0.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fea1bc172bd07a427ee538169b6447433dee018624f1e43ab7d046ccfbffb66f"}, - {file = "msgspec-0.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d80d2c1a8e3ee2b991d7fcf8d8b0904cb4fa68fe4d5abf8739453cffdde418c4"}, - {file = "msgspec-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:21095c687ae624a812a13a7e5d4ea6f3039c8768052ac0fb2818b8744779872a"}, - {file = "msgspec-0.16.0.tar.gz", hash = "sha256:0a3d5441cc8bda37957a1edb52c6f6ff4fcfebcaf20c771ad4cd4eade75c0f1a"}, + {file = "msgspec-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7bb4fa0021198cc87ece9fd4cbaa7241782f6c1d7b4fab71bb59c30e83c95c8"}, + {file = "msgspec-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab367dd0b8a8c989d295e8ef086305e41a3c73fa3208322da4826f04e24b3c5"}, + {file = "msgspec-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccef9b4216c28f470855540d2649cbcf9eefd74c17984259ed3cbbaaa818c5d"}, + {file = "msgspec-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb9f68ae2068a96b29d92cc4d142440e616b0a077b53a68b3445c62a2f31edc"}, + {file = "msgspec-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3adc3f4016aa5ea1bd51c8d5c2a38172f1965dd231e03defed04ff2a0effb361"}, + {file = "msgspec-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:98995159d95b4ed65c8cc185bd877fec67f41cb23925237a82efb066ef778fa8"}, + {file = "msgspec-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e7e016cd79dc12935b6f5e8ff358ea5908cb84756c84a9fa2f3d8e5405740f3"}, + {file = "msgspec-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1a0c8390014ee2b65d0d36d9cc28e74092166e8f44adf3dcb85e1ef25933787"}, + {file = "msgspec-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8096ca82e9a3e6fa09106bfac27eb8296c4cead02fdd297bb8af87254e58bad"}, + {file = "msgspec-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:025986a5b1fd81c3dd1bb591bbc01ac03844b57685fcf4a14ed07ae1967b6d03"}, + {file = "msgspec-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b0c52118aec347d15f75354b2fa27f1c9cd29b6ec12b74ef2ef988a0d702f5d"}, + {file = "msgspec-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:86ca5710174970d9aeefc2ced4fcc4a27f5ff065840356cf1c747399aa4ad5a8"}, + {file = "msgspec-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d39dc3a44a479f35afcc21d97c4ea90a385afee0b766c7780ca2bda99480e3c"}, + {file = "msgspec-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:79c3ab0bac7dd6492f6c03e347f0e9e5187d9658b8e7c85fdc96dd4820de970b"}, + {file = "msgspec-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:832e841d17bc2b4d74eee6aa16c818074cc9104c9a79466dbd49162600af8e9b"}, + {file = "msgspec-0.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:80021321fea9be9e03c7675f6bdce06d9c91dbfb4aec78044cd243491ed49fa9"}, + {file = "msgspec-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51aec8ae03348525749f4eff4ed59fc7369383877ad65e0f00de3e357c4ecfa6"}, + {file = "msgspec-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93fcff4e7923050cbfb3be8b4763a95862c3bf35d198684f3ebc3c486a1f66a5"}, + {file = "msgspec-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:396908a124f633c3e267ec01b5bffdbefd592b15ddfda63c1af798e0102c35c8"}, + {file = "msgspec-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:23581f8c1ffa488c2c3c78359868ee408528dad5e02af94e87e8d4773d118399"}, + {file = "msgspec-0.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:bc3f9fbb3d127a0c9734d43870b565a809c1fe2a0913d04170fa1222dfb131dc"}, + {file = "msgspec-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:556413e59a8017c12a177218b3d3c16bb7217dc0371853b61e4c98df19515088"}, + {file = "msgspec-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de90a979e48e95ab6175a59f3f256a88984ff599edd5d3948993dfd6ecff83d0"}, + {file = "msgspec-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce2a7da8669887b17bed92d3143aa0cc936fabdadcdd43c9ed09cfaca71a2fc5"}, + {file = "msgspec-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0aded9cd083d3a966610b6b01787106fa6aab495486420163a7b872704767da0"}, + {file = "msgspec-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b5e49b9a302acee0808466b1af31c675d7cf8fd5ee78874109b5a44cb40c67e8"}, + {file = "msgspec-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59ab1e250a84516413dee3542ff2b43f46fe521adfdc01e628198ce94553ff1e"}, + {file = "msgspec-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:5f1b7c9d9f41b8c7724dc206aadbbe84d1641d20a1ff6dab04f0caf2a5bfd438"}, + {file = "msgspec-0.17.0.tar.gz", hash = "sha256:4e2f2faeeb7aef5c2e4c78e375443134238ee966e54bba75cdb1936939673da7"}, ] [package.extras] @@ -1952,46 +1953,44 @@ tomlkit = ">=0.7" [[package]] name = "opentelemetry-api" -version = "1.18.0" +version = "1.19.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.7" files = [ - {file = "opentelemetry_api-1.18.0-py3-none-any.whl", hash = "sha256:d05bcc94ec239fd76fd90d784c5e3ad081a8a1ac2ffc8a2c83a49ace052d1492"}, - {file = "opentelemetry_api-1.18.0.tar.gz", hash = "sha256:2bbf29739fcef268c419e3bf1735566c2e7f81026c14bcc78b62a0b97f8ecf2f"}, + {file = "opentelemetry_api-1.19.0-py3-none-any.whl", hash = "sha256:dcd2a0ad34b691964947e1d50f9e8c415c32827a1d87f0459a72deb9afdf5597"}, + {file = "opentelemetry_api-1.19.0.tar.gz", hash = "sha256:db374fb5bea00f3c7aa290f5d94cea50b659e6ea9343384c5f6c2bb5d5e8db65"}, ] [package.dependencies] deprecated = ">=1.2.6" -importlib-metadata = ">=6.0.0,<6.1.0" -setuptools = ">=16.0" +importlib-metadata = ">=6.0,<7.0" [[package]] name = "opentelemetry-sdk" -version = "1.18.0" +version = "1.19.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.7" files = [ - {file = "opentelemetry_sdk-1.18.0-py3-none-any.whl", hash = "sha256:a097cc1e0db6ff33b4d250a9350dc17975d24a22aa667fca2866e60c51306723"}, - {file = "opentelemetry_sdk-1.18.0.tar.gz", hash = "sha256:cd3230930a2ab288b1df149d261e9cd2bd48dee54ad18465a777831cb6779e90"}, + {file = "opentelemetry_sdk-1.19.0-py3-none-any.whl", hash = "sha256:bb67ad676b1bc671766a40d7fc9d9563854c186fa11f0dc8fa2284e004bd4263"}, + {file = "opentelemetry_sdk-1.19.0.tar.gz", hash = "sha256:765928956262c7a7766eaba27127b543fb40ef710499cad075f261f52163a87f"}, ] [package.dependencies] -opentelemetry-api = "1.18.0" -opentelemetry-semantic-conventions = "0.39b0" -setuptools = ">=16.0" +opentelemetry-api = "1.19.0" +opentelemetry-semantic-conventions = "0.40b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.39b0" +version = "0.40b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.7" files = [ - {file = "opentelemetry_semantic_conventions-0.39b0-py3-none-any.whl", hash = "sha256:0dd7a9dc0dfde2335f643705bba8f7c44182c797bc208b7601f0b8e8211cfd5c"}, - {file = "opentelemetry_semantic_conventions-0.39b0.tar.gz", hash = "sha256:06a9f198574e0dab6ebc072b59d89092cf9f115638a8a02157586769b6b7a69a"}, + {file = "opentelemetry_semantic_conventions-0.40b0-py3-none-any.whl", hash = "sha256:7ebbaf86755a0948902e68637e3ae516c50222c30455e55af154ad3ffe283839"}, + {file = "opentelemetry_semantic_conventions-0.40b0.tar.gz", hash = "sha256:5a7a491873b15ab7c4907bbfd8737645cc87ca55a0a326c1755d1b928d8a0fae"}, ] [[package]] @@ -2368,13 +2367,13 @@ testing = ["coverage (==6.2)", "mypy (==0.931)"] [[package]] name = "pytest-asyncio" -version = "0.21.0" +version = "0.21.1" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"}, - {file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"}, + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] [package.dependencies] @@ -2758,27 +2757,6 @@ files = [ {file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"}, ] -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - [[package]] name = "rfc3986" version = "1.5.0" @@ -2872,13 +2850,13 @@ httpx = ">=0.18,<0.24" [[package]] name = "sentry-sdk" -version = "1.28.0" +version = "1.28.1" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.28.0.tar.gz", hash = "sha256:2281ba98011cfa9bc9bb15c1074b6dd9f7fdfce94033cd25b50f18f078ffed4c"}, - {file = "sentry_sdk-1.28.0-py2.py3-none-any.whl", hash = "sha256:b8b363aaa3f3d6a3acc1aa571efa4db29fb440339fed03560bb1276b8d2c2509"}, + {file = "sentry-sdk-1.28.1.tar.gz", hash = "sha256:dcd88c68aa64dae715311b5ede6502fd684f70d00a7cd4858118f0ba3153a3ae"}, + {file = "sentry_sdk-1.28.1-py2.py3-none-any.whl", hash = "sha256:6bdb25bd9092478d3a817cb0d01fa99e296aea34d404eac3ca0037faa5c2aa0a"}, ] [package.dependencies] @@ -3280,30 +3258,27 @@ files = [ ] [[package]] -name = "types-python-dateutil" -version = "2.8.19.13" -description = "Typing stubs for python-dateutil" +name = "types-protobuf" +version = "4.23.0.1" +description = "Typing stubs for protobuf" optional = false python-versions = "*" files = [ - {file = "types-python-dateutil-2.8.19.13.tar.gz", hash = "sha256:09a0275f95ee31ce68196710ed2c3d1b9dc42e0b61cc43acc369a42cb939134f"}, - {file = "types_python_dateutil-2.8.19.13-py3-none-any.whl", hash = "sha256:0b0e7c68e7043b0354b26a1e0225cb1baea7abb1b324d02b50e2d08f1221043f"}, + {file = "types-protobuf-4.23.0.1.tar.gz", hash = "sha256:7bd5ea122a057b11a82b785d9de464932a1e9175fe977a4128adef11d7f35547"}, + {file = "types_protobuf-4.23.0.1-py3-none-any.whl", hash = "sha256:c926104f69ea62103846681b35b690d8d100ecf86c6cdda16c850a1313a272e4"}, ] [[package]] -name = "types-requests" -version = "2.31.0.1" -description = "Typing stubs for requests" +name = "types-python-dateutil" +version = "2.8.19.13" +description = "Typing stubs for python-dateutil" optional = false python-versions = "*" files = [ - {file = "types-requests-2.31.0.1.tar.gz", hash = "sha256:3de667cffa123ce698591de0ad7db034a5317457a596eb0b4944e5a9d9e8d1ac"}, - {file = "types_requests-2.31.0.1-py3-none-any.whl", hash = "sha256:afb06ef8f25ba83d59a1d424bd7a5a939082f94b94e90ab5e6116bd2559deaa3"}, + {file = "types-python-dateutil-2.8.19.13.tar.gz", hash = "sha256:09a0275f95ee31ce68196710ed2c3d1b9dc42e0b61cc43acc369a42cb939134f"}, + {file = "types_python_dateutil-2.8.19.13-py3-none-any.whl", hash = "sha256:0b0e7c68e7043b0354b26a1e0225cb1baea7abb1b324d02b50e2d08f1221043f"}, ] -[package.dependencies] -types-urllib3 = "*" - [[package]] name = "types-toml" version = "0.10.8.6" @@ -3337,17 +3312,6 @@ files = [ {file = "types_ujson-5.8.0.0-py3-none-any.whl", hash = "sha256:481c27a7bc758fc94de330dcd885ba2fbf5879dd3dfd1c7b6b46f5b98d41ca85"}, ] -[[package]] -name = "types-urllib3" -version = "1.26.25.13" -description = "Typing stubs for urllib3" -optional = false -python-versions = "*" -files = [ - {file = "types-urllib3-1.26.25.13.tar.gz", hash = "sha256:3300538c9dc11dad32eae4827ac313f5d986b8b21494801f1bf97a1ac6c03ae5"}, - {file = "types_urllib3-1.26.25.13-py3-none-any.whl", hash = "sha256:5dbd1d2bef14efee43f5318b5d36d805a489f6600252bb53626d4bfafd95e27c"}, -] - [[package]] name = "typing-extensions" version = "4.7.1" @@ -3529,24 +3493,24 @@ test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "my [[package]] name = "virtualenv" -version = "20.21.1" +version = "20.23.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.21.1-py3-none-any.whl", hash = "sha256:09ddbe1af0c8ed2bb4d6ed226b9e6415718ad18aef9fa0ba023d96b7a8356049"}, - {file = "virtualenv-20.21.1.tar.gz", hash = "sha256:4c104ccde994f8b108163cf9ba58f3d11511d9403de87fb9b4f52bf33dbc8668"}, + {file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"}, + {file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"}, ] [package.dependencies] distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} -platformdirs = ">=2.4,<4" +filelock = ">=3.12,<4" +importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} +platformdirs = ">=3.5.1,<4" [package.extras] -docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"] [[package]] name = "wcwidth" @@ -3934,4 +3898,4 @@ starlite = ["starlite"] [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "a9939be0af9d05df0099f4d873518a6ecc4ec1e35b8f7da8ed236978aeb4199d" +content-hash = "a89864123184a6f7ee513054910c81c01fea2ee135942275bc0ee8cb1112d2b9" diff --git a/pyproject.toml b/pyproject.toml index c5018c0d53..f5a78922f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,63 +62,67 @@ rich = {version = ">=12.0.0", optional = true} pyinstrument = {version = ">=4.0.0", optional = true} [tool.poetry.group.dev.dependencies] -pytest-xdist = {extras = ["psutil"], version = "^3.1.0"} -pytest-cov = "^4.0.0" -pytest = "^7.2" -pytest-emoji = "^0.2.0" -black = ">=22,<24" -pytest-asyncio = ">=0.20.3,<0.22.0" -mypy = "1.3.0" -pytest-mypy-plugins = "^1.10" -pytest-mock = "^3.10" -pytest-django = {version = "^4.5"} asgiref = "^3.2" -pytest-flask = {version = "^1.2.0"} -flask = ">=1.1" -chalice = {version = "^1.22"} -requests = "^2.28.1" +black = ">=22,<24" +ddtrace = "^1.6.4" +email-validator = {version = "^1.1.3", optional = false} freezegun = "^1.2.1" +libcst = {version = "^0.4.7", optional = false} +MarkupSafe = "2.1.2" +nox = "^2023.4.22" +nox-poetry = "^1.0.2" opentelemetry-api = "<2" opentelemetry-sdk = "<2" -Django = ">=3.2" -pydantic = {version = "<2", optional = false} -email-validator = {version = "^1.1.3", optional = false} -uvicorn = ">=0.11.6" -starlette = ">=0.13.6" -sanic = ">=20.12.2" -aiohttp = "^3.7.4.post0" -pytest-aiohttp = "^1.0.3" -types-typed-ast = "^1.5.8" -types-toml = "^0.10.8" -types-ujson = "^5.6.0" -types-requests = "^2.28.11" -types-python-dateutil = "^2.8.19" -types-freezegun = "^1.1.9" -types-chardet = "^5.0.4" -types-certifi = "^2021.10.8" -types-aiofiles = ">=22.1,<24.0" -sanic-testing = "^22.9.0" -fastapi = {version = ">=0.65.0", optional = false} -starlite = {version = ">=1.48.0", optional = false, python = ">=3.8", extras = ["testing"]} -MarkupSafe = "2.1.2" +pygments = "^2.3" +pyinstrument = {version = ">=4.0.0", optional = false} +pytest = "^7.2" +pytest-asyncio = ">=0.20.3,<0.22.0" +pytest-codspeed = "^2.0.0" +pytest-cov = "^4.0.0" +pytest-emoji = "^0.2.0" +pytest-mock = "^3.10" pytest-snapshot = "^0.9.0" -channels = "^3.0.5" -rich = {version = "^12.5.1", optional = false} -libcst = {version = "^0.4.7", optional = false} -ddtrace = "^1.6.4" +pytest-xdist = {extras = ["psutil"], version = "^3.1.0"} python-multipart = ">=0.0.5,<0.0.7" -pygments = "^2.3" -typer = {version = ">=0.7.0", optional = false} +rich = {version = "^12.5.1", optional = false} +sanic-testing = "^22.9.0" sentry-sdk = "^1.14.0" -pyinstrument = {version = ">=4.0.0", optional = false} +typer = {version = ">=0.7.0", optional = false} +types-aiofiles = ">=22.1,<24.0" +types-certifi = "^2021.10.8" +types-chardet = "^5.0.4" +types-freezegun = "^1.1.9" +types-python-dateutil = "^2.8.19" +types-toml = "^0.10.8" +types-typed-ast = "^1.5.8" +types-ujson = "^5.6.0" # added this here manually because poetry doesn't seem to be able to handle it =( botocore = "1.29.124" -nox = "^2023.4.22" -nox-poetry = "^1.0.2" -pytest-codspeed = "^2.0.0" +mypy = "1.3.0" +pytest-mypy-plugins = "^1.10" +types-protobuf = "^4.23.0.1" + +[tool.poetry.group.integrations] +optional = true + +[tool.poetry.group.integrations.dependencies] +aiohttp = "^3.7.4.post0" +chalice = {version = "^1.22"} +channels = "^3.0.5" +Django = ">=3.2" +fastapi = {version = ">=0.65.0", optional = false} +flask = ">=1.1" +pydantic = {version = "<2", optional = false} +pytest-aiohttp = "^1.0.3" +pytest-django = {version = "^4.5"} +pytest-flask = {version = "^1.2.0"} +sanic = ">=20.12.2" +starlette = ">=0.13.6" +starlite = {version = ">=1.48.0", optional = false, python = ">=3.8", extras = ["testing"]} +uvicorn = ">=0.11.6" [tool.poetry.extras] -aiohttp = ["aiohttp", "pytest-aiohttp"] +aiohttp = ["aiohttp"] asgi = ["starlette", "python-multipart"] debug = ["rich", "libcst"] debug-server = ["starlette", "uvicorn", "python-multipart", "typer", "pygments", "rich", "libcst"] @@ -149,19 +153,20 @@ addopts = "--emoji --mypy-ini-file=mypy.ini" DJANGO_SETTINGS_MODULE = "tests.django.django_settings" testpaths = ["tests/"] markers = [ - "django", + "aiohttp", "asgi", - "starlette", + "chalice", "channels", - "sanic", - "aiohttp", + "django_db", + "django", "fastapi", - "chalice", + "flaky", "flask", - "starlite", "pydantic", - "flaky", "relay", + "sanic", + "starlette", + "starlite", ] asyncio_mode = "auto" filterwarnings = [ diff --git a/tests/aiohttp/conftest.py b/tests/aiohttp/conftest.py deleted file mode 100644 index 39d298240a..0000000000 --- a/tests/aiohttp/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Any - -import pytest -import pytest_asyncio - -from strawberry.aiohttp.test.client import GraphQLTestClient -from tests.aiohttp.app import create_app - - -@pytest_asyncio.fixture -async def aiohttp_app_client(event_loop, aiohttp_client) -> Any: - app = create_app(graphiql=True) - event_loop.set_debug(True) - return await aiohttp_client(app) - - -@pytest_asyncio.fixture -async def aiohttp_app_client_no_get(event_loop, aiohttp_client) -> Any: - app = create_app(graphiql=True, allow_queries_via_get=False) - event_loop.set_debug(True) - return await aiohttp_client(app) - - -@pytest.fixture -def graphql_client(aiohttp_app_client) -> GraphQLTestClient: - return GraphQLTestClient(aiohttp_app_client, url="/graphql") diff --git a/tests/aiohttp/test_websockets.py b/tests/aiohttp/test_websockets.py index 83848b80ac..b9bce3457b 100644 --- a/tests/aiohttp/test_websockets.py +++ b/tests/aiohttp/test_websockets.py @@ -1,9 +1,18 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Awaitable, Callable + from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from .app import create_app +if TYPE_CHECKING: + from aiohttp.test_utils import TestClient -async def test_turning_off_graphql_ws(aiohttp_client): +async def test_turning_off_graphql_ws( + aiohttp_client: Callable[..., Awaitable[TestClient]] +) -> None: + from .app import create_app + app = create_app(subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]) aiohttp_app_client = await aiohttp_client(app) @@ -17,7 +26,11 @@ async def test_turning_off_graphql_ws(aiohttp_client): assert data.extra == "Subprotocol not acceptable" -async def test_turning_off_graphql_transport_ws(aiohttp_client): +async def test_turning_off_graphql_transport_ws( + aiohttp_client: Callable[..., Awaitable[TestClient]] +): + from .app import create_app + app = create_app(subscription_protocols=[GRAPHQL_WS_PROTOCOL]) aiohttp_app_client = await aiohttp_client(app) @@ -31,7 +44,11 @@ async def test_turning_off_graphql_transport_ws(aiohttp_client): assert data.extra == "Subprotocol not acceptable" -async def test_turning_off_all_ws_protocols(aiohttp_client): +async def test_turning_off_all_ws_protocols( + aiohttp_client: Callable[..., Awaitable[TestClient]] +): + from .app import create_app + app = create_app(subscription_protocols=[]) aiohttp_app_client = await aiohttp_client(app) @@ -54,7 +71,11 @@ async def test_turning_off_all_ws_protocols(aiohttp_client): assert data.extra == "Subprotocol not acceptable" -async def test_unsupported_ws_protocol(aiohttp_client): +async def test_unsupported_ws_protocol( + aiohttp_client: Callable[..., Awaitable[TestClient]] +): + from .app import create_app + app = create_app(subscription_protocols=[]) aiohttp_app_client = await aiohttp_client(app) @@ -68,7 +89,11 @@ async def test_unsupported_ws_protocol(aiohttp_client): assert data.extra == "Subprotocol not acceptable" -async def test_clients_can_prefer_protocols(aiohttp_client): +async def test_clients_can_prefer_protocols( + aiohttp_client: Callable[..., Awaitable[TestClient]] +): + from .app import create_app + app = create_app( subscription_protocols=[GRAPHQL_WS_PROTOCOL, GRAPHQL_TRANSPORT_WS_PROTOCOL] ) diff --git a/tests/asgi/conftest.py b/tests/asgi/conftest.py deleted file mode 100644 index 3822e4f6ba..0000000000 --- a/tests/asgi/conftest.py +++ /dev/null @@ -1,48 +0,0 @@ -import pathlib - -import pytest -from starlette.testclient import TestClient - -from strawberry.asgi.test import GraphQLTestClient -from tests.asgi.app import create_app - - -@pytest.fixture -def test_client() -> TestClient: - app = create_app() - return TestClient(app) - - -@pytest.fixture -def test_client_keep_alive() -> TestClient: - app = create_app(keep_alive=True, keep_alive_interval=0.1) - return TestClient(app) - - -@pytest.fixture -def test_client_no_graphiql() -> TestClient: - app = create_app(graphiql=False) - return TestClient(app) - - -@pytest.fixture -def test_client_no_get() -> TestClient: - app = create_app(allow_queries_via_get=False) - return TestClient(app) - - -@pytest.fixture -def graphql_client(test_client) -> GraphQLTestClient: - return GraphQLTestClient(test_client) - - -def pytest_collection_modifyitems(config, items): - # automatically mark tests with 'starlette' if they are in the asgi subfolder - - rootdir = pathlib.Path(config.rootdir) - - for item in items: - rel_path = pathlib.Path(item.fspath).relative_to(rootdir) - - if str(rel_path).startswith("tests/asgi"): - item.add_marker(pytest.mark.starlette) diff --git a/tests/asgi/test_async.py b/tests/asgi/test_async.py index bf79fb3b98..81fc9d19f4 100644 --- a/tests/asgi/test_async.py +++ b/tests/asgi/test_async.py @@ -1,26 +1,33 @@ -import typing +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional import pytest -from starlette.testclient import TestClient import strawberry -from strawberry.asgi import GraphQL + +if TYPE_CHECKING: + from starlette.testclient import TestClient @pytest.fixture def test_client() -> TestClient: + from starlette.testclient import TestClient + + from strawberry.asgi import GraphQL + @strawberry.type class Query: @strawberry.field - async def hello(self, name: typing.Optional[str] = None) -> str: + async def hello(self, name: Optional[str] = None) -> str: return f"Hello {name or 'world'}" async_schema = strawberry.Schema(Query) - app = GraphQL(async_schema) + app = GraphQL[None, None](async_schema) return TestClient(app) -def test_simple_query(test_client): +def test_simple_query(test_client: TestClient): response = test_client.post("/", json={"query": "{ hello }"}) assert response.json() == {"data": {"hello": "Hello world"}} diff --git a/tests/asgi/test_websockets.py b/tests/asgi/test_websockets.py index 8b4566d747..511661358a 100644 --- a/tests/asgi/test_websockets.py +++ b/tests/asgi/test_websockets.py @@ -1,12 +1,14 @@ import pytest -from starlette.testclient import TestClient -from starlette.websockets import WebSocketDisconnect from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from tests.asgi.app import create_app def test_turning_off_graphql_ws(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.asgi.app import create_app + app = create_app(subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]) test_client = TestClient(app) @@ -18,6 +20,11 @@ def test_turning_off_graphql_ws(): def test_turning_off_graphql_transport_ws(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.asgi.app import create_app + app = create_app(subscription_protocols=[GRAPHQL_WS_PROTOCOL]) test_client = TestClient(app) @@ -29,6 +36,11 @@ def test_turning_off_graphql_transport_ws(): def test_turning_off_all_ws_protocols(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.asgi.app import create_app + app = create_app(subscription_protocols=[]) test_client = TestClient(app) @@ -46,6 +58,11 @@ def test_turning_off_all_ws_protocols(): def test_unsupported_ws_protocol(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.asgi.app import create_app + app = create_app(subscription_protocols=[]) test_client = TestClient(app) @@ -57,6 +74,10 @@ def test_unsupported_ws_protocol(): def test_clients_can_prefer_protocols(): + from starlette.testclient import TestClient + + from tests.asgi.app import create_app + app = create_app( subscription_protocols=[GRAPHQL_WS_PROTOCOL, GRAPHQL_TRANSPORT_WS_PROTOCOL] ) diff --git a/tests/channels/conftest.py b/tests/channels/conftest.py deleted file mode 100644 index 95442c5677..0000000000 --- a/tests/channels/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path - -import pytest - - -def pytest_collection_modifyitems(config, items): - # automatically mark tests with 'channels' if they are in the channels subfolder - - rootdir = Path(config.rootdir) - - for item in items: - rel_path = Path(item.fspath).relative_to(rootdir) - - if str(rel_path).startswith("tests/channels"): - item.add_marker(pytest.mark.channels) diff --git a/tests/channels/test_layers.py b/tests/channels/test_layers.py index 64ca18cca8..b868a0826b 100644 --- a/tests/channels/test_layers.py +++ b/tests/channels/test_layers.py @@ -1,12 +1,10 @@ +from __future__ import annotations + import asyncio -from typing import Generator +from typing import TYPE_CHECKING, Generator import pytest -from channels.layers import get_channel_layer -from channels.testing import WebsocketCommunicator -from strawberry.channels import GraphQLWSConsumer -from strawberry.channels.handlers.base import ChannelsConsumer from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( CompleteMessage, @@ -18,9 +16,15 @@ ) from tests.views.schema import schema +if TYPE_CHECKING: + from channels.testing import WebsocketCommunicator + @pytest.fixture async def ws() -> Generator[WebsocketCommunicator, None, None]: + from channels.testing import WebsocketCommunicator + from strawberry.channels import GraphQLWSConsumer + client = WebsocketCommunicator( GraphQLWSConsumer.as_asgi(schema=schema), "/graphql", @@ -35,6 +39,8 @@ async def ws() -> Generator[WebsocketCommunicator, None, None]: async def test_no_layers(): + from strawberry.channels.handlers.base import ChannelsConsumer + consumer = ChannelsConsumer() # Mimic lack of layers. If layers is not installed/configured in channels, # consumer.channel_layer will be `None` @@ -49,11 +55,13 @@ async def test_no_layers(): await consumer.channel_listen("foobar").__anext__() with pytest.raises(RuntimeError, match=msg): - async with consumer.listen_to_channel("foobar") as cm: + async with consumer.listen_to_channel("foobar"): pass async def test_channel_listen(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -94,6 +102,8 @@ async def test_channel_listen(ws: WebsocketCommunicator): async def test_channel_listen_with_confirmation(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -138,6 +148,8 @@ async def test_channel_listen_with_confirmation(ws: WebsocketCommunicator): async def test_channel_listen_timeout(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -164,6 +176,8 @@ async def test_channel_listen_timeout(ws: WebsocketCommunicator): async def test_channel_listen_timeout_cm(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -194,6 +208,8 @@ async def test_channel_listen_timeout_cm(ws: WebsocketCommunicator): async def test_channel_listen_no_message_on_channel(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -228,6 +244,8 @@ async def test_channel_listen_no_message_on_channel(ws: WebsocketCommunicator): async def test_channel_listen_no_message_on_channel_cm(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -266,6 +284,8 @@ async def test_channel_listen_no_message_on_channel_cm(ws: WebsocketCommunicator async def test_channel_listen_group(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -322,6 +342,8 @@ async def test_channel_listen_group(ws: WebsocketCommunicator): async def test_channel_listen_group_cm(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -382,6 +404,8 @@ async def test_channel_listen_group_cm(ws: WebsocketCommunicator): async def test_channel_listen_group_twice(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() @@ -470,6 +494,8 @@ async def test_channel_listen_group_twice(ws: WebsocketCommunicator): async def test_channel_listen_group_twice_cm(ws: WebsocketCommunicator): + from channels.layers import get_channel_layer + await ws.send_json_to(ConnectionInitMessage().as_dict()) response = await ws.receive_json_from() diff --git a/tests/channels/test_router.py b/tests/channels/test_router.py index 337fc7d4a2..3a8ed08dd0 100644 --- a/tests/channels/test_router.py +++ b/tests/channels/test_router.py @@ -2,7 +2,6 @@ import pytest -from strawberry.channels.router import GraphQLProtocolTypeRouter from tests.views.schema import schema @@ -14,6 +13,8 @@ def _fake_asgi(): @mock.patch("strawberry.channels.router.GraphQLWSConsumer.as_asgi") @pytest.mark.parametrize("pattern", ["^graphql", "^foo"]) def test_included_paths(ws_asgi: mock.Mock, http_asgi: mock.Mock, pattern: str): + from strawberry.channels.router import GraphQLProtocolTypeRouter + http_ret = _fake_asgi() http_asgi.return_value = http_ret @@ -42,6 +43,8 @@ def test_included_paths_with_django_app( http_asgi: mock.Mock, pattern: str, ): + from strawberry.channels.router import GraphQLProtocolTypeRouter + http_ret = _fake_asgi() http_asgi.return_value = http_ret diff --git a/tests/channels/test_testing.py b/tests/channels/test_testing.py index 3b95e7481e..e691abac33 100644 --- a/tests/channels/test_testing.py +++ b/tests/channels/test_testing.py @@ -1,24 +1,32 @@ -from typing import Generator +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generator import pytest -from strawberry.channels.handlers.ws_handler import GraphQLWSConsumer -from strawberry.channels.testing import GraphQLWebsocketCommunicator from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from tests.views.schema import schema -application = GraphQLWSConsumer.as_asgi(schema=schema, keep_alive_interval=50) +if TYPE_CHECKING: + from strawberry.channels.testing import GraphQLWebsocketCommunicator @pytest.fixture(params=[GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL]) -async def communicator(request) -> Generator[GraphQLWebsocketCommunicator, None, None]: +async def communicator( + request: Any, +) -> Generator[GraphQLWebsocketCommunicator, None, None]: + from strawberry.channels import GraphQLWSConsumer + from strawberry.channels.testing import GraphQLWebsocketCommunicator + + application = GraphQLWSConsumer.as_asgi(schema=schema, keep_alive_interval=50) + async with GraphQLWebsocketCommunicator( protocol=request.param, application=application, path="/graphql" ) as client: yield client -async def test_simple_subscribe(communicator): +async def test_simple_subscribe(communicator: GraphQLWebsocketCommunicator): async for res in communicator.subscribe( query='subscription { echo(message: "Hi") }' ): diff --git a/tests/channels/test_ws_handler.py b/tests/channels/test_ws_handler.py index b40ab35516..88310bb617 100644 --- a/tests/channels/test_ws_handler.py +++ b/tests/channels/test_ws_handler.py @@ -1,14 +1,24 @@ import pytest -from channels.testing.websocket import WebsocketCommunicator -from strawberry.channels.handlers.graphql_transport_ws_handler import ( - GraphQLTransportWSHandler, -) -from strawberry.channels.handlers.graphql_ws_handler import GraphQLWSHandler -from strawberry.channels.handlers.ws_handler import GraphQLWSConsumer -from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from tests.views.schema import schema +try: + from channels.testing.websocket import WebsocketCommunicator + from strawberry.channels.handlers.graphql_transport_ws_handler import ( + GraphQLTransportWSHandler, + ) + from strawberry.channels.handlers.graphql_ws_handler import GraphQLWSHandler + from strawberry.channels.handlers.ws_handler import GraphQLWSConsumer +except ImportError: + pytestmark = pytest.mark.skip("Channels is not installed") + GraphQLWSHandler = None + GraphQLTransportWSHandler = None + +from strawberry.subscriptions import ( + GRAPHQL_TRANSPORT_WS_PROTOCOL, + GRAPHQL_WS_PROTOCOL, +) + async def test_wrong_protocol(): GraphQLWSConsumer.as_asgi(schema=schema) diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 194f82cab1..b6874ff53f 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -1,12 +1,19 @@ +from __future__ import annotations + import sys +from typing import TYPE_CHECKING import pytest -from starlette.testclient import TestClient +from pytest_mock import MockFixture from typer.testing import CliRunner +if TYPE_CHECKING: + from starlette.testclient import TestClient + from typer import Typer + @pytest.fixture -def cli_runner(mocker) -> CliRunner: +def cli_runner(mocker: MockFixture) -> CliRunner: # Mock of uvicorn.run uvicorn_run_patch = mocker.patch("uvicorn.run") uvicorn_run_patch.return_value = True @@ -14,10 +21,19 @@ def cli_runner(mocker) -> CliRunner: @pytest.fixture -def debug_server_client(mocker) -> TestClient: +def debug_server_client(mocker: MockFixture) -> TestClient: + from starlette.testclient import TestClient + schema_import_path = "tests.fixtures.sample_package.sample_module" mocker.patch.object(sys, "argv", ["strawberry", "server", schema_import_path]) from strawberry.cli.debug_server import app return TestClient(app) + + +@pytest.fixture +def cli_app() -> Typer: + from strawberry.cli.app import app + + return app diff --git a/tests/cli/test_codegen.py b/tests/cli/test_codegen.py index f63fef07cd..b5b631ace1 100644 --- a/tests/cli/test_codegen.py +++ b/tests/cli/test_codegen.py @@ -2,9 +2,9 @@ from typing import List import pytest +from typer import Typer from typer.testing import CliRunner -from strawberry.cli.app import app from strawberry.cli.commands.codegen import ConsolePlugin from strawberry.codegen import CodegenFile, CodegenResult, QueryCodegenPlugin from strawberry.codegen.types import GraphQLOperation, GraphQLType @@ -56,10 +56,12 @@ def query_file_path(tmp_path: Path) -> Path: return output_path -def test_codegen(cli_runner: CliRunner, query_file_path: Path, tmp_path: Path): +def test_codegen( + cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path +): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( - app, + cli_app, [ "codegen", "-p", @@ -81,11 +83,11 @@ def test_codegen(cli_runner: CliRunner, query_file_path: Path, tmp_path: Path): def test_codegen_passing_plugin_symbol( - cli_runner: CliRunner, query_file_path: Path, tmp_path: Path + cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( - app, + cli_app, [ "codegen", "-p", @@ -107,11 +109,11 @@ def test_codegen_passing_plugin_symbol( def test_codegen_returns_error_when_symbol_does_not_exist( - cli_runner: CliRunner, query_file_path: Path, tmp_path: Path + cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( - app, + cli_app, [ "codegen", "-p", @@ -125,17 +127,18 @@ def test_codegen_returns_error_when_symbol_does_not_exist( ) assert result.exit_code == 1 + assert result.exception assert result.exception.args == ( "module 'tests.cli.test_codegen' has no attribute 'SomePlugin'", ) def test_codegen_returns_error_when_module_does_not_exist( - cli_runner: CliRunner, query_file_path: Path, tmp_path: Path + cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( - app, + cli_app, [ "codegen", "-p", @@ -153,11 +156,11 @@ def test_codegen_returns_error_when_module_does_not_exist( def test_codegen_returns_error_when_does_not_find_plugin( - cli_runner: CliRunner, query_file_path: Path, tmp_path: Path + cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( - app, + cli_app, [ "codegen", "-p", @@ -175,11 +178,11 @@ def test_codegen_returns_error_when_does_not_find_plugin( def test_codegen_finds_our_plugins( - cli_runner: CliRunner, query_file_path: Path, tmp_path: Path + cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( - app, + cli_app, [ "codegen", "-p", @@ -201,11 +204,11 @@ def test_codegen_finds_our_plugins( def test_can_use_custom_cli_plugin( - cli_runner: CliRunner, query_file_path: Path, tmp_path: Path + cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( - app, + cli_app, [ "codegen", "--cli-plugin", diff --git a/tests/cli/test_export_schema.py b/tests/cli/test_export_schema.py index 188edfac80..f12c14c4c4 100644 --- a/tests/cli/test_export_schema.py +++ b/tests/cli/test_export_schema.py @@ -1,11 +1,10 @@ +from typer import Typer from typer.testing import CliRunner -from strawberry.cli.app import app - -def test_schema_export(cli_runner: CliRunner): +def test_schema_export(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:schema" - result = cli_runner.invoke(app, ["export-schema", selector]) + result = cli_runner.invoke(cli_app, ["export-schema", selector]) assert result.exit_code == 0 assert result.stdout == ( @@ -20,25 +19,26 @@ def test_schema_export(cli_runner: CliRunner): ) -def test_default_schema_symbol_name(cli_runner: CliRunner): +def test_default_schema_symbol_name(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module" - result = cli_runner.invoke(app, ["export-schema", selector]) + result = cli_runner.invoke(cli_app, ["export-schema", selector]) assert result.exit_code == 0 -def test_app_dir_option(cli_runner: CliRunner): +def test_app_dir_option(cli_app: Typer, cli_runner: CliRunner): selector = "sample_module" result = cli_runner.invoke( - app, ["export-schema", "--app-dir=./tests/fixtures/sample_package", selector] + cli_app, + ["export-schema", "--app-dir=./tests/fixtures/sample_package", selector], ) assert result.exit_code == 0 -def test_invalid_module(cli_runner: CliRunner): +def test_invalid_module(cli_app: Typer, cli_runner: CliRunner): selector = "not.existing.module" - result = cli_runner.invoke(app, ["export-schema", selector]) + result = cli_runner.invoke(cli_app, ["export-schema", selector]) expected_error = "Error: No module named 'not'" @@ -46,9 +46,9 @@ def test_invalid_module(cli_runner: CliRunner): assert expected_error in result.stdout.replace("\n", "") -def test_invalid_symbol(cli_runner: CliRunner): +def test_invalid_symbol(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:not.existing.symbol" - result = cli_runner.invoke(app, ["export-schema", selector]) + result = cli_runner.invoke(cli_app, ["export-schema", selector]) expected_error = ( "Error: module 'tests.fixtures.sample_package.sample_module' " @@ -59,9 +59,9 @@ def test_invalid_symbol(cli_runner: CliRunner): assert expected_error in result.stdout.replace("\n", "") -def test_invalid_schema_instance(cli_runner: CliRunner): +def test_invalid_schema_instance(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:not_a_schema" - result = cli_runner.invoke(app, ["export-schema", selector]) + result = cli_runner.invoke(cli_app, ["export-schema", selector]) expected_error = "Error: The `schema` must be an instance of strawberry.Schema" diff --git a/tests/cli/test_server.py b/tests/cli/test_server.py index 774c384b99..30047481f8 100644 --- a/tests/cli/test_server.py +++ b/tests/cli/test_server.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import re import sys import pytest -import uvicorn +from pytest_mock import MockerFixture +from starlette.testclient import TestClient +from typer import Typer from typer.testing import CliRunner -from strawberry.cli.app import app - BOOT_MSG = "Running strawberry on http://0.0.0.0:8000/graphql" + if sys.platform != "win32": # UTF-8 chars are not supported by default console on Windows BOOT_MSG += " 🍓" @@ -15,18 +18,23 @@ BOOT_MSG += "\n" -def test_cli_cmd_server(cli_runner: CliRunner): +def test_cli_cmd_server(cli_app: Typer, cli_runner: CliRunner): + import uvicorn + schema = "tests.fixtures.sample_package.sample_module" - result = cli_runner.invoke(app, ["server", schema]) + result = cli_runner.invoke(cli_app, ["server", schema]) assert result.exit_code == 0 assert uvicorn.run.call_count == 1 assert re.match(BOOT_MSG, result.stdout) -def test_cli_cmd_server_app_dir_option(cli_runner: CliRunner): +def test_cli_cmd_server_app_dir_option(cli_app: Typer, cli_runner: CliRunner): + import uvicorn + result = cli_runner.invoke( - app, ["server", "--app-dir=./tests/fixtures/sample_package", "sample_module"] + cli_app, + ["server", "--app-dir=./tests/fixtures/sample_package", "sample_module"], ) assert result.exit_code == 0 @@ -34,16 +42,16 @@ def test_cli_cmd_server_app_dir_option(cli_runner: CliRunner): assert re.match(BOOT_MSG, result.stdout) -def test_default_schema_symbol_name(cli_runner: CliRunner): +def test_default_schema_symbol_name(cli_app: Typer, cli_runner: CliRunner): schema = "tests.fixtures.sample_package.sample_module" - result = cli_runner.invoke(app, ["server", schema]) + result = cli_runner.invoke(cli_app, ["server", schema]) assert result.exit_code == 0 -def test_invalid_module(cli_runner: CliRunner): +def test_invalid_module(cli_app: Typer, cli_runner: CliRunner): schema = "not.existing.module" - result = cli_runner.invoke(app, ["server", schema]) + result = cli_runner.invoke(cli_app, ["server", schema]) expected_error = "Error: No module named 'not'" @@ -51,9 +59,9 @@ def test_invalid_module(cli_runner: CliRunner): assert expected_error in result.stdout -def test_invalid_symbol(cli_runner: CliRunner): +def test_invalid_symbol(cli_app: Typer, cli_runner: CliRunner): schema = "tests.fixtures.sample_package.sample_module:not.existing.symbol" - result = cli_runner.invoke(app, ["server", schema]) + result = cli_runner.invoke(cli_app, ["server", schema]) expected_error = ( "Error: module 'tests.fixtures.sample_package.sample_module' " @@ -64,9 +72,9 @@ def test_invalid_symbol(cli_runner: CliRunner): assert expected_error in result.stdout.replace("\n", "") -def test_invalid_schema_instance(cli_runner: CliRunner): +def test_invalid_schema_instance(cli_app: Typer, cli_runner: CliRunner): schema = "tests.fixtures.sample_package.sample_module:not_a_schema" - result = cli_runner.invoke(app, ["server", schema]) + result = cli_runner.invoke(cli_app, ["server", schema]) expected_error = "Error: The `schema` must be an instance of strawberry.Schema" @@ -75,11 +83,13 @@ def test_invalid_schema_instance(cli_runner: CliRunner): @pytest.mark.parametrize("dependency", ["uvicorn", "starlette"]) -def test_missing_debug_server_dependencies(cli_runner: CliRunner, mocker, dependency): +def test_missing_debug_server_dependencies( + cli_app: Typer, cli_runner: CliRunner, mocker: MockerFixture, dependency: str +): mocker.patch.dict(sys.modules, {dependency: None}) schema = "tests.fixtures.sample_package.sample_module" - result = cli_runner.invoke(app, ["server", schema]) + result = cli_runner.invoke(cli_app, ["server", schema]) assert result.exit_code == 1 assert result.stdout == ( @@ -89,7 +99,7 @@ def test_missing_debug_server_dependencies(cli_runner: CliRunner, mocker, depend ) -def test_debug_server_routes(debug_server_client): +def test_debug_server_routes(debug_server_client: TestClient): for path in ["/", "/graphql"]: response = debug_server_client.get(path) assert response.status_code == 200 diff --git a/tests/cli/test_upgrade.py b/tests/cli/test_upgrade.py index b449953ca6..ccbff7347a 100644 --- a/tests/cli/test_upgrade.py +++ b/tests/cli/test_upgrade.py @@ -1,16 +1,17 @@ from pathlib import Path from pytest_snapshot.plugin import Snapshot +from typer import Typer from typer.testing import CliRunner -from strawberry.cli.app import app - HERE = Path(__file__).parent -def test_upgrade_returns_error_code_if_codemod_does_not_exist(cli_runner: CliRunner): +def test_upgrade_returns_error_code_if_codemod_does_not_exist( + cli_app: Typer, cli_runner: CliRunner +): result = cli_runner.invoke( - app, + cli_app, ["upgrade", "a_random_codemod", "."], ) @@ -19,7 +20,7 @@ def test_upgrade_returns_error_code_if_codemod_does_not_exist(cli_runner: CliRun def test_upgrade_works_annotated_unions( - cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot + cli_app: Typer, cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot ): source = HERE / "fixtures/unions.py" @@ -27,7 +28,7 @@ def test_upgrade_works_annotated_unions( target.write_text(source.read_text()) result = cli_runner.invoke( - app, + cli_app, ["upgrade", "--python-target", "3.11", "annotated-union", str(target)], ) @@ -39,7 +40,7 @@ def test_upgrade_works_annotated_unions( def test_upgrade_works_annotated_unions_target_python( - cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot + cli_app: Typer, cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot ): source = HERE / "fixtures/unions.py" @@ -47,7 +48,7 @@ def test_upgrade_works_annotated_unions_target_python( target.write_text(source.read_text()) result = cli_runner.invoke( - app, + cli_app, ["upgrade", "--python-target", "3.8", "annotated-union", str(target)], ) @@ -59,7 +60,7 @@ def test_upgrade_works_annotated_unions_target_python( def test_upgrade_works_annotated_unions_typing_extensions( - cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot + cli_app: Typer, cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot ): source = HERE / "fixtures/unions.py" @@ -67,7 +68,7 @@ def test_upgrade_works_annotated_unions_typing_extensions( target.write_text(source.read_text()) result = cli_runner.invoke( - app, + cli_app, [ "upgrade", "--use-typing-extensions", diff --git a/tests/conftest.py b/tests/conftest.py index 8be7f83d35..f21ec3b27b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,5 +22,19 @@ def pytest_collection_modifyitems(config: pytest.Config, items: List[pytest.Item for item in items: rel_path = pathlib.Path(item.fspath).relative_to(rootdir) - if "pydantic" in rel_path.parts: - item.add_marker(pytest.mark.pydantic) + markers = [ + "aiohttp", + "asgi", + "chalice", + "channels", + "django", + "fastapi", + "flask", + "pydantic", + "sanic", + "starlite", + ] + + for marker in markers: + if marker in rel_path.parts: + item.add_marker(getattr(pytest.mark, marker)) diff --git a/tests/django/conftest.py b/tests/django/conftest.py index 12330d59b8..288185723b 100644 --- a/tests/django/conftest.py +++ b/tests/django/conftest.py @@ -1,23 +1,17 @@ -import pathlib +from __future__ import annotations -import pytest -from django.test.client import Client - -from strawberry.django.test import GraphQLTestClient - - -def pytest_collection_modifyitems(config, items): - # automatically mark tests with 'django' if they are in the django subfolder +from typing import TYPE_CHECKING - rootdir = pathlib.Path(config.rootdir) - - for item in items: - rel_path = pathlib.Path(item.fspath).relative_to(rootdir) +import pytest - if str(rel_path).startswith("tests/django"): - item.add_marker(pytest.mark.django) +if TYPE_CHECKING: + from strawberry.django.test import GraphQLTestClient @pytest.fixture() def graphql_client() -> GraphQLTestClient: + from django.test.client import Client + + from strawberry.django.test import GraphQLTestClient + return GraphQLTestClient(Client()) diff --git a/tests/django/test_dataloaders.py b/tests/django/test_dataloaders.py index 1036094fc3..835e6a5c1e 100644 --- a/tests/django/test_dataloaders.py +++ b/tests/django/test_dataloaders.py @@ -1,38 +1,49 @@ import json -from typing import List +from typing import List, Tuple -import django import pytest from asgiref.sync import sync_to_async -from django.test.client import RequestFactory +from pytest_mock import MockerFixture import strawberry from strawberry.dataloader import DataLoader -from strawberry.django.views import AsyncGraphQLView -from .app.models import Example +try: + import django + + DJANGO_VERSION: Tuple[int, int, int] = django.VERSION +except ImportError: + DJANGO_VERSION = (0, 0, 0) + pytestmark = [ pytest.mark.asyncio, pytest.mark.skipif( - django.VERSION < (3, 1), + DJANGO_VERSION < (3, 1), reason="Async views are only supported in Django >= 3.1", ), ] def _prepare_db(): - ids = [] - - for index in range(5): - ids.append(Example.objects.create(name=f"This is a demo async {index}").id) + from .app.models import Example - return ids + return [ + Example.objects.create(name=f"This is a demo async {index}").pk + for index in range(5) + ] +@pytest.mark.django @pytest.mark.django_db -async def test_fetch_data_from_db(mocker): - def _sync_batch_load(keys): +async def test_fetch_data_from_db(mocker: MockerFixture): + from django.test.client import RequestFactory + + from strawberry.django.views import AsyncGraphQLView + + from .app.models import Example + + def _sync_batch_load(keys: List[str]): data = Example.objects.filter(id__in=keys) return list(data) @@ -42,12 +53,12 @@ def _sync_batch_load(keys): ids = await prepare_db() - async def idx(keys) -> List[Example]: + async def idx(keys: List[str]) -> List[Example]: return await batch_load(keys) mock_loader = mocker.Mock(side_effect=idx) - loader = DataLoader(load_fn=mock_loader) + loader = DataLoader[str, Example](load_fn=mock_loader) @strawberry.type class Query: diff --git a/tests/fastapi/conftest.py b/tests/fastapi/conftest.py deleted file mode 100644 index f355695b15..0000000000 --- a/tests/fastapi/conftest.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from fastapi.testclient import TestClient -from tests.fastapi.app import create_app - - -@pytest.fixture -def test_client() -> TestClient: - app = create_app() - return TestClient(app) - - -@pytest.fixture -def test_client_keep_alive() -> TestClient: - app = create_app(keep_alive=True, keep_alive_interval=0.1) - return TestClient(app) - - -@pytest.fixture -def test_client_no_graphiql() -> TestClient: - app = create_app(graphiql=False) - return TestClient(app) diff --git a/tests/fastapi/test_async.py b/tests/fastapi/test_async.py index 3a08135eb2..aa134634bf 100644 --- a/tests/fastapi/test_async.py +++ b/tests/fastapi/test_async.py @@ -1,18 +1,25 @@ -import typing +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional import pytest -from starlette.testclient import TestClient import strawberry -from tests.fastapi.app import create_app + +if TYPE_CHECKING: + from starlette.testclient import TestClient @pytest.fixture def test_client() -> TestClient: + from starlette.testclient import TestClient + + from tests.fastapi.app import create_app + @strawberry.type class Query: @strawberry.field - async def hello(self, name: typing.Optional[str] = None) -> str: + async def hello(self, name: Optional[str] = None) -> str: return f"Hello {name or 'world'}" async_schema = strawberry.Schema(Query) @@ -20,7 +27,7 @@ async def hello(self, name: typing.Optional[str] = None) -> str: return TestClient(app) -def test_simple_query(test_client): +def test_simple_query(test_client: TestClient): response = test_client.post("/graphql", json={"query": "{ hello }"}) assert response.json() == {"data": {"hello": "Hello world"}} diff --git a/tests/fastapi/test_context.py b/tests/fastapi/test_context.py index 681df1ecc6..7b668e0096 100644 --- a/tests/fastapi/test_context.py +++ b/tests/fastapi/test_context.py @@ -1,14 +1,10 @@ import asyncio -from typing import AsyncGenerator, Dict +from typing import Any, AsyncGenerator, Dict import pytest -from starlette.websockets import WebSocketDisconnect import strawberry -from fastapi import Depends, FastAPI -from fastapi.testclient import TestClient from strawberry.exceptions import InvalidCustomContext -from strawberry.fastapi import BaseContext, GraphQLRouter from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( CompleteMessage, @@ -31,6 +27,8 @@ def test_base_context(): + from strawberry.fastapi import BaseContext + base_context = BaseContext() assert base_context.request is None assert base_context.background_tasks is None @@ -38,10 +36,14 @@ def test_base_context(): def test_with_explicit_class_context_getter(): + from fastapi import Depends, FastAPI + from fastapi.testclient import TestClient + from strawberry.fastapi import BaseContext, GraphQLRouter + @strawberry.type class Query: @strawberry.field - def abc(self, info: Info) -> str: + def abc(self, info: Info[Any, None]) -> str: assert info.context.request is not None assert info.context.strawberry == "explicitly rocks" assert info.context.connection_params is None @@ -59,7 +61,7 @@ def get_context(custom_context: CustomContext = Depends(custom_context_dependenc app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) + graphql_app = GraphQLRouter[Any, None](schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) @@ -70,10 +72,14 @@ def get_context(custom_context: CustomContext = Depends(custom_context_dependenc def test_with_implicit_class_context_getter(): + from fastapi import Depends, FastAPI + from fastapi.testclient import TestClient + from strawberry.fastapi import BaseContext, GraphQLRouter + @strawberry.type class Query: @strawberry.field - def abc(self, info: Info) -> str: + def abc(self, info: Info[Any, None]) -> str: assert info.context.request is not None assert info.context.strawberry == "implicitly rocks" assert info.context.connection_params is None @@ -89,7 +95,7 @@ def get_context(custom_context: CustomContext = Depends()): app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) + graphql_app = GraphQLRouter[Any, None](schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) @@ -100,10 +106,14 @@ def get_context(custom_context: CustomContext = Depends()): def test_with_dict_context_getter(): + from fastapi import Depends, FastAPI + from fastapi.testclient import TestClient + from strawberry.fastapi import GraphQLRouter + @strawberry.type class Query: @strawberry.field - def abc(self, info: Info) -> str: + def abc(self, info: Info[Any, None]) -> str: assert info.context.get("request") is not None assert "connection_params" not in info.context.keys() assert info.context.get("strawberry") == "rocks" @@ -117,7 +127,7 @@ def get_context(value: str = Depends(custom_context_dependency)) -> Dict[str, st app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) + graphql_app = GraphQLRouter[Any, None](schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) @@ -128,17 +138,21 @@ def get_context(value: str = Depends(custom_context_dependency)) -> Dict[str, st def test_without_context_getter(): + from fastapi import FastAPI + from fastapi.testclient import TestClient + from strawberry.fastapi import GraphQLRouter + @strawberry.type class Query: @strawberry.field - def abc(self, info: Info) -> str: + def abc(self, info: Info[Any, None]) -> str: assert info.context.get("request") is not None assert info.context.get("strawberry") is None return "abc" app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema, context_getter=None) + graphql_app = GraphQLRouter[None, None](schema, context_getter=None) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) @@ -149,10 +163,14 @@ def abc(self, info: Info) -> str: def test_with_invalid_context_getter(): + from fastapi import Depends, FastAPI + from fastapi.testclient import TestClient + from strawberry.fastapi import GraphQLRouter + @strawberry.type class Query: @strawberry.field - def abc(self, info: Info) -> str: + def abc(self, info: Info[Any, None]) -> str: assert info.context.get("request") is not None assert info.context.get("strawberry") is None return "abc" @@ -165,7 +183,7 @@ def get_context(value: str = Depends(custom_context_dependency)) -> str: app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) + graphql_app = GraphQLRouter[Any, None](schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) @@ -180,6 +198,10 @@ def get_context(value: str = Depends(custom_context_dependency)) -> str: def test_class_context_injects_connection_params_over_transport_ws(): + from fastapi import Depends, FastAPI + from fastapi.testclient import TestClient + from strawberry.fastapi import BaseContext, GraphQLRouter + @strawberry.type class Query: x: str = "hi" @@ -188,7 +210,7 @@ class Query: class Subscription: @strawberry.subscription async def connection_params( - self, info: Info, delay: float = 0 + self, info: Info[Any, None], delay: float = 0 ) -> AsyncGenerator[str, None]: assert info.context.request is not None await asyncio.sleep(delay) @@ -205,7 +227,7 @@ def get_context(context: Context = Depends()) -> Context: app = FastAPI() schema = strawberry.Schema(query=Query, subscription=Subscription) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) + graphql_app = GraphQLRouter[Any, None](schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) @@ -240,6 +262,12 @@ def get_context(context: Context = Depends()) -> Context: def test_class_context_injects_connection_params_over_ws(): + from starlette.websockets import WebSocketDisconnect + + from fastapi import Depends, FastAPI + from fastapi.testclient import TestClient + from strawberry.fastapi import BaseContext, GraphQLRouter + @strawberry.type class Query: x: str = "hi" @@ -248,7 +276,7 @@ class Query: class Subscription: @strawberry.subscription async def connection_params( - self, info: Info, delay: float = 0 + self, info: Info[Any, None], delay: float = 0 ) -> AsyncGenerator[str, None]: assert info.context.request is not None await asyncio.sleep(delay) @@ -265,7 +293,7 @@ def get_context(context: Context = Depends()) -> Context: app = FastAPI() schema = strawberry.Schema(query=Query, subscription=Subscription) - graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) + graphql_app = GraphQLRouter[Any, None](schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) diff --git a/tests/fastapi/test_openapi.py b/tests/fastapi/test_openapi.py index d1ffdf28be..ede69ace39 100644 --- a/tests/fastapi/test_openapi.py +++ b/tests/fastapi/test_openapi.py @@ -1,6 +1,4 @@ import strawberry -from fastapi import FastAPI -from strawberry.fastapi import GraphQLRouter @strawberry.type @@ -9,9 +7,12 @@ class Query: def test_enable_graphiql_view_and_allow_queries_via_get(): + from fastapi import FastAPI + from strawberry.fastapi import GraphQLRouter + app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema) + graphql_app = GraphQLRouter[None, None](schema) app.include_router(graphql_app, prefix="/graphql") assert "get" in app.openapi()["paths"]["/graphql"] @@ -19,9 +20,14 @@ def test_enable_graphiql_view_and_allow_queries_via_get(): def test_disable_graphiql_view_and_allow_queries_via_get(): + from fastapi import FastAPI + from strawberry.fastapi import GraphQLRouter + app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema, graphiql=False, allow_queries_via_get=False) + graphql_app = GraphQLRouter[None, None]( + schema, graphiql=False, allow_queries_via_get=False + ) app.include_router(graphql_app, prefix="/graphql") assert "get" not in app.openapi()["paths"]["/graphql"] diff --git a/tests/fastapi/test_router.py b/tests/fastapi/test_router.py index 787eb0b915..d965d60cc2 100644 --- a/tests/fastapi/test_router.py +++ b/tests/fastapi/test_router.py @@ -1,12 +1,14 @@ import pytest -from starlette.testclient import TestClient import strawberry -from fastapi import FastAPI -from strawberry.fastapi import GraphQLRouter def test_include_router_prefix(): + from starlette.testclient import TestClient + + from fastapi import FastAPI + from strawberry.fastapi import GraphQLRouter + @strawberry.type class Query: @strawberry.field @@ -15,7 +17,7 @@ def abc(self) -> str: app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema) + graphql_app = GraphQLRouter[None, None](schema) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) @@ -26,6 +28,11 @@ def abc(self) -> str: def test_graphql_router_path(): + from starlette.testclient import TestClient + + from fastapi import FastAPI + from strawberry.fastapi import GraphQLRouter + @strawberry.type class Query: @strawberry.field @@ -34,7 +41,7 @@ def abc(self) -> str: app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema, path="/graphql") + graphql_app = GraphQLRouter[None, None](schema, path="/graphql") app.include_router(graphql_app) test_client = TestClient(app) @@ -45,6 +52,9 @@ def abc(self) -> str: def test_missing_path_and_prefix(): + from fastapi import FastAPI + from strawberry.fastapi import GraphQLRouter + @strawberry.type class Query: @strawberry.field @@ -53,7 +63,7 @@ def abc(self) -> str: app = FastAPI() schema = strawberry.Schema(query=Query) - graphql_app = GraphQLRouter(schema) + graphql_app = GraphQLRouter[None, None](schema) with pytest.raises(Exception) as exc: app.include_router(graphql_app) diff --git a/tests/fastapi/test_websockets.py b/tests/fastapi/test_websockets.py index 0d9aabd9fe..de729c9f32 100644 --- a/tests/fastapi/test_websockets.py +++ b/tests/fastapi/test_websockets.py @@ -1,15 +1,17 @@ +from typing import Any + import pytest -from starlette.testclient import TestClient -from starlette.websockets import WebSocketDisconnect import strawberry -from fastapi import FastAPI -from strawberry.fastapi.router import GraphQLRouter from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL -from tests.fastapi.app import create_app def test_turning_off_graphql_ws(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.fastapi.app import create_app + app = create_app(subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]) test_client = TestClient(app) @@ -21,6 +23,11 @@ def test_turning_off_graphql_ws(): def test_turning_off_graphql_transport_ws(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.fastapi.app import create_app + app = create_app(subscription_protocols=[GRAPHQL_WS_PROTOCOL]) test_client = TestClient(app) @@ -32,6 +39,11 @@ def test_turning_off_graphql_transport_ws(): def test_turning_off_all_ws_protocols(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.fastapi.app import create_app + app = create_app(subscription_protocols=[]) test_client = TestClient(app) @@ -49,6 +61,11 @@ def test_turning_off_all_ws_protocols(): def test_unsupported_ws_protocol(): + from starlette.testclient import TestClient + from starlette.websockets import WebSocketDisconnect + + from tests.fastapi.app import create_app + app = create_app(subscription_protocols=[]) test_client = TestClient(app) @@ -60,6 +77,10 @@ def test_unsupported_ws_protocol(): def test_clients_can_prefer_protocols(): + from starlette.testclient import TestClient + + from tests.fastapi.app import create_app + app = create_app( subscription_protocols=[GRAPHQL_WS_PROTOCOL, GRAPHQL_TRANSPORT_WS_PROTOCOL] ) @@ -77,14 +98,19 @@ def test_clients_can_prefer_protocols(): def test_with_custom_encode_json(): + from starlette.testclient import TestClient + + from fastapi import FastAPI + from strawberry.fastapi.router import GraphQLRouter + @strawberry.type class Query: @strawberry.field def abc(self) -> str: return "abc" - class MyRouter(GraphQLRouter): - def encode_json(self, data): + class MyRouter(GraphQLRouter[None, None]): + def encode_json(self, response_data: Any): return '"custom"' app = FastAPI() diff --git a/tests/http/clients/__init__.py b/tests/http/clients/__init__.py index cf808da33f..e69de29bb2 100644 --- a/tests/http/clients/__init__.py +++ b/tests/http/clients/__init__.py @@ -1,39 +0,0 @@ -""" -This module provides abstracted HTTP Clients which internally -use different framework integrations. This allows us to write -unittests that can be run for different frameworks. -""" - -from .aiohttp import AioHttpClient -from .asgi import AsgiHttpClient -from .async_django import AsyncDjangoHttpClient -from .async_flask import AsyncFlaskHttpClient -from .base import HttpClient, WebSocketClient -from .chalice import ChaliceHttpClient -from .channels import ChannelsHttpClient, SyncChannelsHttpClient -from .django import DjangoHttpClient -from .fastapi import FastAPIHttpClient -from .flask import FlaskHttpClient -from .sanic import SanicHttpClient - -try: - from .starlite import StarliteHttpClient -except ModuleNotFoundError: - StarliteHttpClient = None - -__all__ = [ - "AioHttpClient", - "AsgiHttpClient", - "AsyncDjangoHttpClient", - "AsyncFlaskHttpClient", - "ChannelsHttpClient", - "ChaliceHttpClient", - "DjangoHttpClient", - "FastAPIHttpClient", - "FlaskHttpClient", - "HttpClient", - "SanicHttpClient", - "StarliteHttpClient", - "SyncChannelsHttpClient", - "WebSocketClient", -] diff --git a/tests/http/conftest.py b/tests/http/conftest.py index ae98ad73ab..487a1328ea 100644 --- a/tests/http/conftest.py +++ b/tests/http/conftest.py @@ -1,58 +1,54 @@ -import sys -from typing import Type +import importlib +from typing import Any, Generator, Type import pytest -from .clients import ( - AioHttpClient, - AsgiHttpClient, - AsyncDjangoHttpClient, - AsyncFlaskHttpClient, - ChaliceHttpClient, - ChannelsHttpClient, - DjangoHttpClient, - FastAPIHttpClient, - FlaskHttpClient, - HttpClient, - SanicHttpClient, - StarliteHttpClient, - SyncChannelsHttpClient, -) - - -@pytest.fixture( - params=[ - pytest.param(AioHttpClient, marks=pytest.mark.aiohttp), - pytest.param(AsgiHttpClient, marks=pytest.mark.asgi), - pytest.param(AsyncDjangoHttpClient, marks=pytest.mark.django), - pytest.param(AsyncFlaskHttpClient, marks=pytest.mark.flask), - pytest.param(ChaliceHttpClient, marks=pytest.mark.chalice), - pytest.param(DjangoHttpClient, marks=pytest.mark.django), - pytest.param(FastAPIHttpClient, marks=pytest.mark.fastapi), - pytest.param(FlaskHttpClient, marks=pytest.mark.flask), - pytest.param(SanicHttpClient, marks=pytest.mark.sanic), - pytest.param(ChannelsHttpClient, marks=pytest.mark.channels), - pytest.param( - # SyncChannelsHttpClient uses @database_sync_to_async and therefore - # needs pytest.mark.django_db - SyncChannelsHttpClient, - marks=[pytest.mark.channels, pytest.mark.django_db], +from .clients.base import HttpClient + + +def _get_http_client_classes() -> Generator[Any, None, None]: + for client, module, marks in [ + ("AioHttpClient", "aiohttp", [pytest.mark.aiohttp]), + ("AsgiHttpClient", "asgi", [pytest.mark.asgi]), + ("AsyncDjangoHttpClient", "async_django", [pytest.mark.django]), + ("AsyncFlaskHttpClient", "async_flask", [pytest.mark.flask]), + ("ChannelsHttpClient", "channels", [pytest.mark.channels]), + ("ChaliceHttpClient", "chalice", [pytest.mark.chalice]), + ("DjangoHttpClient", "django", [pytest.mark.django]), + ("FastAPIHttpClient", "fastapi", [pytest.mark.fastapi]), + ("FlaskHttpClient", "flask", [pytest.mark.flask]), + ("SanicHttpClient", "sanic", [pytest.mark.sanic]), + ("StarliteHttpClient", "starlite", [pytest.mark.starlite]), + ( + "SyncChannelsHttpClient", + "channels", + [pytest.mark.channels, pytest.mark.django_db], ), - pytest.param( - StarliteHttpClient, + ]: + try: + client_class = getattr( + importlib.import_module(f".{module}", package="tests.http.clients"), + client, + ) + except ImportError: + client_class = None + + yield pytest.param( + client_class, marks=[ - pytest.mark.starlite, + *marks, pytest.mark.skipif( - sys.version_info < (3, 8), reason="Starlite requires Python 3.8+" + client_class is None, reason=f"Client {client} not found" ), ], - ), - ] -) -def http_client_class(request) -> Type[HttpClient]: + ) + + +@pytest.fixture(params=_get_http_client_classes()) +def http_client_class(request: Any) -> Type[HttpClient]: return request.param @pytest.fixture() -def http_client(http_client_class) -> HttpClient: +def http_client(http_client_class: Type[HttpClient]) -> HttpClient: return http_client_class() diff --git a/tests/http/test_graphiql.py b/tests/http/test_graphiql.py index c96401fbb7..0d0d0fc562 100644 --- a/tests/http/test_graphiql.py +++ b/tests/http/test_graphiql.py @@ -2,7 +2,7 @@ import pytest -from .clients import HttpClient +from .clients.base import HttpClient @pytest.mark.parametrize("header_value", ["text/html", "*/*"]) diff --git a/tests/http/test_http.py b/tests/http/test_http.py index 0c316e3a7c..6c4d781aa7 100644 --- a/tests/http/test_http.py +++ b/tests/http/test_http.py @@ -1,6 +1,6 @@ import pytest -from .clients import HttpClient +from .clients.base import HttpClient @pytest.mark.parametrize("method", ["delete", "head", "put", "patch"]) diff --git a/tests/http/test_mutation.py b/tests/http/test_mutation.py index 920e606406..b5505bad34 100644 --- a/tests/http/test_mutation.py +++ b/tests/http/test_mutation.py @@ -1,4 +1,4 @@ -from .clients import HttpClient +from .clients.base import HttpClient async def test_mutation(http_client: HttpClient): diff --git a/tests/http/test_process_result.py b/tests/http/test_process_result.py index 438d3d60ca..fdc852af72 100644 --- a/tests/http/test_process_result.py +++ b/tests/http/test_process_result.py @@ -7,7 +7,7 @@ from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult -from .clients import HttpClient +from .clients.base import HttpClient def process_result(result: ExecutionResult) -> GraphQLHTTPResponse: diff --git a/tests/http/test_query.py b/tests/http/test_query.py index b3e99b8fc2..a1fc35164b 100644 --- a/tests/http/test_query.py +++ b/tests/http/test_query.py @@ -2,7 +2,7 @@ import pytest -from .clients import HttpClient +from .clients.base import HttpClient @pytest.mark.parametrize("method", ["get", "post"]) diff --git a/tests/http/test_query_via_get.py b/tests/http/test_query_via_get.py index aa033d23c4..f86812c3a6 100644 --- a/tests/http/test_query_via_get.py +++ b/tests/http/test_query_via_get.py @@ -1,4 +1,4 @@ -from .clients import HttpClient +from .clients.base import HttpClient async def test_sending_empty_query(http_client_class): diff --git a/tests/http/test_upload.py b/tests/http/test_upload.py index 8bc9532925..86871c9f2a 100644 --- a/tests/http/test_upload.py +++ b/tests/http/test_upload.py @@ -1,19 +1,22 @@ +import contextlib import json from io import BytesIO from typing import Type import pytest +from urllib3 import encode_multipart_formdata -import aiohttp - -from .clients import HttpClient -from .clients.chalice import ChaliceHttpClient +from .clients.base import HttpClient @pytest.fixture() def http_client(http_client_class: Type[HttpClient]) -> HttpClient: - if http_client_class is ChaliceHttpClient: - pytest.xfail(reason="Chalice does not support uploads") + with contextlib.suppress(ImportError): + from .clients.chalice import ChaliceHttpClient + + if http_client_class is ChaliceHttpClient: + pytest.xfail(reason="Chalice does not support uploads") + return http_client_class() @@ -32,6 +35,7 @@ async def test_upload(http_client: HttpClient): files={"textFile": f}, ) + assert response.json.get("errors") is None assert response.json["data"] == {"readText": "strawberry"} @@ -161,23 +165,22 @@ async def test_extra_form_data_fields_are_ignored(http_client: HttpClient): file_map = json.dumps({"textFile": ["variables.textFile"]}) extra_field_data = json.dumps({}) - form_data = aiohttp.FormData() - form_data.add_field("textFile", f, filename="textFile.txt") - form_data.add_field("operations", operations) - form_data.add_field("map", file_map) - form_data.add_field("extra_field", extra_field_data) - - buffer = FakeWriter() - writer = form_data() + f = BytesIO(b"strawberry") + fields = { + "operations": operations, + "map": file_map, + "extra_field": extra_field_data, + "textFile": ("textFile.txt", f.read(), "text/plain"), + } - await writer.write(buffer) # type: ignore + data, header = encode_multipart_formdata(fields) response = await http_client.post( url="/graphql", - data=buffer.value, + data=data, headers={ - "content-type": writer.content_type, - "content-length": f"{len(buffer.value)}", + "content-type": header, + "content-length": f"{len(data)}", }, ) @@ -198,27 +201,26 @@ async def test_sending_invalid_form_data(http_client: HttpClient): ) +@pytest.mark.aiohttp async def test_sending_invalid_json_body(http_client: HttpClient): f = BytesIO(b"strawberry") operations = "}" file_map = json.dumps({"textFile": ["variables.textFile"]}) - form_data = aiohttp.FormData() - form_data.add_field("textFile", f, filename="textFile.txt") - form_data.add_field("operations", operations) - form_data.add_field("map", file_map) - - buffer = FakeWriter() - writer = form_data() + fields = { + "operations": operations, + "map": file_map, + "textFile": ("textFile.txt", f.read(), "text/plain"), + } - await writer.write(buffer) # type: ignore + data, header = encode_multipart_formdata(fields) response = await http_client.post( "/graphql", - data=buffer.value, + data=data, headers={ - "content-type": writer.content_type, - "content-length": f"{len(buffer.value)}", + "content-type": header, + "content-length": f"{len(data)}", }, ) diff --git a/tests/schema/test_pydantic.py b/tests/schema/test_pydantic.py index 793862429b..312ad00694 100644 --- a/tests/schema/test_pydantic.py +++ b/tests/schema/test_pydantic.py @@ -1,5 +1,4 @@ import pytest -from pydantic import BaseModel, Field import strawberry @@ -7,6 +6,8 @@ def test_use_alias_as_gql_name(): + from pydantic import BaseModel, Field + class UserModel(BaseModel): age_: int = Field(..., alias="age_alias") @@ -38,6 +39,8 @@ class Query: def test_do_not_use_alias_as_gql_name(): + from pydantic import BaseModel, Field + class UserModel(BaseModel): age_: int = Field(..., alias="age_alias") diff --git a/tests/starlite/test_response_status.py b/tests/starlite/test_response_status.py index ac7cf5d596..f3dd710ca0 100644 --- a/tests/starlite/test_response_status.py +++ b/tests/starlite/test_response_status.py @@ -1,7 +1,6 @@ import sys import pytest -from starlette import status import strawberry from strawberry.types import Info @@ -26,7 +25,7 @@ class Query: @strawberry.field def abc(self, info: Info) -> str: assert info.context.get("response") is not None - info.context["response"].status_code = status.HTTP_418_IM_A_TEAPOT + info.context["response"].status_code = 418 return "abc" schema = strawberry.Schema(query=Query) diff --git a/tests/views/schema.py b/tests/views/schema.py index 129d9dbad7..e5a3dc5377 100644 --- a/tests/views/schema.py +++ b/tests/views/schema.py @@ -26,12 +26,13 @@ def get_results(self) -> Dict[str, str]: def _read_file(text_file: Upload) -> str: - from starlette.datastructures import UploadFile + with contextlib.suppress(ModuleNotFoundError): + from starlette.datastructures import UploadFile - # allow to keep this function synchronous, starlette's files have - # async methods for reading - if isinstance(text_file, UploadFile): - text_file = text_file.file._file # type: ignore + # allow to keep this function synchronous, starlette's files have + # async methods for reading + if isinstance(text_file, UploadFile): + text_file = text_file.file._file # type: ignore with contextlib.suppress(ModuleNotFoundError): from starlite import UploadFile as StarliteUploadFile diff --git a/tests/websockets/conftest.py b/tests/websockets/conftest.py index f5c4d14025..00cfa6ac14 100644 --- a/tests/websockets/conftest.py +++ b/tests/websockets/conftest.py @@ -1,36 +1,42 @@ -from typing import Type +import importlib +from typing import Any, Generator, Type import pytest -from ..http.clients import ( - AioHttpClient, - AsgiHttpClient, - ChannelsHttpClient, - FastAPIHttpClient, - HttpClient, - StarliteHttpClient, -) - -clients = [ - AioHttpClient, - AsgiHttpClient, - FastAPIHttpClient, - ChannelsHttpClient, -] -ids = ["aio", "asgi", "fastapi", "channels"] -if StarliteHttpClient: - clients.append(StarliteHttpClient) - ids.append("starlite") - - -@pytest.fixture( - params=clients, - ids=ids, -) -def http_client_class(request) -> Type[HttpClient]: +from ..http.clients.base import HttpClient + + +def _get_http_client_classes() -> Generator[Any, None, None]: + for client, module, marks in [ + ("AioHttpClient", "aiohttp", [pytest.mark.aiohttp]), + ("AsgiHttpClient", "asgi", [pytest.mark.asgi]), + ("ChannelsHttpClient", "channels", [pytest.mark.channels]), + ("FastAPIHttpClient", "fastapi", [pytest.mark.fastapi]), + ("StarliteHttpClient", "starlite", [pytest.mark.starlite]), + ]: + try: + client_class = getattr( + importlib.import_module(f"tests.http.clients.{module}"), client + ) + except ImportError: + client_class = None + + yield pytest.param( + client_class, + marks=[ + *marks, + pytest.mark.skipif( + client_class is None, reason=f"Client {client} not found" + ), + ], + ) + + +@pytest.fixture(params=_get_http_client_classes()) +def http_client_class(request: Any) -> Type[HttpClient]: return request.param @pytest.fixture() -def http_client(http_client_class, event_loop) -> HttpClient: +def http_client(http_client_class: Type[HttpClient]) -> HttpClient: return http_client_class() diff --git a/tests/websockets/test_graphql_transport_ws.py b/tests/websockets/test_graphql_transport_ws.py index f472a8ec13..f3b3b54050 100644 --- a/tests/websockets/test_graphql_transport_ws.py +++ b/tests/websockets/test_graphql_transport_ws.py @@ -1,9 +1,12 @@ +from __future__ import annotations + import asyncio +import contextlib import json import sys import time from datetime import timedelta -from typing import AsyncGenerator, Type +from typing import TYPE_CHECKING, Any, AsyncGenerator, Type from unittest.mock import Mock, patch try: @@ -27,11 +30,11 @@ SubscribeMessage, SubscribeMessagePayload, ) -from tests.http.clients import AioHttpClient, ChannelsHttpClient from tests.http.clients.base import DebuggableGraphQLTransportWSMixin from tests.views.schema import Schema -from ..http.clients import HttpClient, WebSocketClient +if TYPE_CHECKING: + from ..http.clients.base import HttpClient, WebSocketClient @pytest_asyncio.fixture @@ -110,11 +113,17 @@ async def test_ws_messages_must_be_text(ws_raw: WebSocketClient): ws.assert_reason("WebSocket message type must be text") -async def test_connection_init_timeout(request, http_client_class: Type[HttpClient]): - if http_client_class == AioHttpClient: - pytest.skip( - "Closing a AIOHTTP WebSocket from a task currently doesnt work as expected" - ) +async def test_connection_init_timeout( + request: Any, http_client_class: Type[HttpClient] +): + with contextlib.suppress(ImportError): + from tests.http.clients.aiohttp import AioHttpClient + + if http_client_class == AioHttpClient: + pytest.skip( + "Closing a AIOHTTP WebSocket from a " + "task currently doesn't work as expected" + ) test_client = http_client_class() test_client.create_app(connection_init_wait_timeout=timedelta(seconds=0)) @@ -164,7 +173,7 @@ async def test_connection_init_timeout_cancellation( reason="Task name was introduced in 3.8 and we need it for this test", ) async def test_close_twice( - mocker: MockerFixture, request, http_client_class: Type[HttpClient] + mocker: MockerFixture, request: Any, http_client_class: Type[HttpClient] ): test_client = http_client_class() test_client.create_app(connection_init_wait_timeout=timedelta(seconds=0.25)) @@ -850,10 +859,15 @@ async def test_error_handler_for_timeout(http_client: HttpClient): Test that the error handler is called when the timeout task encounters an error """ - if isinstance(http_client, ChannelsHttpClient): - pytest.skip("Can't patch on_init for this client") + with contextlib.suppress(ImportError): + from tests.http.clients.channels import ChannelsHttpClient + + if isinstance(http_client, ChannelsHttpClient): + pytest.skip("Can't patch on_init for this client") + if not AsyncMock: pytest.skip("Don't have AsyncMock") + ws = ws_raw handler = None errorhandler = AsyncMock() diff --git a/tests/websockets/test_graphql_ws.py b/tests/websockets/test_graphql_ws.py index 6a07fb6ac4..83b7e66782 100644 --- a/tests/websockets/test_graphql_ws.py +++ b/tests/websockets/test_graphql_ws.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import asyncio -from typing import AsyncGenerator +from typing import TYPE_CHECKING, AsyncGenerator import pytest import pytest_asyncio @@ -18,7 +20,8 @@ GQL_STOP, ) -from ..http.clients import AioHttpClient, HttpClient, WebSocketClient +if TYPE_CHECKING: + from ..http.clients.aiohttp import HttpClient, WebSocketClient @pytest_asyncio.fixture @@ -408,7 +411,12 @@ async def test_task_cancellation_separation(aiohttp_app_client: HttpClient): # repr(Task) to check whether expected tasks are running. # This only works for aiohttp, where we are using the same event loop # on the client side and server. - aio = aiohttp_app_client == AioHttpClient + try: + from ..http.clients.aiohttp import AioHttpClient + + aio = aiohttp_app_client == AioHttpClient # type: ignore + except ImportError: + aio = False def get_result_handler_tasks(): return [