Coverage for lazy_settings/conf/__init__.py: 66%

128 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-11 03:57 +0330

1""" 

2Settings and configuration for Django. 

3 

4Read values from the module specified by the DJANGO_SETTINGS_MODULE environment 

5variable, and then from django.conf.global_settings; see the global_settings.py 

6for a list of all possible variables. 

7""" 

8 

9import importlib 

10import os 

11from types import ModuleType 

12from typing import Any, Self 

13 

14from lazy_settings.exceptions import ImproperlyConfigured 

15from lazy_settings.utils.functional import LazyObject, empty 

16 

17ENVIRONMENT_VARIABLE = "SETTINGS_MODULE" 

18 

19 

20class SettingsReference(str): 

21 """ 

22 String subclass which references a current settings value. It's treated as 

23 the value in memory but serializes to a settings.NAME attribute reference. 

24 """ 

25 

26 def __new__(self, value: Any, setting_name: str) -> Self: 

27 return str.__new__(self, value) 

28 

29 def __init__(self, value: str, setting_name: str) -> None: 

30 self.setting_name = setting_name 

31 

32 

33class LazySettings(LazyObject["Settings | UserSettingsHolder"]): 

34 """ 

35 A lazy proxy for either global Django settings or a custom settings object. 

36 The user can manually configure settings prior to using them. Otherwise, 

37 Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE. 

38 """ 

39 

40 def _setup(self, name: str | None = None) -> None: 

41 """ 

42 Load the settings module pointed to by the environment variable. This 

43 is used the first time settings are needed, if the user hasn't 

44 configured settings manually. 

45 """ 

46 settings_module: str | None = os.environ.get(ENVIRONMENT_VARIABLE) 

47 if not settings_module: 

48 desc = ("setting %s" % name) if name else "settings" 

49 raise ImproperlyConfigured( 

50 "Requested %s, but settings are not configured. " 

51 "You must either define the environment variable %s " 

52 "or call settings.configure() before accessing settings." 

53 % (desc, ENVIRONMENT_VARIABLE) 

54 ) 

55 

56 self._wrapped = Settings(settings_module) 

57 

58 def __repr__(self) -> str: 

59 # Hardcode the class name as otherwise it yields 'Settings'. 

60 if self._wrapped is empty: 

61 return "<LazySettings [Unevaluated]>" 

62 return '<LazySettings "%(settings_module)s">' % { 

63 "settings_module": self._wrapped.SETTINGS_MODULE, # type: ignore[attr-defined] 

64 } 

65 

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

67 """Return the value of a setting and cache it in self.__dict__.""" 

68 if (_wrapped := self._wrapped) is empty: 

69 self._setup(name) 

70 _wrapped = self._wrapped 

71 val = getattr(_wrapped, name) 

72 

73 self.__dict__[name] = val 

74 return val 

75 

76 def __setattr__(self, name: str, value: Any) -> None: 

77 """ 

78 Set the value of setting. Clear all cached values if _wrapped changes 

79 (@override_settings does this) or clear single values when set. 

80 """ 

81 if name == "_wrapped": 

82 self.__dict__.clear() 

83 else: 

84 self.__dict__.pop(name, None) 

85 super().__setattr__(name, value) 

86 

87 def __delattr__(self, name: str) -> None: 

88 """Delete a setting and clear it from cache if needed.""" 

89 super().__delattr__(name) 

90 self.__dict__.pop(name, None) 

91 

92 def __iter__(self): 

93 if self._wrapped is empty: 

94 raise ImproperlyConfigured( 

95 "settings are not configured. " 

96 "You must either define the environment variable %s " 

97 "or call settings.configure() before accessing settings." 

98 % (ENVIRONMENT_VARIABLE) 

99 ) 

100 for setting in self._wrapped: 

101 yield setting 

102 

103 def configure(self, **options) -> None: 

104 """ 

105 Called to manually configure the settings. 

106 """ 

107 if not self.configured: 

108 raise RuntimeError("Settings already configured.") 

109 holder = UserSettingsHolder() 

110 for name, value in options.items(): 

