"""Extends the import capability of YAML files."""__author__='Paul Landes'fromtypingimportDict,Any,Sequence,List,Set,Union,ClassVarimportloggingimportrandomimportstringfrom.import(ConfigurableError,Serializer,Configurable,DictionaryConfig,ImportIniConfig)logger=logging.getLogger(__name__)
[docs]classImportTreeConfig(DictionaryConfig):"""A :class:`.Configurable` that give :class:`.ImortIniConfig` capabilities to any gree based configuration, and specifically :class:`.ImportYamlConfig`. """DEFAULT_TYPE_MAP:ClassVar[Dict[str,str]]={'yml':'condyaml','conf':'importini'}
def_get_config(self)->Dict[str,Any]:config:Dict[str,Any]=super()._get_config()ifself._import_sectionisnotNone:self._import_tree(self._import_section,config)self._import_section=Nonereturnconfigdef_create_sec_name(self)->str:"""Return a unique section name based off this (parent) section name."""sec_name:str=self._parent_section_nameifsec_nameisNoneorlen(sec_name)==0:sec_name=''rand=''.join(random.choices(string.ascii_lowercase,k=10))returnf'{sec_name}_{rand}'def_create_import_config(self,sec:Dict[str,Any])->Dict[str,Any]:"""Return a dict with the :class:`.Configurable` import sections and a list of cleanups. """defmap_cf(obj)->str:ifisinstance(obj,Dict)andlen(obj)==1:t=next(iter(obj.items()))returnf'{t[0]}: {t[1]}'returnobjser:Serializer=self.serializer# create a unique import section namesec_name:str=self._create_sec_name()# sections to remove aftercleanups:List[str]=sec.pop(ImportIniConfig.CLEANUPS_NAME,[])# the extension to Configurable classtype_map:str=sec.pop(ImportIniConfig.TYPE_MAP,self.DEFAULT_TYPE_MAP)# get the config files to load (plural and singular nomenclatures)files:Union[str,Sequence[Any]]=sec.get(ImportIniConfig.CONFIG_FILES,[])sfile:str=sec.pop(ImportIniConfig.SINGLE_CONFIG_FILE,None)# find entries that don't belongunknown_entries:Set[str]= \
(set(sec.keys())-ImportIniConfig.IMPORT_SECTION_FIELDS)iflen(unknown_entries)>0:raiseConfigurableError(f'Unknown configuration entries: {unknown_entries}')# adding files/resources to load can be as a string, list, or what the# YAML parser creates as a list of dicts for ``resource:...``; validateifisinstance(files,str):files=tuple(filter(lambdas:len(s)>0,files.split('\n')))ifsfileisnotNone:files.append(ser.format_option(sfile))files=tuple(map(map_cf,files))iflen(files)==0:raiseConfigurableError(f'No configuration files set: {self}')# add the processed files to the inmport sectionsec[ImportIniConfig.CONFIG_FILES]=ser.format_option(files)# and the current section (yaml based) import sectionifself._parent_section_nameisnotNone:cleanups.append(self._parent_section_name)# remove ``type: treeimport`` from the sectionsec.pop(ImportIniConfig.TYPE_NAME,None)# create the import sectionimp_sec:Dict[str,Any]={ImportIniConfig.SECTIONS_SECTION:ser.format_option([sec_name])}# use the serializer to convert Python objects and types to stringsop:strforopin(ImportIniConfig.REFS_NAME,ImportIniConfig.CLEANUPS_NAME):val:Any=sec.pop(op,None)ifvalisnotNone:imp_sec[op]=ser.format_option(val)# same for the top level ``import`` sectionforopin(ImportIniConfig.ENABLED_NAME,):val:Any=sec.pop(op,None)ifvalisnotNone:sec[op]=ser.format_option(val)sec[ImportIniConfig.TYPE_MAP]=ser.format_option(type_map)# create the config with the top level ``import`` and import sectionconfig=DictionaryConfig({ImportIniConfig.IMPORT_SECTION:imp_sec,sec_name:sec})return{'config':config,'cleanups':cleanups}def_import_tree(self,source:Dict[str,Any],target:Dict[str,Any]):"""Create a new :class:`.ImportIniConfig` with import sections to load configuration for the section build for this instance. :param source: the section in the YAML that tells us what to import :param target: the data to populate with the imported data """# create the input to ImportIniConfigimport_data:Dict[str,Any]=self._create_import_config(source)cleanups:List[str]=import_data['cleanups']# create the import configurable instance with importini contextiniconfig=ImportIniConfig(import_data['config'],children=self.parent.children,parent=self.parent)# preemptively remove sections so they deep dicts don't interfere with# the copying of sectionsiflen(cleanups)>0:iflogger.isEnabledFor(logging.DEBUG):logger.debug(f'removing {cleanups}')parent:Configurable=self.parentwhileparentisnotNone:ifparent._is_initialized():forsecincleanups:parent.remove_section(sec)parent=parent.parent# copy the imported sections to our configdc=DictionaryConfig(target)iniconfig.copy_sections(dc)