Skip to content

Commit

Permalink
Use importlib for package resources (yaml, csv)
Browse files Browse the repository at this point in the history
importlib.resources is the standard recommended way of accessing
non-python files in a python package. Introduced in 3.9, this replaces
the old pkg_resources 3rd party library. It uses Traversable for file
paths, a Pathlib Path like object that can deal with the quirks of the
python import system.
  • Loading branch information
ThirteenFish committed Nov 7, 2024
1 parent 2445597 commit 0e88836
Show file tree
Hide file tree
Showing 9 changed files with 51 additions and 51 deletions.
18 changes: 11 additions & 7 deletions oresat_configs/_yaml_to_od.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Convert OreSat configs to ODs."""

import os
from collections import namedtuple
from copy import deepcopy
from importlib import abc, resources
from typing import Union

import canopen
Expand All @@ -17,7 +17,7 @@
from .card_info import Card
from .constants import Consts, __version__

STD_OBJS_FILE_NAME = f"{os.path.dirname(os.path.abspath(__file__))}/standard_objects.yaml"
STD_OBJS_FILE_NAME = resources.files("oresat_configs") / "standard_objects.yaml"

RPDO_COMM_START = 0x1400
RPDO_PARA_START = 0x1600
Expand Down Expand Up @@ -474,11 +474,11 @@ def _add_all_rpdo_data(


def _load_std_objs(
file_path: str, node_ids: dict[str, int]
file_path: abc.Traversable, node_ids: dict[str, int]
) -> dict[str, Union[Variable, Record, Array]]:
"""Load the standard objects."""

with open(file_path, "r") as f:
with file_path.open() as f:
std_objs_raw = load(f, Loader=CLoader)

std_objs = {}
Expand Down Expand Up @@ -565,8 +565,11 @@ def _load_configs(config_paths: ConfigPaths) -> dict[str, CardConfig]:
if paths is None:
continue

card_config = CardConfig.from_yaml(paths[0])
common_config = CardConfig.from_yaml(paths[1])
with resources.as_file(paths[0]) as path:
card_config = CardConfig.from_yaml(path)

with resources.as_file(paths[1]) as path:
common_config = CardConfig.from_yaml(path)

conf = CardConfig()
conf.std_objects = list(set(common_config.std_objects + card_config.std_objects))
Expand All @@ -579,7 +582,8 @@ def _load_configs(config_paths: ConfigPaths) -> dict[str, CardConfig]:
conf.tpdos = common_config.tpdos + card_config.tpdos

if len(paths) > 2:
overlay_config = CardConfig.from_yaml(paths[2])
with resources.as_file(paths[2]) as path:
overlay_config = CardConfig.from_yaml(path)
# because conf is cached by CardConfig, if multiple missions are loaded, the cached
# version should not be modified because the changes will persist to later loaded
# missions.
Expand Down
30 changes: 15 additions & 15 deletions oresat_configs/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
"""OreSat od base configs."""

import os
from importlib import abc, resources
from typing import Optional

ConfigPaths = dict[str, Optional[tuple[str, ...]]]
ConfigPaths = dict[str, Optional[tuple[abc.Traversable, ...]]]

_CONFIGS_DIR = os.path.dirname(os.path.abspath(__file__))
FW_COMMON_CONFIG_PATH = f"{_CONFIGS_DIR}/fw_common.yaml"
SW_COMMON_CONFIG_PATH = f"{_CONFIGS_DIR}/sw_common.yaml"
C3_CONFIG_PATH = f"{_CONFIGS_DIR}/c3.yaml"
BAT_CONFIG_PATH = f"{_CONFIGS_DIR}/battery.yaml"
SOLAR_CONFIG_PATH = f"{_CONFIGS_DIR}/solar.yaml"
ADCS_CONFIG_PATH = f"{_CONFIGS_DIR}/adcs.yaml"
RW_CONFIG_PATH = f"{_CONFIGS_DIR}/reaction_wheel.yaml"
GPS_CONFIG_PATH = f"{_CONFIGS_DIR}/gps.yaml"
ST_CONFIG_PATH = f"{_CONFIGS_DIR}/star_tracker.yaml"
DXWIFI_CONFIG_PATH = f"{_CONFIGS_DIR}/dxwifi.yaml"
CFC_CONFIG_PATH = f"{_CONFIGS_DIR}/cfc.yaml"
DIODE_CONFIG_PATH = f"{_CONFIGS_DIR}/diode_test.yaml"
_CONFIGS_DIR = resources.files(__name__)
FW_COMMON_CONFIG_PATH = _CONFIGS_DIR / "fw_common.yaml"
SW_COMMON_CONFIG_PATH = _CONFIGS_DIR / "sw_common.yaml"
C3_CONFIG_PATH = _CONFIGS_DIR / "c3.yaml"
BAT_CONFIG_PATH = _CONFIGS_DIR / "battery.yaml"
SOLAR_CONFIG_PATH = _CONFIGS_DIR / "solar.yaml"
ADCS_CONFIG_PATH = _CONFIGS_DIR / "adcs.yaml"
RW_CONFIG_PATH = _CONFIGS_DIR / "reaction_wheel.yaml"
GPS_CONFIG_PATH = _CONFIGS_DIR / "gps.yaml"
ST_CONFIG_PATH = _CONFIGS_DIR / "star_tracker.yaml"
DXWIFI_CONFIG_PATH = _CONFIGS_DIR / "dxwifi.yaml"
CFC_CONFIG_PATH = _CONFIGS_DIR / "cfc.yaml"
DIODE_CONFIG_PATH = _CONFIGS_DIR / "diode_test.yaml"
5 changes: 3 additions & 2 deletions oresat_configs/beacon_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from dataclasses import dataclass, field
from importlib.abc import Traversable

from dacite import from_dict
from yaml import CLoader, load
Expand Down Expand Up @@ -80,9 +81,9 @@ class BeaconConfig:
"""

