Reduced cyclomatic complexity - Each function now does one thing

Better readability - Descriptive function names explain intent
Easier testing - Helper functions can be tested independently
Consistent style - f-strings used throughout
Removed code smells - No more pass statements in conditionals, no assert for validation
This commit is contained in:
nicolargo 2026-01-03 09:00:05 +01:00
parent 5cbbe91e1f
commit 8b76cd458e
1 changed files with 111 additions and 116 deletions

View File

@ -66,6 +66,16 @@ def convert_attribute_to_dict(attr):
} }
# Keys for attributes that should be formatted with auto_unit (large byte values)
LARGE_VALUE_KEYS = frozenset([
"bytesWritten",
"bytesRead",
"dataUnitsRead",
"dataUnitsWritten",
"hostReadCommands",
"hostWriteCommands",
])
NVME_ATTRIBUTE_LABELS = { NVME_ATTRIBUTE_LABELS = {
"criticalWarning": "Number of critical warnings", "criticalWarning": "Number of critical warnings",
"_temperature": "Temperature (°C)", "_temperature": "Temperature (°C)",
@ -110,81 +120,64 @@ def convert_nvme_attribute_to_dict(key, value):
} }
def _process_standard_attributes(device_stats, attributes, hide_attributes):
"""Process standard SMART attributes and add them to device_stats."""
for attribute in attributes:
if attribute is None or attribute.name in hide_attributes:
continue
attrib_dict = convert_attribute_to_dict(attribute)
num = attrib_dict.pop('num', None)
if num is None:
logger.debug(f'Smart plugin error - Skip attribute with no num: {attribute}')
continue
device_stats[num] = attrib_dict
def _process_nvme_attributes(device_stats, if_attributes, hide_attributes):
"""Process NVMe-specific attributes and add them to device_stats."""
if not isinstance(if_attributes, NvmeAttributes):
return
for idx, (attr, value) in enumerate(vars(if_attributes).items(), start=1):
attrib_dict = convert_nvme_attribute_to_dict(attr, value)
if attrib_dict['name'] in hide_attributes:
continue
# Verify the value is serializable to prevent rendering errors
if value is not None:
try:
str(value)
except Exception:
logger.debug(f'Unable to serialize attribute {attr} from NVME')
attrib_dict['value'] = None
attrib_dict['raw'] = None
device_stats[idx] = attrib_dict
def get_smart_data(hide_attributes): def get_smart_data(hide_attributes):
""" """Get SMART attribute data.
Get SMART attribute data
:return: list of multi leveled dictionaries Returns a list of dictionaries, each containing:
each dict has a key "DeviceName" with the identification of the device in smartctl - 'DeviceName': Device identification string
also has keys of the SMART attribute id, with value of another dict of the attributes - Numeric keys: SMART attribute dictionaries with flags, raw values, etc.
[
{
"DeviceName": "/dev/sda blahblah",
"1":
{
"flags": "..",
"raw": "..",
etc,
}
...
}
]
""" """
stats = [] stats = []
# get all devices
try: try:
devlist = DeviceList() devlist = DeviceList()
except TypeError as e: except TypeError as e:
# Catch error (see #1806)
logger.debug(f'Smart plugin error - Can not grab device list ({e})') logger.debug(f'Smart plugin error - Can not grab device list ({e})')
global import_error_tag global import_error_tag
import_error_tag = True import_error_tag = True
return stats return stats
for dev in devlist.devices: for dev in devlist.devices:
stats.append( device_stats = {'DeviceName': f'{dev.name} {dev.model}'}
{ _process_standard_attributes(device_stats, dev.attributes, hide_attributes)
'DeviceName': f'{dev.name} {dev.model}', _process_nvme_attributes(device_stats, dev.if_attributes, hide_attributes)
} stats.append(device_stats)
)
for attribute in dev.attributes:
if attribute is None:
pass
elif attribute.name in hide_attributes:
pass
else:
attrib_dict = convert_attribute_to_dict(attribute)
# we will use the attribute number as the key
num = attrib_dict.pop('num', None)
try:
assert num is not None
except Exception as e:
# we should never get here, but if we do, continue to next iteration and skip this attribute
logger.debug(f'Smart plugin error - Skip the attribute {attribute} ({e})')
continue
stats[-1][num] = attrib_dict
if isinstance(dev.if_attributes, NvmeAttributes):
idx = 0
for attr in dev.if_attributes.__dict__.keys():
idx += 1
attrib_dict = convert_nvme_attribute_to_dict(attr, dev.if_attributes.__dict__[attr])
if attrib_dict['name'] in hide_attributes:
pass
else:
try:
if (
dev.if_attributes.__dict__[attr] is not None
): # make sure the value is serializable to prevent errors in rendering
str(dev.if_attributes.__dict__[attr])
except Exception:
logger.debug(f'Unable to serialize attribute {attr} from NVME')
attrib_dict['value'] = None
attrib_dict['raw'] = None
finally:
stats[-1][idx] = attrib_dict
return stats return stats
@ -194,25 +187,23 @@ class SmartPlugin(GlancesPluginModel):
def __init__(self, args=None, config=None, stats_init_value=[]): def __init__(self, args=None, config=None, stats_init_value=[]):
"""Init the plugin.""" """Init the plugin."""
# check if user is admin
if not is_admin() and args: if not is_admin() and args:
disable(args, "smart") disable(args, "smart")
logger.debug("Current user is not admin, HDD SMART plugin disabled.") logger.debug("Current user is not admin, HDD SMART plugin disabled.")
super().__init__(args=args, config=config) super().__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True self.display_curse = True
self.hide_attributes = self._parse_hide_attributes(config)
if 'hide_attributes' in config.as_dict()['smart']: def _parse_hide_attributes(self, config):
logger.info( """Parse and return the list of attributes to hide from config."""
'Followings SMART attributes wil not be displayed: {}'.format( smart_config = config.as_dict().get('smart', {})
config.as_dict()['smart']['hide_attributes'] hide_attr_str = smart_config.get('hide_attributes', '')
) if hide_attr_str:
) logger.info(f'Following SMART attributes will not be displayed: {hide_attr_str}')
self.hide_attributes = config.as_dict()['smart']['hide_attributes'].split(',') return hide_attr_str.split(',')
else: return []
self.hide_attributes = []
@property @property
def hide_attributes(self): def hide_attributes(self):
@ -249,59 +240,63 @@ class SmartPlugin(GlancesPluginModel):
"""Return the key of the list.""" """Return the key of the list."""
return 'DeviceName' return 'DeviceName'
def _format_raw_value(self, stat):
"""Format a raw SMART attribute value for display."""
raw = stat['raw']
if raw is None:
return ""
if stat['key'] in LARGE_VALUE_KEYS:
return self.auto_unit(raw)
return str(raw)
def _get_sorted_stat_keys(self, device_stat):
"""Get sorted attribute keys from device stats, excluding DeviceName."""
keys = [k for k in device_stat if k != 'DeviceName']
try:
return sorted(keys, key=int)
except ValueError:
# Some keys may not be numeric (see #2904)
return keys
def _add_device_stats(self, ret, device_stat, max_width, name_max_width):
"""Add a device's SMART stats to the curse output."""
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(f'{device_stat["DeviceName"][:max_width]:{max_width}}'))
for key in self._get_sorted_stat_keys(device_stat):
stat = device_stat[key]
ret.append(self.curse_new_line())
# Attribute name
name = stat['name'][: name_max_width - 1].replace('_', ' ')
ret.append(self.curse_add_line(f' {name:{name_max_width - 1}}'))
# Attribute value
try:
value_str = self._format_raw_value(stat)
ret.append(self.curse_add_line(f'{value_str:>8}'))
except Exception:
logger.debug(f"Failed to serialize {key}")
ret.append(self.curse_add_line(""))
def msg_curse(self, args=None, max_width=None): def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface.""" """Return the dict to display in the curse interface."""
# Init the return message
ret = [] ret = []
# Only process if stats exist...
if import_error_tag or not self.stats or self.is_disabled(): if import_error_tag or not self.stats or self.is_disabled():
return ret return ret
# Max size for the interface name if not max_width:
if max_width:
name_max_width = max_width - 6
else:
# No max_width defined, return an empty curse message
logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.") logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.")
return ret return ret
name_max_width = max_width - 6
# Header # Header
msg = '{:{width}}'.format('SMART disks', width=name_max_width) ret.append(self.curse_add_line(f'{"SMART disks":{name_max_width}}', "TITLE"))
ret.append(self.curse_add_line(msg, "TITLE"))
# Data # Device data
for device_stat in self.stats: for device_stat in self.stats:
# New line self._add_device_stats(ret, device_stat, max_width, name_max_width)
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(device_stat['DeviceName'][:max_width], width=max_width)
ret.append(self.curse_add_line(msg))
try:
device_stat_sorted = sorted([i for i in device_stat if i != 'DeviceName'], key=int)
except ValueError:
# Catch ValueError, see #2904
device_stat_sorted = [i for i in device_stat if i != 'DeviceName']
for smart_stat in device_stat_sorted:
ret.append(self.curse_new_line())
msg = ' {:{width}}'.format(
device_stat[smart_stat]['name'][: name_max_width - 1].replace('_', ' '), width=name_max_width - 1
)
ret.append(self.curse_add_line(msg))
try:
raw = device_stat[smart_stat]['raw']
if device_stat[smart_stat]['key'] in [
"bytesWritten",
"bytesRead",
"dataUnitsRead",
"dataUnitsWritten",
"hostReadCommands",
"hostWriteCommands",
]:
msg = '{:>8}'.format("" if raw is None else self.auto_unit(raw))
else:
msg = '{:>8}'.format("" if raw is None else str(raw))
ret.append(self.curse_add_line(msg))
except Exception:
logger.debug(f"Failed to serialize {smart_stat}")
ret.append(self.curse_add_line(""))
return ret return ret