From 6e2a867f822ec94c7a78501d07c1db40ef174409 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 21 Feb 2026 15:30:07 -0500 Subject: [PATCH 1/3] New CLI infrastructure and updated info command --- arcade/__main__.py | 4 +- arcade/cli/__init__.py | 1 + arcade/cli/cli.py | 59 ++++++++++++++++++++++++++++ arcade/cli/commands/__init__.py | 4 ++ arcade/cli/commands/base.py | 21 ++++++++++ arcade/cli/commands/info.py | 38 ++++++++++++++++++ arcade/gl/backends/opengl/context.py | 2 + arcade/gl/backends/webgl/context.py | 1 + arcade/management/__init__.py | 48 ---------------------- pyproject.toml | 2 +- 10 files changed, 129 insertions(+), 51 deletions(-) create mode 100644 arcade/cli/__init__.py create mode 100644 arcade/cli/cli.py create mode 100644 arcade/cli/commands/__init__.py create mode 100644 arcade/cli/commands/base.py create mode 100644 arcade/cli/commands/info.py delete mode 100644 arcade/management/__init__.py diff --git a/arcade/__main__.py b/arcade/__main__.py index 1a318f0781..a2baf83815 100644 --- a/arcade/__main__.py +++ b/arcade/__main__.py @@ -1,4 +1,4 @@ -from arcade.management import show_info +from arcade.cli import run_arcade_cli if __name__ == "__main__": - show_info() + run_arcade_cli() diff --git a/arcade/cli/__init__.py b/arcade/cli/__init__.py new file mode 100644 index 0000000000..9479acd41a --- /dev/null +++ b/arcade/cli/__init__.py @@ -0,0 +1 @@ +from .cli import run_arcade_cli \ No newline at end of file diff --git a/arcade/cli/cli.py b/arcade/cli/cli.py new file mode 100644 index 0000000000..e57d680b23 --- /dev/null +++ b/arcade/cli/cli.py @@ -0,0 +1,59 @@ +import argparse +import sys +from typing import Type + +from .commands import BaseCommand, InfoCommand + +class CLI: + def __init__(self): + self.commands: dict[str, BaseCommand] = {} + self.prog: str = "arcade" + self.description: str = "Arcade Game Library CLI" + + def register_command(self, command_class: Type[BaseCommand]) -> None: + command = command_class() # type: ignore BaseCommand has different constructor than it's implementations + self.commands[command.name] = command + + def create_parser(self) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog=self.prog, + description=self.description, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + subparsers = parser.add_subparsers( + dest="command", + help="Available commands" + ) + + for command_name, command in self.commands.items(): + command_parser = subparsers.add_parser( + command_name, + help=command.help, + description=command.description, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + command.add_arguments(command_parser) + + return parser + + def run(self) -> int: + parser = self.create_parser() + args = parser.parse_args() + + if args.command is None: + parser.print_help() + return 0 + + try: + command = self.commands[args.command] + return command.handle(args) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +def run_arcade_cli(): + cli = CLI() + cli.register_command(InfoCommand) + return cli.run() \ No newline at end of file diff --git a/arcade/cli/commands/__init__.py b/arcade/cli/commands/__init__.py new file mode 100644 index 0000000000..773d1c01cc --- /dev/null +++ b/arcade/cli/commands/__init__.py @@ -0,0 +1,4 @@ +from .base import BaseCommand +from .info import InfoCommand + +__all__ = ["BaseCommand", "InfoCommand"] diff --git a/arcade/cli/commands/base.py b/arcade/cli/commands/base.py new file mode 100644 index 0000000000..da06cc64c3 --- /dev/null +++ b/arcade/cli/commands/base.py @@ -0,0 +1,21 @@ +import argparse +from abc import ABC, abstractmethod + + +class BaseCommand(ABC): + name: str + description: str + help: str + + def __init__(self, name: str, description: str, help: str) -> None: + self.name = name + self.description = description + self.help = help + + @abstractmethod + def add_arguments(self, parser: argparse.ArgumentParser) -> None: + pass + + @abstractmethod + def handle(self, args: argparse.Namespace) -> int: + pass diff --git a/arcade/cli/commands/info.py b/arcade/cli/commands/info.py new file mode 100644 index 0000000000..4ae3f2cf36 --- /dev/null +++ b/arcade/cli/commands/info.py @@ -0,0 +1,38 @@ +import argparse +import sys +from typing import override + +import arcade +import pyglet +import PIL + +from .base import BaseCommand + + +class InfoCommand(BaseCommand): + def __init__(self): + super().__init__( + name="info", + description="Print Arcade and System Information", + help="Print information about the installed Arcade version and system specifications", + ) + + @override + def add_arguments(self, parser: argparse.ArgumentParser) -> None: + pass + + @override + def handle(self, args: argparse.Namespace) -> int: + window = arcade.Window(visible=False) + version_str = f"Arcade {arcade.__version__}" + print() + print(version_str) + print("-" * len(version_str)) + print("vendor:", window.ctx.info.VENDOR) + print("device:", window.ctx.info.RENDERER) + print("renderer:", window.ctx.info.CTX_INFO) # type: ignore + print("python:", sys.version) + print("platform:", sys.platform) + print("pyglet version:", pyglet.version) + print("PIL version:", PIL.__version__) + return 1 diff --git a/arcade/gl/backends/opengl/context.py b/arcade/gl/backends/opengl/context.py index ca110c5d8b..dab2fc70a6 100644 --- a/arcade/gl/backends/opengl/context.py +++ b/arcade/gl/backends/opengl/context.py @@ -455,6 +455,8 @@ def __init__(self, ctx): self.MAJOR_VERSION = self.get(gl.GL_MAJOR_VERSION) """Major version number of the OpenGL API supported by the current context.""" + self.CTX_INFO = f"opengl {self.MAJOR_VERSION}.{self.MINOR_VERSION}" + self.MAX_COLOR_TEXTURE_SAMPLES = self.get(gl.GL_MAX_COLOR_TEXTURE_SAMPLES) """Maximum number of samples in a color multisample texture""" diff --git a/arcade/gl/backends/webgl/context.py b/arcade/gl/backends/webgl/context.py index 4ed4bd6429..7e39ec9ba6 100644 --- a/arcade/gl/backends/webgl/context.py +++ b/arcade/gl/backends/webgl/context.py @@ -395,6 +395,7 @@ class WebGLInfo(Info): def __init__(self, ctx: WebGLContext): super().__init__(ctx) self._ctx = ctx + self.CTX_INFO = "webgl" def get_int_tuple(self, enum, length: int): # TODO: this might not work diff --git a/arcade/management/__init__.py b/arcade/management/__init__.py deleted file mode 100644 index 1b098fcb3c..0000000000 --- a/arcade/management/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path -import shutil -import sys -import pyglet -import arcade -import PIL - -EXAMPLE_PATH = Path(__file__).parent.parent.resolve() / "examples" - - -def execute_from_command_line(): - if len(sys.argv) == 1: - show_info() - return - - command = sys.argv[1] - if command == "startproject": - start_project(sys.argv[2]) - else: - print("Unsupported command") - - -def show_info(): - window = arcade.Window() - version_str = f"Arcade {arcade.__version__}" - print() - print(version_str) - print("-" * len(version_str)) - print("vendor:", window.ctx.info.VENDOR) - print("renderer:", window.ctx.info.RENDERER) - # TODO: Abstracted GL backend - # The context doesn't necessarily have this, this will need changed later - # Probably the context needs to provide an info function that will spit back relevent stuff - # rather than hardcoding the things we want here - print("version:", window.ctx.gl_version) # type: ignore - print("python:", sys.version) - print("platform:", sys.platform) - print("pyglet version:", pyglet.version) - print("PIL version:", PIL.__version__) - - -def start_project(path_str: str): - path = Path(path_str) - if path.exists(): - print("File already exists") - - shutil.copy(EXAMPLE_PATH / "starting_template.py", path) - print("Created", path) diff --git a/pyproject.toml b/pyproject.toml index 04f865cf65..7db78ad5f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ dev = [ testing_libraries = ["pytest", "pytest-mock", "pytest-cov", "pyyaml==6.0.1"] [project.scripts] -arcade = "arcade.management:execute_from_command_line" +arcade = "arcade.cli:run_arcade_cli" [project.entry-points.pyinstaller40] hook-dirs = "arcade.__pyinstaller:get_hook_dirs" From 70f788b4e47a8248306b54c788b35abf5736fa9b Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 21 Feb 2026 15:36:50 -0500 Subject: [PATCH 2/3] formatting --- arcade/cli/__init__.py | 2 +- arcade/cli/cli.py | 12 +++++------- arcade/cli/commands/info.py | 5 +++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/arcade/cli/__init__.py b/arcade/cli/__init__.py index 9479acd41a..217c3738e4 100644 --- a/arcade/cli/__init__.py +++ b/arcade/cli/__init__.py @@ -1 +1 @@ -from .cli import run_arcade_cli \ No newline at end of file +from .cli import run_arcade_cli diff --git a/arcade/cli/cli.py b/arcade/cli/cli.py index e57d680b23..7b80d8204c 100644 --- a/arcade/cli/cli.py +++ b/arcade/cli/cli.py @@ -4,6 +4,7 @@ from .commands import BaseCommand, InfoCommand + class CLI: def __init__(self): self.commands: dict[str, BaseCommand] = {} @@ -18,20 +19,17 @@ def create_parser(self) -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog=self.prog, description=self.description, - formatter_class=argparse.RawDescriptionHelpFormatter + formatter_class=argparse.RawDescriptionHelpFormatter, ) - subparsers = parser.add_subparsers( - dest="command", - help="Available commands" - ) + subparsers = parser.add_subparsers(dest="command", help="Available commands") for command_name, command in self.commands.items(): command_parser = subparsers.add_parser( command_name, help=command.help, description=command.description, - formatter_class=argparse.RawDescriptionHelpFormatter + formatter_class=argparse.RawDescriptionHelpFormatter, ) command.add_arguments(command_parser) @@ -56,4 +54,4 @@ def run(self) -> int: def run_arcade_cli(): cli = CLI() cli.register_command(InfoCommand) - return cli.run() \ No newline at end of file + return cli.run() diff --git a/arcade/cli/commands/info.py b/arcade/cli/commands/info.py index 4ae3f2cf36..a1253a9ce9 100644 --- a/arcade/cli/commands/info.py +++ b/arcade/cli/commands/info.py @@ -2,9 +2,10 @@ import sys from typing import override -import arcade -import pyglet import PIL +import pyglet + +import arcade from .base import BaseCommand From 26b4b4b745e0fe6a99001e9f0407b575558d0a67 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 21 Feb 2026 15:38:22 -0500 Subject: [PATCH 3/3] typing.override didn't exist until 3.12 --- arcade/cli/commands/info.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/arcade/cli/commands/info.py b/arcade/cli/commands/info.py index a1253a9ce9..ba1328d61d 100644 --- a/arcade/cli/commands/info.py +++ b/arcade/cli/commands/info.py @@ -1,6 +1,5 @@ import argparse import sys -from typing import override import PIL import pyglet @@ -18,11 +17,9 @@ def __init__(self): help="Print information about the installed Arcade version and system specifications", ) - @override def add_arguments(self, parser: argparse.ArgumentParser) -> None: pass - @override def handle(self, args: argparse.Namespace) -> int: window = arcade.Window(visible=False) version_str = f"Arcade {arcade.__version__}"