Skip to content

Commit

Permalink
feat: Dynamic Output rendering
Browse files Browse the repository at this point in the history
Each operation can be broken down to multiple jobs. For instance, the
update operation can be broken down into: setting up bench dirs, env,
backups, requirements, etc.

Each step has a lot of output since Frappe requires a lot of complex
stuff to be done as a pre-requisite. Bench tries to simplify a lot in a
single command. Once, a step is completed, it's output is not really
required. So, we can ignore it to reduce the noise.

Here's where the `bench.cli.fancy` variable kicks in. Along with the
refactored log and new step wrapper, it makes the above thing possible.

At this point, there's no way to set this var from the end user...given
I'm still developing this. In the later commits, the idea is to pass a
flag similar to pip's --use-feature flag.
  • Loading branch information
gavindsouza committed Nov 17, 2021
1 parent 3995b92 commit f117959
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 2 deletions.
6 changes: 6 additions & 0 deletions bench/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
restart_supervisor_processes,
restart_systemd_processes,
)
from bench.utils.render import step


logger = logging.getLogger(bench.PROJECT_NAME)

Expand Down Expand Up @@ -137,6 +139,7 @@ def __init__(
self.bench = bench
super().__init__(name, branch, *args, **kwargs)

@step(title="Fetching App {repo}", success="App {repo} Fetched")
def get(self):
branch = f"--branch {self.tag}" if self.tag else ""
shallow = "--depth 1" if self.bench.shallow_clone else ""
Expand All @@ -150,6 +153,7 @@ def get(self):
cwd=os.path.join(self.bench.name, "apps"),
)

@step(title="Archiving App {repo}", success="App {repo} Archived")
def remove(self):
active_app_path = os.path.join("apps", self.repo)
archived_path = os.path.join("archived", "apps")
Expand All @@ -160,6 +164,7 @@ def remove(self):
log(f"App moved from {active_app_path} to {archived_app_path}")
shutil.move(active_app_path, archived_app_path)

@step(title="Installing App {repo}", success="App {repo} Installed")
def install(self, skip_assets=False, verbose=True):
from bench.utils.app import get_app_name

Expand All @@ -174,6 +179,7 @@ def install(self, skip_assets=False, verbose=True):
app=app_name, bench_path=self.bench.name, verbose=verbose, skip_assets=skip_assets,
)

@step(title="Uninstalling App {repo}", success="App {repo} Uninstalled")
def uninstall(self):
env_python = get_env_cmd("python", bench_path=self.bench.name)
self.bench.run(f"{env_python} -m pip uninstall -y {self.repo}")
Expand Down
8 changes: 8 additions & 0 deletions bench/bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
get_venv_path,
get_env_cmd,
)
from bench.utils.render import step


if TYPE_CHECKING:
Expand Down Expand Up @@ -119,6 +120,7 @@ def uninstall(self, app):
# self.build() - removed because it seems unnecessary
self.reload()

@step(title="Building Bench Assets", success="Bench Assets Built")
def build(self):
# build assets & stuff
run_frappe_cmd("build", bench_path=self.name)
Expand Down Expand Up @@ -214,12 +216,14 @@ def __init__(self, bench: Bench):
self.bench = bench
self.cwd = self.bench.cwd

@step(title="Setting Up Directories", success="Directories Set Up")
def dirs(self):
os.makedirs(self.bench.name, exist_ok=True)

for dirname in paths_in_bench:
os.makedirs(os.path.join(self.bench.name, dirname), exist_ok=True)

@step(title="Setting Up Environment", success="Environment Set Up")
def env(self, python="python3"):
"""Setup env folder
- create env if not exists
Expand All @@ -238,6 +242,7 @@ def env(self, python="python3"):
if os.path.exists(frappe):
self.run(f"{env_python} -m pip install -q -U -e {frappe}")

@step(title="Setting Up Bench Config", success="Bench Config Set Up")
def config(self, redis=True, procfile=True):
"""Setup config folder
- create pids folder
Expand All @@ -260,12 +265,14 @@ def logging(self):

return setup_logging(bench_path=self.bench.name)

@step(title="Setting Up Bench Patches", success="Bench Patches Set Up")
def patches(self):
shutil.copy(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "patches", "patches.txt"),
os.path.join(self.bench.name, "patches.txt"),
)

@step(title="Setting Up Backups Cronjob", success="Backups Cronjob Set Up")
def backups(self):
# TODO: to something better for logging data? - maybe a wrapper that auto-logs with more context
logger.log("setting up backups")
Expand All @@ -288,6 +295,7 @@ def backups(self):

logger.log("backups were set up")

@step(title="Setting Up Bench Dependencies", success="Bench Dependencies Set Up")
def requirements(self):
from bench.utils.bench import update_requirements
update_requirements(bench=self.bench)
Expand Down
8 changes: 6 additions & 2 deletions bench/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@
)
from bench.utils.bench import get_env_cmd

fancy = True
from_command_line = False
# these variables are used to show dynamic outputs on the terminal
fancy = False
bench.LOG_BUFFER = []

# set when commands are executed via the CLI
from_command_line = False

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

Expand Down
65 changes: 65 additions & 0 deletions bench/utils/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# imports - standard imports
import sys
from io import StringIO

# imports - third party imports
import click


class Capturing(list):
"""
Util to consume the stdout encompassed in it and push it to a list
with Capturing() as output:
subprocess.check_output("ls", shell=True)
print(output)
# ["b'Applications\\nDesktop\\nDocuments\\nDownloads\\n'"]
"""

def __enter__(self):
self._stdout = sys.stdout
sys.stdout = self._stringio = StringIO()
return self

def __exit__(self, *args):
self.extend(self._stringio.getvalue().splitlines())
del self._stringio # free up some memory
sys.stdout = self._stdout


def step(title: str = None, success: str = None):
"""Supposed to be wrapped around the smallest possible atomic step in a given operation.
For instance, `building assets` is a step in the update operation.
"""

def innfn(fn):
def wrapper_fn(*args, **kwargs):
import bench.cli

if bench.cli.from_command_line and bench.cli.fancy:
kw = args[0].__dict__

_title = f"{click.style('⏼', fg='bright_yellow')} {title.format(**kw)}"
click.secho(_title)

retval = fn(*args)

if bench.cli.from_command_line and bench.cli.fancy:
click.clear()

for l in bench.LOG_BUFFER:
click.secho(l["message"], fg=l["color"])

_success = f"{click.style('✔', fg='green')} {success.format(**kw)}"
click.echo(_success)

bench.LOG_BUFFER.append(
{"message": _success, "color": None,}
)

return retval

return wrapper_fn

return innfn

0 comments on commit f117959

Please sign in to comment.