Coverage for python/lsst/images/_transforms/_ast.py: 4%
177 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 08:43 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 08:43 +0000
1# This file is part of lsst-images.
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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14from collections.abc import Iterable
15from typing import TYPE_CHECKING, Any, ClassVar, Self
17__all__ = (
18 "USING_STARLINK_PYAST",
19 "Channel",
20 "CmpFrame",
21 "CmpMap",
22 "FitsChan",
23 "Frame",
24 "FrameSet",
25 "Mapping",
26 "ShiftMap",
27 "SkyFrame",
28 "StringStream",
29 "UnitMap",
30)
32if TYPE_CHECKING:
33 import starlink.Ast
35 USING_STARLINK_PYAST = True
36else:
37 try:
38 from astshim import (
39 Channel,
40 CmpFrame,
41 CmpMap,
42 FitsChan,
43 Frame,
44 FrameDict,
45 FrameSet,
46 Mapping,
47 Object,
48 ShiftMap,
49 SkyFrame,
50 StringStream,
51 UnitMap,
52 )
53 except ImportError:
54 import starlink.Ast
56 USING_STARLINK_PYAST = True
57 else:
58 USING_STARLINK_PYAST = False
61if USING_STARLINK_PYAST: 61 ↛ 63line 61 didn't jump to line 63 because the condition on line 61 was never true
63 class StringStream:
64 """A bridge object that mimics both astshim.StringStream and the
65 interface expected by starlink.Ast.Channel.
67 Notes
68 -----
69 This object can be constructed like an `astshim.StringStream`, but its
70 `astsink` and `astsource` methods actually correspond to the
71 `starlink.Ast` interface, so we can use `starlink.Ast` objects to
72 implement the `FitsChan` classes in this module
73 """
75 def __init__(self, text: str = ""):
76 if "\n" in text or "\r" in text:
77 self._lines = text.splitlines()
78 elif text and len(text) % 80 == 0:
79 # Astropy WCS.to_header_string() yields a single concatenated
80 # FITS header block; FitsChan expects one card per source line.
81 self._lines = [text[n : n + 80] for n in range(0, len(text), 80)]
82 else:
83 self._lines = text.splitlines()
85 def astsource(self) -> str | None:
86 if not self._lines:
87 return None
88 return self._lines.pop(0)
90 def astsink(self, line: str) -> None:
91 self._lines.append(line)
93 def to_string(self) -> str:
94 if not self._lines:
95 return ""
96 return "\n".join(self._lines) + "\n"
98 def getSinkData(self) -> str:
99 return self.to_string()
101 class Object:
102 """Bridge class that exposes the `astshim.Object` interface while
103 being backed by an `astshim.Ast.Object`.
104 """
106 def __init__(self, impl: starlink.Ast.Object):
107 if not isinstance(impl, self._IMPL_TYPE):
108 raise TypeError(f"{type(self).__name__} cannot wrap {type(impl).__name__}.")
109 self._impl = impl
111 _IMPL_TYPE: ClassVar[type[starlink.Ast.Object]] = starlink.Ast.Object
113 def show(self, showComments: bool = True) -> str:
114 sink = StringStream()
115 options = "" if showComments else "Comment=0"
116 chan = starlink.Ast.Channel(None, sink, options=options)
117 chan.write(self._impl)
118 return sink.to_string()
120 def __eq__(self, other: object) -> bool:
121 if not isinstance(other, Object) or type(self) is not type(other):
122 return NotImplemented
123 # ``astshim.Object`` ships a structural ``__eq__``; mirror that on
124 # the starlink-pyast wrapper by comparing the AST channel
125 # serialisation, which is the canonical content-faithful
126 # representation for AST objects. Strip comments so cosmetic
127 # changes between equivalent objects do not break equality.
128 return self.show(showComments=False) == other.show(showComments=False)
130 __hash__ = None # type: ignore[assignment]
132 @classmethod
133 def fromString(cls, serialized: str) -> Self:
134 source = StringStream(serialized)
135 channel = starlink.Ast.Channel(source)
136 return cls._wrap(channel.read())
138 @classmethod
139 def _wrap(cls, impl: starlink.Ast.Object) -> Self:
140 subcls = cls._most_derived_type(impl)
141 result = object.__new__(subcls)
142 Object.__init__(result, impl)
143 return result
145 @classmethod
146 def _most_derived_type(cls, impl: starlink.Ast.Object) -> type[Self]:
147 for subcls in cls.__subclasses__():
148 if isinstance(impl, subcls._IMPL_TYPE):
149 return subcls._most_derived_type(impl)
150 return cls
152 class Mapping(Object):
153 _IMPL_TYPE: ClassVar[type[starlink.Ast.Mapping]] = starlink.Ast.Mapping
155 def simplified(self) -> Mapping:
156 return Mapping._wrap(self._impl.simplify())
158 def applyForward(self, xy: Any) -> Any:
159 return self._impl.tran(xy, True)
161 def applyInverse(self, xy: Any) -> Any:
162 return self._impl.tran(xy, False)
164 def then(self, other: Mapping) -> Mapping:
165 return Mapping._wrap(starlink.Ast.CmpMap(self._impl, other._impl, True))
167 def inverted(self) -> Mapping:
168 copy = self._impl.copy()
169 copy.invert()
170 return Mapping._wrap(copy)
172 class UnitMap(Mapping):
173 def __init__(self, n_coord: int):
174 super().__init__(starlink.Ast.UnitMap(n_coord))
176 _IMPL_TYPE: ClassVar[type[starlink.Ast.UnitMap]] = starlink.Ast.UnitMap
178 class ShiftMap(Mapping):
179 def __init__(self, shift: Iterable[float]):
180 super().__init__(starlink.Ast.ShiftMap(list(shift)))
182 _IMPL_TYPE: ClassVar[type[starlink.Ast.ShiftMap]] = starlink.Ast.ShiftMap
184 class CmpMap(Mapping):
185 def __init__(self, map_a: Mapping, map_b: Mapping, series: bool):
186 super().__init__(starlink.Ast.CmpMap(map_a._impl, map_b._impl, series))
188 _IMPL_TYPE: ClassVar[type[starlink.Ast.CmpMap]] = starlink.Ast.CmpMap
190 class Frame(Mapping):
191 def __init__(self, n_axes: int, options: str = ""):
192 super().__init__(starlink.Ast.Frame(n_axes, options))
194 _IMPL_TYPE: ClassVar[type[starlink.Ast.Frame]] = starlink.Ast.Frame
196 @property
197 def ident(self) -> str:
198 return self._impl.Ident
200 @property
201 def domain(self) -> str:
202 return self._impl.Domain
204 @domain.setter
205 def domain(self, value: str) -> None:
206 self._impl.Domain = value
208 def setUnit(self, axis: int, unit: str) -> None:
209 setattr(self._impl, f"Unit_{axis}", unit)
211 def getUnit(self, axis: int) -> str:
212 return getattr(self._impl, f"Unit_{axis}")
214 def setLabel(self, axis: int, label: str) -> None:
215 setattr(self._impl, f"Label_{axis}", label)
217 def getBottom(self, axis: int) -> float:
218 return getattr(self._impl, f"Bottom_{axis}")
220 def getTop(self, axis: int) -> float:
221 return getattr(self._impl, f"Top_{axis}")
223 class SkyFrame(Frame):
224 def __init__(self, options: str = ""):
225 Object.__init__(self, starlink.Ast.SkyFrame(options))
227 _IMPL_TYPE: ClassVar[type[starlink.Ast.SkyFrame]] = starlink.Ast.SkyFrame
229 class CmpFrame(Frame):
230 def __init__(self, frame_a: Frame, frame_b: Frame):
231 Object.__init__(self, starlink.Ast.CmpFrame(frame_a._impl, frame_b._impl, ""))
233 _IMPL_TYPE: ClassVar[type[starlink.Ast.CmpFrame]] = starlink.Ast.CmpFrame
235 class FrameSet(Frame):
236 def __init__(self, base_frame: Frame):
237 Object.__init__(self, starlink.Ast.FrameSet(base_frame._impl))
239 BASE: ClassVar[int] = 1
240 _IMPL_TYPE: ClassVar[type[starlink.Ast.FrameSet]] = starlink.Ast.FrameSet
242 @property
243 def nFrame(self) -> int:
244 return self._impl.Nframe
246 @property
247 def base(self) -> int:
248 return self._impl.Base
250 @base.setter
251 def base(self, value: int) -> None:
252 self._impl.Base = value
254 @property
255 def current(self) -> int:
256 return self._impl.Current
258 @current.setter
259 def current(self, value: int) -> None:
260 self._impl.Current = value
262 def addFrame(self, iframe: int, mapping: Mapping, frame: Frame) -> None:
263 self._impl.addframe(iframe, mapping._impl, frame._impl)
265 def getFrame(self, iframe: int, copy: bool = True) -> Frame:
266 result = self._impl.getframe(iframe)
267 if copy:
268 result = result.copy()
269 return Frame._wrap(result)
271 def getMapping(self, iframe1: int | None = None, iframe2: int | None = None) -> Mapping:
272 if iframe1 is None:
273 iframe1 = self.base
274 if iframe2 is None:
275 iframe2 = self.current
276 return Mapping._wrap(self._impl.getmapping(iframe1, iframe2))
278 class FrameDict(FrameSet):
279 def __init__(self, obj: Object):
280 Object.__init__(self, obj._impl)
282 class FitsChan(Object):
283 def __init__(self, stream: StringStream | None = None, options: str = ""):
284 source = stream if stream is not None else None
285 sink = stream if stream is not None else None
286 super().__init__(starlink.Ast.FitsChan(source, sink, options))
288 _IMPL_TYPE: ClassVar[type[starlink.Ast.FitsChan]] = starlink.Ast.FitsChan
290 def read(self) -> Any:
291 return Object._wrap(self._impl.read())
293 def write(self, obj: Any) -> int:
294 return self._impl.write(obj._impl)
296 def setFitsI(self, keyword: str, value: int) -> None:
297 self._impl.setfitsI(keyword, value, "", 1)
299 def __iter__(self) -> Any:
300 return iter(self._impl)
302 class Channel(Object):
303 def __init__(self, stream: StringStream, options: str = ""):
304 super().__init__(starlink.Ast.Channel(None, stream, options))
306 _IMPL_TYPE: ClassVar[type[starlink.Ast.Channel]] = starlink.Ast.Channel
308 def write(self, obj: Object) -> int:
309 return self._impl.write(obj._impl)