Coverage for src/turbo_themes/models.py: 97%

106 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-04 10:58 +0000

1# SPDX-License-Identifier: MIT 

2"""Type definitions for Turbo Themes. 

3 

4Provides typed access to theme tokens loaded from tokens.json. 

5Replaces the complex quicktype-generated types with a simpler implementation. 

6""" 

7 

8from __future__ import annotations 

9 

10from dataclasses import dataclass, field 

11from enum import Enum 

12from typing import Any 

13 

14 

15class Appearance(Enum): 

16 """Theme appearance (light or dark).""" 

17 

18 LIGHT = "light" 

19 DARK = "dark" 

20 

21 

22class TokenNamespace: 

23 """Dynamic namespace for accessing nested token values. 

24 

25 Converts dict keys to attributes for convenient access like: 

26 tokens.background.base 

27 tokens.text.primary 

28 """ 

29 

30 def __init__(self, data: dict[str, Any]) -> None: 

31 self._data = data 

32 for key, value in data.items(): 

33 if isinstance(value, dict): 

34 setattr(self, key, TokenNamespace(value)) 

35 else: 

36 setattr(self, key, value) 

37 

38 def __repr__(self) -> str: 

39 return f"TokenNamespace({self._data!r})" 

40 

41 def __getattr__(self, name: str) -> Any: 

42 # Return None for missing attributes instead of raising 

43 return None 

44 

45 def to_dict(self) -> dict[str, Any]: 

46 """Convert back to dictionary. 

47 

48 Returns: 

49 The underlying dictionary data. 

50 """ 

51 return self._data 

52 

53 

54@dataclass 

55class Tokens: 

56 """Design tokens for a theme. 

57 

58 Provides attribute access to nested token categories: 

59 tokens.background.base 

60 tokens.text.primary 

61 tokens.state.info 

62 """ 

63 

64 _data: dict[str, Any] = field(repr=False) 

65 

66 # Core token categories (always present) 

67 accent: TokenNamespace = field(init=False) 

68 background: TokenNamespace = field(init=False) 

69 border: TokenNamespace = field(init=False) 

70 brand: TokenNamespace = field(init=False) 

71 content: TokenNamespace = field(init=False) 

72 state: TokenNamespace = field(init=False) 

73 text: TokenNamespace = field(init=False) 

74 typography: TokenNamespace = field(init=False) 

75 

76 # Optional token categories 

77 animation: TokenNamespace | None = field(init=False, default=None) 

78 components: TokenNamespace | None = field(init=False, default=None) 

79 elevation: TokenNamespace | None = field(init=False, default=None) 

80 opacity: TokenNamespace | None = field(init=False, default=None) 

81 spacing: TokenNamespace | None = field(init=False, default=None) 

82 

83 def __post_init__(self) -> None: 

84 """Initialize token namespaces from data dict.""" 

85 for key, value in self._data.items(): 

86 if isinstance(value, dict): 86 ↛ 89line 86 didn't jump to line 89 because the condition on line 86 was always true

87 setattr(self, key, TokenNamespace(value)) 

88 else: 

89 setattr(self, key, value) 

90 

91 @classmethod 

92 def from_dict(cls, data: dict[str, Any]) -> Tokens: 

93 """Create Tokens from a dictionary. 

94 

95 Args: 

96 data: Dictionary containing token categories. 

97 

98 Returns: 

99 Tokens instance with parsed token namespaces. 

100 """ 

101 return cls(_data=data) 

102 

103 def to_dict(self) -> dict[str, Any]: 

104 """Convert back to dictionary. 

105 

106 Returns: 

107 The underlying dictionary data. 

108 """ 

109 return self._data 

110 

111 

112@dataclass 

113class ThemeValue: 

114 """A single theme definition with metadata and tokens.""" 

115 

116 id: str 

117 label: str 

118 vendor: str 

119 appearance: Appearance 

120 tokens: Tokens 

121 description: str | None = None 

122 icon_url: str | None = None 

123 

124 @classmethod 

125 def from_dict(cls, data: dict[str, Any]) -> ThemeValue: 

126 """Create ThemeValue from a dictionary. 

127 

128 Args: 

129 data: Dictionary containing theme metadata and tokens. 

130 

131 Returns: 

132 ThemeValue instance with parsed data. 

133 """ 

134 return cls( 

135 id=data["id"], 

136 label=data["label"], 

137 vendor=data["vendor"], 

138 appearance=Appearance(data["appearance"]), 

139 tokens=Tokens.from_dict(data["tokens"]), 

140 description=data.get("$description"), 

141 icon_url=data.get("iconUrl"), 

142 ) 

