Coverage for src/lektor_ng/environment/config_old.py: 93%

151 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-31 01:05 +0000

1import copy 

2import os 

3import re 

4from collections import OrderedDict 

5from urllib.parse import urlsplit 

6 

7from werkzeug.utils import cached_property 

8 

9from lektor_ng.constants import PRIMARY_ALT 

10from lektor_ng.i18n import get_i18n_block 

11from lektor_ng.inifile import IniFile 

12from lektor_ng.utils import bool_from_string, secure_url 

13 

14DEFAULT_CONFIG = { 

15 "EPHEMERAL_RECORD_CACHE_SIZE": 500, 

16 "ATTACHMENT_TYPES": { 

17 # Only enable image formats here that we can handle in imagetools. 

18 # Right now this is limited to jpg, png and gif. 

19 ".jpg": "image", 

20 ".jpeg": "image", 

21 ".png": "image", 

22 ".gif": "image", 

23 ".svg": "image", 

24 ".avi": "video", 

25 ".mpg": "video", 

26 ".mpeg": "video", 

27 ".wmv": "video", 

28 ".ogv": "video", 

29 ".mp4": "video", 

30 ".mp3": "audio", 

31 ".wav": "audio", 

32 ".ogg": "audio", 

33 ".pdf": "document", 

34 ".doc": "document", 

35 ".docx": "document", 

36 ".htm": "document", 

37 ".html": "document", 

38 ".txt": "text", 

39 ".log": "text", 

40 }, 

41 "PROJECT": { 

42 "name": None, 

43 "locale": "en_US", 

44 "url": None, 

45 "url_style": "relative", 

46 }, 

47 "THEME_SETTINGS": {}, 

48 "PACKAGES": {}, 

49 "ALTERNATIVES": OrderedDict(), 

50 "PRIMARY_ALTERNATIVE": None, 

51 "SERVERS": {}, 

52} 

53 

54 

55def update_config_from_ini(config, inifile): 

56 for section_name in ("ATTACHMENT_TYPES", "PROJECT", "PACKAGES", "THEME_SETTINGS"): 

57 section_config = inifile.section_as_dict(section_name.lower()) 

58 config[section_name].update(section_config) 

59 

60 for sect in inifile.sections(): 

61 if sect.startswith("servers."): 

62 server_id = sect.split(".")[1] 

63 config["SERVERS"][server_id] = inifile.section_as_dict(sect) 

64 elif sect.startswith("alternatives."): 

65 alt = sect.split(".")[1] 

66 config["ALTERNATIVES"][alt] = { 

67 "name": get_i18n_block(inifile, f"alternatives.{alt}.name"), 

68 "url_prefix": inifile.get(f"alternatives.{alt}.url_prefix"), 

69 "url_suffix": inifile.get(f"alternatives.{alt}.url_suffix"), 

70 "primary": inifile.get_bool(f"alternatives.{alt}.primary"), 

71 "locale": inifile.get(f"alternatives.{alt}.locale", "en_US"), 

72 } 

73 

74 for alt, alt_data in config["ALTERNATIVES"].items(): 

75 if alt_data["primary"]: 

76 config["PRIMARY_ALTERNATIVE"] = alt 

77 break 

78 else: 

79 if config["ALTERNATIVES"]: 

80 raise RuntimeError("Alternatives defined but no primary set.") 

81 

82 

83class ServerInfo: 

84 def __init__(self, id, name_i18n, target, enabled=True, default=False, extra=None): 

85 self.id = id 

86 self.name_i18n = name_i18n 

87 self.target = target 

88 self.enabled = enabled 

89 self.default = default 

90 self.extra = extra or {} 

91 

92 @property 

93 def name(self): 

94 return self.name_i18n.get("en") or self.id 

95 

96 @property 

97 def short_target(self): 

98 match = re.match(r"([a-z]+)://([^/]+)", self.target) 

99 if match is not None: 

100 protocol, server = match.groups() 

101 return f"{server} via {protocol}" 

102 return self.target 

103 

104 def to_json(self): 

105 return { 

106 "id": self.id, 

107 "name": self.name, 

108 "name_i18n": self.name_i18n, 

109 "target": self.target, 

110 "short_target": self.short_target, 

111 "enabled": self.enabled, 

112 "default": self.default, 

113 "extra": self.extra, 

114 } 

115 

116 

117class Config: 

118 def __init__(self, filename=None): 

119 self.filename = filename 

120 self.values = copy.deepcopy(DEFAULT_CONFIG) 

121 

122 if filename is not None and os.path.isfile(filename): 

123 inifile = IniFile(filename) 

124 update_config_from_ini(self.values, inifile) 

125 

126 def __getitem__(self, name): 

127 return self.values[name] 

128 

129 @property 

130 def site_locale(self): 

131 """The locale of this project.""" 

132 return self.values["PROJECT"]["locale"] 

133 