@classmethod
def from_yaml(cls, config_path: str) -> BeaconConfig:
def from_yaml(cls, config_path: Traversable) -> BeaconConfig:
"""Load a beacon YAML config file."""

with open(config_path, "r") as f:
with config_path.open() as f:
config_raw = load(f, Loader=CLoader)
return from_dict(data_class=cls, data=config_raw)
5 changes: 3 additions & 2 deletions oresat_configs/card_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from dataclasses import dataclass, field
from functools import cache
from pathlib import Path
from typing import Any, Optional, Union

from dacite import from_dict
Expand Down Expand Up @@ -234,9 +235,9 @@ class CardConfig:

@classmethod
@cache
def from_yaml(cls, config_path: str) -> CardConfig:
def from_yaml(cls, config_path: Path) -> CardConfig:
"""Load a card YAML config file."""

with open(config_path, "r") as f:
with config_path.open() as f:
config_raw = load(f, Loader=CLoader)
return from_dict(data_class=cls, data=config_raw)
2 changes: 1 addition & 1 deletion oresat_configs/card_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def cards_from_csv(mission: Consts) -> dict[str, Card]:
"""Turns cards.csv into a dict of names->Cards, filtered by the current mission"""

path = mission.paths.CARDS_CSV_PATH
with open(path, "r") as f:
with path.open() as f:
reader = csv.DictReader(f)
cols = set(reader.fieldnames) if reader.fieldnames else set()
expect = {f.name for f in fields(Card)}
Expand Down
13 changes: 5 additions & 8 deletions oresat_configs/oresat0/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""OreSat0 object dictionary and beacon constants."""

import os
from importlib.resources import files

from ..base import (
ADCS_CONFIG_PATH,
Expand All @@ -15,13 +15,10 @@
ConfigPaths,
)

_CONFIGS_DIR = os.path.dirname(os.path.abspath(__file__))

BAT_OVERLAY_CONFIG_PATH = f"{_CONFIGS_DIR}/battery_overlay.yaml"

BEACON_CONFIG_PATH: str = f"{_CONFIGS_DIR}/beacon.yaml"

CARDS_CSV_PATH = f"{_CONFIGS_DIR}/cards.csv"
_CONFIGS_DIR = files(__name__)
BAT_OVERLAY_CONFIG_PATH = _CONFIGS_DIR / "battery_overlay.yaml"
BEACON_CONFIG_PATH = _CONFIGS_DIR / "beacon.yaml"
CARDS_CSV_PATH = _CONFIGS_DIR / "cards.csv"

CARD_CONFIGS_PATH: ConfigPaths = {
"c3": (C3_CONFIG_PATH, SW_COMMON_CONFIG_PATH),
Expand Down
10 changes: 4 additions & 6 deletions oresat_configs/oresat0_5/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""OreSat0.5 object dictionary and beacon constants."""

