From e5dd8ed345f63aabe06d8451cd4c4634fc2c3867 Mon Sep 17 00:00:00 2001 From: Denis Laxalde Date: Fri, 19 Apr 2024 12:14:13 +0200 Subject: [PATCH] Add non-negative counterparts of --no-XXX options We need to set the default value of "flag" options (those built using the flag() helper function) to None in order to discriminate unspecified option (value=None) from unset option (value=False). --- CHANGELOG.md | 3 + README.md | 25 ++--- docs/man/pg_activity.1 | 89 +++++++++--------- docs/man/pg_activity.pod | 60 ++++++------ pgactivity/cli.py | 48 +++++----- pgactivity/config.py | 62 ++++++------- tests/test_cli.py | 35 ++++--- tests/test_cli_help.txt | 92 +++++++++++++++++++ .../{test_cli.txt => test_cli_help_py38.txt} | 12 ++- tests/test_config.py | 11 ++- tests/test_ui.txt | 1 + 11 files changed, 276 insertions(+), 162 deletions(-) create mode 100644 tests/test_cli_help.txt rename tests/{test_cli.txt => test_cli_help_py38.txt} (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441b2692..355cdd87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ * The color of cells in the process table can now be customized through the configuration file. +* Add non-negative counterparts of many `--no-...` command-line option, thus + allowing to enable respective feature/behaviour even if disabled in the + configuration. (Requires Python 3.9 or higher.) ### Fixed diff --git a/README.md b/README.md index ef23990c..af502d26 100644 --- a/README.md +++ b/README.md @@ -134,17 +134,20 @@ ex: Process table display options: These options may be used hide some columns from the processes table. - --no-pid Disable PID. - --no-database Disable DATABASE. - --no-user Disable USER. - --no-client Disable CLIENT. - --no-cpu Disable CPU%. - --no-mem Disable MEM%. - --no-read Disable READ/s. - --no-write Disable WRITE/s. - --no-time Disable TIME+. - --no-wait Disable W. - --no-app-name Disable APP. + --pid, --no-pid Enable/disable PID. + --database, --no-database + Enable/disable DATABASE. + --user, --no-user Enable/disable USER. + --client, --no-client + Enable/disable CLIENT. + --cpu, --no-cpu Enable/disable CPU%. + --mem, --no-mem Enable/disable MEM%. + --read, --no-read Enable/disable READ/s. + --write, --no-write Enable/disable WRITE/s. + --time, --no-time Enable/disable TIME+. + --wait, --no-wait Enable/disable W. + --app-name, --no-app-name + Enable/disable APP. Header display options: --no-inst-info Display instance information. diff --git a/docs/man/pg_activity.1 b/docs/man/pg_activity.1 index cb3c6d1f..e98cae6e 100644 --- a/docs/man/pg_activity.1 +++ b/docs/man/pg_activity.1 @@ -371,25 +371,20 @@ required by another session. It shows following information: .Vb 1 \& Store running queries as CSV. .Ve -.IP "\fB\-\-no\-db\-size\fR" 2 -.IX Item "--no-db-size" +.IP "\fB\-\-db\-size\fR, \fB\-\-no\-db\-size\fR" 2 +.IX Item "--db-size, --no-db-size" .Vb 1 -\& Skip total size of DB. +\& Enable/disable total size of DB. .Ve -.IP "\fB\-\-no\-tempfiles\fR" 2 -.IX Item "--no-tempfiles" +.IP "\fB\-\-tempfiles\fR, \fB\-\-no\-tempfiles\fR" 2 +.IX Item "--tempfiles, --no-tempfiles" .Vb 1 -\& Skip tempfile count and size. +\& Enable/disable tempfile count and size. .Ve -.IP "\fB\-\-no\-walreceiver\fR" 2 -.IX Item "--no-walreceiver" +.IP "\fB\-\-walreceiver\fR, \fB\-\-no\-walreceiver\fR" 2 +.IX Item "--walreceiver, --no-walreceiver" .Vb 1 -\& Skip walreceiver checks. -.Ve -.IP "\fB\-\-no\-walreceiver\fR" 2 -.IX Item "--no-walreceiver" -.Vb 1 -\& Skip walreceiver checks. +\& Enable/disable walreceiver checks. .Ve .IP "\fB\-w, \-\-wrap\-query\fR" 2 .IX Item "-w, --wrap-query" @@ -442,60 +437,60 @@ required by another session. It shows following information: .Ve .SS "\s-1PROCESS DISPLAY OPTIONS\s0" .IX Subsection "PROCESS DISPLAY OPTIONS" -.IP "\fB\-\-no\-pid\fR" 2 -.IX Item "--no-pid" +.IP "\fB\-\-pid\fR, \fB\-\-no\-pid\fR" 2 +.IX Item "--pid, --no-pid" .Vb 1 -\& Disable PID. +\& Enable/disable PID. .Ve -.IP "\fB\-\-no\-database\fR" 2 -.IX Item "--no-database" +.IP "\fB\-\-database\fR, \fB\-\-no\-database\fR" 2 +.IX Item "--database, --no-database" .Vb 1 -\& Disable DATABASE. +\& Enable/disable DATABASE. .Ve -.IP "\fB\-\-no\-user\fR" 2 -.IX Item "--no-user" +.IP "\fB\-\-user\fR, \fB\-\-no\-user\fR" 2 +.IX Item "--user, --no-user" .Vb 1 -\& Disable USER. +\& Enable/disable USER. .Ve -.IP "\fB\-\-no\-client\fR" 2 -.IX Item "--no-client" +.IP "\fB\-\-client\fR, \fB\-\-no\-client\fR" 2 +.IX Item "--client, --no-client" .Vb 1 -\& Disable CLIENT. +\& Enable/disable CLIENT. .Ve -.IP "\fB\-\-no\-cpu\fR" 2 -.IX Item "--no-cpu" +.IP "\fB\-\-cpu\fR, \fB\-\-no\-cpu\fR" 2 +.IX Item "--cpu, --no-cpu" .Vb 1 -\& Disable CPU%. +\& Enable/disable CPU%. .Ve -.IP "\fB\-\-no\-mem\fR" 2 -.IX Item "--no-mem" +.IP "\fB\-\-mem\fR, \fB\-\-no\-mem\fR" 2 +.IX Item "--mem, --no-mem" .Vb 1 -\& Disable MEM%. +\& Enable/disable MEM%. .Ve -.IP "\fB\-\-no\-read\fR" 2 -.IX Item "--no-read" +.IP "\fB\-\-read\fR, \fB\-\-no\-read\fR" 2 +.IX Item "--read, --no-read" .Vb 1 -\& Disable READ/s. +\& Enable/disable READ/s. .Ve -.IP "\fB\-\-no\-write\fR" 2 -.IX Item "--no-write" +.IP "\fB\-\-write\fR, \fB\-\-no\-write\fR" 2 +.IX Item "--write, --no-write" .Vb 1 -\& Disable WRITE/s. +\& Enable/disable WRITE/s. .Ve -.IP "\fB\-\-no\-time\fR" 2 -.IX Item "--no-time" +.IP "\fB\-\-time\fR, \fB\-\-no\-time\fR" 2 +.IX Item "--time, --no-time" .Vb 1 -\& Disable TIME+. +\& Enable/disable TIME+. .Ve -.IP "\fB\-\-no\-wait\fR" 2 -.IX Item "--no-wait" +.IP "\fB\-\-wait\fR, \fB\-\-no\-wait\fR" 2 +.IX Item "--wait, --no-wait" .Vb 1 -\& Disable W. +\& Enable/disable W. .Ve -.IP "\fB\-\-no\-app\-name\fR" 2 -.IX Item "--no-app-name" +.IP "\fB\-\-app\-name\fR, \fB\-\-no\-app\-name\fR" 2 +.IX Item "--app-name, --no-app-name" .Vb 1 -\& Disable APP. +\& Enable/disable APP. .Ve .SS "\s-1HEADER DISPLAY OPTIONS\s0" .IX Subsection "HEADER DISPLAY OPTIONS" diff --git a/docs/man/pg_activity.pod b/docs/man/pg_activity.pod index ef07f39d..fecc757b 100644 --- a/docs/man/pg_activity.pod +++ b/docs/man/pg_activity.pod @@ -256,21 +256,17 @@ required by another session. It shows following information: Store running queries as CSV. -=item B<--no-db-size> +=item B<--db-size>, B<--no-db-size> - Skip total size of DB. + Enable/disable total size of DB. -=item B<--no-tempfiles> +=item B<--tempfiles>, B<--no-tempfiles> - Skip tempfile count and size. + Enable/disable tempfile count and size. -=item B<--no-walreceiver> +=item B<--walreceiver>, B<--no-walreceiver> - Skip walreceiver checks. - -=item B<--no-walreceiver> - - Skip walreceiver checks. + Enable/disable walreceiver checks. =item B<-w, --wrap-query> @@ -322,49 +318,49 @@ required by another session. It shows following information: =over 2 -=item B<--no-pid> +=item B<--pid>, B<--no-pid> - Disable PID. + Enable/disable PID. -=item B<--no-database> +=item B<--database>, B<--no-database> - Disable DATABASE. + Enable/disable DATABASE. -=item B<--no-user> +=item B<--user>, B<--no-user> - Disable USER. + Enable/disable USER. -=item B<--no-client> +=item B<--client>, B<--no-client> - Disable CLIENT. + Enable/disable CLIENT. -=item B<--no-cpu> +=item B<--cpu>, B<--no-cpu> - Disable CPU%. + Enable/disable CPU%. -=item B<--no-mem> +=item B<--mem>, B<--no-mem> - Disable MEM%. + Enable/disable MEM%. -=item B<--no-read> +=item B<--read>, B<--no-read> - Disable READ/s. + Enable/disable READ/s. -=item B<--no-write> +=item B<--write>, B<--no-write> - Disable WRITE/s. + Enable/disable WRITE/s. -=item B<--no-time> +=item B<--time>, B<--no-time> - Disable TIME+. + Enable/disable TIME+. -=item B<--no-wait> +=item B<--wait>, B<--no-wait> - Disable W. + Enable/disable W. -=item B<--no-app-name> +=item B<--app-name>, B<--no-app-name> - Disable APP. + Enable/disable APP. =back diff --git a/pgactivity/cli.py b/pgactivity/cli.py index af5d111c..2a97e6b3 100755 --- a/pgactivity/cli.py +++ b/pgactivity/cli.py @@ -1,11 +1,11 @@ from __future__ import annotations +import argparse import logging import os import socket import sys import time -from argparse import ArgumentParser from io import StringIO from typing import Any @@ -45,12 +45,20 @@ def configure_logger(debug_file: str | None = None) -> StringIO: return memory_string -def flag(p: Any, spec: str, *, dest: str, help: str) -> None: - p.add_argument(spec, dest=dest, help=help, action="store_false", default=True) +def flag(p: Any, spec: str, *, dest: str, feature: str) -> None: + assert not spec.startswith("--no-") and spec.startswith("--"), spec + if sys.version_info < (3, 9): + spec = f"--no-{spec[2:]}" + action = "store_false" + help = f"Disable {feature}." + else: + action = argparse.BooleanOptionalAction + help = f"Enable/disable {feature}." + p.add_argument(spec, dest=dest, help=help, action=action, default=None) -def get_parser() -> ArgumentParser: - parser = ArgumentParser( +def get_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( usage="%(prog)s [options] [connection string]", description=( "htop like application for PostgreSQL server activity monitoring." @@ -103,11 +111,9 @@ def get_parser() -> ArgumentParser: metavar="FILEPATH", default=None, ) - flag(group, "--no-db-size", dest="dbsize", help="Skip total size of DB.") - flag( - group, "--no-tempfiles", dest="tempfiles", help="Skip tempfile count and size." - ) - flag(group, "--no-walreceiver", dest="walreceiver", help="Skip walreceiver checks.") + flag(group, "--db-size", dest="dbsize", feature="total size of DB") + flag(group, "--tempfiles", dest="tempfiles", feature="tempfile count and size") + flag(group, "--walreceiver", dest="walreceiver", feature="walreceiver checks") group.add_argument( "-w", "--wrap-query", @@ -215,17 +221,17 @@ def get_parser() -> ArgumentParser: "Process table display options", "These options may be used hide some columns from the processes table.", ) - flag(group, "--no-pid", dest="pid", help="Disable PID.") - flag(group, "--no-database", dest="database", help="Disable DATABASE.") - flag(group, "--no-user", dest="user", help="Disable USER.") - flag(group, "--no-client", dest="client", help="Disable CLIENT.") - flag(group, "--no-cpu", dest="cpu", help="Disable CPU%%.") - flag(group, "--no-mem", dest="mem", help="Disable MEM%%.") - flag(group, "--no-read", dest="read", help="Disable READ/s.") - flag(group, "--no-write", dest="write", help="Disable WRITE/s.") - flag(group, "--no-time", dest="time", help="Disable TIME+.") - flag(group, "--no-wait", dest="wait", help="Disable W.") - flag(group, "--no-app-name", dest="appname", help="Disable APP.") + flag(group, "--pid", dest="pid", feature="PID") + flag(group, "--database", dest="database", feature="DATABASE") + flag(group, "--user", dest="user", feature="USER") + flag(group, "--client", dest="client", feature="CLIENT") + flag(group, "--cpu", dest="cpu", feature="CPU%%") + flag(group, "--mem", dest="mem", feature="MEM%%") + flag(group, "--read", dest="read", feature="READ/s") + flag(group, "--write", dest="write", feature="WRITE/s") + flag(group, "--time", dest="time", feature="TIME+") + flag(group, "--wait", dest="wait", feature="W") + flag(group, "--app-name", dest="appname", feature="APP") group = parser.add_argument_group("Header display options") group.add_argument( diff --git a/pgactivity/config.py b/pgactivity/config.py index 4c481542..4d5e81bd 100644 --- a/pgactivity/config.py +++ b/pgactivity/config.py @@ -110,17 +110,17 @@ def load( config: Configuration | None, *, is_local: bool, - appname: bool, - client: bool, - cpu: bool, - database: bool, - mem: bool, - pid: bool, - read: bool, - time: bool, - user: bool, - wait: bool, - write: bool, + appname: bool | None, + client: bool | None, + cpu: bool | None, + database: bool | None, + mem: bool | None, + pid: bool | None, + read: bool | None, + time: bool | None, + user: bool | None, + wait: bool | None, + write: bool | None, **kwargs: Any, ) -> Flag: """Build a Flag value from command line options.""" @@ -128,29 +128,23 @@ def load( flag = cls.from_config(config) else: flag = cls.all() - if not pid: - flag ^= cls.PID - if not database: - flag ^= cls.DATABASE - if not user: - flag ^= cls.USER - if not client: - flag ^= cls.CLIENT - if not cpu: - flag ^= cls.CPU - if not mem: - flag ^= cls.MEM - if not read: - flag ^= cls.READ - if not write: - flag ^= cls.WRITE - if not time: - flag ^= cls.TIME - if not wait: - flag ^= cls.WAIT - if not appname: - flag ^= cls.APPNAME - + for opt, value in ( + (appname, cls.APPNAME), + (client, cls.CLIENT), + (cpu, cls.CPU), + (database, cls.DATABASE), + (mem, cls.MEM), + (pid, cls.PID), + (read, cls.READ), + (time, cls.TIME), + (user, cls.USER), + (wait, cls.WAIT), + (write, cls.WRITE), + ): + if opt is True: + flag |= value + elif opt is False: + flag ^= value # Remove some if no running against local pg server. if not is_local and (flag & cls.CPU): flag ^= cls.CPU diff --git a/tests/test_cli.py b/tests/test_cli.py index 83753c33..4de9ccee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,7 @@ +import sys + +import pytest + from pgactivity import cli @@ -12,8 +16,8 @@ def test_parser() -> None: "rds": False, "output": None, "dbsize": False, - "tempfiles": True, - "walreceiver": True, + "tempfiles": None, + "walreceiver": None, "wrap_query": True, "durationmode": "1", "minduration": 0, @@ -26,15 +30,15 @@ def test_parser() -> None: "username": None, "dbname": None, "pid": False, - "database": True, - "user": True, - "client": True, - "cpu": True, - "mem": True, - "read": True, - "write": True, - "time": True, - "wait": True, + "database": None, + "user": None, + "client": None, + "cpu": None, + "mem": None, + "read": None, + "write": None, + "time": None, + "wait": None, "appname": False, "header_show_instance": None, "header_show_system": None, @@ -42,3 +46,12 @@ def test_parser() -> None: "hide_queries_in_logs": False, "refresh": 2, } + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason="require python >= 3.9") +def test_parser_flag_on() -> None: + parser = cli.get_parser() + ns = parser.parse_args(["--pid", "--no-app-name"]) + assert ns.pid is True + assert ns.appname is False + assert ns.wait is None diff --git a/tests/test_cli_help.txt b/tests/test_cli_help.txt new file mode 100644 index 00000000..1a750392 --- /dev/null +++ b/tests/test_cli_help.txt @@ -0,0 +1,92 @@ +>>> import sys +>>> import pytest +>>> if sys.version_info < (3, 9): +... pytest.skip("only applies for Python version >= 3.9") +... + +>>> from pgactivity import cli +>>> parser = cli.get_parser() +>>> parser.print_help() +usage: pytest [options] [connection string] + +htop like application for PostgreSQL server activity monitoring. + +Configuration: + -P PROFILE, --profile PROFILE + Configuration profile matching a PROFILE.conf file in + ${XDG_CONFIG_HOME:~/.config}/pg_activity/ or + /etc/pg_activity/, or a built-in profile. + +Options: + --blocksize BLOCKSIZE + Filesystem blocksize (default: 4096). + --rds Enable support for AWS RDS (implies --no-tempfiles and + filters out the rdsadmin database from space + calculation). + --output FILEPATH Store running queries as CSV. + --db-size, --no-db-size + Enable/disable total size of DB. + --tempfiles, --no-tempfiles + Enable/disable tempfile count and size. + --walreceiver, --no-walreceiver + Enable/disable walreceiver checks. + -w, --wrap-query Wrap query column instead of truncating. + --duration-mode DURATION_MODE + Duration mode. Values: 1-QUERY(default), + 2-TRANSACTION, 3-BACKEND. + --min-duration SECONDS + Don't display queries with smaller than specified + duration (in seconds). + --filter FIELD:REGEX Filter activities with a (case insensitive) regular + expression applied on selected fields. Known fields + are: dbname. + --debug-file DEBUG_FILE + Enable debug and write it to DEBUG_FILE. + --version show program's version number and exit. + --help Show this help message and exit. + +Connection Options: + connection string A valid connection string to the database, e.g.: + 'host=HOSTNAME port=PORT user=USER dbname=DBNAME'. + -h HOSTNAME, --host HOSTNAME + Database server host or socket directory. + -p PORT, --port PORT Database server port. + -U USERNAME, --username USERNAME + Database user name. + -d DBNAME, --dbname DBNAME + Database name to connect to. + +Process table display options: + These options may be used hide some columns from the processes table. + + --pid, --no-pid Enable/disable PID. + --database, --no-database + Enable/disable DATABASE. + --user, --no-user Enable/disable USER. + --client, --no-client + Enable/disable CLIENT. + --cpu, --no-cpu Enable/disable CPU%. + --mem, --no-mem Enable/disable MEM%. + --read, --no-read Enable/disable READ/s. + --write, --no-write Enable/disable WRITE/s. + --time, --no-time Enable/disable TIME+. + --wait, --no-wait Enable/disable W. + --app-name, --no-app-name + Enable/disable APP. + +Header display options: + --no-inst-info Hide instance information. + --no-sys-info Hide system information. + --no-proc-info Hide workers process information. + +Other display options: + --hide-queries-in-logs + Disable log_min_duration_statements and + log_min_duration_sample for pg_activity. + --refresh REFRESH Refresh rate. Values: 0.5, 1, 2, 3, 4, 5 (default: 2). + +The connection string can be in the form of a list of Key/Value parameters or +an URI as described in the PostgreSQL documentation. The parsing is delegated +to the libpq: different versions of the client library may support different +formats or parameters (for example, connection URIs are only supported from +libpq 9.2). diff --git a/tests/test_cli.txt b/tests/test_cli_help_py38.txt similarity index 92% rename from tests/test_cli.txt rename to tests/test_cli_help_py38.txt index b4013db1..48a3c46e 100644 --- a/tests/test_cli.txt +++ b/tests/test_cli_help_py38.txt @@ -1,3 +1,9 @@ +>>> import sys +>>> import pytest +>>> if sys.version_info >= (3, 9): +... pytest.skip("only applies for Python version < 3.9") +... + >>> from pgactivity import cli >>> parser = cli.get_parser() >>> parser.print_help() @@ -18,9 +24,9 @@ Options: filters out the rdsadmin database from space calculation). --output FILEPATH Store running queries as CSV. - --no-db-size Skip total size of DB. - --no-tempfiles Skip tempfile count and size. - --no-walreceiver Skip walreceiver checks. + --no-db-size Disable total size of DB. + --no-tempfiles Disable tempfile count and size. + --no-walreceiver Disable walreceiver checks. -w, --wrap-query Wrap query column instead of truncating. --duration-mode DURATION_MODE Duration mode. Values: 1-QUERY(default), diff --git a/tests/test_config.py b/tests/test_config.py index 9003341f..7ec95247 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -24,11 +24,12 @@ def test_flag_load() -> None: "cpu": True, "database": True, "mem": True, - "pid": True, + "pid": None, "read": True, + "relation": True, "time": True, "user": True, - "wait": True, + "wait": None, "write": True, } flag = Flag.load(None, is_local=True, **options) @@ -52,7 +53,11 @@ def test_flag_load() -> None: ) cfg = Configuration( name="test", - values=dict(pid=UISection(hidden=True), relation=UISection(hidden=False)), + values=dict( + pid=UISection(hidden=True), + relation=UISection(hidden=False), + wait=UISection(hidden=False), + ), ) flag = Flag.load(cfg, is_local=False, **options) assert ( diff --git a/tests/test_ui.txt b/tests/test_ui.txt index 3be0aee0..b33cab19 100644 --- a/tests/test_ui.txt +++ b/tests/test_ui.txt @@ -826,6 +826,7 @@ Use another set of options: ... client=False, ... dbsize=False, ... tempfiles=True, +... user=None, ... walreceiver=True, ... output=str(output), ... ),