Skip to content

Commit 8534d59

Browse files
committed
refactor(backend filesystem): Refactor to access to a delegated filesystem wrapper pattern.
Improve code organization by encapsulating JSON schema operations within a dedicated FilesystemJSONWithSchema class. - Replace direct vehicle_components attribute with vehicle_components_fs.data for data access - Delegate JSON operations (load, save, validate) to the new filesystem wrapper - Update all references throughout the codebase to use the new pattern
1 parent 6b29ddc commit 8534d59

7 files changed

+144
-232
lines changed

ardupilot_methodic_configurator/backend_filesystem.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ def re_init(self, vehicle_dir: str, vehicle_type: str, blank_component_data: boo
115115

116116
if blank_component_data:
117117
self.wipe_component_info()
118-
if self.vehicle_components and "Components" in self.vehicle_components:
119-
self.save_vehicle_components_json_data(self.vehicle_components, self.vehicle_dir)
118+
if self.vehicle_components_fs.data and "Components" in self.vehicle_components_fs.data:
119+
self.save_vehicle_components_json_data(self.vehicle_components_fs.data, self.vehicle_dir)
120120

121121
if not self.fw_version:
122122
self.fw_version = self.get_fc_fw_version_from_vehicle_components_json()
@@ -159,7 +159,7 @@ def vehicle_configuration_files_exist(self, vehicle_dir: str) -> bool:
159159
if platform_system() == "Windows":
160160
vehicle_configuration_files = [f.lower() for f in vehicle_configuration_files]
161161
pattern = re_compile(r"^\d{2}_.*\.param$")
162-
if self.vehicle_components_json_filename in vehicle_configuration_files and any(
162+
if self.vehicle_components_fs.json_filename in vehicle_configuration_files and any(
163163
pattern.match(f) for f in vehicle_configuration_files
164164
):
165165
return True
@@ -524,7 +524,7 @@ def zip_files(self, files_to_zip: list[tuple[bool, str]]) -> None:
524524
"00_default.param",
525525
"apm.pdef.xml",
526526
self.configuration_steps_filename,
527-
self.vehicle_components_json_filename,
527+
self.vehicle_components_fs.json_filename,
528528
"vehicle.jpg",
529529
"last_uploaded_filename.txt",
530530
"tempcal_gyro.png",
@@ -700,8 +700,12 @@ def backup_fc_parameters_to_file(
700700

701701
def get_eval_variables(self) -> dict[str, dict[str, Any]]:
702702
variables = {}
703-
if hasattr(self, "vehicle_components") and self.vehicle_components and "Components" in self.vehicle_components:
704-
variables["vehicle_components"] = self.vehicle_components["Components"]
703+
if (
704+
hasattr(self, "vehicle_components_fs")
705+
and self.vehicle_components_fs.data
706+
and "Components" in self.vehicle_components_fs.data
707+
):
708+
variables["vehicle_components"] = self.vehicle_components_fs.data["Components"]
705709
if hasattr(self, "doc_dict") and self.doc_dict:
706710
variables["doc_dict"] = self.doc_dict
707711
return variables

ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py

Lines changed: 21 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@
2323
from re import match as re_match
2424
from typing import Any, Union
2525

26-
from jsonschema import ValidationError, validate, validators
27-
2826
from ardupilot_methodic_configurator import _
27+
from ardupilot_methodic_configurator.backend_filesystem_json_with_schema import FilesystemJSONWithSchema
2928
from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
3029
from ardupilot_methodic_configurator.data_model_vehicle_components_json_schema import VehicleComponentsJsonSchema
3130
from ardupilot_methodic_configurator.middleware_template_overview import TemplateOverview
@@ -35,10 +34,9 @@ class VehicleComponents:
3534
"""Load and save vehicle components configurations from a JSON file."""
3635

3736
def __init__(self, save_component_to_system_templates: bool = False) -> None:
38-
self.vehicle_components_json_filename = "vehicle_components.json"
39-
self.vehicle_components_schema_filename = "vehicle_components_schema.json"
40-
self.vehicle_components: Union[None, dict[str, Any]] = None
41-
self.schema: Union[None, dict[Any, Any]] = None
37+
self.vehicle_components_fs: FilesystemJSONWithSchema = FilesystemJSONWithSchema(
38+
"vehicle_components.json", "vehicle_components_schema.json"
39+
)
4240
self.save_component_to_system_templates = save_component_to_system_templates
4341

4442
def load_schema(self) -> dict:
@@ -47,35 +45,7 @@ def load_schema(self) -> dict:
4745
4846
:return: The schema as a dictionary
4947
"""
50-
if self.schema is not None:
51-
return self.schema
52-
53-
# Determine the location of the schema file
54-
schema_path = os_path.join(os_path.dirname(__file__), self.vehicle_components_schema_filename)
55-
56-
try:
57-
with open(schema_path, encoding="utf-8") as file:
58-
loaded_schema: dict[Any, Any] = json_load(file)
59-
60-
# Validate the schema itself against the JSON Schema meta-schema
61-
try:
62-
# Get the Draft7Validator class which has the META_SCHEMA property
63-
validator_class = validators.Draft7Validator
64-
meta_schema = validator_class.META_SCHEMA
65-
66-
# Validate the loaded schema against the meta-schema
67-
validate(instance=loaded_schema, schema=meta_schema)
68-
logging_debug(_("Schema file '%s' is valid."), schema_path)
69-
except Exception as e: # pylint: disable=broad-exception-caught
70-
logging_error(_("Schema file '%s' is not a valid JSON Schema: %s"), schema_path, str(e))
71-
72-
self.schema = loaded_schema
73-
return self.schema
74-
except FileNotFoundError:
75-
logging_error(_("Schema file '%s' not found."), schema_path)
76-
except JSONDecodeError:
77-
logging_error(_("Error decoding JSON schema from file '%s'."), schema_path)
78-
return {}
48+
return self.vehicle_components_fs.load_schema()
7949

8050
def load_component_templates(self) -> dict[str, list[dict]]:
8151
"""
@@ -285,113 +255,42 @@ def validate_vehicle_components(self, data: dict) -> tuple[bool, str]:
285255
:param data: The vehicle components data to validate
286256
:return: A tuple of (is_valid, error_message)
287257
"""
288-
schema = self.load_schema()
289-
if not schema:
290-
return False, _("Could not load validation schema")
291-
292-
try:
293-
validate(instance=data, schema=schema)
294-
return True, ""
295-
except ValidationError as e:
296-
return False, f"{_('Validation error')}: {e.message}"
258+
return self.vehicle_components_fs.validate_json_against_schema(data)
297259

298260
def load_vehicle_components_json_data(self, vehicle_dir: str) -> dict[Any, Any]:
299-
data: dict[Any, Any] = {}
300-
filepath = os_path.join(vehicle_dir, self.vehicle_components_json_filename)
301-
try:
302-
with open(filepath, encoding="utf-8") as file:
303-
data = json_load(file)
261+
return self.vehicle_components_fs.load_json_data(vehicle_dir)
304262

305-
# Validate the loaded data against the schema
306-
is_valid, error_message = self.validate_vehicle_components(data)
307-
if not is_valid:
308-
logging_error(_("Invalid vehicle components file '%s': %s"), filepath, error_message)
309-
# We still return the data even if invalid for debugging purposes
310-
except FileNotFoundError:
311-
# Normal users do not need this information
312-
logging_debug(_("File '%s' not found in %s."), self.vehicle_components_json_filename, vehicle_dir)
313-
except JSONDecodeError:
314-
logging_error(_("Error decoding JSON data from file '%s'."), filepath)
315-
self.vehicle_components = data
316-
return data
317-
318-
def save_vehicle_components_json_data(self, data: dict, vehicle_dir: str) -> tuple[bool, str]: # noqa: PLR0911 # pylint: disable=too-many-return-statements
263+
def save_vehicle_components_json_data(self, data: dict, vehicle_dir: str) -> tuple[bool, str]:
319264
"""
320265
Save the vehicle components data to a JSON file.
321266
322267
:param data: The vehicle components data to save
323268
:param vehicle_dir: The directory to save the file in
324269
:return: A tuple of (error_occurred, error_message)
325270
"""
326-
# Validate before saving
327-
# commented out until https://github.com/ArduPilot/MethodicConfigurator/pull/237 gets merged
328-
# is_valid, error_message = self.validate_vehicle_components(data)
329-
# if not is_valid:
330-
# msg = _("Cannot save invalid vehicle components data: {}").format(error_message)
331-
# logging_error(msg)
332-
# return True, msg
333-
334-
filepath = os_path.join(vehicle_dir, self.vehicle_components_json_filename)
335-
try:
336-
with open(filepath, "w", encoding="utf-8", newline="\n") as file:
337-
json_dump(data, file, indent=4)
338-
except FileNotFoundError:
339-
msg = _("Directory '{}' not found").format(vehicle_dir)
340-
logging_error(msg)
341-
return True, msg
342-
except PermissionError:
343-
msg = _("Permission denied when writing to file '{}'").format(filepath)
344-
logging_error(msg)
345-
return True, msg
346-
except IsADirectoryError:
347-
msg = _("Path '{}' is a directory, not a file").format(filepath)
348-
logging_error(msg)
349-
return True, msg
350-
except OSError as e:
351-
msg = _("OS error when writing to file '{}': {}").format(filepath, str(e))
352-
logging_error(msg)
353-
return True, msg
354-
except TypeError as e:
355-
msg = _("Type error when serializing data to JSON: {}").format(str(e))
356-
logging_error(msg)
357-
return True, msg
358-
except ValueError as e:
359-
msg = _("Value error when serializing data to JSON: {}").format(str(e))
360-
logging_error(msg)
361-
return True, msg
362-
except Exception as e: # pylint: disable=broad-exception-caught
363-
# Still have a fallback for truly unexpected errors
364-
msg = _("Unexpected error saving data to file '{}': {}").format(filepath, str(e))
365-
logging_error(msg)
366-
return True, msg
367-
368-
return False, ""
271+
return self.vehicle_components_fs.save_json_data(data, vehicle_dir)
369272

370273
def get_fc_fw_type_from_vehicle_components_json(self) -> str:
371-
if self.vehicle_components and "Components" in self.vehicle_components:
372-
components = self.vehicle_components["Components"]
373-
else:
374-
components = None
274+
data = self.vehicle_components_fs.data
275+
components = data["Components"] if data and "Components" in data else None
375276
if components:
376277
fw_type: str = components.get("Flight Controller", {}).get("Firmware", {}).get("Type", "")
377278
if fw_type in self.supported_vehicles():
378279
return fw_type
379-
error_msg = _("Firmware type {fw_type} in {self.vehicle_components_json_filename} is not supported")
380-
logging_error(error_msg.format(**locals()))
280+
error_msg = _("Firmware type {fw_type} in {filename} is not supported")
281+
logging_error(error_msg.format(fw_type=fw_type, filename=self.vehicle_components_fs.json_filename))
381282
return ""
382283

383284
def get_fc_fw_version_from_vehicle_components_json(self) -> str:
384-
if self.vehicle_components and "Components" in self.vehicle_components:
385-
components = self.vehicle_components["Components"]
386-
else:
387-
components = None
285+
data = self.vehicle_components_fs.data
286+
components = data["Components"] if data and "Components" in data else None
388287
if components:
389288
version_str: str = components.get("Flight Controller", {}).get("Firmware", {}).get("Version", "")
390289
version_str = version_str.lstrip().split(" ")[0] if version_str else ""
391290
if re_match(r"^\d+\.\d+\.\d+$", version_str):
392291
return version_str
393-
error_msg = _("FW version string {version_str} on {self.vehicle_components_json_filename} is invalid")
394-
logging_error(error_msg.format(**locals()))
292+
error_msg = _("FW version string {version_str} on {filename} is invalid")
293+
logging_error(error_msg.format(version_str=version_str, filename=self.vehicle_components_fs.json_filename))
395294
return ""
396295

397296
@staticmethod
@@ -410,7 +309,7 @@ def get_vehicle_components_overviews() -> dict[str, TemplateOverview]:
410309
:return: A dictionary mapping subdirectory paths to TemplateOverview instances.
411310
"""
412311
vehicle_components_dict = {}
413-
file_to_find = VehicleComponents().vehicle_components_json_filename
312+
file_to_find = VehicleComponents().vehicle_components_fs.json_filename
414313
template_default_dir = ProgramSettings.get_templates_base_dir()
415314
for root, _dirs, files in os_walk(template_default_dir):
416315
if file_to_find in files:
@@ -437,8 +336,9 @@ def wipe_component_info(self) -> None:
437336
Preserves the complete structure of the dictionary including all branches and leaves,
438337
but sets leaf values to empty values based on their type.
439338
"""
440-
if self.vehicle_components is not None:
441-
self._recursively_clear_dict(self.vehicle_components)
339+
data = self.vehicle_components_fs.data
340+
if data is not None:
341+
self._recursively_clear_dict(data)
442342

443343
def _recursively_clear_dict(self, data: Union[dict, list, float, bool, str]) -> None:
444344
"""

ardupilot_methodic_configurator/frontend_tkinter_directory_selection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def on_select_directory(self) -> bool:
200200
self.local_filesystem.vehicle_dir = self.directory
201201

202202
if not self.local_filesystem.vehicle_configuration_files_exist(self.directory):
203-
_filename = self.local_filesystem.vehicle_components_json_filename
203+
_filename = self.local_filesystem.vehicle_components_fs.json_filename
204204
error_msg = _("Selected directory must contain files matching \\d\\d_*\\.param and a {_filename} file")
205205
messagebox.showerror(_("Invalid Vehicle Directory Selected"), error_msg.format(**locals()))
206206
return False

tests/test_backend_filesystem.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ def test_get_eval_variables_with_none(self) -> None:
383383
lfs = LocalFilesystem(
384384
"vehicle_dir", "vehicle_type", None, allow_editing_template_files=False, save_component_to_system_templates=False
385385
)
386-
lfs.vehicle_components = None
386+
lfs.vehicle_components_fs.data = None
387387
lfs.doc_dict = None
388388
result = lfs.get_eval_variables()
389389
assert not result
@@ -483,7 +483,7 @@ def test_get_eval_variables(self) -> None:
483483
assert not result
484484

485485
# Test with components and doc_dict
486-
lfs.vehicle_components = {"Components": {"test": "value"}}
486+
lfs.vehicle_components_fs.data = {"Components": {"test": "value"}}
487487
lfs.doc_dict = {"param": "doc"}
488488
result = lfs.get_eval_variables()
489489
assert "vehicle_components" in result

0 commit comments

Comments
 (0)