diff --git a/src/groundlight/cli.py b/src/groundlight/cli.py index 9c967fae..7ea962d5 100644 --- a/src/groundlight/cli.py +++ b/src/groundlight/cli.py @@ -5,18 +5,36 @@ from typing_extensions import get_origin from groundlight import Groundlight +from groundlight.client import ApiTokenError +from groundlight.config import API_TOKEN_HELP_MESSAGE -cli_app = typer.Typer(no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}) +cli_app = typer.Typer( + no_args_is_help=True, + context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 800}, +) def class_func_to_cli(method): """ - Given the class method, simplify the typing on the inputs so that Typer can accept the method + Given the class method, create a method with the identical signature to provide the help documentation and + but only instantiates the class when the method is actually called. """ - @wraps(method) + # We create a fake class and fake method so we have the correct annotations for typer to use + # When we wrap the fake method, we only use the fake method's name to access the real method + # and attach it to a Groundlight instance that we create at function call time + class FakeClass: + pass + + fake_instance = FakeClass() + fake_method = method.__get__(fake_instance, FakeClass) + + @wraps(fake_method) def wrapper(*args, **kwargs): - print(method(*args, **kwargs)) # this is where we output to the console + gl = Groundlight() + gl_method = vars(Groundlight)[fake_method.__name__] + gl_bound_method = gl_method.__get__(gl, Groundlight) + print(gl_bound_method(*args, **kwargs)) # this is where we output to the console # not recommended practice to directly change annotations, but gets around Typer not supporting Union types for name, annotation in method.__annotations__.items(): @@ -30,15 +48,15 @@ def wrapper(*args, **kwargs): def groundlight(): - gl = Groundlight() - - # For each method in the Groundlight class, create a function that can be called from the command line - for name, method in vars(Groundlight).items(): - if callable(method) and not name.startswith("_"): - attached_method = method.__get__(gl) - cli_func = class_func_to_cli(attached_method) - cli_app.command()(cli_func) - cli_app() + try: + # For each method in the Groundlight class, create a function that can be called from the command line + for name, method in vars(Groundlight).items(): + if callable(method) and not name.startswith("_"): + cli_func = class_func_to_cli(method) + cli_app.command()(cli_func) + cli_app() + except ApiTokenError: + print(API_TOKEN_HELP_MESSAGE) if __name__ == "__main__": diff --git a/src/groundlight/client.py b/src/groundlight/client.py index a7d7948c..06096c4b 100644 --- a/src/groundlight/client.py +++ b/src/groundlight/client.py @@ -11,7 +11,7 @@ from openapi_client.model.detector_creation_input import DetectorCreationInput from groundlight.binary_labels import Label, convert_display_label_to_internal, convert_internal_label_to_display -from groundlight.config import API_TOKEN_VARIABLE_NAME, API_TOKEN_WEB_URL +from groundlight.config import API_TOKEN_HELP_MESSAGE, API_TOKEN_VARIABLE_NAME from groundlight.images import ByteStreamWrapper, parse_supported_image_types from groundlight.internalapi import ( GroundlightApiClient, @@ -62,13 +62,7 @@ def __init__(self, endpoint: Optional[str] = None, api_token: Optional[str] = No # Retrieve the API token from environment variable api_token = os.environ[API_TOKEN_VARIABLE_NAME] except KeyError as e: - raise ApiTokenError( - ( - "No API token found. Please put your token in an environment variable " - f'named "{API_TOKEN_VARIABLE_NAME}". If you don\'t have a token, you can ' - f"create one at {API_TOKEN_WEB_URL}" - ), - ) from e + raise ApiTokenError(API_TOKEN_HELP_MESSAGE) from e configuration.api_key["ApiToken"] = api_token diff --git a/src/groundlight/config.py b/src/groundlight/config.py index 2eb592ff..c92acb32 100644 --- a/src/groundlight/config.py +++ b/src/groundlight/config.py @@ -8,3 +8,9 @@ "API_TOKEN_VARIABLE_NAME", "DEFAULT_ENDPOINT", ] + +API_TOKEN_HELP_MESSAGE = ( + "No API token found. Please put your token in an environment variable " + f'named "{API_TOKEN_VARIABLE_NAME}". If you don\'t have a token, you can ' + f"create one at {API_TOKEN_WEB_URL}" +) diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py index 1558245f..9e8b99fe 100644 --- a/test/unit/test_cli.py +++ b/test/unit/test_cli.py @@ -1,6 +1,8 @@ +import os import re import subprocess from datetime import datetime +from unittest.mock import patch def test_list_detector(): @@ -72,12 +74,19 @@ def test_detector_and_image_queries(): assert completed_process.returncode == 0 +@patch.dict(os.environ, {}) def test_help(): completed_process = subprocess.run(["groundlight"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) assert completed_process.returncode == 0 - completed_process = subprocess.run(["groundlight"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + completed_process = subprocess.run(["groundlight", "-h"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) assert completed_process.returncode == 0 - completed_process = subprocess.run(["groundlight"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + completed_process = subprocess.run( + ["groundlight", "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + assert completed_process.returncode == 0 + completed_process = subprocess.run( + ["groundlight", "get-detector", "-h"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) assert completed_process.returncode == 0