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