Coverage for python/lsst/afw/table/_schema.py: 38%

89 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-15 02:49 -0700

1# This file is part of afw. 

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/>. 

21 

22__all__ = ["Key", "Field", "SchemaItem"] 

23 

24import numpy as np 

25import fnmatch 

26import re 

27import collections 

28import astropy.units 

29 

30import lsst.geom 

31from lsst.utils import continueClass, TemplateMeta 

32 

33from ._table import _Key, _Field, _SchemaItem, Schema 

34 

35# Objects we prefer to use over the C++ string name for 

36# Key/Field/SchemaItem types. 

37_dtypes = { 

38 "String": str, 

39 "B": np.uint8, 

40 "U": np.uint16, 

41 "I": np.int32, 

42 "L": np.int64, 

43 "F": np.float32, 

44 "D": np.float64, 

45 "Angle": lsst.geom.Angle, 

46} 

47 

48 

49class Key(metaclass=TemplateMeta): 

50 pass 

51 

52 

53class Field(metaclass=TemplateMeta): 

54 pass 

55 

56 

57class SchemaItem(metaclass=TemplateMeta): 

58 pass 

59 

60 

61def _registerInstantiations(abc, types): 

62 """Iterate over a private dict (filled by template instantiations in C++) 

63 to register template instantiations a TemplateMeta ABCs. 

64 

65 If an entry for the type string exists in _dtypes, we use that instead of 

66 the string as the key, and use the string as an alias. 

67 """ 

68 for k, v in types.items(): 

69 dtype = _dtypes.get(k, None) 

70 if dtype is not None: 

71 abc.register(dtype, v) 

72 abc.alias(k, v) 

73 else: 

74 abc.register(k, v) 

75 

76 

77# _Key, _Field, and _SchemaItem are {str: class} dicts populated by the C++ 

78# wrappers. The keys are a superset of those in _dtypes. 

79_registerInstantiations(Key, _Key) 

80_registerInstantiations(Field, _Field) 

81_registerInstantiations(SchemaItem, _SchemaItem) 

82 

83# Also register `float->D` as an alias; can't include 

84# in _dtypes because we have (and prefer) np.float64 there. 

85Key.alias(float, _Key["D"]) 

86Field.alias(float, _Field["D"]) 

87SchemaItem.alias(float, _SchemaItem["D"]) 

88 

89 

90@continueClass 

91class Schema: # noqa: F811 

92 

93 def getOrderedNames(self): 

94 """Return a list of field names in the order the fields were added to the Schema. 

95 

96 Returns 

97 ------- 

98 names : `List` 

99 Field names in order they were added to the Schema. 

100 """ 

101 names = [] 

102 

103 def func(item): 

104 names.append(item.field.getName()) 

105 self.forEach(func) 

106 return names 

107 

108 def __iter__(self): 

109 """Iterate over the items in the Schema. 

110 """ 

111 items = [] 

112 self.forEach(items.append) 

113 return iter(items) 

114 

115 def checkUnits(self, parse_strict='raise'): 

116 """Check that all units in the Schema are valid Astropy unit strings. 

117 

118 Parameters 

119 ---------- 

120 parse_strict : `str`, optional 

121 One of 'raise' (default), 'warn', or 'strict', indicating how to 

122 handle unrecognized unit strings. See also astropy.units.Unit. 

123 """ 

124 def func(item): 

125 astropy.units.Unit(item.field.getUnits(), 

126 parse_strict=parse_strict) 

127 self.forEach(func) 

128 

129 def addField(self, field, type=None, doc="", units="", size=None, 

130 doReplace=False, parse_strict="raise"): 

131 """Add a field to the Schema. 

132 

133 Parameters 

134 ---------- 

135 field : `str` or `Field` 

136 The string name of the Field, or a fully-constructed Field object. 

137 If the latter, all other arguments besides doReplace are ignored. 

138 type : `str`, optional 

139 The type of field to create. Valid types are the keys of the 

140 afw.table.Field dictionary. 

141 doc : `str` 

142 Documentation for the field. 

143 unit : `str` 

144 Units for the field, or an empty string if unitless. 

145 size : `int` 

146 Size of the field; valid for string and array fields only. 

147 doReplace : `bool` 

148 If a field with this name already exists, replace it instead of 

149 raising pex.exceptions.InvalidParameterError. 

150 parse_strict : `str` 

151 One of 'raise' (default), 'warn', or 'strict', indicating how to 

152 handle unrecognized unit strings. See also astropy.units.Unit. 

153 

154 Returns 

155 ------- 

156 result : 

157 Result of the `Field` addition. 

158 """ 