import os
from importlib.resources import files

from ..base import (
ADCS_CONFIG_PATH,
Expand All @@ -18,11 +18,9 @@
ConfigPaths,
)

_CONFIGS_DIR = os.path.dirname(os.path.abspath(__file__))

BEACON_CONFIG_PATH: str = f"{_CONFIGS_DIR}/beacon.yaml"

CARDS_CSV_PATH = f"{_CONFIGS_DIR}/cards.csv"
_CONFIGS_DIR = files(__name__)
BEACON_CONFIG_PATH = _CONFIGS_DIR / "beacon.yaml"
CARDS_CSV_PATH = _CONFIGS_DIR / "cards.csv"

CARD_CONFIGS_PATH: ConfigPaths = {
"c3": (C3_CONFIG_PATH, SW_COMMON_CONFIG_PATH),
Expand Down
10 changes: 4 additions & 6 deletions oresat_configs/oresat1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""OreSat1 object dictionary and beacon constants."""

import os
from importlib.resources import files

from ..base import (
ADCS_CONFIG_PATH,
Expand All @@ -17,11 +17,9 @@
ConfigPaths,
)

_CONFIGS_DIR = os.path.dirname(os.path.abspath(__file__))

BEACON_CONFIG_PATH: str = f"{_CONFIGS_DIR}/beacon.yaml"

CARDS_CSV_PATH = f"{_CONFIGS_DIR}/cards.csv"
_CONFIGS_DIR = files(__name__)
BEACON_CONFIG_PATH = _CONFIGS_DIR / "beacon.yaml"
CARDS_CSV_PATH = _CONFIGS_DIR / "cards.csv"

CARD_CONFIGS_PATH: ConfigPaths = {
"c3": (C3_CONFIG_PATH, SW_COMMON_CONFIG_PATH),
Expand Down
9 changes: 5 additions & 4 deletions tests/test_config_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Unit tests for ensuring yaml config files match up with corresponding dataclasses"""

import unittest
from importlib.abc import Traversable
from typing import Any

from dacite import from_dict # , Config
Expand All @@ -20,20 +21,20 @@ class ConfigTypes(unittest.TestCase):
"""

@staticmethod
def load_yaml(path: str) -> Any:
def load_yaml(path: Traversable) -> Any:
"""Helper that wraps loading yaml from a path"""
with open(path) as f:
with path.open() as f:
config = f.read()
return load(config, Loader=Loader)

def dtype_subtest(self, path: str, dtype: Any, data: Any) -> None:
def dtype_subtest(self, path: Traversable, dtype: Any, data: Any) -> None:
"""The main check that gets done, creates a new subtest for each check"""
with self.subTest(path=path, dtype=dtype):
# raises WrongTypeError if the types don't check out
# when we're ready, use the config below to ensure every yaml field is consumed
from_dict(dtype, data) # , Config(strict=True, strict_unions_match=True))

def check_types(self, path: str, dtype: Any) -> None:
def check_types(self, path: Traversable, dtype: Any) -> None:
"""Helper that combines load_yaml() and dtype_subtest()"""
self.dtype_subtest(path, dtype, self.load_yaml(path))

Expand Down

0 comments on commit 0e88836

Please sign in to comment.