From dbacf97250894afd9175546e1be5c72b34f617cc Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sat, 26 Mar 2022 21:49:41 -0500 Subject: [PATCH 01/14] replace use of scripts with cross-platform entry_points --- MANIFEST.in | 2 -- Makefile | 2 +- gql/cli.py | 40 ++++++++++++++++++++++++++++++++++++++++ setup.py | 6 +++--- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 73d59a18..c0f653ab 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,8 +11,6 @@ include Makefile include tox.ini -include scripts/gql-cli - include gql/py.typed recursive-include tests *.py *.graphql *.cnf *.yaml *.pem diff --git a/Makefile b/Makefile index 6baff50f..2275092c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean tests docs -SRC_PYTHON := gql tests scripts/gql-cli docs/code_examples +SRC_PYTHON := gql tests docs/code_examples dev-setup: python pip install -e ".[test]" diff --git a/gql/cli.py b/gql/cli.py index 1e248081..7e936d7a 100644 --- a/gql/cli.py +++ b/gql/cli.py @@ -1,7 +1,9 @@ +import asyncio import json import logging import sys from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter +from signal import SIGINT, SIGTERM from typing import Any, Dict, Optional from graphql import GraphQLError, print_schema @@ -403,3 +405,41 @@ async def main(args: Namespace) -> int: exit_code = 1 return exit_code + + +def gql_cli() -> None: + """Synchronously invoke ``main`` with the parsed command line arguments. + + Formerly ``scripts/gql-cli``, now registered as an ``entry_point`` + """ + # Get arguments from command line + parser = get_parser(with_examples=True) + args = parser.parse_args() + + try: + # Create a new asyncio event loop + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Create a gql-cli task with the supplied arguments + main_task = asyncio.ensure_future(main(args), loop=loop) + + # Add signal handlers to close gql-cli cleanly on Control-C + for signal in [SIGINT, SIGTERM]: + loop.add_signal_handler(signal, main_task.cancel) + + # Run the asyncio loop to execute the task + exit_code = 0 + try: + exit_code = loop.run_until_complete(main_task) + finally: + loop.close() + + # Return with the correct exit code + sys.exit(exit_code) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + gql_cli() diff --git a/setup.py b/setup.py index 07bab00e..952932cf 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ "yarl>=1.6,<2.0", ] -scripts = [ - "scripts/gql-cli", +console_scripts = [ + "gql-cli=gql.cli:gql_cli", ] tests_requires = [ @@ -106,5 +106,5 @@ include_package_data=True, zip_safe=False, platforms="any", - scripts=scripts, + entry_scripts={"console_scripts": console_scripts}, ) From ee2858a2215414dc681984cae55f8e372e00de69 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sat, 26 Mar 2022 21:54:23 -0500 Subject: [PATCH 02/14] build wheel --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a5800732..2a6cdc6b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,9 +18,9 @@ jobs: - name: Build wheel and source tarball run: | pip install wheel - python setup.py sdist + python setup.py sdist bdist_wheel - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@v1.1.0 with: user: __token__ - password: ${{ secrets.pypi_password }} \ No newline at end of file + password: ${{ secrets.pypi_password }} From 26f6fc92a4140382b0d045df961f167a1884af5e Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 08:37:59 -0500 Subject: [PATCH 03/14] remove legacy script --- scripts/gql-cli | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100755 scripts/gql-cli diff --git a/scripts/gql-cli b/scripts/gql-cli deleted file mode 100755 index b2a079a3..00000000 --- a/scripts/gql-cli +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -import asyncio -import sys -from signal import SIGINT, SIGTERM - -from gql.cli import get_parser, main - -# Get arguments from command line -parser = get_parser(with_examples=True) -args = parser.parse_args() - -try: - # Create a new asyncio event loop - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Create a gql-cli task with the supplied arguments - main_task = asyncio.ensure_future(main(args), loop=loop) - - # Add signal handlers to close gql-cli cleanly on Control-C - for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler(signal, main_task.cancel) - - # Run the asyncio loop to execute the task - exit_code = 0 - try: - exit_code = loop.run_until_complete(main_task) - finally: - loop.close() - - # Return with the correct exit code - sys.exit(exit_code) -except KeyboardInterrupt: - pass From 8d9cefa22f4e2a20afe267c43cacdced6f059850 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 10:50:50 -0500 Subject: [PATCH 04/14] fix entry_point name --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 952932cf..a9c0faca 100644 --- a/setup.py +++ b/setup.py @@ -106,5 +106,5 @@ include_package_data=True, zip_safe=False, platforms="any", - entry_scripts={"console_scripts": console_scripts}, + entry_points={"console_scripts": console_scripts}, ) From 41500c615890ed047821a501c8722056dfb544b5 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 11:58:39 -0500 Subject: [PATCH 05/14] add console_scripts tests, pytest-console-scripts --- gql/cli.py | 6 +--- setup.py | 1 + tests/test_console_scripts.py | 56 +++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 tests/test_console_scripts.py diff --git a/gql/cli.py b/gql/cli.py index 7e936d7a..cc349ff5 100644 --- a/gql/cli.py +++ b/gql/cli.py @@ -437,9 +437,5 @@ def gql_cli() -> None: # Return with the correct exit code sys.exit(exit_code) - except KeyboardInterrupt: + except KeyboardInterrupt: # pragma: no cover pass - - -if __name__ == "__main__": - gql_cli() diff --git a/setup.py b/setup.py index a9c0faca..974ad011 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ "parse==1.15.0", "pytest==6.2.5", "pytest-asyncio==0.16.0", + "pytest-console-scripts==1.3.1", "pytest-cov==3.0.0", "mock==4.0.2", "vcrpy==4.0.2", diff --git a/tests/test_console_scripts.py b/tests/test_console_scripts.py new file mode 100644 index 00000000..2a1ddfb0 --- /dev/null +++ b/tests/test_console_scripts.py @@ -0,0 +1,56 @@ +"""tests of console_scripts entry_point (formerly scripts/gql-cli)""" +import io +import json + +import pytest + +from gql import __version__ + +from .test_aiohttp import query1_server_answer, query1_server_answer_data, query1_str + + +def test_cli_ep_version(script_runner): + ret = script_runner.run("gql-cli", "--version") + + assert ret.success + + assert ret.stdout == f"v{__version__}\n" + assert ret.stderr == "" + + +@pytest.mark.asyncio +@pytest.mark.script_launch_mode("subprocess") +async def test_ep_aiohttp_using_cli( + event_loop, aiohttp_server, monkeypatch, script_runner, run_sync_test +): + from aiohttp import web + + async def handler(request): + return web.Response(text=query1_server_answer, content_type="application/json") + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = str(server.make_url("/")) + + def test_code(): + + monkeypatch.setattr("sys.stdin", io.StringIO(query1_str)) + + ret = script_runner.run( + "gql-cli", url, "--verbose", stdin=io.StringIO(query1_str) + ) + + assert ret.success + + # Check that the result has been printed on stdout + captured_out = str(ret.stdout).strip() + + expected_answer = json.loads(query1_server_answer_data) + print(f"Captured: {captured_out}") + received_answer = json.loads(captured_out) + + assert received_answer == expected_answer + + await run_sync_test(event_loop, server, test_code) From 317998bc1e92d2306b7489eeb09fce825f4c22d4 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 15:00:12 -0500 Subject: [PATCH 06/14] handle unimplemented signals on windows --- gql/cli.py | 6 +++++- tests/test_console_scripts.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gql/cli.py b/gql/cli.py index cc349ff5..b40f0548 100644 --- a/gql/cli.py +++ b/gql/cli.py @@ -426,7 +426,11 @@ def gql_cli() -> None: # Add signal handlers to close gql-cli cleanly on Control-C for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler(signal, main_task.cancel) + try: + loop.add_signal_handler(signal, main_task.cancel) + except NotImplementedError: # pragma: no cover + # not all signals supported on all platforms + pass # Run the asyncio loop to execute the task exit_code = 0 diff --git a/tests/test_console_scripts.py b/tests/test_console_scripts.py index 2a1ddfb0..ed5bec8e 100644 --- a/tests/test_console_scripts.py +++ b/tests/test_console_scripts.py @@ -20,7 +20,7 @@ def test_cli_ep_version(script_runner): @pytest.mark.asyncio @pytest.mark.script_launch_mode("subprocess") -async def test_ep_aiohttp_using_cli( +async def test_cli_ep_aiohttp_using_cli( event_loop, aiohttp_server, monkeypatch, script_runner, run_sync_test ): from aiohttp import web From 2f24dbe0efc1d9507e9c4397fc06a7a850d7df1c Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 15:04:34 -0500 Subject: [PATCH 07/14] handle more windows signals --- gql/cli.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gql/cli.py b/gql/cli.py index b40f0548..fa33db90 100644 --- a/gql/cli.py +++ b/gql/cli.py @@ -1,9 +1,9 @@ import asyncio import json import logging +import signal as signal_module import sys from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter -from signal import SIGINT, SIGTERM from typing import Any, Dict, Optional from graphql import GraphQLError, print_schema @@ -425,7 +425,12 @@ def gql_cli() -> None: main_task = asyncio.ensure_future(main(args), loop=loop) # Add signal handlers to close gql-cli cleanly on Control-C - for signal in [SIGINT, SIGTERM]: + for signal_name in ["SIGINT", "SIGTERM", "CTRL_C_EVENT", "CTRL_BREAK_EVENT"]: + signal = getattr(signal_module, signal_name) + + if signal is None: + continue + try: loop.add_signal_handler(signal, main_task.cancel) except NotImplementedError: # pragma: no cover From 5a33a3a850279e498d40aeee48d80251331a88c0 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 15:06:24 -0500 Subject: [PATCH 08/14] getattr needs default argument --- gql/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gql/cli.py b/gql/cli.py index fa33db90..10a8959a 100644 --- a/gql/cli.py +++ b/gql/cli.py @@ -426,7 +426,7 @@ def gql_cli() -> None: # Add signal handlers to close gql-cli cleanly on Control-C for signal_name in ["SIGINT", "SIGTERM", "CTRL_C_EVENT", "CTRL_BREAK_EVENT"]: - signal = getattr(signal_module, signal_name) + signal = getattr(signal_module, signal_name, None) if signal is None: continue From 0db64aedbe54ae9a30fb6ca4ba5eb1b6a79fb376 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 15:11:22 -0500 Subject: [PATCH 09/14] move aiohttp import inside entry_point test --- tests/test_console_scripts.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_console_scripts.py b/tests/test_console_scripts.py index ed5bec8e..382fc017 100644 --- a/tests/test_console_scripts.py +++ b/tests/test_console_scripts.py @@ -6,8 +6,6 @@ from gql import __version__ -from .test_aiohttp import query1_server_answer, query1_server_answer_data, query1_str - def test_cli_ep_version(script_runner): ret = script_runner.run("gql-cli", "--version") @@ -25,6 +23,12 @@ async def test_cli_ep_aiohttp_using_cli( ): from aiohttp import web + from .test_aiohttp import ( + query1_server_answer, + query1_server_answer_data, + query1_str, + ) + async def handler(request): return web.Response(text=query1_server_answer, content_type="application/json") From 28d97fa16b781cf785e2fb06116e5f0f5c30c002 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 15:28:19 -0500 Subject: [PATCH 10/14] move console_script tests back into integrated files --- tests/test_aiohttp.py | 38 ++++++++++++++++++++++ tests/test_cli.py | 11 +++++++ tests/test_console_scripts.py | 60 ----------------------------------- 3 files changed, 49 insertions(+), 60 deletions(-) delete mode 100644 tests/test_console_scripts.py diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index ab02e8f5..2535ddb3 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -1016,6 +1016,44 @@ async def handler(request): assert received_answer == expected_answer +@pytest.mark.asyncio +@pytest.mark.script_launch_mode("subprocess") +async def test_aiohttp_using_cli_ep( + event_loop, aiohttp_server, monkeypatch, script_runner, run_sync_test +): + from aiohttp import web + + async def handler(request): + return web.Response(text=query1_server_answer, content_type="application/json") + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = str(server.make_url("/")) + + def test_code(): + + monkeypatch.setattr("sys.stdin", io.StringIO(query1_str)) + + ret = script_runner.run( + "gql-cli", url, "--verbose", stdin=io.StringIO(query1_str) + ) + + assert ret.success + + # Check that the result has been printed on stdout + captured_out = str(ret.stdout).strip() + + expected_answer = json.loads(query1_server_answer_data) + print(f"Captured: {captured_out}") + received_answer = json.loads(captured_out) + + assert received_answer == expected_answer + + await run_sync_test(event_loop, server, test_code) + + @pytest.mark.asyncio async def test_aiohttp_using_cli_invalid_param( event_loop, aiohttp_server, monkeypatch, capsys diff --git a/tests/test_cli.py b/tests/test_cli.py index 8df47a63..c0a59a3e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,8 @@ import pytest +from gql import __version__ + from gql.cli import ( get_execute_args, get_parser, @@ -338,3 +340,12 @@ def test_cli_get_transport_no_protocol(parser): with pytest.raises(ValueError): get_transport(args) + + +def test_cli_ep_version(script_runner): + ret = script_runner.run("gql-cli", "--version") + + assert ret.success + + assert ret.stdout == f"v{__version__}\n" + assert ret.stderr == "" diff --git a/tests/test_console_scripts.py b/tests/test_console_scripts.py deleted file mode 100644 index 382fc017..00000000 --- a/tests/test_console_scripts.py +++ /dev/null @@ -1,60 +0,0 @@ -"""tests of console_scripts entry_point (formerly scripts/gql-cli)""" -import io -import json - -import pytest - -from gql import __version__ - - -def test_cli_ep_version(script_runner): - ret = script_runner.run("gql-cli", "--version") - - assert ret.success - - assert ret.stdout == f"v{__version__}\n" - assert ret.stderr == "" - - -@pytest.mark.asyncio -@pytest.mark.script_launch_mode("subprocess") -async def test_cli_ep_aiohttp_using_cli( - event_loop, aiohttp_server, monkeypatch, script_runner, run_sync_test -): - from aiohttp import web - - from .test_aiohttp import ( - query1_server_answer, - query1_server_answer_data, - query1_str, - ) - - async def handler(request): - return web.Response(text=query1_server_answer, content_type="application/json") - - app = web.Application() - app.router.add_route("POST", "/", handler) - server = await aiohttp_server(app) - - url = str(server.make_url("/")) - - def test_code(): - - monkeypatch.setattr("sys.stdin", io.StringIO(query1_str)) - - ret = script_runner.run( - "gql-cli", url, "--verbose", stdin=io.StringIO(query1_str) - ) - - assert ret.success - - # Check that the result has been printed on stdout - captured_out = str(ret.stdout).strip() - - expected_answer = json.loads(query1_server_answer_data) - print(f"Captured: {captured_out}") - received_answer = json.loads(captured_out) - - assert received_answer == expected_answer - - await run_sync_test(event_loop, server, test_code) From d380c47274fb92a8445523f6e4bef981906424c4 Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sun, 27 Mar 2022 15:35:24 -0500 Subject: [PATCH 11/14] linting --- tests/test_cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c0a59a3e..e7b760e1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,7 +3,6 @@ import pytest from gql import __version__ - from gql.cli import ( get_execute_args, get_parser, From f81c3edd19f47a280a1cad49d45528703957435a Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Mon, 28 Mar 2022 21:01:03 -0500 Subject: [PATCH 12/14] add terminal report to coverage job --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 870493aa..04c5f12a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -71,6 +71,6 @@ jobs: python -m pip install --upgrade pip pip install .[test] - name: Test with coverage - run: pytest --cov=gql --cov-report=xml tests + run: pytest --cov=gql --cov-report=xml --cov-report=term-missing tests - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 From f3a3e526906bdeb68ae91d2902a50b4beea6250a Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Mon, 28 Mar 2022 21:07:19 -0500 Subject: [PATCH 13/14] ensure up-to-date wheel is installed in all CI environments --- .github/workflows/lint.yml | 2 +- .github/workflows/tests.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dffc5c4b..6ed6d6ea 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: python-version: 3.8 - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel pip install tox - name: Run lint and static type checks run: tox diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 04c5f12a..c7d934a0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel pip install tox tox-gh-actions - name: Test with tox run: tox @@ -52,7 +52,7 @@ jobs: python-version: 3.8 - name: Install dependencies with only ${{ matrix.dependency }} extra dependency run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel pip install .[${{ matrix.dependency }},test_no_transport] - name: Test with --${{ matrix.dependency }}-only run: pytest tests --${{ matrix.dependency }}-only @@ -68,7 +68,7 @@ jobs: python-version: 3.8 - name: Install test dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel pip install .[test] - name: Test with coverage run: pytest --cov=gql --cov-report=xml --cov-report=term-missing tests From f5413483e00340822c5411e74e40256e5b2ac948 Mon Sep 17 00:00:00 2001 From: Hanusz Leszek Date: Sat, 9 Apr 2022 18:04:14 +0200 Subject: [PATCH 14/14] Trying to fix coverage report with pip install -e --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7d934a0..a0631101 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,7 +69,7 @@ jobs: - name: Install test dependencies run: | python -m pip install --upgrade pip wheel - pip install .[test] + pip install -e.[test] - name: Test with coverage run: pytest --cov=gql --cov-report=xml --cov-report=term-missing tests - name: Upload coverage to Codecov