134 def get_servers(self, public=False): 

135 """Returns a list of servers.""" 

136 rv = {} 

137 for server in self.values["SERVERS"]: 

138 server_info = self.get_server(server, public=public) 

139 if server_info is None: 

140 continue 

141 rv[server] = server_info 

142 return rv 

143 

144 def get_default_server(self, public=False): 

145 """Returns the default server.""" 

146 choices = [] 

147 for server in self.values["SERVERS"]: 

148 server_info = self.get_server(server, public=public) 

149 if server_info is not None: 

150 if server_info.default: 

151 return server_info 

152 choices.append(server_info) 

153 if len(choices) == 1: 

154 return choices[0] 

155 return None 

156 

157 def get_server(self, name, public=False): 

158 """Looks up a server info by name.""" 

159 info = self.values["SERVERS"].get(name) 

160 if info is None or "target" not in info: 

161 return None 

162 info = info.copy() 

163 target = info.pop("target") 

164 if public: 

165 target = secure_url(target) 

166 return ServerInfo( 

167 id=name, 

168 name_i18n=get_i18n_block(info, "name", pop=True), 

169 target=target, 

170 enabled=bool_from_string(info.pop("enabled", None), True), 

171 default=bool_from_string(info.pop("default", None), False), 

172 extra=info, 

173 ) 

174 

175 def is_valid_alternative(self, alt): 

176 """Checks if an alternative ID is known.""" 

177 if alt == PRIMARY_ALT: 

178 return True 

179 return alt in self.values["ALTERNATIVES"] 

180 

181 def list_alternatives(self): 

182 """Returns a sorted list of alternative IDs.""" 

183 return sorted(self.values["ALTERNATIVES"]) 

184 

185 def iter_alternatives(self): 

186 """Iterates over all alternatives. If the system is disabled this 

187 yields '_primary'. 

188 """ 

189 found = False 

190 for alt in self.values["ALTERNATIVES"]: 

191 if alt != PRIMARY_ALT: 

192 yield alt 

193 found = True 

194 if not found: 

195 yield PRIMARY_ALT 

196 

197 def get_alternative(self, alt): 

198 """Returns the config setting of the given alt.""" 

199 if alt == PRIMARY_ALT: 

200 alt = self.primary_alternative 

201 return self.values["ALTERNATIVES"].get(alt) 

202 

203 def get_alternative_url_prefixes(self): 

204 """Returns a list of alternative url prefixes by length.""" 

205 items = [(v["url_prefix"].lstrip("/"), k) for k, v in self.values["ALTERNATIVES"].items() if v["url_prefix"]] 

206 items.sort(key=lambda x: -len(x[0])) 

207 return items 

208 

209 def get_alternative_url_suffixes(self): 

210 """Returns a list of alternative url suffixes by length.""" 

211 items = [(v["url_suffix"].rstrip("/"), k) for k, v in self.values["ALTERNATIVES"].items() if v["url_suffix"]] 

212 items.sort(key=lambda x: -len(x[0])) 

213 return items 

214 

215 def get_alternative_url_span(self, alt=PRIMARY_ALT): 

216 """Returns the URL span (prefix, suffix) for an alt.""" 

217 if alt == PRIMARY_ALT: 

218 alt = self.primary_alternative 

219 cfg = self.values["ALTERNATIVES"].get(alt) 

220 if cfg is not None: 

221 return cfg["url_prefix"] or "", cfg["url_suffix"] or "" 

222 return "", "" 

223 

224 @cached_property 

225 def primary_alternative_is_rooted(self): 

226 """`True` if the primary alternative is sitting at the root of 

227 the URL handler. 

228 """ 

229 primary = self.primary_alternative 

230 if primary is None: 

231 return True 

232 

233 cfg = self.values["ALTERNATIVES"].get(primary) 

234 if not (cfg["url_prefix"] or "").lstrip("/") and not (cfg["url_suffix"] or "").rstrip("/"): 

235 return True 

236 

237 return False 

238 

239 @property 

240 def primary_alternative(self): 

241 """The identifier that acts as primary alternative.""" 

242 return self.values["PRIMARY_ALTERNATIVE"] 

243 

244 @cached_property 

245 def base_url(self): 

246 """The external base URL.""" 

247 url = self.values["PROJECT"].get("url") 

248 if url and urlsplit(url).scheme: 

249 return url.rstrip("/") + "/" 

250 return None 

251 

252 @cached_property 

253 def base_path(self): 

254 """The base path of the URL.""" 

255 url = self.values["PROJECT"].get("url") 

256 if url: 

257 return urlsplit(url).path.rstrip("/") + "/" 

258 return "/" 

259 

260 @cached_property 

261 def url_style(self): 

262 """The intended URL style.""" 

263 style = self.values["PROJECT"].get("url_style") 

264 if style in ("relative", "absolute", "external"): 

265 return style 

266 return "relative"