Skip to content

Commit

Permalink
refactor(cli): Commands Resolution
Browse files Browse the repository at this point in the history
The implementations so far were hacks that worked for the most used
commands but broken down when challenged or expected to maintain
documented usages [eg: custom app commands].

The current implementation consists of a two step approach:
1. figure out command name that user is trying to execute
2. pass the directive to the app (bench, frappe or other) that consists of the cmd

---

For tackling #1, get_cmd_from_sysargv contains exhaustive rules that
cover all (that i know and ive come across) combinations of valid
frappe commands.

For problem #2, a simple check in click's Group object does the trick.

Tested with possible known commands with combinations of context flags
and params, with bench, frappe & external app's commands
  • Loading branch information
gavindsouza committed Jul 27, 2022
1 parent beac865 commit a6f1964
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 37 deletions.
59 changes: 31 additions & 28 deletions bench/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# imports - standard imports
import atexit
from contextlib import contextmanager
import json
from logging import Logger
import os
import pwd
import sys
Expand All @@ -25,7 +27,7 @@
is_root,
log,
setup_logging,
parse_sys_argv,
get_cmd_from_sysargv,
)
from bench.utils.bench import get_env_cmd

Expand All @@ -35,23 +37,41 @@
is_envvar_warn_set = None
from_command_line = False # set when commands are executed via the CLI
bench.LOG_BUFFER = []
sys_argv = None

change_uid_msg = "You should not run this command as root"
src = os.path.dirname(__file__)


@contextmanager
def execute_cmd(check_for_update=True, command: str = None, logger: Logger = None):
if check_for_update:
atexit.register(check_latest_version)

try:
yield
except BaseException as e:
return_code = getattr(e, "code", 1)

if isinstance(e, Exception):
click.secho(f"ERROR: {e}", fg="red")

if return_code:
logger.warning(f"{command} executed with exit code {return_code}")

raise e


def cli():
global from_command_line, bench_config, is_envvar_warn_set, verbose, sys_argv
global from_command_line, bench_config, is_envvar_warn_set, verbose

from_command_line = True
command = " ".join(sys.argv)
argv = set(sys.argv)
is_envvar_warn_set = not (os.environ.get("BENCH_DEVELOPER") or os.environ.get("CI"))
is_cli_command = len(sys.argv) > 1 and not argv.intersection({"src", "--version"})
sys_argv = parse_sys_argv()
cmd_from_sys = get_cmd_from_sysargv()

if "--verbose" in sys_argv.options:
if "--verbose" in argv:
verbose = True

change_working_directory()
Expand All @@ -69,8 +89,8 @@ def cli():
if (
is_envvar_warn_set
and is_cli_command
and is_dist_editable(bench.PROJECT_NAME)
and not bench_config.get("developer_mode")
and is_dist_editable(bench.PROJECT_NAME)
):
log(
"bench is installed in editable mode!\n\nThis is not the recommended mode"
Expand All @@ -95,31 +115,14 @@ def cli():
print(get_frappe_help())
return

if (
sys_argv.commands.intersection(get_cached_frappe_commands())
or sys_argv.commands.intersection(get_frappe_commands())
):
if cmd_from_sys in bench_command.commands:
with execute_cmd(check_for_update=not is_cli_command, command=command, logger=logger):
bench_command()
elif cmd_from_sys in get_frappe_commands():
frappe_cmd()

if sys.argv[1] in Bench(".").apps:
else:
app_cmd()

if not is_cli_command:
atexit.register(check_latest_version)

try:
bench_command()
except BaseException as e:
return_code = getattr(e, "code", 1)

if isinstance(e, Exception):
click.secho(f"ERROR: {e}", fg="red")

if return_code:
logger.warning(f"{command} executed with exit code {return_code}")

raise e


def check_uid():
if cmd_requires_root() and not is_root():
Expand Down
46 changes: 37 additions & 9 deletions bench/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def which(executable: str, raise_err: bool = False) -> str:
return exec_


def setup_logging(bench_path=".") -> "logger":
def setup_logging(bench_path=".") -> logging.Logger:
LOG_LEVEL = 15
logging.addLevelName(LOG_LEVEL, "LOG")

Expand Down Expand Up @@ -529,13 +529,41 @@ def copy(self):
return _dict(dict(self).copy())


def parse_sys_argv():
sys_argv = _dict(options=set(), commands=set())
def get_cmd_from_sysargv():
"""Identify and segregate tokens to options and command
for c in sys.argv[1:]:
if c.startswith("-"):
sys_argv.options.add(c)
else:
sys_argv.commands.add(c)
For Command: `bench --profile --site frappeframework.com migrate --no-backup`
sys.argv: ["/home/frappe/.local/bin/bench", "--profile", "--site", "frappeframework.com", "migrate", "--no-backup"]
Actual command run: migrate
"""
# context is passed as options to frappe's bench_helper
from bench.bench import Bench
frappe_context = _dict(
params={"--site"},
flags={"--verbose", "--profile", "--force"}
)
cmd_from_ctx = None
sys_argv = sys.argv[1:]
skip_next = False

for arg in sys_argv:
if skip_next:
skip_next = False
continue

if arg in frappe_context.flags:
continue

elif arg in frappe_context.params:
skip_next = True
continue

if sys_argv.index(arg) == 0 and arg in Bench(".").apps:
continue

cmd_from_ctx = arg

break

return sys_argv
return cmd_from_ctx

0 comments on commit a6f1964

Please sign in to comment.