Skip to content

Commit 3230177

Browse files
committed
Experiment with moving starting implementations up a level in the CLI.
1 parent 843d3bb commit 3230177

File tree

1 file changed

+125
-100
lines changed

1 file changed

+125
-100
lines changed

bowtie/_cli.py

Lines changed: 125 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from importlib.resources import files
77
from io import BytesIO
88
from pathlib import Path
9-
from typing import TYPE_CHECKING, Any, Literal, ParamSpec, TextIO
9+
from typing import TYPE_CHECKING, Literal, ParamSpec, Protocol
1010
import asyncio
1111
import json
1212
import logging
@@ -51,7 +51,8 @@
5151
)
5252

5353
if TYPE_CHECKING:
54-
from collections.abc import Callable, Iterable, Mapping
54+
from collections.abc import Awaitable, Callable, Iterable, Mapping
55+
from typing import Any, TextIO
5556

5657
from referencing.jsonschema import Schema, SchemaRegistry
5758

@@ -172,6 +173,70 @@ def run(context: click.Context, *args: P.args, **kwargs: P.kwargs) -> None:
172173
return run
173174

174175

176+
class ImplementationSubcommand(Protocol):
177+
def __call__(
178+
self,
179+
implementations: Iterable[Implementation],
180+
**kwargs: Any,
181+
) -> Awaitable[int]:
182+
...
183+
184+
185+
def implementation_subcommand(fn: ImplementationSubcommand):
186+
"""
187+
Define a Bowtie subcommand which starts up some implementations.
188+
189+
Runs the wrapped function with only the successfully started
190+
implementations.
191+
"""
192+
193+
async def run(image_names: list[str], **kwargs: Any) -> int:
194+
exit_code = 0
195+
start = _start(
196+
image_names=image_names,
197+
make_validator=validator_for_dialect,
198+
reporter=_report.Reporter(write=lambda **_: None), # type: ignore[reportUnknownArgumentType]
199+
)
200+
201+
running: list[Implementation] = []
202+
async with start as implementations:
203+
for each in implementations:
204+
try:
205+
implementation = await each
206+
except NoSuchImage as error:
207+
exit_code |= _EX_CONFIG
208+
click.echo( # FIXME: respect a possible --quiet
209+
f"❗ (error): {error.name!r} is not a "
210+
"known Bowtie implementation.",
211+
file=sys.stderr,
212+
)
213+
continue
214+
215+
try:
216+
implementation.info()
217+
except StartupFailed:
218+
exit_code |= _EX_CONFIG
219+
click.echo( # FIXME: respect a possible --quiet
220+
f"❗ (error): {implementation.name} failed to start",
221+
file=sys.stderr,
222+
)
223+
continue
224+
225+
running.append(implementation)
226+
227+
exit_code |= await fn(implementations=running, **kwargs)
228+
229+
return exit_code
230+
231+
@subcommand
232+
@IMPLEMENTATION
233+
@wraps(fn)
234+
def cmd(image_names: list[str], *args: P.args, **kwargs: P.kwargs) -> int:
235+
return asyncio.run(run(*args, image_names=image_names, **kwargs))
236+
237+
return cmd
238+
239+
175240
@subcommand
176241
@click.option(
177242
"--input",
@@ -627,7 +692,7 @@ async def _info(image_names: list[str], format: _F):
627692
return exit_code
628693

629694

630-
@subcommand
695+
@implementation_subcommand # type: ignore[reportArgumentType]
631696
@click.option(
632697
"-q",
633698
"--quiet",
@@ -640,115 +705,75 @@ async def _info(image_names: list[str], format: _F):
640705
help="Don't print any output, just exit with nonzero status on failure.",
641706
)
642707
@FORMAT
643-
@IMPLEMENTATION
644-
def smoke(**kwargs: Any):
645-
"""
646-
Smoke test one or more implementations for basic correctness.
647-
"""
648-
return asyncio.run(_smoke(**kwargs))
649-
650-
651-
async def _smoke(
652-
image_names: list[str],
708+
async def smoke(
709+
implementations: Iterable[Implementation],
653710
format: _F,
654711
echo: Callable[..., None],
655-
):
656-
reporter = _report.Reporter(write=lambda **_: None) # type: ignore[reportUnknownArgumentType]
712+
) -> int:
657713
exit_code = 0
658714

659-
match format:
660-
case "json":
661-
662-
def finish() -> None:
663-
echo(json.dumps(serializable, indent=2))
664-
665-
case "pretty":
666-
667-
def finish():
668-
if exit_code:
669-
echo("\n❌ some failures", file=sys.stderr)
670-
else:
671-
echo("\n✅ all passed", file=sys.stderr)
672-
673-
async with _start(
674-
image_names=image_names,
675-
make_validator=validator_for_dialect,
676-
reporter=reporter,
677-
) as starting:
678-
for each in starting:
679-
try:
680-
implementation = await each
681-
except NoSuchImage as error:
682-
exit_code |= _EX_CONFIG
683-
echo(
684-
f"❗ (error): {error.name!r} is not a known Bowtie implementation.",
685-
file=sys.stderr,
686-
)
687-
continue
715+
for implementation in implementations:
716+
echo(f"Testing {implementation.name!r}...\n", file=sys.stderr)
717+
718+
# FIXME: All dialects / and/or newest dialect with proper sort
719+
dialect = max(implementation.info().dialects, key=str)
720+
runner = await implementation.start_speaking(dialect)
721+
722+
cases = [
723+
TestCase(
724+
description="allow-everything schema",
725+
schema={"$schema": str(dialect)},
726+
tests=[
727+
Test(description="First", instance=1, valid=True),
728+
Test(description="Second", instance="foo", valid=True),
729+
],
730+
),
731+
TestCase(
732+
description="allow-nothing schema",
733+
schema={"$schema": str(dialect), "not": {}},
734+
tests=[
735+
Test(description="First", instance=12, valid=False),
736+
],
737+
),
738+
]
688739

689-
echo(f"Testing {implementation.name!r}...\n", file=sys.stderr)
740+
match format:
741+
case "json":
742+
serializable: list[dict[str, Any]] = []
690743

691-
try:
692-
implementation.info()
693-
except StartupFailed:
694-
exit_code |= _EX_CONFIG
695-
click.echo(" ❗ (error): startup failed")
696-
continue
744+
def see(seq_case: SeqCase, result: SeqResult): # type: ignore[reportRedeclaration]
745+
serializable.append( # noqa: B023
746+
dict(
747+
case=seq_case.case.without_expected_results(),
748+
result=asdict(result.result),
749+
),
750+
)
697751

698-
# FIXME: All dialects / and/or newest dialect with proper sort
699-
dialect = max(implementation.info().dialects, key=str)
700-
runner = await implementation.start_speaking(dialect)
701-
702-
cases = [
703-
TestCase(
704-
description="allow-everything schema",
705-
schema={"$schema": str(dialect)},
706-
tests=[
707-
Test(description="First", instance=1, valid=True),
708-
Test(description="Second", instance="foo", valid=True),
709-
],
710-
),
711-
TestCase(
712-
description="allow-nothing schema",
713-
schema={"$schema": str(dialect), "not": {}},
714-
tests=[
715-
Test(description="First", instance=12, valid=False),
716-
],
717-
),
718-
]
752+
case "pretty":
719753

720-
match format:
721-
case "json":
722-
serializable: list[dict[str, Any]] = []
723-
724-
def see(seq_case: SeqCase, result: SeqResult): # type: ignore[reportRedeclaration]
725-
serializable.append( # noqa: B023
726-
dict(
727-
case=seq_case.case.without_expected_results(),
728-
result=asdict(result.result),
729-
),
730-
)
754+
def see(seq_case: SeqCase, response: SeqResult):
755+
signs = "".join(
756+
"❗" if succeeded is None else "✓" if succeeded else "✗"
757+
for succeeded in response.compare()
758+
)
759+
echo(f" · {seq_case.case.description}: {signs}")
731760

732-
case "pretty":
761+
for seq_case in SeqCase.for_cases(cases):
762+
result = await seq_case.run(runner=runner)
763+
if result.unsuccessful().causes_stop:
764+
exit_code |= _EX_DATAERR
765+
see(seq_case, result)
733766

734-
def see(seq_case: SeqCase, response: SeqResult):
735-
signs = "".join(
736-
"❗"
737-
if succeeded is None
738-
else "✓"
739-
if succeeded
740-
else "✗"
741-
for succeeded in response.compare()
742-
)
743-
echo(f" · {seq_case.case.description}: {signs}")
767+
match format:
768+
case "json":
769+
echo(json.dumps(serializable, indent=2)) # type: ignore[reportPossiblyUnboundVariable]
744770

745-
for seq_case in SeqCase.for_cases(cases):
746-
result = await seq_case.run(runner=runner)
747-
if result.unsuccessful().causes_stop:
748-
exit_code |= _EX_DATAERR
749-
see(seq_case, result)
771+
case "pretty":
772+
if exit_code:
773+
echo("\n❌ some failures", file=sys.stderr)
774+
else:
775+
echo("\n✅ all passed", file=sys.stderr)
750776

751-
finish()
752777
return exit_code
753778

754779

0 commit comments

Comments
 (0)