Coverage for python / lsst / analysis / tools / contexts / _baseContext.py: 38%

88 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-06 09:07 +0000

1# This file is part of analysis_tools. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21from __future__ import annotations 

22 

23__all__ = ("ContextMeta", "Context", "ContextType", "ContextApplier") 

24 

25from collections.abc import Callable, Iterable 

26from functools import partial, update_wrapper 

27from typing import TYPE_CHECKING, cast, overload 

28 

29from lsst.pex.config.configurableActions import ConfigurableActionStruct 

30 

31if TYPE_CHECKING: 

32 from ..interfaces import AnalysisAction 

33 

34"""This is a module which defines all the implementation details for the 

35`Context` base class. 

36""" 

37 

38 

39class GetterStandin: 

40 def __init__(self, base: Context | ContextMeta): 

41 self.base = base 

42 

43 def __call__(self) -> Iterable[ContextMeta]: 

44 return self.base._contexts 

45 

46 

47class ContextGetter: 

48 r"""Return all the individual `Context\ s` that are part of an overall 

49 `Context`. 

50 

51 In the case of a single `Context` subclass, this will be a 

52 set with one element. If the `Context` is a joint `Context` (created 

53 from more than one individual `Context`\ s, this will be a set of all 

54 joined `Context`\ s. 

55 

56 Returns 

57 ------- 

58 result : `typing.Iterable` of `ContextMeta` 

59 """ 

60 

61 def __get__(self, instance, klass) -> Callable[..., Iterable[ContextMeta]]: 

62 if instance is not None: 

63 return GetterStandin(instance) 

64 else: 

65 return GetterStandin(klass) 

66 

67 

68class ContextApplier: 

69 @overload 

70 def __get__( 

71 self, instance: AnalysisAction, klass: type[AnalysisAction] | None = None 

72 ) -> Callable[[ContextType], None]: ... 

73 

74 @overload 

75 def __get__(self, instance: None, klass: type[AnalysisAction] | None = None) -> ContextApplier: ... 

76 

77 def __get__( 

78 self, instance: AnalysisAction | None, klass: type[AnalysisAction] | None = None 

79 ) -> Callable[[ContextType], None] | ContextApplier: 

80 if instance is None: 

81 return self 

82 part = cast(Callable[[ContextType], None], partial(self.applyContext, instance)) 

83 part = update_wrapper(part, self.applyContext) 

84 return part 

85 

86 def __set__(self, instance: AnalysisAction, context: ContextType) -> None: 

87 self.applyContext(instance, context) 

88 

89 @staticmethod 

90 def applyContext(instance: AnalysisAction, context: ContextType) -> None: 

91 r"""Apply a `Context` to an `AnalysisAction` recursively. 

92 

93 Generally this method is called from within an `AnalysisTool` to 

94 configure all `AnalysisAction`\ s at one time to make sure that they 

95 all are consistently configured. However, it is permitted to call this 

96 method if you are aware of the effects, or from within a specific 

97 execution environment like a python shell or notebook. 

98 

99 Parameters 

100 ---------- 

101 context : `Context` 

102 The specific execution context, this may be a single context or 

103 a joint context, see `Context` for more info. 

104 """ 

105 # imported here to avoid circular imports at module scope 

106 from ..interfaces import AnalysisAction 

107 

108 for ctx in context.getContexts(): 

109 ctx.apply(instance) 

110 for field in instance._fields: 

111 match getattr(instance, field): 

112 case AnalysisAction() as singleField: 

113 singleField.applyContext 

114 singleField.applyContext(context) 

115 # type ignore because MyPy is not seeing Pipe_tasks imports 

116 # correctly (its not formally typed) 

117 case ConfigurableActionStruct() as multiField: # type: ignore 

118 subField: AnalysisAction 

119 for subField in multiField: 

120 subField.applyContext(context) 

121 

122 

123class ContextMeta(type): 

124 """Metaclass for `Context`, this handles ensuring singleton behavior for 

125 each `Context`. It also provides the functionality of joining contexts 

126 together using the | operator. 

127 """ 

128 

129 _contexts: set[ContextMeta] 

130 

131 def __new__(cls, *args, **kwargs): 

132 result = cast(ContextMeta, super().__new__(cls, *args, *kwargs)) 

133 result._contexts = set() 

134 if result.__name__ != "Context": 

135 result._contexts.add(result) 

136 return result 

137 

138 def apply(cls, action: AnalysisAction) -> None: 

139 """Apply this context to a given `AnalysisAction` 

140 

141 This method checks to see if an `AnalysisAction` is aware of this 

142 `Context`. If it is it calls the actions context method (the class name 

143 with the first letter lower case) 

144 

145 Parameters 

146 ---------- 

147 action : `AnalysisAction` 

148 The action to apply the `Context` to. 

149 """ 

150 name = cls.__name__ 

151 name = f"{name[0].lower()}{name[1:]}" 

152 if hasattr(action, name): 

153 getattr(action, name)() 

154 

155 # ignore the conflict with the super type, because we are doing a 

156 # join 

157 def __or__(cls, other: ContextMeta | Context) -> Context: # type: ignore 

158 """Join multiple Contexts together into a new `Context` instance. 

159 

160 Parameters 

161 ---------- 

162 other : `ContextMeta` or `Context` 

163 #The other `context` to join together with the current `Context` 

164 

165 Returns 

166 ------- 

167 jointContext : `Context` 

168 #A `Context` that is the join of this `Context` and the other. 

169 """ 

170 if not isinstance(other, (Context, ContextMeta)): 

171 raise NotImplementedError() 

172 ctx = Context() 

173 ctx._contexts = set() 

174 ctx._contexts |= cls._contexts 

175 ctx._contexts |= other._contexts 

176 return ctx 

177 

178 @staticmethod 

179 def _makeStr(obj: Context | ContextMeta) -> str: 

180 ctxs = list(obj.getContexts()) 

181 if len(ctxs) == 1: 

182 return ctxs[0].__name__ 

183 else: 

184 return "|".join(ctx.__name__ for ctx in ctxs) 

185 

186 def __str__(cls) -> str: 

187 return cls._makeStr(cls) 

188 

189 def __repr__(cls) -> str: 

190 return str(cls) 

191 

192 getContexts = ContextGetter() 

193 

194 

195class Context(metaclass=ContextMeta): 

196 """A Base Context class. 

197 

198 Instances of this class are used to hold joins of multiple contexts. 

199 

200 Subclassing this class creates a new independent context. 

201 """ 

202 

203 _contexts: set[ContextMeta] 

204 

205 getContexts = ContextGetter() 

206 

207 def __str__(self) -> str: 

208 return type(self)._makeStr(self) 

209 

210 def __repr__(self) -> str: 

211 return str(self) 

212 

213 def __or__(self, other: type[Context] | Context) -> Context: 

214 """Join multiple Contexts together into a new `Context` instance. 

215 

216 Parameters 

217 ---------- 

218 other : `ContextMeta` or `Context` 

219 The other `context` to join together with the current `Context` 

220 

221 Returns 

222 ------- 

223 jointContext : `Context` 

224 A `Context` that is the join of this `Context` and the other. 

225 """ 

226 if not isinstance(other, (Context, ContextMeta)): 

227 raise NotImplementedError() 

228 ctx = Context() 

229 ctx._contexts = set() 

230 ctx._contexts |= self._contexts 

231 ctx._contexts |= other._contexts 

232 return ctx 

233 

234 

235type ContextType = Context | type[Context] 

236"""A type alias to use in contexts where either a Context type or instance 

237should be accepted (which is most places) 

238"""