159 if isinstance(field, str): 

160 field = Field[type](field, doc=doc, units=units, 

161 size=size, parse_strict=parse_strict) 

162 return field._addTo(self, doReplace) 

163 

164 def extract(self, *patterns, **kwargs): 

165 """Extract a dictionary of {<name>: <schema-item>} in which the field 

166 names match the given shell-style glob pattern(s). 

167 

168 Any number of glob patterns may be passed; the result will be the 

169 union of all the result of each glob considered separately. 

170 

171 Parameters 

172 ---------- 

173 patterns : Array of `str` 

174 List of glob patterns to use to select field names. 

175 kwargs : `dict` 

176 Dictionary of additional keyword arguments. May contain: 

177 

178 ``regex`` : `str` or `re` pattern 

179 A regular expression to be used in addition to any 

180 glob patterns passed as positional arguments. Note 

181 that this will be compared with re.match, not 

182 re.search. 

183 ``sub`` : `str` 

184 A replacement string (see re.MatchObject.expand) used 

185 to set the dictionary keys of any fields matched by 

186 regex. 

187 ``ordered`` : `bool`, optional 

188 If True, a collections.OrderedDict will be returned 

189 instead of a standard dict, with the order 

190 corresponding to the definition order of the 

191 Schema. Default is False. 

192 

193 Returns 

194 ------- 

195 d : `dict` 

196 Dictionary of extracted name-schema item sets. 

197 

198 Raises 

199 ------ 

200 ValueError 

201 Raised if the `sub` keyword argument is invalid without 

202 the `regex` argument. 

203 

204 Also raised if an unknown keyword argument is supplied. 

205 """ 

206 if kwargs.pop("ordered", False): 

207 d = collections.OrderedDict() 

208 else: 

209 d = dict() 

210 regex = kwargs.pop("regex", None) 

211 sub = kwargs.pop("sub", None) 

212 if sub is not None and regex is None: 

213 raise ValueError( 

214 "'sub' keyword argument to extract is invalid without 'regex' argument") 

215 if kwargs: 

216 kwargsStr = ", ".join(kwargs.keys()) 

217 raise ValueError(f"Unrecognized keyword arguments for extract: {kwargsStr}") 

218 for item in self: 

219 trueName = item.field.getName() 

220 names = [trueName] 

221 for alias, target in self.getAliasMap().items(): 

222 if trueName.startswith(target): 

223 names.append(trueName.replace(target, alias, 1)) 

224 for name in names: 

225 if regex is not None: 

226 m = re.match(regex, name) 

227 if m is not None: 

228 if sub is not None: 

229 name = m.expand(sub) 

230 d[name] = item 

231 continue # continue middle loop so we don't match the same name twice 

232 for pattern in patterns: 

233 if fnmatch.fnmatchcase(name, pattern): 

234 d[name] = item 

235 break # break inner loop so we don't match the same name twice 

236 return d 

237 

238 def __reduce__(self): 

239 """For pickle support.""" 

240 fields = [] 

241 for item in self: 

242 fields.append(item.field) 

243 return (makeSchemaFromFields, (fields,)) 

244 

245 

246def makeSchemaFromFields(fields): 

247 """Create a Schema from a sequence of Fields. For pickle support. 

248 

249 Parameters 

250 ---------- 

251 fields : `tuple` ['lsst.afw.table.Field'] 

252 The fields to construct the new Schema from. 

253 

254 Returns 

255 ------- 

256 schema : `lsst.afw.table.Schema` 

257 The constructed Schema. 

258 """ 

259 schema = Schema() 

260 for field in fields: 

261 schema.addField(field) 

262 return schema