Coverage for apio/managers/profile.py: 92%
60 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""Manage the apio profile file"""
11import json
12from pathlib import Path
13from apio.common import apio_console
14from apio.common.debug_util import is_debug
15from apio.common.apio_console import cout, fatal_error
16from apio.common.apio_themes import THEMES_TABLE
17from apio.common.apio_styles import EMPH3
18from apio.utils import util
21class Profile:
22 """Class for managing the apio profile file
23 ex. ~/.apio/profile.json
24 """
26 # -- Only these instance vars are allowed.
27 __slots__ = (
28 "_profile_path",
29 "preferences",
30 )
32 def __init__(
33 self,
34 home_dir: Path,
35 ):
36 """remote_config_url_template is a url string with the
37 placeholder {major} and {minor} for the apio's major and minor
38 version. '"""
40 # ---- Set the default parameters
42 # User preferences
43 self.preferences: dict[str, str] = {}
45 # -- Cache the profile file path
46 # -- Ex. '/home/obijuan/.apio/profile.json'
47 self._profile_path = home_dir / "profile.json"
49 # -- Read the profile from file, if exists.
50 self._maybe_load_profile_file()
52 def set_preferences_theme(self, theme: str):
53 """Set prefer theme name."""
54 self.preferences["theme"] = theme
55 self._save()
56 self.apply_color_preferences()
58 @staticmethod
59 def apply_color_preferences() -> None:
60 """Apply currently preferred theme."""
61 # -- Make sure the console is configured, with the default theme,
62 # -- before reading the preferences. Reading the preferences resolves
63 # -- the apio home dir which may exit with a console error message,
64 # -- for example if the home dir path contains a space.
65 apio_console.configure()
67 # -- If not specified, read the theme from file.
68 theme: str = Profile.read_preferences_theme()
70 # -- Apply to the apio console.
71 apio_console.configure(theme_name=theme)
73 @staticmethod
74 def read_preferences_theme(*, default: str = "light") -> str:
75 """Returns the value of the theme preference or default if not
76 specified. This is a static method because we may need this value
77 before creating the profile object, for example when printing command
78 help.
79 """
81 profile_path = util.resolve_home_dir() / "profile.json"
83 if not profile_path.exists():
84 return default
86 try:
87 with open(profile_path, "r", encoding="utf8") as f:
88 # -- Get the colors preferences value, if exists.
89 data = json.load(f)
90 preferences = data.get("preferences", {})
91 theme = preferences.get("theme", default)
92 except (OSError, ValueError, AttributeError):
93 # -- A corrupt profile file. Not reporting it here since
94 # -- _load_profile_file() reports it with a proper error message.
95 return default
97 # -- Fall back to the default for unknown theme names or values,
98 # -- e.g. from a hand edited or old profile file, since
99 # -- apio_console.configure() accepts only known theme names.
100 if not isinstance(theme, str) or theme not in THEMES_TABLE:
101 return default
103 return theme
105 def _maybe_load_profile_file(self):
106 """Load the profile file if exists, e.g.
107 /home/obijuan/.apio/profile.json)
108 """
110 # -- If profile file doesn't exist then nothing to do.
111 if not self._profile_path.exists():
112 return
114 # -- Read the profile file as a json dict and extract its fields.
115 # -- Handle invalid content gracefully, e.g. a corrupt or hand
116 # -- edited file, since this runs on every apio command.
117 try:
118 with open(self._profile_path, "r", encoding="utf8") as f:
119 data = json.load(f)
121 # -- Extract the fields. If remote config is of a different
122 # -- apio version, drop it.
123 self.preferences = data.get("preferences", {})
125 # -- Perform a shallow sanity check.
126 # -- TODO: Perform a full json validation.
127 assert isinstance(
128 self.preferences, dict
129 ), "profile.preferences is not a dict"
131 except (OSError, ValueError, AttributeError, AssertionError) as e:
132 fatal_error(
133 f"Invalid profile file {self._profile_path}",
134 cause=e,
135 info="You can delete the file, "
136 + "Apio will recreate it automatically.",
137 )
139 def _save(self):
140 """Save the profile file"""
142 # -- Create the enclosing folder, if it does not exist yet
143 path = self._profile_path.parent
144 if not path.exists(): 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 path.mkdir()
147 # -- Construct the json dict.
148 data = {}
149 if self.preferences: 149 ↛ 153line 149 didn't jump to line 153 because the condition on line 149 was always true
150 data["preferences"] = self.preferences
152 # -- Write to profile file.
153 with open(self._profile_path, "w", encoding="utf8") as f:
154 json.dump(data, f, indent=2)
156 # -- Dump for debugging.
157 if is_debug(1): 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true
158 cout("Saved profile:", style=EMPH3)
159 cout(json.dumps(data, indent=2))