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

95 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-01 03:18 -0800

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.pipe.tasks.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__( 

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

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

72 ... 

73 

74 @overload 

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

76 ... 

77 

78 def __get__( 

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

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

81 if instance is None: 

82 return self 

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

84 part = update_wrapper(part, self.applyContext) 

85 return part 

86 

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

88 self.applyContext(instance, context) 

89 

90 @staticmethod 

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

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

93 

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

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

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

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

98 execution environment like a python shell or notebook. 

99 

100 Parameters 

101 ---------- 

102 context : `Context` 

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

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

105 """ 

106 # imported here to avoid circular imports at module scope 

107 from ..interfaces import AnalysisAction 

108 

109 for ctx in context.getContexts(): 

110 ctx.apply(instance) 

111 for field in instance._fields: 

112 match getattr(instance, field): 

113 case AnalysisAction() as singleField: 

114 singleField.applyContext 

115 singleField.applyContext(context) 

116 # type ignore because MyPy is not seeing Pipe_tasks imports 

117 # correctly (its not formally typed) 

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

119 subField: AnalysisAction 

120 for subField in multiField: 

121 subField.applyContext(context) 

122 

123 

124class ContextMeta(type): 

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

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

127 together using the | operator. 

128 """ 

129 

130 _contexts: set[ContextMeta] 

131 

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

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

134 result._contexts = set() 

135 if result.__name__ != "Context": 

136 result._contexts.add(result) 

137 return result 

138 

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

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

141 

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

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

144 with the first letter lower case) 

145 

146 Parameters 

147 ---------- 

148 action : `AnalysisAction` 

149 The action to apply the `Context` to. 

150 """ 

151 name = cls.__name__ 

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

153 if hasattr(action, name): 

154 getattr(action, name)() 

155 

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

157 # join 

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

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

160 

161 Parameters 

162 ---------- 

163 other : `ContextMeta` or `Context` 

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

165 

166 Returns 

167 ------- 

168 jointContext : `Context` 

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

170 """ 

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

172 raise NotImplementedError() 

173 ctx = Context() 

174 ctx._contexts = set() 

175 ctx._contexts |= cls._contexts 

176 ctx._contexts |= other._contexts 

177 return ctx 

178 

179 @staticmethod 

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

181 ctxs = list(obj.getContexts()) 

182 if len(ctxs) == 1: 

183 return ctxs[0].__name__ 

184 else: 

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

186 

187 def __str__(cls) -> str: 

188 return cls._makeStr(cls) 

189 

190 def __repr__(cls) -> str: 

191 return str(cls) 

192 

193 getContexts = ContextGetter() 

194 

195 

196class Context(metaclass=ContextMeta): 

197 """A Base Context class. 

198 

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

200 

201 Subclassing this class creates a new independent context. 

202 """ 

203 

204 _contexts: set[ContextMeta] 

205 

206 getContexts = ContextGetter() 

207 

208 def __str__(self) -> str: 

209 return type(self)._makeStr(self) 

210 

211 def __repr__(self) -> str: 

212 return str(self) 

213 

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

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

216 

217 Parameters 

218 ---------- 

219 other : `ContextMeta` or `Context` 

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

221 

222 Returns 

223 ------- 

224 jointContext : `Context` 

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

226 """ 

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

228 raise NotImplementedError() 

229 ctx = Context() 

230 ctx._contexts = set() 

231 ctx._contexts |= self._contexts 

232 ctx._contexts |= other._contexts 

233 return ctx 

234 

235 

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

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

238should be accepted (which is most places) 

239"""