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

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 

8 

9"""Manage the apio profile file""" 

10 

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 

19 

20 

21class Profile: 

22 """Class for managing the apio profile file 

23 ex. ~/.apio/profile.json 

24 """ 

25 

26 # -- Only these instance vars are allowed. 

27 __slots__ = ( 

28 "_profile_path", 

29 "preferences", 

30 ) 

31 

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. '""" 

39 

40 # ---- Set the default parameters 

41 

42 # User preferences 

43 self.preferences: dict[str, str] = {} 

44 

45 # -- Cache the profile file path 

46 # -- Ex. '/home/obijuan/.apio/profile.json' 

47 self._profile_path = home_dir / "profile.json" 

48 

49 # -- Read the profile from file, if exists. 

50 self._maybe_load_profile_file() 

51 

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() 

57 

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() 

66 

67 # -- If not specified, read the theme from file. 

68 theme: str = Profile.read_preferences_theme() 

69 

70 # -- Apply to the apio console. 

71 apio_console.configure(theme_name=theme) 

72 

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 """ 

80 

81 profile_path = util.resolve_home_dir() / "profile.json" 

82 

83 if not profile_path.exists(): 

84 return default 

85 

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 

96 

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 

102 

103 return theme 

104 

105 def _maybe_load_profile_file(self): 

106 """Load the profile file if exists, e.g. 

107 /home/obijuan/.apio/profile.json) 

108 """ 

109 

110 # -- If profile file doesn't exist then nothing to do. 

111 if not self._profile_path.exists(): 

112 return 

113 

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) 

120 

121 # -- Extract the fields. If remote config is of a different 

122 # -- apio version, drop it. 

123 self.preferences = data.get("preferences", {}) 

124 

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" 

130 

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 ) 

138 

139 def _save(self): 

140 """Save the profile file""" 

141 

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() 

146 

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 

151 

152 # -- Write to profile file. 

153 with open(self._profile_path, "w", encoding="utf8") as f: 

154 json.dump(data, f, indent=2) 

155 

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))