Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions src/groundlight/cli.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the help message, it truncates the descriptions of the sub-commands. Is there a way to have it print out the full line?

Commands:
  add-label                       Add a new label to an image query.
  create-detector                 Create a new detector with a...
  get-detector                    Get a detector by id
  get-detector-by-name            Get a detector by name
  get-image-query                 Get an image query by id
  get-or-create-detector          Tries to look up the detector by...
  list-detectors                  List out detectors you own
  list-image-queries              List out image queries you own
  start-inspection                For users with Inspection Reports...
  stop-inspection                 For users with Inspection Reports...
  submit-image-query              Evaluates an image with Groundlight.
  update-detector-confidence-threshold
                                  Updates the confidence threshold...
  update-inspection-metadata      For users with Inspection Reports...
  wait-for-confident-result       Waits for an image query result's...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the help page for a specific command, the description's formatting can be a little weird because it doesn't respect single newlines. (I understand that typer.Argument() would fix this if we could use it, but it doesn't work because of Union types).

❯ poetry run groundlight add-label -h
Usage: groundlight add-label [OPTIONS] IMAGE_QUERY LABEL

  Add a new label to an image query.  This answers the detector's question.
  :param image_query: Either an ImageQuery object (returned from
  `submit_image_query`) or an image_query id as a string. :param label: The
  string "YES" or the string "NO" in answer to the query.

Arguments:
  IMAGE_QUERY  [required]
  LABEL        [required]

Options:
  -h, --help  Show this message and exit.

But I think the fix is actually pretty easy -- when we want a newline to show up in the generated message, we should just put in a blank line in the docstring. For example:

    def add_label(self, image_query: Union[ImageQuery, str], label: Union[Label, str]):
        """Add a new label to an image query.  This answers the detector's question.

        :param image_query: Either an ImageQuery object (returned from `submit_image_query`) or
        an image_query id as a string.

        :param label: The string "YES" or the string "NO" in answer to the query.
        """

Which then looks better:

poetry run groundlight add-label -h
Usage: groundlight add-label [OPTIONS] IMAGE_QUERY LABEL

  Add a new label to an image query.  This answers the detector's question.

  :param image_query: Either an ImageQuery object (returned from
  `submit_image_query`) or an image_query id as a string.

  :param label: The string "YES" or the string "NO" in answer to the query.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstrings are undergoing continual improvement, they'll all hopefully use the double new line once we finish working out the sphinx stuff.

Turns out not truncating lines was just a matter of adding a param to the context_settings of the typer app.

Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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__":
Expand Down
10 changes: 2 additions & 8 deletions src/groundlight/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions src/groundlight/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)
Comment on lines +12 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great idea to standardize this

13 changes: 11 additions & 2 deletions test/unit/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import re
import subprocess
from datetime import datetime
from unittest.mock import patch


def test_list_detector():
Expand Down Expand Up @@ -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


Expand Down