Coverage for python/astro_metadata_translator/translator.py : 42%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# This file is part of astro_metadata_translator. # # Developed for the LSST Data Management System. # This product includes software developed by the LSST Project # (http://www.lsst.org). # See the LICENSE file at the top-level directory of this distribution # for details of code ownership. # # Use of this source code is governed by a 3-clause BSD-style # license that can be found in the LICENSE file.
"""Decorator to cache the result of a translation method.
Especially useful when a translation uses many other translation methods. Should be used only on ``to_x()`` methods.
Parameters ---------- func : `function` Translation method to cache. method : `str`, optional Name of the translation method to cache. Not needed if the decorator is used around a normal method, but necessary when the decorator is being used in a metaclass.
Returns ------- wrapped : `function` Method wrapped by the caching function. """
if name not in self._translation_cache: self._translation_cache[name] = func(self) return self._translation_cache[name]
"""Per-instrument metadata translation support
Parameters ---------- header : `dict`-like Representation of an instrument header that can be manipulated as if it was a `dict`. filename : `str`, optional Name of the file whose header is being translated. For some datasets with missing header information this can sometimes allow for some fixups in translations. """
# These are all deliberately empty in the base class.
"""Dict of one-to-one mappings for header translation from standard property to corresponding keyword."""
"""Dict defining a constant for specified standard properties."""
"""All registered metadata translation classes."""
"""Name of instrument understood by this translation class."""
def defined_in_this_class(cls, name): """Report if the specified class attribute is defined specifically in this class.
Parameters ---------- name : `str` Name of the attribute to test.
Returns ------- in_class : `bool` `True` if there is a attribute of that name defined in this specific subclass. `False` if the method is not defined in this specific subclass but is defined in a parent class. Returns `None` if the attribute is not defined anywhere in the class hierarchy (which can happen if translators have typos in their mapping tables).
Notes ----- Retrieves the attribute associated with the given name. Then looks in all the parent classes to determine whether that attribute comes from a parent class or from the current class. Attributes are compared using `id()`. """ # The attribute to compare.
# Get all the classes in the hierarchy
# Remove the first entry from the list since that will be the # current class
# Some attributes may only exist in subclasses. Skip base classes # that are missing the attribute (such as object).
def _make_const_mapping(property_key, constant): """Make a translator method that returns a constant value.
Parameters ---------- property_key : `str` Name of the property to be calculated (for the docstring). constant : `str` or `numbers.Number` Value to return for this translator.
Returns ------- f : `function` Function returning the constant. """ return constant
else: return_type = type(constant).__name__ property_doc = f"Returns constant value for '{property_key}' property"
Returns ------- translation : `{return_type}` Translated property. """
unit=None, checker=None): """Make a translator method returning a header value.
The header value can be converted to a `~astropy.units.Quantity` if desired, and can also have its value validated.
See `MetadataTranslator.validate_value()` for details on the use of default parameters.
Parameters ---------- property_key : `str` Name of the translator to be constructed (for the docstring). header_key : `str` or `list` of `str` Name of the key to look up in the header. If a `list` each key will be tested in turn until one matches. This can deal with header styles that evolve over time. default : `numbers.Number` or `astropy.units.Quantity`, `str`, optional If not `None`, default value to be used if the parameter read from the header is not defined or if the header is missing. minimum : `numbers.Number` or `astropy.units.Quantity`, optional If not `None`, and if ``default`` is not `None`, minimum value acceptable for this parameter. maximum : `numbers.Number` or `astropy.units.Quantity`, optional If not `None`, and if ``default`` is not `None`, maximum value acceptable for this parameter. unit : `astropy.units.Unit`, optional If not `None`, the value read from the header will be converted to a `~astropy.units.Quantity`. Only supported for numeric values. checker : `function`, optional Callback function to be used by the translator method in case the keyword is not present. Function will be executed as if it is a method of the translator class. Running without raising an exception will allow the default to be used. Should usually raise `KeyError`.
Returns ------- t : `function` Function implementing a translator with the specified parameters. """ else: return_type = "str` or `numbers.Number" property_doc = f"Map '{header_key}' header keyword to '{property_key}' property"
if unit is not None: q = self.quantity_from_card(header_key, unit, default=default, minimum=minimum, maximum=maximum, checker=checker) # Convert to Angle if this quantity is an angle if return_type == "astropy.coordinates.Angle": q = Angle(q) return q
keywords = header_key if isinstance(header_key, list) else [header_key] for key in keywords: if self.is_key_ok(key): value = self._header[key] if default is not None and not isinstance(value, str): value = self.validate_value(value, default, minimum=minimum, maximum=maximum) self._used_these_cards(key) break else: # No keywords found, use default, checking first, or raise # A None default is only allowed if a checker is provided. if checker is not None: try: checker(self) return default except Exception: raise KeyError(f"Could not find {keywords} in header") value = default elif default is not None: value = default else: raise KeyError(f"Could not find {keywords} in header")
# If we know this is meant to be a string, force to a string. # Sometimes headers represent items as integers which generically # we want as strings (eg OBSID). Sometimes also floats are # written as "NaN" strings. casts = {"str": str, "float": float, "int": int} if return_type in casts and not isinstance(value, casts[return_type]) and value is not None: value = casts[return_type](value)
return value
# Docstring inheritance means it is confusing to specify here # exactly which header value is being used.
Returns ------- translation : `{return_type}` Translated value derived from the header. """
def __init_subclass__(cls, **kwargs): """Register all subclasses with the base class and create dynamic translator methods.
The method provides two facilities. Firstly, every subclass of `MetadataTranslator` that includes a ``name`` class property is registered as a translator class that could be selected when automatic header translation is attempted. Only name translator subclasses that correspond to a complete instrument. Translation classes providing generic translation support for multiple instrument translators should not be named.
The second feature of this method is to convert simple translations to full translator methods. Sometimes a translation is fixed (for example a specific instrument name should be used) and rather than provide a full ``to_property()`` translation method the mapping can be defined in a class variable named ``_constMap``. Similarly, for one-to-one trivial mappings from a header to a property, ``_trivialMap`` can be defined. Trivial mappings are a dict mapping a generic property to either a header keyword, or a tuple consisting of the header keyword and a dict containing key value pairs suitable for the `MetadataTranslator.quantity_from_card()` method. """
# Only register classes with declared names
# Check that we have not inherited constant/trivial mappings from # parent class that we have already applied. Empty maps are always # assumed okay if cls._trivial_map and cls.defined_in_this_class("_trivial_map") else {}
# Check for shadowing log.warning("%s: defined in both const_map and trivial_map: %s", cls.__name__, ", ".join(both))
# Must be one of trivial or constant. If in both then constant # overrides trivial. location = "by _trivial_map" if name in constants: location = "by _const_map" log.warning("%s: %s is defined explicitly but will be replaced %s", cls.__name__, name, location)
# Go through the trival mappings for this class and create # corresponding translator methods log.warning(f"Unexpected trivial translator for '{property_key}' defined in {cls}")
# Go through the constant mappings for this class and create # corresponding translator methods log.warning(f"Unexpected constant translator for '{property_key}' defined in {cls}")
self._header = header self.filename = filename self._used_cards = set()
# Cache assumes header is read-only once stored in object self._translation_cache = {}
"""Indicate whether this translation class can translate the supplied header.
Parameters ---------- header : `dict`-like Header to convert to standardized form. filename : `str`, optional Name of file being translated.
Returns ------- can : `bool` `True` if the header is recognized by this class. `False` otherwise. """ raise NotImplementedError()
"""Helper method for `can_translate` allowing options.
Parameters ---------- header : `dict`-like Header to convert to standardized form. options : `dict` Headers to try to determine whether this header can be translated by this class. If a card is found it will be compared with the expected value and will return that comparison. Each card will be tried in turn until one is found. filename : `str`, optional Name of file being translated.
Returns ------- can : `bool` `True` if the header is recognized by this class. `False` otherwise.
Notes ----- Intended to be used from within `can_translate` implementations for specific translators. Is not intended to be called directly from `determine_translator`. """ for card, value in options.items(): if card in header: return header[card] == value return False
"""Determine a translation class by examining the header
Parameters ---------- header : `dict`-like Representation of a header. filename : `str`, optional Name of file being translated.
Returns ------- translator : `MetadataTranslator` Translation class that knows how to extract metadata from the supplied header.
Raises ------ ValueError None of the registered translation classes understood the supplied header. """ for name, trans in cls.translators.items(): if trans.can_translate(header, filename=filename): log.debug(f"Using translation class {name}") return trans else: raise ValueError(f"None of the registered translation classes {list(cls.translators.keys())}" " understood this header")
"""Indicate that the supplied cards have been used for translation.
Parameters ---------- args : sequence of `str` Keywords used to process a translation. """ self._used_cards.update(set(args))
"""Cards used during metadata extraction.
Returns ------- used : `frozenset` of `str` Cards used when extracting metadata. """ return frozenset(self._used_cards)
"""Validate the supplied value, returning a new value if out of range
Parameters ---------- value : `float` Value to be validated. default : `float` Default value to use if supplied value is invalid or out of range. Assumed to be in the same units as the value expected in the header. minimum : `float` Minimum possible valid value, optional. If the calculated value is below this value, the default value will be used. maximum : `float` Maximum possible valid value, optional. If the calculated value is above this value, the default value will be used.
Returns ------- value : `float` Either the supplied value, or a default value. """ if value is None or math.isnan(value): value = default else: if minimum is not None and value < minimum: value = default elif maximum is not None and value > maximum: value = default return value
def is_keyword_defined(header, keyword): """Return `True` if the value associated with the named keyword is present in the supplied header and defined.
Parameters ---------- header : `dict`-lik Header to use as reference. keyword : `str` Keyword to check against header.
Returns ------- is_defined : `bool` `True` if the header is present and not-`None`. `False` otherwise. """ if keyword not in header: return False
if header[keyword] is None: return False
# Special case Astropy undefined value if isinstance(header[keyword], astropy.io.fits.card.Undefined): return False
return True
"""Search paths to use when searching for header fix up correction files.
Returns ------- paths : `list` Directory paths to search. Can be an empty list if no special directories are defined. """ return []
"""Return `True` if the value associated with the named keyword is present in this header and defined.
Parameters ---------- keyword : `str` Keyword to check against header.
Returns ------- is_ok : `bool` `True` if the header is present and not-`None`. `False` otherwise. """ return self.is_keyword_defined(self._header, keyword)
"""Are the supplied keys all present and defined?
Parameters ---------- keywords : iterable of `str` Keywords to test.
Returns ------- all_ok : `bool` `True` if all supplied keys are present and defined. """ for k in keywords: if not self.is_key_ok(k): return False return True
"""Calculate a Astropy Quantity from a header card and a unit.
Parameters ---------- keywords : `str` or `list` of `str` Keyword to use from header. If a list each keyword will be tried in turn until one matches. unit : `astropy.units.UnitBase` Unit of the item in the header. default : `float`, optional Default value to use if the header value is invalid. Assumed to be in the same units as the value expected in the header. If None, no default value is used. minimum : `float`, optional Minimum possible valid value, optional. If the calculated value is below this value, the default value will be used. maximum : `float`, optional Maximum possible valid value, optional. If the calculated value is above this value, the default value will be used. checker : `function`, optional Callback function to be used by the translator method in case the keyword is not present. Function will be executed as if it is a method of the translator class. Running without raising an exception will allow the default to be used. Should usually raise `KeyError`.
Returns ------- q : `astropy.units.Quantity` Quantity representing the header value.
Raises ------ KeyError The supplied header key is not present. """ keywords = keywords if isinstance(keywords, list) else [keywords] for k in keywords: if self.is_key_ok(k): value = self._header[k] keyword = k break else: if checker is not None: try: checker(self) value = default if value is not None: value = u.Quantity(value, unit=unit) return value except Exception: pass raise KeyError(f"Could not find {keywords} in header") if isinstance(value, str): # Sometimes the header has the wrong type in it but this must # be a number if we are creating a quantity. value = float(value) self._used_these_cards(keyword) if default is not None: value = self.validate_value(value, default, maximum=maximum, minimum=minimum) return u.Quantity(value, unit=unit)
"""Join values of all defined keywords with the specified delimiter.
Parameters ---------- keywords : iterable of `str` Keywords to look for in header. delim : `str`, optional Character to use to join the values together.
Returns ------- joined : `str` String formed from all the keywords found in the header with defined values joined by the delimiter. Empty string if no defined keywords found. """ values = [] for k in keywords: if self.is_key_ok(k): values.append(self._header[k]) self._used_these_cards(k)
if values: joined = delim.join(str(v) for v in values) else: joined = ""
return joined
def to_detector_unique_name(self): """Return a unique name for the detector.
Base class implementation attempts to combine ``detector_name`` with ``detector_group``. Group is only used if not `None`.
Can be over-ridden by specialist translator class.
Returns ------- name : `str` ``detector_group``_``detector_name`` if ``detector_group`` is defined, else the ``detector_name`` is assumed to be unique. If neither return a valid value an exception is raised.
Raises ------ NotImplementedError Raised if neither detector_name nor detector_group is defined. """ name = self.to_detector_name() group = self.to_detector_group()
if group is None and name is None: raise NotImplementedError("Can not determine unique name from detector_group and detector_name")
if group is not None: return f"{group}_{name}"
return name
"""Create a an abstract translation method for this property.
Parameters ---------- property : `str` Name of the translator for property to be created. doc : `str` Description of the property. return_typedoc : `str` Type string of this property (used in the doc string). return_type : `class` Type of this property.
Returns ------- m : `function` Translator method for this property. """ raise NotImplementedError(f"Translator for '{property}' undefined.")
{doc}
Returns ------- {property} : `{return_typedoc}` The translated property. """
# Make abstract methods for all the translators methods. # Unfortunately registering them as abstractmethods does not work # as these assignments come after the class has been created. # Assigning to __abstractmethods__ directly does work but interacts # poorly with the metaclass automatically generating methods from # _trivialMap and _constMap.
# Allow for concrete translator methods to exist in the base class # These translator methods can be defined in terms of other properties
abstractmethod(_make_abstract_translator_method(name, *description))) else:
"""Translator where all the translations are stubbed out and issue warnings.
This translator can be used as a base class whilst developing a new translator. It allows testing to proceed without being required to fully define all translation methods. Once complete the class should be removed from the inheritance tree.
"""
"""Create a stub translation method for this property that calls the base method and catches `NotImplementedError`.
Parameters ---------- cls : `class` Class to use when referencing `super()`. This would usually be `StubTranslator`. property : `str` Name of the translator for property to be created. doc : `str` Description of the property. return_typedoc : `str` Type string of this property (used in the doc string). return_type : `class` Type of this property.
Returns ------- m : `function` Stub translator method for this property. """
parent = getattr(super(cls, self), method, None) try: if parent is not None: return parent() except NotImplementedError: pass
warnings.warn(f"Please implement translator for property '{property}' for translator {self}", stacklevel=3) return None
{doc}
Calls the base class translation method and if that fails with `NotImplementedError` issues a warning reminding the implementer to override this method.
Returns ------- {property} : `None` or `{return_typedoc}` Always returns `None`. """
# Create stub translation methods for each property. These stubs warn # rather than fail and should be overridden by translators. name, *description)) |