Merge pull request #861 from kdmukai/2025-12_settings_cleanup_v2

[Cleanup] `SettingsDefinition` minor improvements, better test maintainability
This commit is contained in:
Nick Klockenga
2026-02-18 22:22:26 -05:00
committed by GitHub
3 changed files with 68 additions and 42 deletions
+24 -27
View File
@@ -1,4 +1,5 @@
import os import os
import pathlib
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, List from typing import Any, List
@@ -182,6 +183,7 @@ class SettingsConstants:
LOCALE__VIETNAMESE: "Tiếng Việt (Vietnamese)", LOCALE__VIETNAMESE: "Tiếng Việt (Vietnamese)",
} }
@classmethod @classmethod
def get_detected_languages(cls) -> list[tuple[str, str]]: def get_detected_languages(cls) -> list[tuple[str, str]]:
""" """
@@ -189,22 +191,20 @@ class SettingsConstants:
Scans the filesystem to autodiscover which language codes are onboard. Scans the filesystem to autodiscover which language codes are onboard.
""" """
# Will normally be the launch dir (where main.py is located)... # Back out from the models/ dir to reach the seedsigner root
cwd = os.getcwd() models_dir = pathlib.Path(__file__).parent.resolve()
seedsigner_root = models_dir.parent.resolve()
# ...except when running the tests which happens one dir higher
if "src" not in cwd:
cwd = os.path.join(cwd, "src")
# Pre-load English since there's no "en" entry in the translations folder; also # Pre-load English since there's no "en" entry in the translations folder; also
# it should always appear first in the list anyway. # it should always appear first in the list anyway.
detected_languages = [(cls.LOCALE__ENGLISH, cls.ALL_LOCALES[cls.LOCALE__ENGLISH])] detected_languages = [(cls.LOCALE__ENGLISH, cls.ALL_LOCALES[cls.LOCALE__ENGLISH])]
locales_present = set() locales_present = set()
for root, dirs, files in os.walk(os.path.join(cwd, "seedsigner", "resources", "seedsigner-translations", "l10n")): for root, dirs, files in os.walk(os.path.join(seedsigner_root, "resources", "seedsigner-translations", "l10n")):
for file in [f for f in files if f.endswith(".mo")]: for file in [f for f in files if f.endswith(".mo")]:
# `root` will be [...]seedsigner/resources/seedsigner-translations/l10n/pt_BR/LC_MESSAGES # `root` will be [...]seedsigner/resources/seedsigner-translations/l10n/pt_BR/LC_MESSAGES
locales_present.add(root.split(f"l10n{ os.sep }")[1].split(os.sep)[0]) # Isolate the language code from the path
locales_present.add(root.rsplit(os.sep, 2)[-2])
for locale in cls.ALL_LOCALES.keys(): for locale in cls.ALL_LOCALES.keys():
if locale in locales_present: if locale in locales_present:
@@ -453,6 +453,9 @@ class SettingsEntry:
elif type(self.default_value) == tuple: elif type(self.default_value) == tuple:
self.default_value = self.default_value[0] self.default_value = self.default_value[0]
if not self.abbreviated_name:
self.abbreviated_name = self.attr_name
@property @property
def selection_options_display_names(self) -> List[str]: def selection_options_display_names(self) -> List[str]:
@@ -776,15 +779,25 @@ class SettingsDefinition:
@classmethod @classmethod
def get_defaults(cls) -> dict: def get_defaults(cls, use_abbreviated_name: bool = False, skip_hidden: bool = False) -> dict:
"""
* use_abbreviated_name: Only used by the test suite.
* skip_hidden: Only used by the test suite.
"""
as_dict = {} as_dict = {}
for entry in SettingsDefinition.settings_entries: for entry in SettingsDefinition.settings_entries:
if skip_hidden and entry.visibility == SettingsConstants.VISIBILITY__HIDDEN:
continue
if not use_abbreviated_name:
attr_name = entry.attr_name
else:
attr_name = entry.abbreviated_name
if type(entry.default_value) == list: if type(entry.default_value) == list:
# Must copy the default_value list, otherwise we'll inadvertently change # Must copy the default_value list, otherwise we'll inadvertently change
# defaults when updating these attrs # defaults when updating these attrs
as_dict[entry.attr_name] = list(entry.default_value) as_dict[attr_name] = list(entry.default_value)
else: else:
as_dict[entry.attr_name] = entry.default_value as_dict[attr_name] = entry.default_value
return as_dict return as_dict
@@ -797,19 +810,3 @@ class SettingsDefinition:
output["settings_entries"].append(settings_entry.to_dict()) output["settings_entries"].append(settings_entry.to_dict())
return output return output
if __name__ == "__main__":
import json
import os
hostname = os.uname()[1]
if hostname == "seedsigner-os":
output_file = "/mnt/microsd/settings_definition.json"
else:
output_file = "settings_definition.json"
with open(output_file, 'w') as json_file:
json.dump(SettingsDefinition.to_dict(), json_file, indent=4)
+40 -13
View File
@@ -1,6 +1,8 @@
import json import json
import pytest import pytest
from base import BaseTest from base import BaseTest
from seedsigner.models.settings import InvalidSettingsQRData, Settings from seedsigner.models.settings import InvalidSettingsQRData, Settings
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
@@ -98,14 +100,35 @@ class TestSettings(BaseTest):
_verify_defaults_loaded(SettingsConstants.SETTING__SIG_TYPES) _verify_defaults_loaded(SettingsConstants.SETTING__SIG_TYPES)
class SettingsQRBase(BaseTest):
"""
Reusable base test class for testing SettingsQR data.
"""
def setup_method(self):
super().setup_method()
settingsqr_dict = SettingsDefinition.get_defaults(use_abbreviated_name=True, skip_hidden=True)
# Build baseline SettingsQR config based on defaults
self.settingsqr_prefix = "settings::v1"
self.settingsqr_default_attrs_str = ""
for abbreviated_attr_name, value in settingsqr_dict.items():
if isinstance(value, list):
value_str = ",".join(value)
else:
value_str = str(value)
self.settingsqr_default_attrs_str += f" {abbreviated_attr_name}={value_str}"
class TestSettingsQRParser(SettingsQRBase):
def test_parse_settingsqr_data(self): def test_parse_settingsqr_data(self):
""" """
SettingsQR parser should successfully parse a valid settingsqr input string and SettingsQR parser should successfully parse a valid settingsqr input string and
return the resulting config_name and formatted settings_update_dict. return the resulting config_name and formatted settings_update_dict.
""" """
settings_name = "Test SettingsQR" settings_name = "Test SettingsQR"
settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_qr=urca,sta xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" settingsqr_data = f"""{self.settingsqr_prefix} name={ settings_name.replace(" ", "_") } {self.settingsqr_default_attrs_str}"""
# First explicitly set settings that differ from the settingsqr_data # First explicitly set settings that differ from the settingsqr_data
self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED) self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED)
@@ -129,35 +152,37 @@ class TestSettings(BaseTest):
def test_settingsqr_version(self): def test_settingsqr_version(self):
""" SettingsQR parser should accept SettingsQR v1 and reject any others """ """ SettingsQR parser should accept SettingsQR v1 and reject any others """
settingsqr_data = "settings::v1 name=Foo" settingsqr_data = f"{self.settingsqr_prefix} {self.settingsqr_default_attrs_str}"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
# Accepts update with no Exceptions # Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict) self.settings.update(new_settings=settings_update_dict)
settingsqr_data = "settings::v2 name=Foo" settingsqr_data = f"settings::v2 {self.settingsqr_default_attrs_str}"
with pytest.raises(InvalidSettingsQRData) as e: with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data) Settings.parse_settingsqr(settingsqr_data)
assert "Unsupported SettingsQR version" in str(e.value) assert "Unsupported SettingsQR version" in str(e.value)
# Should also fail if version omitted # Should also fail if version omitted
settingsqr_data = "settings name=Foo" settingsqr_data = f"settings {self.settingsqr_default_attrs_str}"
with pytest.raises(InvalidSettingsQRData) as e: with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data) Settings.parse_settingsqr(settingsqr_data)
# And if "settings" is omitted entirely # And if "settings" is omitted entirely
settingsqr_data = "name=Foo" settingsqr_data = self.settingsqr_default_attrs_str
with pytest.raises(InvalidSettingsQRData) as e: with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data) Settings.parse_settingsqr(settingsqr_data)
def test_settingsqr_ignores_unrecognized_setting(self): def test_settingsqr_ignores_unrecognized_setting(self):
""" SettingsQR parser should ignore unrecognized settings """ """ SettingsQR parser should ignore unrecognized settings """
settingsqr_data = "settings::v1 name=Foo favorite_food=bacon passphrase=E" settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK)
unknown_attr = "favorite_food"
settingsqr_data = f"{self.settingsqr_prefix} {unknown_attr}=bacon {settings_entry.abbreviated_name}={settings_entry.default_value}"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert "favorite_food" not in settings_update_dict assert unknown_attr not in settings_update_dict
assert "passphrase" in settings_update_dict assert settings_entry.attr_name in settings_update_dict
# Accepts update with no Exceptions # Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict) self.settings.update(new_settings=settings_update_dict)
@@ -165,10 +190,11 @@ class TestSettings(BaseTest):
def test_settingsqr_fails_unrecognized_option(self): def test_settingsqr_fails_unrecognized_option(self):
""" SettingsQR parser should fail if a settings has an unrecognized option """ """ SettingsQR parser should fail if a settings has an unrecognized option """
settingsqr_data = "settings::v1 name=Foo passphrase=Yep" settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK)
settingsqr_data = f"{self.settingsqr_prefix} {settings_entry.abbreviated_name}=fake_value"
with pytest.raises(InvalidSettingsQRData) as e: with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data) Settings.parse_settingsqr(settingsqr_data)
assert "passphrase" in str(e.value) assert settings_entry.attr_name in str(e.value)
def test_settingsqr_fails_empty_values(self): def test_settingsqr_fails_empty_values(self):
@@ -181,10 +207,11 @@ class TestSettings(BaseTest):
def test_settingsqr_parses_line_break_separators(self): def test_settingsqr_parses_line_break_separators(self):
""" SettingsQR parser should read line breaks as acceptable separators """ """ SettingsQR parser should read line breaks as acceptable separators """
settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\npassphrase=E\n" attrs_with_line_breaks = self.settingsqr_default_attrs_str.replace(' ', '\n')
settingsqr_data = f"{self.settingsqr_prefix}\n{attrs_with_line_breaks}"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert len(settings_update_dict.keys()) == 3 assert len(settings_update_dict.keys()) == self.settingsqr_default_attrs_str.count('=')
# Accepts update with no Exceptions # Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict) self.settings.update(new_settings=settings_update_dict)
+4 -2
View File
@@ -1,15 +1,17 @@
from test_settings import SettingsQRBase
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
class TestSettingsQRDecoder:
class TestSettingsQRDecoder(SettingsQRBase):
def test_decode_settingsqr(self): def test_decode_settingsqr(self):
""" """
Assume the QR reader decodes the SettingsQR content correctly and begin this test Assume the QR reader decodes the SettingsQR content correctly and begin this test
with parsing the result. with parsing the result.
""" """
settings_name = "Test SettingsQR" settings_name = "Test SettingsQR"
settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" settings_qr_str = f"""{self.settingsqr_prefix} name={ settings_name.replace(" ", "_") } {self.settingsqr_default_attrs_str}"""
# Now parse the settings_qr_str # Now parse the settings_qr_str
decoder = DecodeQR() decoder = DecodeQR()