Source code for hifis_surveyval.models.mixins.mixins

# hifis-surveyval
# Framework to help developing analysis scripts for the HIFIS Software survey.
#
# SPDX-FileCopyrightText: 2021 HIFIS Software <support@hifis.net>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
This module provides mixins for model classes with certain properties.

They are designed to co-operate with other mixins and forwards unused
initialization arguments down to other mixins in the inheritance order.
"""
from abc import ABC, abstractmethod
from typing import Set, Optional, Iterable, Union, Dict

from hifis_surveyval.models.mixins.uses_settings import UsesSettings
from hifis_surveyval.models.translated import Translated


[docs]class HasMandatory(ABC): """ This mixin provides functionality for optional mandatory indicators. Model elements may require something to be present (e.g. an answer) directly or indirectly as a child of this object. """ YAML_TOKEN = "mandatory" """The token used in metadata YAML files to indicate mandatory-ness."""
[docs] def __init__(self, is_mandatory: bool, *args, **kwargs): """ Initialize an object with mandatory-ness indicator. Args: is_mandatory: Whether this object has mandatory elements or not. *args: Will be forwarded to other mixins in the initialization order. **kwargs: Will be forwarded to other mixins in the initialization order. """ super(HasMandatory, self).__init__(*args, **kwargs) self._is_mandatory = is_mandatory
[docs] @abstractmethod def is_mandatory_fulfilled( self, check_for: Union[str, Iterable[str]] ) -> Dict[str, bool]: """ Check if the mandatory condition is fulfilled for given participants. This is not affected by whether the Question or QuestionCollection is marked as mandatory or not. This function checks if the participants DO fulfil the mandatory condition, but not if they SHOULD. For the latter see the `is_mandatory' - property. Args: check_for: Either any iterable type, providing participant IDs as strings or a single string providing one participant ID. These are the IDs for which the fulfillment of the mandatory condition is checked. Returns: A dictionary mapping each input participant ID to a boolean value indicating whether they fulfil the mandatory condition (i.e. the value for the respective participant ID will be 'True') or not. """ pass
@property def is_mandatory(self) -> bool: """ Check whether this question is marked as mandatory. Mandatory questions are expected to be answered by participants. Returns: True, if the question was marked as mandatory in the metadata, False otherwise """ return self._is_mandatory
[docs]class HasLabel(ABC): """ This mixin provides a label property. This is used as a shorthand for objects with more complex descriptions that do not fit nicely in some places (e.g. as labels for graph axis). """ YAML_TOKEN = "label" """The token used in metadata YAML files to identify labels."""
[docs] def __init__(self, label: str, *args, **kwargs): """ Initialize a labelled object. Args: label: The label to be given to the object. *args: Will be forwarded to other mixins in the initialization order. **kwargs: Will be forwarded to other mixins in the initialization order. """ super(HasLabel, self).__init__(*args, **kwargs) self._label = label
@property def label(self) -> str: """ Get the current label of the object. Returns: The current object label. """ return self._label
[docs] def relabel(self, new_label: str) -> None: """ Set a new label for this object. If the new labels string representation is empty, nothing will be changed. Args: new_label: The new label to be used for the object. If required, the input will be cast to string before processing. """ if not isinstance(new_label, str): new_label = str(new_label) if new_label: self._label = new_label
[docs]class HasText(ABC): """ This mixin provides a text property. This is used as a more detailed description of the object, e.g. a verbatim question text. These texts may be translated, so when accessing them, providing a language is often required. """ YAML_TOKEN = "text" """The token used in metadata YAML files to identify labels."""
[docs] def __init__(self, translations: Translated, *args, **kwargs) -> None: """ Initialize an object with a translatable description. Args: translations: The possible translations of the description. *args: Will be forwarded to other mixins in the initialization order. **kwargs: Will be forwarded to other mixins in the initialization order. """ super(HasText, self).__init__(*args, **kwargs) self._text: Translated = translations
[docs] def text(self, language_code: str) -> str: """ Get the description text in a specific language. Args: language_code: The IETF code for the language. Returns: The translated description. if available. Raises: KeyError: If no translation for the requested language (with or without region code) can be found. """ return self._text.get_translation(language_code=language_code)
[docs]class HasID(UsesSettings): """ This is the abstract superclass for all objects that carry an ID. The ID is expected to be a string (or be convertible into such and to be unique among all identifiable objects. IDs are separated by a HIERARCHY_SEPARATOR and the part after the last separator forms the so-called "short ID". If no hierarchical parent_id is given, the short ID and the full ID are the same. """ # TODO Move the repective YAML token in here known_ids: Set[str] = set()
[docs] def __init__( self, object_id: str, parent_id: Optional[str] = None, *args, **kwargs, ) -> None: """ Create a new identifiable object with a given ID. The class will track all known IDs to prevent duplicates. A full ID is formed by merging the parent's full ID (if it exists) and the object's ID. Args: object_id: A string serving as an ID to the object. It must be neither None nor empty. parent_id: (Optional, Default=None) The full ID of another identifiable object that forms the hierarchical parent of this one. Used to generate the full ID. *args: Will be forwarded to other mixins in the initialization order. **kwargs: Will be forwarded to other mixins in the initialization order. Raises: ValueError: Signals either a duplicate or invalid object_id """ super(HasID, self).__init__(*args, **kwargs) if not object_id: raise ValueError( "ID of an identifiable object may neither be empty nor None" ) self._full_id: str = ( f"{parent_id}{self._settings.HIERARCHY_SEPARATOR}{object_id}" if parent_id else object_id ) if self._full_id in HasID.known_ids: raise ValueError(f"Attempted to assign duplicate ID {object_id}") HasID.known_ids.add(self._full_id)
def __del__(self) -> None: """ Deconstruct an identifiable object. The used ID will be removed from the known IDs and can be re-used. """ try: HasID.known_ids.remove(self._full_id) # FIXME For some reason removing the full ID from the list of # known IDs fails due to them already being removed. But why? # This has been put into this little exception-catch box to not # spam the command line output, but I would prefer to understand # better what is going on here… except KeyError: pass @property def short_id(self) -> str: """ Get the short ID of this object (without parent_id IDs). Returns: The string identifying this object with respect to its siblings """ return self._full_id.split(self._settings.HIERARCHY_SEPARATOR)[-1] # TODO: Decide whether to cache the short id @property def full_id(self) -> str: """ Get the full ID of the object (includes parent_id IDs). Returns: The string identifying the object with respect to any other HasID """ return self._full_id def __str__(self) -> str: """Return the full ID as representation of this identifiable object.""" return self.full_id