Skip to content

Commit

Permalink
Make headers info display configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
dlax committed Feb 20, 2024
1 parent b8bc003 commit 5c3c68d
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

* The *rollback ratio* is now displayed in the "global" header (#385).
* Let the display of header's sections be configurable.

### Changed

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,13 @@ ex:
read from `${XDG_CONFIG_HOME:~/.config}/pg_activity.conf` or
`/etc/pg_activity.conf` in that order. Command-line options may override
configuration file settings.
This is used to control how columns in the processes table are rendered, e.g.:
This is used to control how columns in the processes table are rendered or which
items of the header should be displayed, e.g.:
```ini
[header]
system_info = yes
worker_info = no

[client]
hidden = yes

Expand Down
6 changes: 3 additions & 3 deletions pgactivity/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,23 +321,23 @@ def get_parser() -> ArgumentParser:
dest="header_instance_info",
action="store_false",
help="Hide instance information.",
default=True,
default=None,
)
# --no-sys-info
group.add_argument(
"--no-sys-info",
dest="header_system_info",
action="store_false",
help="Hide system information.",
default=True,
default=None,
)
# --no-proc-info
group.add_argument(
"--no-proc-info",
dest="header_worker_info",
action="store_false",
help="Hide workers process information.",
default=True,
default=None,
)

group = parser.add_argument_group("Other display options")
Expand Down
50 changes: 43 additions & 7 deletions pgactivity/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import enum
import os
from pathlib import Path
from typing import IO, Any, Dict, List, Optional, Type, TypeVar
from typing import IO, Any, Dict, List, Optional, Type, TypeVar, Union

import attr
from attr import validators
Expand Down Expand Up @@ -160,20 +160,50 @@ def load(
return flag


class BaseSectionMixin:
@classmethod
def check_options(
cls: type[attr.AttrsInstance], section: configparser.SectionProxy
) -> list[str]:
known_options = {f.name for f in attr.fields(cls)}
unknown_options = set(section) - set(known_options)
if unknown_options:
raise ValueError(f"invalid option(s): {', '.join(sorted(unknown_options))}")
return list(sorted(known_options))


@attr.s(auto_attribs=True, frozen=True, slots=True)
class UISection:
class HeaderSection(BaseSectionMixin):
instance_info: bool = True
system_info: bool = True
worker_info: bool = True

_T = TypeVar("_T", bound="HeaderSection")

@classmethod
def from_config_section(cls: Type[_T], section: configparser.SectionProxy) -> _T:
values: Dict[str, bool] = {}
for optname in cls.check_options(section):
try:
value = section.getboolean(optname)
except configparser.NoOptionError:
continue
if value is not None:
values[optname] = value
return cls(**values)


@attr.s(auto_attribs=True, frozen=True, slots=True)
class UISection(BaseSectionMixin):
hidden: bool = False
width: Optional[int] = attr.ib(default=None, validator=validators.optional(gt(0)))

_T = TypeVar("_T", bound="UISection")

@classmethod
def from_config_section(cls: Type[_T], section: configparser.SectionProxy) -> _T:
cls.check_options(section)
values: Dict[str, Any] = {}
known_options = {f.name: f for f in attr.fields(cls)}
unknown_options = set(section) - set(known_options)
if unknown_options:
raise ValueError(f"invalid option(s): {', '.join(sorted(unknown_options))}")
try:
hidden = section.getboolean("hidden")
except configparser.NoOptionError:
Expand All @@ -192,9 +222,12 @@ def from_config_section(cls: Type[_T], section: configparser.SectionProxy) -> _T
ETC = Path("/etc")


class Configuration(Dict[str, UISection]):
class Configuration(Dict[str, Union[HeaderSection, UISection]]):
_T = TypeVar("_T", bound="Configuration")

def header(self) -> Optional[HeaderSection]:
return self.get("header") # type: ignore[return-value]

@classmethod
def parse(cls: Type[_T], f: IO[str], name: str) -> _T:
r"""Parse configuration from 'f'.
Expand Down Expand Up @@ -250,6 +283,9 @@ def parse(cls: Type[_T], f: IO[str], name: str) -> _T:
if section:
raise InvalidSection(p.default_section, name)
continue
if sname == "header":
config[sname] = HeaderSection.from_config_section(section)
continue
if sname not in known_sections:
raise InvalidSection(sname, name)
try:
Expand Down
18 changes: 16 additions & 2 deletions pgactivity/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from attr import validators

from . import colors, compat, pg, utils
from .config import Configuration, Flag
from .config import Configuration, Flag, HeaderSection, UISection


class Pct(float):
Expand Down Expand Up @@ -214,6 +214,19 @@ class UIHeader:
system_info: bool = True
worker_info: bool = True

@classmethod
def make(
cls, config: Optional[HeaderSection], **options: bool | None
) -> "UIHeader":
"""Build a UIHeader from configuration and command-line options, the latter
taking precedence over the former.
"""
values = {}
if config is not None:
values.update(attr.asdict(config))
values.update({k: v for k, v in options.items() if v is not None})
return cls(**values)

def toggle_system_info(self) -> None:
"""Toggle the 'system_info' attribute.
Expand Down Expand Up @@ -289,7 +302,7 @@ def make(
**kwargs: Any,
) -> "UI":
if header is None:
header = UIHeader()
header = UIHeader.make(config.header() if config else None)

possible_columns: Dict[str, Column] = {}

Expand All @@ -300,6 +313,7 @@ def add_column(key: str, name: str, **kwargs: Any) -> None:
except KeyError:
pass
else:
assert isinstance(cfg, UISection), cfg
if cfg.width is not None:
kwargs["min_width"] = kwargs["max_width"] = cfg.width
assert key not in possible_columns, f"duplicated key {key}"
Expand Down
3 changes: 2 additions & 1 deletion pgactivity/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def main(

flag = Flag.load(config, is_local=is_local, **vars(options))
ui = types.UI.make(
header=types.UIHeader(
header=types.UIHeader.make(
config.header() if config else None,
instance_info=options.header_instance_info,
system_info=options.header_system_info,
worker_info=options.header_worker_info,
Expand Down
16 changes: 14 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ def asdict(cfg: Configuration) -> Dict[str, Any]:
cfg = Configuration.lookup(user_config_home=tmp_path)
assert cfg is None

(tmp_path / "pg_activity.conf").write_text("\n".join(["[client]", "width=5"]))
(tmp_path / "pg_activity.conf").write_text(
"\n".join(
[
"[client]",
"width=5",
"[header]",
"instance_info=no",
]
)
)
cfg = Configuration.lookup(user_config_home=tmp_path)
assert cfg is not None and asdict(cfg) == {"client": {"hidden": False, "width": 5}}
assert cfg is not None and asdict(cfg) == {
"client": {"hidden": False, "width": 5},
"header": {"instance_info": False, "system_info": True, "worker_info": True},
}

0 comments on commit 5c3c68d

Please sign in to comment.