111 if not name.isupper(): 

112 raise TypeError("Setting %r must be uppercase." % name) 

113 setattr(holder, name, value) 

114 self._wrapped = holder 

115 

116 @property 

117 def configured(self) -> bool: 

118 """Return True if the settings have already been configured.""" 

119 return self._wrapped is not empty 

120 

121 def register(self, settings_module: ModuleType) -> None: 

122 if self._wrapped is empty: 

123 self._setup() 

124 self._wrapped.register(settings_module) # type: ignore[attr-defined] 

125 

126 def clear(self) -> None: 

127 if self._wrapped is empty: 

128 return 

129 self._wrapped.clear() # type: ignore[attr-defined] 

130 

131 

132class Settings: 

133 def __init__(self, settings_module: str): 

134 # store the settings module in case someone later cares 

135 self.SETTINGS_MODULE = settings_module 

136 

137 mod = importlib.import_module(self.SETTINGS_MODULE) 

138 

139 self._explicit_settings = set() 

140 for setting in dir(mod): 

141 if setting.isupper(): 

142 setting_value = getattr(mod, setting) 

143 

144 setattr(self, setting, setting_value) 

145 self._explicit_settings.add(setting) 

146 

147 def __repr__(self) -> str: 

148 return '<%(cls)s "%(settings_module)s">' % { 

149 "cls": self.__class__.__name__, 

150 "settings_module": self.SETTINGS_MODULE, 

151 } 

152 

153 def __iter__(self): 

154 for setting in dir(self): 

155 if setting.isupper(): 

156 yield setting 

157 

158 def is_overridden(self, setting: str) -> bool: 

159 return setting in self._explicit_settings 

160 

161 def register(self, settings_module: ModuleType) -> None: 

162 for setting in dir(settings_module): 

163 if setting.isupper() and setting not in self: 

164 setattr(self, setting, getattr(settings_module, setting)) 

165 

166 def clear(self) -> None: 

167 for attr in self: 

168 if attr.isupper(): 

169 delattr(self, attr) 

170 

171 

172class UserSettingsHolder: 

173 """Holder for user configured settings.""" 

174 

175 # SETTINGS_MODULE doesn't make much sense in the manually configured 

176 # (standalone) case. 

177 SETTINGS_MODULE: None = None 

178 

179 def __init__(self, settings=None): 

180 self.__dict__["_deleted"] = set() 

181 

182 # if settings is None, it won't have any upper case attribute 

183 for setting in dir(settings): 

184 if setting not in self and setting.isupper(): 

185 setattr(self, setting, getattr(settings, setting)) 

186 

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

188 if not name.isupper() or name in self._deleted: 

189 raise AttributeError 

190 

191 return getattr(self, name) 

192 

193 def __setattr__(self, name: str, value: Any) -> None: 

194 self._deleted.discard(name) 

195 super().__setattr__(name, value) 

196 

197 def __delattr__(self, name: str) -> None: 

198 self._deleted.add(name) 

199 if hasattr(self, name): 

200 super().__delattr__(name) 

201 

202 def __dir__(self) -> list: 

203 return sorted( 

204 s 

205 for s in [ 

206 *self.__dict__, 

207 ] 

208 if s not in self._deleted 

209 ) 

210 

211 def __repr__(self): 

212 return "<%(cls)s>" % { 

213 "cls": self.__class__.__name__, 

214 } 

215 

216 def __iter__(self): 

217 for setting in dir(self): 

218 if setting.isupper(): 

219 yield setting 

220 

221 def register(self, settings_module: ModuleType | Settings) -> None: 

222 for setting in dir(settings_module): 

223 if setting.isupper() and setting not in dir(self): 

224 setattr(self, setting, getattr(settings_module, setting)) 

225 

226 def clear(self): 

227 for attr in dir(self): 

228 if attr.isupper(): 

229 delattr(self, attr) 

230 

231 def is_overridden(self, setting: str) -> bool: 

232 deleted = setting in self._deleted 

233 set_locally = setting in self.__dict__ 

234 return deleted or set_locally 

235 

236 

237settings = LazySettings()