API: add defaults handling to SharedPreferences and only save non-defaults

This commit is contained in:
Thomas Farstrike
2025-12-02 12:08:47 +01:00
parent 00d0cb1952
commit 27d1af9931
5 changed files with 320 additions and 18 deletions
+23 -1
View File
@@ -410,7 +410,7 @@ Current stable version: 0.3.3 (as of latest CHANGELOG entry)
```python
from mpos.config import SharedPreferences
# Load preferences
# Basic usage
prefs = SharedPreferences("com.example.myapp")
value = prefs.get_string("key", "default_value")
number = prefs.get_int("count", 0)
@@ -422,6 +422,28 @@ editor.put_string("key", "value")
editor.put_int("count", 42)
editor.put_dict("data", {"key": "value"})
editor.commit()
# Using constructor defaults (reduces config file size)
# Values matching defaults are not saved to disk
prefs = SharedPreferences("com.example.myapp", defaults={
"brightness": -1,
"volume": 50,
"theme": "dark"
})
# Returns constructor default (-1) if not stored
brightness = prefs.get_int("brightness") # Returns -1
# Method defaults override constructor defaults
brightness = prefs.get_int("brightness", 100) # Returns 100
# Stored values override all defaults
prefs.edit().put_int("brightness", 75).commit()
brightness = prefs.get_int("brightness") # Returns 75
# Setting to default value removes it from storage (auto-cleanup)
prefs.edit().put_int("brightness", -1).commit()
# brightness is no longer stored in config.json, saves space
```
**Intent system**: Launch activities and pass data
@@ -467,7 +467,7 @@ class CameraApp(Activity):
try:
# Basic image adjustments
brightness = prefs.get_int("brightness", 0)
brightness = prefs.get_int("brightness", CameraSettingsActivity.DEFAULTS.get("brightness"))
cam.set_brightness(brightness)
contrast = prefs.get_int("contrast", 0)
@@ -9,7 +9,7 @@ from mpos.content.intent import Intent
class CameraSettingsActivity(Activity):
"""Settings activity for comprehensive camera configuration."""
DEFAULT_WIDTH = 320 # 240 would be better but webcam doesn't support this (yet)
DEFAULT_WIDTH = 240 # 240 would be better but webcam doesn't support this (yet)
DEFAULT_HEIGHT = 240
DEFAULT_COLORMODE = True
DEFAULT_SCANQR_WIDTH = 960
@@ -31,6 +31,10 @@ class CameraSettingsActivity(Activity):
scale_default=False
binning_default=False
DEFAULTS = {
"brightness": 1,
}
# Resolution options for desktop/webcam
WEBCAM_RESOLUTIONS = [
("160x120", "160x120"),
@@ -291,7 +295,7 @@ class CameraSettingsActivity(Activity):
self.ui_controls["resolution"] = dropdown
# Brightness
brightness = prefs.get_int("brightness", 0)
brightness = prefs.get_int("brightness", self.DEFAULTS.get("brightness"))
slider, label, cont = self.create_slider(tab, "Brightness", -2, 2, brightness, "brightness")
self.ui_controls["brightness"] = slider
+81 -14
View File
@@ -2,10 +2,11 @@ import ujson
import os
class SharedPreferences:
def __init__(self, appname, filename="config.json"):
"""Initialize with appname and filename for preferences."""
def __init__(self, appname, filename="config.json", defaults=None):
"""Initialize with appname, filename, and optional defaults for preferences."""
self.appname = appname
self.filename = filename
self.defaults = defaults if defaults is not None else {}
self.filepath = f"data/{self.appname}/{self.filename}"
self.data = {}
self.load()
@@ -36,31 +37,80 @@ class SharedPreferences:
def get_string(self, key, default=None):
"""Retrieve a string value for the given key, with a default if not found."""
to_return = self.data.get(key)
if to_return is None and default is not None:
to_return = default
if to_return is None:
# Method default takes precedence
if default is not None:
to_return = default
# Fall back to constructor default
elif key in self.defaults:
to_return = self.defaults[key]
return to_return
def get_int(self, key, default=0):
"""Retrieve an integer value for the given key, with a default if not found."""
try:
return int(self.data.get(key, default))
except (TypeError, ValueError):
if key in self.data:
try:
return int(self.data[key])
except (TypeError, ValueError):
return default
# Key not in stored data, check defaults
# Method default takes precedence if explicitly provided (not the hardcoded 0)
# Otherwise use constructor default if exists
if default != 0:
return default
if key in self.defaults:
try:
return int(self.defaults[key])
except (TypeError, ValueError):
return 0
return 0
def get_bool(self, key, default=False):
"""Retrieve a boolean value for the given key, with a default if not found."""
try:
return bool(self.data.get(key, default))
except (TypeError, ValueError):
if key in self.data:
try:
return bool(self.data[key])
except (TypeError, ValueError):
return default
# Key not in stored data, check defaults
# Method default takes precedence if explicitly provided (not the hardcoded False)
# Otherwise use constructor default if exists
if default != False:
return default
if key in self.defaults:
try:
return bool(self.defaults[key])
except (TypeError, ValueError):
return False
return False
def get_list(self, key, default=None):
"""Retrieve a list for the given key, with a default if not found."""
return self.data.get(key, default if default is not None else [])
if key in self.data:
return self.data[key]
# Key not in stored data, check defaults
# Method default takes precedence if provided
if default is not None:
return default
# Fall back to constructor default
if key in self.defaults:
return self.defaults[key]
# Return empty list as hardcoded fallback
return []
def get_dict(self, key, default=None):
"""Retrieve a dictionary for the given key, with a default if not found."""
return self.data.get(key, default if default is not None else {})
if key in self.data:
return self.data[key]
# Key not in stored data, check defaults
# Method default takes precedence if provided
if default is not None:
return default
# Fall back to constructor default
if key in self.defaults:
return self.defaults[key]
# Return empty dict as hardcoded fallback
return {}
def edit(self):
"""Return an Editor object to modify preferences."""
@@ -197,14 +247,31 @@ class Editor:
self.temp_data = {}
return self
def _filter_defaults(self, data):
"""Remove keys from data that match constructor defaults."""
if not self.preferences.defaults:
return data
filtered = {}
for key, value in data.items():
if key in self.preferences.defaults:
if value != self.preferences.defaults[key]:
filtered[key] = value
# else: skip saving, matches default
else:
filtered[key] = value # No default, always save
return filtered
def apply(self):
"""Save changes to the file asynchronously (emulated)."""
self.preferences.data = self.temp_data.copy()
filtered_data = self._filter_defaults(self.temp_data)
self.preferences.data = filtered_data
self.preferences.save_config()
def commit(self):
"""Save changes to the file synchronously."""
self.preferences.data = self.temp_data.copy()
filtered_data = self._filter_defaults(self.temp_data)
self.preferences.data = filtered_data
self.preferences.save_config()
return True
+209
View File
@@ -475,4 +475,213 @@ class TestSharedPreferences(unittest.TestCase):
self.assertEqual(loaded["settings"]["theme"], "dark")
self.assertEqual(loaded["settings"]["limits"][2], 30)
# Tests for default values feature
def test_constructor_defaults_basic(self):
"""Test that constructor defaults are returned when key is missing."""
defaults = {"brightness": -1, "enabled": True, "name": "default"}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# No values stored yet, should return constructor defaults
self.assertEqual(prefs.get_int("brightness"), -1)
self.assertEqual(prefs.get_bool("enabled"), True)
self.assertEqual(prefs.get_string("name"), "default")
def test_method_default_precedence(self):
"""Test that method defaults override constructor defaults."""
defaults = {"brightness": -1, "enabled": False, "name": "default"}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Method defaults should take precedence when different from hardcoded defaults
self.assertEqual(prefs.get_int("brightness", 50), 50)
# For booleans, we can only test when method default differs from hardcoded False
self.assertEqual(prefs.get_bool("enabled", True), True)
self.assertEqual(prefs.get_string("name", "override"), "override")
def test_stored_value_precedence(self):
"""Test that stored values override all defaults."""
defaults = {"brightness": -1, "enabled": True, "name": "default"}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Store some values
prefs.edit().put_int("brightness", 75).put_bool("enabled", False).put_string("name", "stored").commit()
# Reload and verify stored values override defaults
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertEqual(prefs2.get_int("brightness"), 75)
self.assertEqual(prefs2.get_bool("enabled"), False)
self.assertEqual(prefs2.get_string("name"), "stored")
# Method defaults should not override stored values
self.assertEqual(prefs2.get_int("brightness", 100), 75)
self.assertEqual(prefs2.get_bool("enabled", True), False)
self.assertEqual(prefs2.get_string("name", "method"), "stored")
def test_default_values_not_saved(self):
"""Test that values matching defaults are not written to disk."""
defaults = {"brightness": -1, "enabled": True, "name": "default"}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Set values matching defaults
prefs.edit().put_int("brightness", -1).put_bool("enabled", True).put_string("name", "default").commit()
# Reload and verify values are returned correctly
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertEqual(prefs2.get_int("brightness"), -1)
self.assertEqual(prefs2.get_bool("enabled"), True)
self.assertEqual(prefs2.get_string("name"), "default")
# Verify raw data doesn't contain the keys (they weren't saved)
self.assertFalse("brightness" in prefs2.data)
self.assertFalse("enabled" in prefs2.data)
self.assertFalse("name" in prefs2.data)
def test_cleanup_removes_defaults(self):
"""Test that setting a value to its default removes it from storage."""
defaults = {"brightness": -1}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Store a non-default value
prefs.edit().put_int("brightness", 75).commit()
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertIn("brightness", prefs2.data)
self.assertEqual(prefs2.get_int("brightness"), 75)
# Change it back to default
prefs2.edit().put_int("brightness", -1).commit()
# Reload and verify it's been removed from storage
prefs3 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertFalse("brightness" in prefs3.data)
self.assertEqual(prefs3.get_int("brightness"), -1)
def test_none_as_valid_default(self):
"""Test that None can be used as a constructor default value."""
defaults = {"optional_string": None, "optional_list": None}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Should return None for these keys
self.assertIsNone(prefs.get_string("optional_string"))
self.assertIsNone(prefs.get_list("optional_list"))
# Store some values
prefs.edit().put_string("optional_string", "value").put_list("optional_list", [1, 2]).commit()
# Reload
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertEqual(prefs2.get_string("optional_string"), "value")
self.assertEqual(prefs2.get_list("optional_list"), [1, 2])
def test_empty_collection_defaults(self):
"""Test empty lists and dicts as constructor defaults."""
defaults = {"items": [], "settings": {}}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Should return empty collections
self.assertEqual(prefs.get_list("items"), [])
self.assertEqual(prefs.get_dict("settings"), {})
# These should not be saved to disk
prefs.edit().put_list("items", []).put_dict("settings", {}).commit()
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertFalse("items" in prefs2.data)
self.assertFalse("settings" in prefs2.data)
def test_defaults_with_nested_structures(self):
"""Test that defaults work with complex nested structures."""
defaults = {
"config": {"theme": "dark", "size": 12},
"items": [1, 2, 3]
}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Constructor defaults should work
self.assertEqual(prefs.get_dict("config"), {"theme": "dark", "size": 12})
self.assertEqual(prefs.get_list("items"), [1, 2, 3])
# Exact match should not be saved
prefs.edit().put_dict("config", {"theme": "dark", "size": 12}).commit()
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertFalse("config" in prefs2.data)
# Modified value should be saved
prefs2.edit().put_dict("config", {"theme": "light", "size": 12}).commit()
prefs3 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertIn("config", prefs3.data)
self.assertEqual(prefs3.get_dict("config")["theme"], "light")
def test_backward_compatibility(self):
"""Test that existing code without defaults parameter still works."""
# Old style initialization (no defaults parameter)
prefs = SharedPreferences(self.test_app_name)
# Should work exactly as before
prefs.edit().put_string("key", "value").put_int("count", 42).commit()
prefs2 = SharedPreferences(self.test_app_name)
self.assertEqual(prefs2.get_string("key"), "value")
self.assertEqual(prefs2.get_int("count"), 42)
def test_type_conversion_with_defaults(self):
"""Test type conversion works correctly with constructor defaults."""
defaults = {"number": -1, "flag": True}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Store string representations
prefs.edit().put_string("number", "123").put_string("flag", "false").commit()
# get_int and get_bool should handle conversion
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
# Note: the stored values are strings, not ints/bools, so they're different from defaults
self.assertIn("number", prefs2.data)
self.assertIn("flag", prefs2.data)
def test_multiple_editors_with_defaults(self):
"""Test that multiple edit sessions work correctly with defaults."""
defaults = {"brightness": -1, "volume": 50}
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# First editor session
editor1 = prefs.edit()
editor1.put_int("brightness", 75)
editor1.commit()
# Second editor session
editor2 = prefs.edit()
editor2.put_int("volume", 80)
editor2.commit()
# Verify both values
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertEqual(prefs2.get_int("brightness"), 75)
self.assertEqual(prefs2.get_int("volume"), 80)
self.assertIn("brightness", prefs2.data)
self.assertIn("volume", prefs2.data)
# Set one back to default
prefs2.edit().put_int("brightness", -1).commit()
prefs3 = SharedPreferences(self.test_app_name, defaults=defaults)
self.assertFalse("brightness" in prefs3.data)
self.assertEqual(prefs3.get_int("brightness"), -1)
def test_partial_defaults(self):
"""Test that some keys can have defaults while others don't."""
defaults = {"brightness": -1} # Only brightness has a default
prefs = SharedPreferences(self.test_app_name, defaults=defaults)
# Save multiple values
prefs.edit().put_int("brightness", -1).put_int("volume", 50).put_string("name", "test").commit()
# Reload
prefs2 = SharedPreferences(self.test_app_name, defaults=defaults)
# brightness matches default, should not be in data
self.assertFalse("brightness" in prefs2.data)
self.assertEqual(prefs2.get_int("brightness"), -1)
# volume and name have no defaults, should be in data
self.assertIn("volume", prefs2.data)
self.assertIn("name", prefs2.data)
self.assertEqual(prefs2.get_int("volume"), 50)
self.assertEqual(prefs2.get_string("name"), "test")