Refactor logging and formatting

This commit is contained in:
Daniel Girtler 2026-05-15 22:06:16 +10:00
parent 6ea2de87d3
commit 5fbdbffcc4
9 changed files with 123 additions and 124 deletions

View File

@ -6,9 +6,9 @@ from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password, User
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.user.user_menu import select_users
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -65,7 +65,7 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
users: list[User] | None = item.value
if users:
return FormattedOutput.as_table(users)
return as_table(users)
return None
def _prev_root_pwd(self, item: MenuItem) -> str | None:

View File

@ -35,8 +35,9 @@ from archinstall.lib.models.device import (
Unit,
_DeviceInfo,
)
from archinstall.lib.output import FormattedOutput, debug
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -221,7 +222,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
for mod in device_mods:
# create partition table
partition_table = FormattedOutput.as_table(mod.partitions)
partition_table = as_table(mod.partitions)
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
@ -230,7 +231,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
# create btrfs table
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
for partition in btrfs_partitions:
output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
output_btrfs += as_table(partition.btrfs_subvols) + '\n'
output = output_partition + output_btrfs
return output.rstrip()
@ -246,12 +247,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
for vol_gp in lvm_config.vol_groups:
pv_table = FormattedOutput.as_table(vol_gp.pvs)
pv_table = as_table(vol_gp.pvs)
output += '{}:\n{}'.format(tr('Physical volumes'), pv_table)
output += f'\nVolume Group: {vol_gp.name}'
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
lvm_volumes = as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
return output
@ -302,7 +303,7 @@ async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | N
dev = device_handler.get_device(device.path)
if dev and dev.partition_infos:
return FormattedOutput.as_table(dev.partition_infos)
return as_table(dev.partition_infos)
return None
if preset is None:

View File

@ -17,8 +17,8 @@ from archinstall.lib.models.device import (
PartitionModification,
)
from archinstall.lib.models.users import Password
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -199,7 +199,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_partitions(self, item: MenuItem) -> str | None:
if item.value:
output = tr('Partitions to be encrypted') + '\n'
output += FormattedOutput.as_table(item.value)
output += as_table(item.value)
return output.rstrip()
return None
@ -207,7 +207,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_lvm_vols(self, item: MenuItem) -> str | None:
if item.value:
output = tr('LVM volumes to be encrypted') + '\n'
output += FormattedOutput.as_table(item.value)
output += as_table(item.value)
return output.rstrip()
return None

View File

@ -19,8 +19,8 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -479,7 +479,7 @@ class PartitioningList(ListManager[DiskSegment]):
sector_size = device_info.sector_size
text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n'
free_space_table = FormattedOutput.as_table([free_space])
free_space_table = as_table([free_space])
prompt = text + free_space_table + '\n'
max_sectors = free_space.length.format_size(Unit.sectors, sector_size)

View File

@ -27,11 +27,11 @@ from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.network.network_menu import select_network
from archinstall.lib.output import FormattedOutput
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman_menu import PacmanMenu
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_table
from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
@ -299,7 +299,7 @@ class GlobalMenu(AbstractMenu[None]):
if item.value:
network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL:
output = FormattedOutput.as_table(network_config.nics)
output = as_table(network_config.nics)
else:
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
@ -321,7 +321,7 @@ class GlobalMenu(AbstractMenu[None]):
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
if auth_config.users:
output += FormattedOutput.as_table(auth_config.users) + '\n'
output += as_table(auth_config.users) + '\n'
if auth_config.u2f_config:
u2f_config = auth_config.u2f_config
@ -612,7 +612,7 @@ class GlobalMenu(AbstractMenu[None]):
if mirror_config.custom_repositories:
title = tr('Custom repositories')
table = FormattedOutput.as_table(mirror_config.custom_repositories)
table = as_table(mirror_config.custom_repositories)
output += f'{title}:\n\n{table}'
return output.strip()

View File

@ -1,4 +1,4 @@
from archinstall.lib.output import FormattedOutput
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
@ -32,7 +32,7 @@ class MenuHelper[ValueT]:
display_data: dict[str, ValueT | str | None] = {}
if data:
table = FormattedOutput.as_table(data)
table = as_table(data)
rows = table.split('\n')
# these are the header rows of the table

View File

@ -13,8 +13,8 @@ from archinstall.lib.models.mirrors import (
SignOption,
)
from archinstall.lib.models.packages import Repository
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -281,7 +281,7 @@ class MirrorMenu(AbstractSubMenu[MirrorConfiguration]):
return None
custom_mirrors: list[CustomRepository] = item.value
output = FormattedOutput.as_table(custom_mirrors)
output = as_table(custom_mirrors)
return output.strip()
def _prev_custom_servers(self, item: MenuItem) -> str | None:

View File

