Source code for pyfileconf.basemodels.config

import warnings
from typing import Tuple, Optional, Type, Sequence, Any, Dict
import os
from copy import deepcopy

import pandas as pd

from pyfileconf.basemodels.file import ConfigFileBase
from pyfileconf.imports.models.statements.container import ImportStatementContainer
from pyfileconf.assignments.models.container import AssignmentStatementContainer

ImportsAndAssigns = Tuple[ImportStatementContainer, AssignmentStatementContainer]

[docs]class ConfigBase(dict): ##### Scaffolding functions or attributes. Need to override when subclassing #### config_file_class = ConfigFileBase ##### Base class functions and attributes below. Shouldn't usually need to override in subclassing #####
[docs] def __init__(self, d: dict=None, name: str=None, annotations: dict=None, imports: ImportStatementContainer = None, _file: ConfigFileBase=None, begin_assignments: AssignmentStatementContainer=None, klass: Optional[Type] = None, always_import_strs: Optional[Sequence[str]] = None, always_assign_strs: Optional[Sequence[str]] = None, **kwargs): if d is None: d = {} super().__init__(d, **kwargs) if annotations is None: annotations = {} if imports is None: imports = ImportStatementContainer([]) if begin_assignments is None: begin_assignments = AssignmentStatementContainer([]) self.name = name self.annotations = annotations self.imports = imports self._file = _file self.begin_assignments = begin_assignments self.klass = klass self.always_import_strs = always_import_strs self.always_assign_strs = always_assign_strs self._applied_updates: Dict[str, Any] = {}
def __repr__(self): dict_repr = super().__repr__() class_name = self.__class__.__name__ return f'<{class_name}(name={self.name}, {dict_repr})>' def __getattr__(self, attr): try: self[attr] except KeyError: raise AttributeError(attr) def __dir__(self): return self.keys() # argument names to match dict.update
[docs] def update(self, E=None, pyfileconf_persist: bool = True, **F): # type: ignore if E is None: E = {} # Track the updates so they can be applied later all_updates = {**E, **F} if pyfileconf_persist: self._applied_updates.update(all_updates) super().update(E, **F)
[docs] def to_file(self, filepath: str): if self._file is None: output_file = self.config_file_class( filepath, name=self.name, klass=self.klass, always_import_strs=self.always_import_strs, always_assign_strs=self.always_assign_strs ) else: # In case this is a new filepath for the same config, copy old file contents for use in new filepath output_file = deepcopy(self._file) output_file.filepath = filepath if os.path.exists(filepath): output_file.load() # load any existing config saved in the file, for preserving of user-saved inputs output_file.save(self)
[docs] @classmethod def from_file(cls, filepath: str, name: str = None, klass: Optional[Type] = None, always_import_strs: Optional[Sequence[str]] = None, always_assign_strs: Optional[Sequence[str]] = None): file = cls.config_file_class( filepath, name=name, klass=klass, always_import_strs=always_import_strs, always_assign_strs=always_assign_strs ) return file.load(cls)
[docs] def as_imports_and_assignments(self) -> ImportsAndAssigns: assigns = AssignmentStatementContainer.from_dict_of_varnames_and_ast(self, self.annotations) return self.imports, self.begin_assignments + assigns
[docs] def copy(self): return deepcopy(self)
def _get_new_config_from_file(self): return self.__class__.from_file( self._file.filepath, name=self.name, klass=self.klass, always_import_strs=self.always_import_strs, always_assign_strs=self.always_assign_strs )
[docs] def refresh(self) -> Dict[str, Any]: """ Reloads from the existing, then re-applies any config updates. Useful for when this config depends on the attribute of some other config which was updated. :return: The updates made to the config """ # Reload from file new_config = self._get_new_config_from_file() all_updates = {**new_config, **self._applied_updates} self.update(pyfileconf_persist=False, **all_updates) return all_updates
[docs] def would_update(self, E=None, **F) -> bool: """ Determines whether updates would actually cause a change in the config :param E: dictionary of updates :param F: kwargs of updates :return: whether config would actually change when calling .update with the same arguments """ if E is None: E = {} all_updates = {**E, **F} for key, value in all_updates.items(): if key not in self: continue orig_value = self[key] if not _values_are_equal(orig_value, value): return True return False
[docs] def change_from_refresh(self) -> Dict[str, Any]: """ Determines whether refresh would actually cause a change in the config and returns a dictionary of what would be updated :return: the new config dict that would apply while calling .refresh if it would be updated, otherwise an empty dict """ new_config = self._get_new_config_from_file() final_updates = {**new_config, **self._applied_updates} would_update = self.would_update(final_updates) if would_update: return final_updates return {}
def _values_are_equal(val1: Any, val2: Any) -> bool: # Special handling for pandas if isinstance(val1, (pd.DataFrame, pd.Series)): return val1.equals(val2) elif isinstance(val2, (pd.DataFrame, pd.Series)): # First is not pandas object, so must not be equal return False try: return val1 == val2 except Exception as e: warnings.warn(f'Could not check if values {val1} and {val2} of ' f'type {type(val1)} and {type(val2)} are ' f'equal. Returning False. Got exception: {e}') return False