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

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

7 

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 

16 

17 

18class Profile: 

19 """Class for managing the apio profile file 

20 ex. ~/.apio/profile.json 

21 """ 

22 

23 # -- Only these instance vars are allowed. 

24 __slots__ = ( 

25 "_profile_path", 

26 "preferences", 

27 ) 

28 

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

36 

37 # ---- Set the default parameters 

38 

39 # User preferences 

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

41 

42 # -- Cache the profile file path 

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

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

45 

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

47 self._maybe_load_profile_file() 

48 

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

54 

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

63 

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

65 theme: str = Profile.read_preferences_theme() 

66 

67 # -- Apply to the apio console. 

68 apio_console.configure(theme_name=theme) 

69 

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

77 

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

79 

80 if not profile_path.exists(): 

81 return default 

82 

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 

93 

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 

99 

100 return theme 

101 

102 def _maybe_load_profile_file(self): 

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

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

105 """ 

106 

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

108 if not self._profile_path.exists(): 

109 return 

110 

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) 

117 

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

119 # -- apio version, drop it. 

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

121 

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" 

127 

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 ) 

135 

136 def _save(self): 

137 """Save the profile file""" 

138 

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

143 

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 

148 

149 # -- Write to profile file. 

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

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

152 

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