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 870493aa..a0631101 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,9 +68,9 @@ jobs: python-version: 3.8 - name: Install test dependencies run: | - python -m pip install --upgrade pip - pip install .[test] + python -m pip install --upgrade pip wheel + pip install -e.[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 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 78d82551..27a562b2 100644 --- a/gql/cli.py +++ b/gql/cli.py @@ -1,5 +1,7 @@ +import asyncio import json import logging +import signal as signal_module import sys from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter from typing import Any, Dict, Optional @@ -407,3 +409,46 @@ 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_name in ["SIGINT", "SIGTERM", "CTRL_C_EVENT", "CTRL_BREAK_EVENT"]: + signal = getattr(signal_module, signal_name, None) + + if signal is None: + continue + + 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 + try: + exit_code = loop.run_until_complete(main_task) + finally: + loop.close() + + # Return with the correct exit code + sys.exit(exit_code) + except KeyboardInterrupt: # pragma: no cover + pass 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 diff --git a/setup.py b/setup.py index 1a46c4db..a8b58737 100644 --- a/setup.py +++ b/setup.py @@ -7,14 +7,15 @@ "yarl>=1.6,<2.0", ] -scripts = [ - "scripts/gql-cli", +console_scripts = [ + "gql-cli=gql.cli:gql_cli", ] tests_requires = [ "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", @@ -106,5 +107,5 @@ include_package_data=True, zip_safe=False, platforms="any", - scripts=scripts, + entry_points={"console_scripts": console_scripts}, ) 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 ec268422..9066544b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,7 @@ import pytest +from gql import __version__ from gql.cli import ( get_execute_args, get_parser, @@ -347,3 +348,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 == ""