diff --git a/README.md b/README.md index 23bed7f..72c7272 100644 --- a/README.md +++ b/README.md @@ -251,3 +251,29 @@ or for `zsh` add the following line to your `~/.zshrc` file: ```bash eval "$(_GRIDTK_COMPLETE=zsh_source gridtk)" ``` + +### Adjusting `gridtk list` Output + +By default, `gridtk list` outputs a table which migh not fit the terminal width. +You can adjust the output using the `--wrap` and `--truncate` flags. The `--wrap` +flag wraps the output to fit the terminal width, while the `--truncate` flag +truncates the output to fit the terminal width. + +```bash +$ gridtk list + job-id slurm-id nodes state job-name output dependencies command +-------- ---------- ------- ------------- ---------- ---------------------- -------------- -------------------- + 1 506994 hcne01 COMPLETED (0) gridtk logs/gridtk.506994.out gridtk submit job.sh + +$ gridtk list --wrap # --wrap or -w + job-id slurm- nodes state job-name output depende command + id ncies +-------- -------- ------- -------- ---------- ----------------- --------- ------------- + 1 506994 hcne0 COMPLETE gridtk logs/gridtk.50699 gridtk submit + 1 D (0) 4.out job.sh + +$ gridtk list --truncate # --truncate or -t + job-id slur.. nodes state job-name output depe.. command +-------- -------- ------- ------- ---------- ---------------- -------- ------------- + 1 506994 hc.. COMPL.. gridtk logs/gridtk.50.. gridtk subm.. +``` diff --git a/src/gridtk/cli.py b/src/gridtk/cli.py index 9095521..6a4a790 100644 --- a/src/gridtk/cli.py +++ b/src/gridtk/cli.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import pydoc +import shutil import tempfile from collections import defaultdict @@ -392,6 +393,20 @@ def resubmit( @cli.command(name="list") @job_filters +@click.option( + "-w", + "--wrap", + is_flag=True, + default=False, + help="Wrap the output to the terminal width", +) +@click.option( + "-t", + "--truncate", + is_flag=True, + default=False, + help="Truncate the output to the terminal width", +) @click.pass_context def list_jobs( ctx: click.Context, @@ -399,22 +414,29 @@ def list_jobs( states: list[str], names: list[str], dependents: bool, + wrap: bool, + truncate: bool, ): """List jobs in the queue, similar to sacct and squeue.""" from tabulate import tabulate from .manager import JobManager + def truncate_str(content: str, max_width: int) -> str: + if len(content) > max_width: + return content[: max_width - 3] + ".." + return content + job_manager: JobManager = ctx.meta["job_manager"] with job_manager as session: jobs = job_manager.list_jobs( job_ids=job_ids, states=states, names=names, dependents=dependents ) - table = defaultdict(list) + table: dict[str, list[str]] = defaultdict(list) for job in jobs: table["job-id"].append(job.id) table["slurm-id"].append(job.grid_id) - table["nodes"].append(job.nodes) + table["nodes"].append(str(job.nodes)) table["state"].append(f"{job.state} ({job.exit_code})") table["job-name"].append(job.name) output = job.output_files[0].resolve() @@ -428,7 +450,55 @@ def list_jobs( ",".join([str(dep_job) for dep_job in job.dependencies_ids]) ) table["command"].append("gridtk submit " + " ".join(job.command)) - click.echo(tabulate(table, headers="keys")) + + maxcolwidths = None + full_output = not wrap and not truncate + if not full_output and table: + minimum_column_width = 7 + width_of_spaces = (len(table) - 1) * 2 + terminal_width = max( + len(table) * minimum_column_width + width_of_spaces, + shutil.get_terminal_size().columns, + ) + max_widths = { + "job-id": minimum_column_width, + "slurm-id": minimum_column_width, + "nodes": 0.1, + "state": 0.15, + "job-name": 0.2, + "output": 0.3, + "dependencies": minimum_column_width, + "command": 0.25, + } + left_over_width = ( + terminal_width + - width_of_spaces + - sum(v for v in max_widths.values() if isinstance(v, int)) + ) + for key, value in max_widths.items(): + if isinstance(value, float): + max_widths[key] = int(left_over_width * value) + maxcolwidths = [int(max_widths[key]) for key in table] + if truncate: + for key, rows in table.items(): + table[key] = [ + truncate_str(str(row), int(max_widths[key])) for row in rows + ] + # truncate column names + table = { + truncate_str(key, int(max_widths[key])): value + for key, value in table.items() + } + + if table: + click.echo( + tabulate( + table, + headers="keys", + maxcolwidths=maxcolwidths, + maxheadercolwidths=maxcolwidths, + ) + ) session.commit() diff --git a/tests/test_gridtk.py b/tests/test_gridtk.py index 4ebb39a..b651dbd 100644 --- a/tests/test_gridtk.py +++ b/tests/test_gridtk.py @@ -227,7 +227,14 @@ def test_submit_triple_dash(mock_check_output: Mock, runner): @patch("subprocess.check_output") def test_list_jobs(mock_check_output, runner): - with runner.isolated_filesystem(): + # override shutil.get_terminal_size to return a fixed size with COLUMNS=80 + with runner.isolated_filesystem(), runner.isolation(env={"COLUMNS": "80"}): + # test when there are no jobs + result = runner.invoke(cli, ["list"]) + assert_click_runner_result(result) + assert result.output == "" + + # test when there are jobs submit_job_id = 9876543 _submit_job( runner=runner, mock_check_output=mock_check_output, job_id=submit_job_id @@ -240,6 +247,38 @@ def test_list_jobs(mock_check_output, runner): mock_check_output.assert_called_with( ["sacct", "-j", str(submit_job_id), "--json"], text=True ) + # full command + assert "gridtk submit --wrap sleep\n" in result.output + # full log file name + assert "logs/gridtk.9876543.out " in result.output + + # test gridtk list --truncate + result = runner.invoke(cli, ["list", "--truncate"]) + assert_click_runner_result(result) + assert str(submit_job_id) in result.output + mock_check_output.assert_called_with( + ["sacct", "-j", str(submit_job_id), "--json"], text=True + ) + # truncated command + assert "gridtk s..\n" in result.output + # truncated log file name + assert "logs/gridt.. " in result.output + + # test gridtk list --wrap + result = runner.invoke(cli, ["list", "--wrap"]) + assert_click_runner_result(result) + assert str(submit_job_id) in result.output + mock_check_output.assert_called_with( + ["sacct", "-j", str(submit_job_id), "--json"], text=True + ) + # wraped command + assert "gridtk" in result.output + assert "submit" in result.output + assert "--wrap" in result.output + assert "sleep" in result.output + # wraped log file name + assert "logs/gridtk.9 " in result.output + assert "876543.out " in result.output @patch("subprocess.check_output")