@ -3,114 +3,11 @@ import os
import sys
import urllib.error
import urllib.request
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
from archinstall.lib.utils.util import timestamp
if TYPE_CHECKING:
from _typeshed import DataclassInstance
class FormattedOutput:
@staticmethod
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
filter_list: list[str] = [],
) -> dict[str, Any]:
"""
the original values returned a dataclass as dict thru the call to some specific methods
this version allows thru the parameter class_formatter to call a dynamically selected formatting method.
Can transmit a filter list to the class_formatter,
"""
if class_formatter:
# if invoked per reference it has to be a standard function or a classmethod.
# A method of an instance does not make sense
if callable(class_formatter):
return class_formatter(o, filter_list)
# if is invoked by name we restrict it to a method of the class. No need to mess more
elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)):
func = getattr(o, class_formatter)
return func(filter_list)
raise ValueError('Unsupported formatting call')
elif hasattr(o, 'table_data'):
return o.table_data()
elif hasattr(o, 'json'):
return o.json()
elif is_dataclass(o):
return asdict(o)
else:
return o.__dict__ # type: ignore[unreachable]
@classmethod
def as_table(
cls,
obj: list[Any],
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
filter_list: list[str] = [],
capitalize: bool = False,
) -> str:
"""variant of as_table (subtly different code) which has two additional parameters
filter which is a list of fields which will be shown
class_formatter a special method to format the outgoing data
A general comment, the format selected for the output (a string where every data record is separated by newline)
is for compatibility with a print statement
As_table_filter can be a drop in replacement for as_table
"""
raw_data = [cls._get_values(o, class_formatter, filter_list) for o in obj]
# determine the maximum column size
column_width: dict[str, int] = {}
for o in raw_data:
for k, v in o.items():
if not filter_list or k in filter_list:
column_width.setdefault(k, 0)
column_width[k] = max([column_width[k], len(str(v)), len(k)])
if not filter_list:
filter_list = list(column_width.keys())
# create the header lines
output = ''
key_list = []
for key in filter_list:
width = column_width[key]
key = key.replace('!', '').replace('_', ' ')
if capitalize:
key = key.capitalize()
key_list.append(unicode_ljust(key, width))
output += ' | '.join(key_list) + '\n'
output += '-' * len(output) + '\n'
# create the data lines
for record in raw_data:
obj_data = []
for key in filter_list:
width = column_width.get(key, len(key))
value = record.get(key, '')
if '!' in key:
value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width))
else:
obj_data.append(unicode_ljust(str(value), width))
output += ' | '.join(obj_data) + '\n'
return output
class Logger:
def __init__(self, path: Path | None = None) -> None:

View File

@ -1,5 +1,13 @@
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from typing import TYPE_CHECKING, Any
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
from archinstall.tui.rich import BaseRichTable
if TYPE_CHECKING:
from _typeshed import DataclassInstance
def as_key_value_pair(
entries: dict[str, str | list[str] | bool],
@ -45,3 +53,96 @@ def as_columns(entries: list[str], cols: int) -> str:
output += out_fmt.format(*row) + '\n'
return output
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
filter_list: list[str] = [],
) -> dict[str, Any]:
"""
the original values returned a dataclass as dict thru the call to some specific methods
this version allows thru the parameter class_formatter to call a dynamically selected formatting method.
Can transmit a filter list to the class_formatter,
"""
if class_formatter:
# if invoked per reference it has to be a standard function or a classmethod.
# A method of an instance does not make sense
if callable(class_formatter):
return class_formatter(o, filter_list)
# if is invoked by name we restrict it to a method of the class. No need to mess more
elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)):
func = getattr(o, class_formatter)
return func(filter_list)
raise ValueError('Unsupported formatting call')
elif hasattr(o, 'table_data'):
return o.table_data()
elif hasattr(o, 'json'):
return o.json()
elif is_dataclass(o):
return asdict(o)
else:
return o.__dict__ # type: ignore[unreachable]
def as_table(
obj: list[Any],
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
filter_list: list[str] = [],
capitalize: bool = False,
) -> str:
"""variant of as_table (subtly different code) which has two additional parameters
filter which is a list of fields which will be shown
class_formatter a special method to format the outgoing data
A general comment, the format selected for the output (a string where every data record is separated by newline)
is for compatibility with a print statement
As_table_filter can be a drop in replacement for as_table
"""
raw_data = [_get_values(o, class_formatter, filter_list) for o in obj]
# determine the maximum column size
column_width: dict[str, int] = {}
for o in raw_data:
for k, v in o.items():
if not filter_list or k in filter_list:
column_width.setdefault(k, 0)
column_width[k] = max([column_width[k], len(str(v)), len(k)])
if not filter_list:
filter_list = list(column_width.keys())
# create the header lines
output = ''
key_list = []
for key in filter_list:
width = column_width[key]
key = key.replace('!', '').replace('_', ' ')
if capitalize:
key = key.capitalize()
key_list.append(unicode_ljust(key, width))
output += ' | '.join(key_list) + '\n'
output += '-' * len(output) + '\n'
# create the data lines
for record in raw_data:
obj_data = []
for key in filter_list:
width = column_width.get(key, len(key))
value = record.get(key, '')
if '!' in key:
value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width))
else:
obj_data.append(unicode_ljust(str(value), width))
output += ' | '.join(obj_data) + '\n'
return output