143 

144 def to_dict(self) -> dict[str, Any]: 

145 """Convert back to dictionary. 

146 

147 Returns: 

148 Dictionary representation of the theme. 

149 """ 

150 result = { 

151 "id": self.id, 

152 "label": self.label, 

153 "vendor": self.vendor, 

154 "appearance": self.appearance.value, 

155 "tokens": self.tokens.to_dict(), 

156 } 

157 if self.description: 157 ↛ 159line 157 didn't jump to line 159 because the condition on line 157 was always true

158 result["$description"] = self.description 

159 if self.icon_url: 159 ↛ 161line 159 didn't jump to line 161 because the condition on line 159 was always true

160 result["iconUrl"] = self.icon_url 

161 return result 

162 

163 

164@dataclass 

165class ByVendorValue: 

166 """Vendor metadata.""" 

167 

168 name: str 

169 homepage: str 

170 themes: list[str] 

171 

172 @classmethod 

173 def from_dict(cls, data: dict[str, Any]) -> ByVendorValue: 

174 """Create ByVendorValue from a dictionary. 

175 

176 Args: 

177 data: Dictionary containing vendor metadata. 

178 

179 Returns: 

180 ByVendorValue instance with parsed data. 

181 """ 

182 return cls( 

183 name=data["name"], 

184 homepage=data["homepage"], 

185 themes=data["themes"], 

186 ) 

187 

188 

189@dataclass 

190class Meta: 

191 """Metadata about the token collection.""" 

192 

193 theme_ids: list[str] = field(default_factory=list) 

194 vendors: list[str] = field(default_factory=list) 

195 total_themes: int = 0 

196 light_themes: int = 0 

197 dark_themes: int = 0 

198 

199 @classmethod 

200 def from_dict(cls, data: dict[str, Any]) -> Meta: 

201 """Create Meta from a dictionary. 

202 

203 Args: 

204 data: Dictionary containing collection metadata. 

205 

206 Returns: 

207 Meta instance with parsed data. 

208 """ 

209 return cls( 

210 theme_ids=data.get("themeIds", []), 

211 vendors=data.get("vendors", []), 

212 total_themes=data.get("totalThemes", 0), 

213 light_themes=data.get("lightThemes", 0), 

214 dark_themes=data.get("darkThemes", 0), 

215 ) 

216 

217 

218@dataclass 

219class TurboThemes: 

220 """Root container for all themes and metadata.""" 

221 

222 themes: dict[str, ThemeValue] 

223 by_vendor: dict[str, ByVendorValue] | None = None 

224 meta: Meta | None = None 

225 schema: str | None = None 

226 version: str | None = None 

227 description: str | None = None 

228 generated: str | None = None 

229 

230 @classmethod 

231 def from_dict(cls, data: dict[str, Any]) -> TurboThemes: 

232 """Create TurboThemes from a dictionary. 

233 

234 Args: 

235 data: Dictionary containing themes and metadata. 

236 

237 Returns: 

238 TurboThemes instance with parsed themes. 

239 """ 

240 themes = { 

241 theme_id: ThemeValue.from_dict(theme_data) 

242 for theme_id, theme_data in data.get("themes", {}).items() 

243 } 

244 

245 by_vendor = None 

246 if "byVendor" in data: 

247 by_vendor = { 

248 vendor_id: ByVendorValue.from_dict(vendor_data) 

249 for vendor_id, vendor_data in data["byVendor"].items() 

250 } 

251 

252 meta = None 

253 if "meta" in data: 

254 meta = Meta.from_dict(data["meta"]) 

255 

256 generated = data.get("$generated") 

257 

258 return cls( 

259 themes=themes, 

260 by_vendor=by_vendor, 

261 meta=meta, 

262 schema=data.get("$schema"), 

263 version=data.get("$version"), 

264 description=data.get("$description"), 

265 generated=generated, 

266 ) 

267 

268 

269def turbo_themes_from_dict(data: dict[str, Any]) -> TurboThemes: 

270 """Create TurboThemes from a dictionary. 

271 

272 Compatibility function matching quicktype output. 

273 

274 Args: 

275 data: Dictionary containing themes and metadata. 

276 

277 Returns: 

278 TurboThemes instance with parsed themes. 

279 """ 

280 return TurboThemes.from_dict(data) 

281 

282 

283__all__ = [ 

284 "Appearance", 

285 "ByVendorValue", 

286 "Meta", 

287 "ThemeValue", 

288 "TokenNamespace", 

289 "Tokens", 

290 "TurboThemes", 

291 "turbo_themes_from_dict", 

292]