Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove JSON <--> String conversions when reading/writing files #348

Merged
merged 4 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions software/firmware/bootloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,18 @@ def run_menu(self) -> type:
return launch_class

def main(self):
script_class_name = self.load_state_str()
saved_state = self.load_state_json()
script_class_name = saved_state.get("last_launched", None)
script_class = None

if script_class_name:
script_class = self.get_class_for_name(script_class_name)

if not script_class:
script_class = self.run_menu()
self.save_state_str(f"{script_class.__module__}.{script_class.__name__}")
self.save_state_json(
{"last_launched": f"{script_class.__module__}.{script_class.__name__}"}
)
machine.reset()
else:
# setup the exit handlers, and execute the selection
Expand Down
18 changes: 7 additions & 11 deletions software/firmware/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import os
import json
from file_utils import load_file, delete_file, load_json_data
from file_utils import load_file, delete_file, load_json_file
from collections import namedtuple

Validation = namedtuple("Validation", "is_valid message")
Expand Down Expand Up @@ -192,20 +192,16 @@ def load_config(cls, config_spec: ConfigSpec):
"""If this class has config points, this method validates and returns the config dictionary
as saved in this class's config file, else, returns an empty dict."""
if len(config_spec):
data = load_file(ConfigFile.config_filename(cls))
saved_config = load_json_file(ConfigFile.config_filename(cls))
config = config_spec.default_config()
if not data:
return config
else:
saved_config = load_json_data(data)
validation = config_spec.validate(saved_config)
validation = config_spec.validate(saved_config)

if not validation.is_valid:
raise ValueError(validation.message)
if not validation.is_valid:
raise ValueError(validation.message)

config.update(saved_config)
config.update(saved_config)

return config
return config
else:
return {}

Expand Down
44 changes: 10 additions & 34 deletions software/firmware/europi_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from utime import ticks_diff, ticks_ms
from configuration import ConfigSpec, ConfigFile
from europi_config import EuroPiConfig
from file_utils import load_file, delete_file, load_json_data
from file_utils import load_file, delete_file, load_json_file


class EuroPiScript:
Expand Down Expand Up @@ -170,18 +170,7 @@ def save_state(self):
appropriate save method, such as `save_state_json(state)`. See the
class documentation for a full example.
"""
pass

def save_state_str(self, state: str):
"""Take state in persistence format as a string and write to disk.

.. note::
Be mindful of how often `save_state_str()` is called because
writing to disk too often can slow down the performance of your
script. Only call save state when state has changed and consider
adding a time since last save check to reduce save frequency.
"""
return self._save_state(state)
self.save_state_json({})

def save_state_bytes(self, state: bytes):
"""Take state in persistence format as bytes and write to disk.
Expand All @@ -192,7 +181,9 @@ def save_state_bytes(self, state: bytes):
script. Only call save state when state has changed and consider
adding a time since last save check to reduce save frequency.
"""
return self._save_state(state, mode="wb")
with open(self._state_filename, "wb") as file:
file.write(state)
self._last_saved = ticks_ms()

def save_state_json(self, state: dict):
"""Take state as a dict and save as a json string.
Expand All @@ -203,40 +194,25 @@ def save_state_json(self, state: dict):
script. Only call save state when state has changed and consider
adding a time since last save check to reduce save frequency.
"""
json_str = json.dumps(state)
return self._save_state(json_str)

def _save_state(self, state: str, mode: str = "w"):
with open(self._state_filename, mode) as file:
file.write(state)
self._last_saved = ticks_ms()

def load_state_str(self) -> str:
"""Check disk for saved state, if it exists, return the raw state value as a string.

Check for a previously saved state. If it exists, return state as a
string. If no state is found, an empty string will be returned.
"""
return self._load_state()
with open(self._state_filename, "w") as file:
json.dump(state, file)
self._last_saved = ticks_ms()

def load_state_bytes(self) -> bytes:
"""Check disk for saved state, if it exists, return the raw state value as bytes.

Check for a previously saved state. If it exists, return state as a
byte string. If no state is found, an empty string will be returned.
"""
return self._load_state(mode="rb")
return load_file(self._state_filename, "rb")

def load_state_json(self) -> dict:
"""Load previously saved state as a dict.

Check for a previously saved state. If it exists, return state as a
dict. If no state is found, an empty dictionary will be returned.
"""
return load_json_data(self._load_state())

def _load_state(self, mode: str = "r") -> any:
return load_file(self._state_filename, mode)
return load_json_file(self._state_filename)

def remove_state(self):
"""Remove the state file for this script."""
Expand Down
32 changes: 22 additions & 10 deletions software/firmware/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,37 @@


def load_file(filename, mode: str = "r") -> any:
"""Load a file and return its contents

@param filename The name of the file to load
@param mode The mode to open the file in. Should be "r" for text files or "rb" for binary files

@return The file's contents, either as a string or bytes, depending on mode.
"""
try:
with open(filename, mode) as file:
return file.read()
except OSError as e:
return ""
print(f"Unable to read {filename}: {e}")
if "b" in mode:
return b""
else:
return ""


def load_json_file(filename, mode="r") -> dict:
"""Load a file and return its contents

def load_json_data(json_str):
"""Load previously saved json data as a dict.
@param filename The name of the file to load
@param mode The mode to open the file in. Should be "r" except in very unique circumstances

Check for a previously saved data. If it exists, return data as a
dict. If no data is found, an empty dictionary will be returned.
@return The file's contents as a dict
"""
if json_str == "":
return {}
try:
return json.loads(json_str)
except ValueError as e:
print(f"Unable to decode {json_str}: {e}")
with open(filename, mode) as file:
return json.load(file)
except OSError as e:
print(f"Unable to read JSON data from {filename}: {e}")
return {}


Expand Down
4 changes: 2 additions & 2 deletions software/tests/test_europi_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ def script_for_testing_with_config():


def test_save_state(script_for_testing):
script_for_testing._save_state("test state")
assert script_for_testing._load_state() == "test state"
script_for_testing.save_state_json({"spam": "eggs"})
assert script_for_testing.load_state_json() == {"spam": "eggs"}


def test_state_file_name(script_for_testing):
Expand Down
Loading