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

93 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-03 13:22 +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"""This is a module which defines all the implementation details for the 

24`Context` base class. 

25""" 

26 

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

28 

29from functools import partial, update_wrapper 

30from typing import TYPE_CHECKING, Callable, Iterable, Union, cast, overload 

31 

32from lsst.pex.config.configurableActions import ConfigurableActionStruct 

33 

34if TYPE_CHECKING: 34 ↛ 35line 34 didn't jump to line 35, because the condition on line 34 was never true

35 from ..interfaces import AnalysisAction 

36 

37 

38class GetterStandin: 

39 def __init__(self, base: Union[Context, ContextMeta]): 

40 self.base = base 

41 

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

43 return self.base._contexts 

44 

45 

46class ContextGetter: 

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

48 `Context`. 

49 

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

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

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

53 joined `Context`\ s. 

54 

55 Returns 

56 ------- 

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

58 """ 

59 

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

61 if instance is not None: 

62 return GetterStandin(instance) 

63 else: 

64 return GetterStandin(klass) 

65 

66 

67class ContextApplier: 

68 @overload 

69 def __get__( 69 ↛ exitline 69 didn't jump to the function exit

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

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

72 

73 @overload 

74 def __get__(self, instance: None, klass: type[AnalysisAction] | None = None) -> ContextApplier: ... 74 ↛ exitline 74 didn't return from function '__get__'

75 

76 def __get__( 

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

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

79 if instance is None: 

80 return self 

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

82 part = update_wrapper(part, self.applyContext) 

83 return part 

84 

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

86 self.applyContext(instance, context) 

87 

88 @staticmethod 

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

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

91 

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

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

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

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

96 execution environment like a python shell or notebook. 

97 

98 Parameters 

99 ---------- 

100 context : `Context` 

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

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

103 """ 

104 # imported here to avoid circular imports at module scope 

105 from ..interfaces import AnalysisAction 

106 

107 for ctx in context.getContexts(): 

108 ctx.apply(instance) 

109 for field in instance._fields: 

110 match getattr(instance, field): 

111 case AnalysisAction() as singleField: 

112 singleField.applyContext 

113 singleField.applyContext(context) 

114 # type ignore because MyPy is not seeing Pipe_tasks imports 

115 # correctly (its not formally typed) 

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

117 subField: AnalysisAction 

118 for subField in multiField: 

119 subField.applyContext(context) 

120 

121 

122class ContextMeta(type): 

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

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

125 together using the | operator. 

126 """ 

127 

128 _contexts: set[ContextMeta] 

129 

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

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

132 result._contexts = set() 

133 if result.__name__ != "Context": 

134 result._contexts.add(result) 

135 return result 

136 

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

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

139 

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

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

142 with the first letter lower case) 

143 

144 Parameters 

145 ---------- 

146 action : `AnalysisAction` 

147 The action to apply the `Context` to. 

148 """ 

149 name = cls.__name__ 

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

151 if hasattr(action, name): 

152 getattr(action, name)() 

153 

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

155 # join 

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

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

158 

159 Parameters 

160 ---------- 

161 other : `ContextMeta` or `Context` 

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

163 

164 Returns 

165 ------- 

166 jointContext : `Context` 

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

168 """ 

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

170 raise NotImplementedError() 

171 ctx = Context() 

172 ctx._contexts = set() 

173 ctx._contexts |= cls._contexts 

174 ctx._contexts |= other._contexts 

175 return ctx 

176 

177 @staticmethod 

178 def _makeStr(obj: Union[Context, ContextMeta]) -> str: 

179 ctxs = list(obj.getContexts()) 

180 if len(ctxs) == 1: 

181 return ctxs[0].__name__ 

182 else: 

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

184 

185 def __str__(cls) -> str: 

186 return cls._makeStr(cls) 

187 

188 def __repr__(cls) -> str: 

189 return str(cls) 

190 

191 getContexts = ContextGetter() 

192 

193 

194class Context(metaclass=ContextMeta): 

195 """A Base Context class. 

196 

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

198 

199 Subclassing this class creates a new independent context. 

200 """ 

201 

202 _contexts: set[ContextMeta] 

203 

204 getContexts = ContextGetter() 

205 

206 def __str__(self) -> str: 

207 return type(self)._makeStr(self) 

208 

209 def __repr__(self) -> str: 

210 return str(self) 

211 

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

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

214 

215 Parameters 

216 ---------- 

217 other : `ContextMeta` or `Context` 

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

219 

220 Returns 

221 ------- 

222 jointContext : `Context` 

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

224 """ 

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

226 raise NotImplementedError() 

227 ctx = Context() 

228 ctx._contexts = set() 

229 ctx._contexts |= self._contexts 

230 ctx._contexts |= other._contexts 

231 return ctx 

232 

233 

234ContextType = Union[Context, type[Context]] 

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

236should be accepted (which is most places) 

237"""