diff --git a/dependencies.ini b/dependencies.ini deleted file mode 100644 index b3c3f3c1e7..0000000000 --- a/dependencies.ini +++ /dev/null @@ -1,100 +0,0 @@ -# Instructions: -# -# add library: library = pinned.version.number -# add with no pinned version: library -# remove a library: library = remove -# -# end instructions - -# Reading flow: -# -# The library handler will proceed in the following pattern for reading this file: -# 1. Read in "core" (stuff that comes from conda) -# 2. Read in "forge" (stuff that comes from conda-forge) -# 3. Read in "pip" (stuff that comes from pip) (NOTE: not the pip-only install!) -# 4. If optional, then read in "optional" -# 5. If OS is given, read in either "windows", "mac", or "linux" -# 6. If pip-only install, then read in "pip-install" -# -# Note that each reading-in stage might involve adding or removing libraries, in order. -# If a library is not present but requested to remove, that's fine, no crash, move on. -# -# For more information about the command line arguments to library_handler.sh, see that module. -# Also try -# `python library_handler.sh -h` and -# `python library_handler.sh conda -h` -# -# end reading flow - -##### Typical installation (conda) -# main libraries from main conda -[core] -h5py = 2.9.0 -numpy = 1.18.1 -scipy = 1.2.1 -scikit-learn = 0.21.2 -pandas = 0.24.2 -xarray = 0.12.1 -netcdf4 = 1.4.2 -matplotlib = 3.1.1 -statsmodels = 0.9.0 -cloudpickle = 1.1.1 -tensorflow = 1.13.1 -python = 3 -hdf5 -swig -pylint -coverage -lxml -psutil -pip -importlib_metadata - -# secondary installs from conda forge -[forge] -pyside2 - -# tertiary installs directly from pip -[pip] - -# optional libraries -[optional] -pillow = 6.0.0 - -# operating system alterations (conda) -[windows] -# nothing currently - -[mac] -# nothing currently - -[linux] -# these prevent Intel crash errors with mkl -nomkl -numexpr - -# operating system alterations (pip) -[windows-pip] -# nothing currently - -[mac-pip] -ray - -[linux-pip] -ray - -##### Alternate installs -# install using pip -[pip-install] -hdf5 = remove -swig = remove -pip = remove -python = remove -nomkl = remove -numexpr = remove - -[skip-check] -python -hdf5 -swig -nomkl diff --git a/dependencies.xml b/dependencies.xml new file mode 100644 index 0000000000..ee61295ab1 --- /dev/null +++ b/dependencies.xml @@ -0,0 +1,67 @@ + + +
+ 2.9.0 + 1.18.1 + 1.2.1 + 0.21.2 + 0.24.2 + 0.12.1 + 1.4.2 + 3.1.1 + 0.9.0 + 1.1.1 + 1.13.1 + 3 + + + + + + + + + + 6.0.0 + + + +
+ + remove + remove + remove + remove + remove + remove + +
diff --git a/doc/user_manual/Makefile b/doc/user_manual/Makefile index bdde95c03d..8bb0b00774 100644 --- a/doc/user_manual/Makefile +++ b/doc/user_manual/Makefile @@ -6,7 +6,7 @@ introduction.tex raven_user_manual.tex model.tex runInfo.tex libraries.tex DataM Installation/clone.tex Installation/conda.tex Installation/linux.tex Installation/macosx.tex Installation/main.tex \ Installation/overview.tex Installation/windows.tex advanced_users_templates.tex LATEX_FLAGS=-interaction=nonstopmode -LIB_FILES = ../../scripts/library_handler.py ../../dependencies.ini +LIB_FILES = ../../scripts/library_handler.py ../../dependencies.xml all: raven_user_manual.pdf @@ -43,4 +43,4 @@ conda_command.txt : $(LIB_FILES) ./create_command.sh pip_commands.txt : $(LIB_FILES) - ./create_pip_commands.sh \ No newline at end of file + ./create_pip_commands.sh diff --git a/plugins/heron b/plugins/heron index a6f4d1d871..cee70f43f4 160000 --- a/plugins/heron +++ b/plugins/heron @@ -1 +1 @@ -Subproject commit a6f4d1d87129b1923a52fb624ac4f4ee948d4f12 +Subproject commit cee70f43f4884f0bbb8f9c0c9bb61ca9ae2bdb21 diff --git a/scripts/conversionScripts/deps_ini_to_xml.py b/scripts/conversionScripts/deps_ini_to_xml.py new file mode 100644 index 0000000000..153d085d92 --- /dev/null +++ b/scripts/conversionScripts/deps_ini_to_xml.py @@ -0,0 +1,307 @@ + +""" + Converts old dependency files from .ini to .xml +""" + +import os +import sys +import configparser +import xml.etree.ElementTree as ET + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'framework'))) +from utils import xmlUtils + +def readConfig(inFile): + """ + Reads in ini file. + @ In, inFile, str, path to file object + @ Out, config, ConfigParser, config read in + """ + config = configparser.ConfigParser(allow_no_value=True) + config.read(inFile) + return config + +def convert(config): + """ + Converts config to xml + @ In, config, ConfigParser, old dependencies + @ Out, xml, ET.Element, converted xml + """ + xml = ET.Element('dependencies') + + addCore(config, xml) + addForge(config, xml) + addPip(config, xml) + addOptional(config, xml) + + addWindows(config, xml) + addMac(config, xml) + addLinux(config, xml) + + addWindowsPip(config, xml) + addMacPip(config, xml) + addLinuxPip(config, xml) + + addPipInstall(config,xml) + addSkipCheck(config,xml) + + #print('DEBUGG new xml:') + #print(xmlUtils.prettify(xml)) + return xml + +def addCore(config, xml): + """ + Adds core entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'core' in config: + node = ET.Element('main') + xml.append(node) + print('DEBUGG entries:', list(config['core'].items())) + for lib, vrs in config['core'].items(): + new = ET.Element(lib) + node.append(new) + new.text = vrs + +def addForge(config, xml): + """ + Adds forge entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'forge' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['forge'].items(): + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['source'] = 'forge' + +def addPip(config, xml): + """ + Adds pip entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'pip' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['pip'].items(): + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['source'] = 'pip' + +def addOptional(config, xml): + """ + Adds optional entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'optional' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['optional'].items(): + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['optional'] = 'True' + +def addWindows(config, xml): + """ + Adds windows entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'windows' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['windows'].items(): + new = main.find(lib) + if new is None: + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['os'] = 'windows' + else: + new.attrib['os'] = new.attrib['os']+',windows' + +def addMac(config, xml): + """ + Adds mac entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'mac' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['mac'].items(): + new = main.find(lib) + if new is None: + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['os'] = 'mac' + else: + new.attrib['os'] = new.attrib['os']+',mac' + +def addLinux(config, xml): + """ + Adds linux entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'linux' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['linux'].items(): + new = main.find(lib) + if new is None: + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['os'] = 'linux' + else: + new.attrib['os'] = new.attrib['os']+',linux' + +def addWindowsPip(config, xml): + """ + Adds windows pip entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'windows-pip' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['windows-pip'].items(): + new = main.find(lib) + if new is None: + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['source'] = 'pip' + new.attrib['os'] = 'windows' + else: + new.attrib['os'] = new.attrib['os']+',windows' + +def addMacPip(config, xml): + """ + Adds mac pip entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'mac-pip' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['mac-pip'].items(): + new = main.find(lib) + if new is None: + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['source'] = 'pip' + new.attrib['os'] = 'mac' + else: + new.attrib['os'] = new.attrib['os']+',mac' + +def addLinuxPip(config, xml): + """ + Adds linux pip entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'linux-pip' in config: + main = xml.find('main') + if main is None: + xml.append(ET.Element('main')) + for lib, vrs in config['linux-pip'].items(): + new = main.find(lib) + if new is None: + new = ET.Element(lib) + main.append(new) + new.text = vrs + new.attrib['source'] = 'pip' + new.attrib['os'] = 'linux' + else: + new.attrib['os'] = new.attrib['os']+',linux' + +def addPipInstall(config, xml): + """ + Adds pip alternate install entries to xml + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'pip-install' in config: + alt = ET.Element('alternate') + xml.append(alt) + alt.attrib['name'] = 'pip' + for lib, vrs in config['pip-install'].items(): + new = ET.Element(lib) + alt.append(new) + new.text = vrs + +def addSkipCheck(config, xml): + """ + Adds skip check flags + @ In, config, ConfigParser, original config + @ In, xml, ET.Element, root of dependencies + @ Out, None + """ + if 'skip-check' in config: + for lib, vrs in config['skip-check'].items(): + for install in xml: + for exists in install: + if exists.tag == lib: + exists.attrib['skip_check'] = 'True' + break + +def write(xml, origPath): + """ + Write new dep file + @ In, xml, ET.Element, filled XML with dependencies + @ In, origPath, str, original file path + @ Out, None + """ + newPath = os.path.join(os.path.dirname(origPath), 'dependencies.xml') + xmlUtils.toFile(newPath, xml) + print('Wrote new "dependencies.xml" to {}'.format(newPath)) + + + +if __name__ == '__main__': + usageMessage = 'converter usage: python deps_ini_to_xml.py /path/to/dependencies.ini' + if len(sys.argv) != 2: + print(usageMessage) + sys.exit(1) + oldPath = os.path.abspath(sys.argv[1]) + if not os.path.isfile(oldPath): + print(usageMessage) + print('ERROR: File not found:', oldPath) + sys.exit(1) + config = readConfig(oldPath) + xml = convert(config) + write(xml, oldPath) + + + diff --git a/scripts/library_handler.py b/scripts/library_handler.py index 386e764c4a..736f6905d8 100644 --- a/scripts/library_handler.py +++ b/scripts/library_handler.py @@ -21,6 +21,8 @@ import argparse import subprocess from collections import OrderedDict +import xml.etree.ElementTree as ET +# NOTE: DO NOT use xmlUtils here! we need to avoid importing any non-standard Python library! from update_install_data import loadRC import plugin_handler as pluginHandler @@ -31,12 +33,6 @@ else: impErr = ImportError -# python 2.X uses a different capitalization for configparser -try: - import configparser -except impErr: - import ConfigParser as configparser - try: # python 3.8+ includes this in std lib import importlib_metadata @@ -94,16 +90,16 @@ def checkLibraries(buildReport=False): @ Out, missing, list(tuple(str, str)), list of missing libraries and needed versions @ Out, notQA, list(tuple(str, str, str)), mismatched versions as (libs, need version, found version) """ - missing = [] - notQA = [] + missing = [] # libraries that are not present, that should be + notQA = [] # libraries that are not the correct version, but are present plugins = pluginHandler.getInstalledPlugins() need = getRequiredLibs(plugins=plugins) - skipCheckLibs = getSkipCheckLibs(plugins=plugins) messages = [] - for lib, needVersion in need.items(): + for lib, request in need.items(): # some libs aren't checked from within python - if lib in skipCheckLibs: + if request['skip_check']: continue + needVersion = request['version'] found, msg, foundVersion = checkSingleLibrary(lib, version=needVersion) if not found: missing.append((lib, needVersion)) @@ -214,31 +210,29 @@ def findLibAndVersionSubprocess(lib, version=None): foundVersion = None return True, foundExists, foundVersion -def getRequiredLibs(useOS=None, installMethod=None, addOptional=False, limit=None, plugins=None): +def getRequiredLibs(useOS=None, installMethod=None, addOptional=False, limitSources=None, plugins=None): """ Assembles dictionary of required libraries. @ In, useOS, str, optional, if provided then assume given operating system @ In, installMethod, str, optional, if provided then assume given install method @ In, addOptional, bool, optional, if True then add optional libraries to list - @ In, limit, list(str), optional, limit sections that are read in + @ In, limitSources, list(str), optional, limit sections that are read in @ In, plugins, list(tuple(str,str)), optional, plugins (name, location) that should be added to the required libs @ Out, libs, dict, dictionary of libraries {name: version} """ - mainConfigFile = os.path.abspath(os.path.expanduser(os.path.join(os.path.dirname(__file__), - '..', 'dependencies.ini'))) - config = _readDependencies(mainConfigFile) + if plugins is None: + plugins = [] opSys = _getOperatingSystem(override=useOS) install = _getInstallMethod(override=installMethod) - libs = _parseLibs(config, opSys, install, addOptional=addOptional, limit=limit) - # extend config with plugin libs + mainConfigFile = os.path.abspath(os.path.expanduser(os.path.join(os.path.dirname(__file__), + '..', 'dependencies.xml'))) + sourceFiles = [mainConfigFile] for pluginName, pluginLoc in plugins: - pluginConfigFile = os.path.join(pluginLoc, 'dependencies.ini') + pluginConfigFile = os.path.join(pluginLoc, 'dependencies.xml') if os.path.isfile(pluginConfigFile): - pluginConfig = _readDependencies(pluginConfigFile) - pluginLibs = _parseLibs(pluginConfig, opSys, install, addOptional=addOptional, limit=limit) - pluginLibs = _checkForUpdates(libs, pluginLibs, pluginName) - libs.update(pluginLibs) + sourceFiles.append(pluginConfigFile) + libs = _combineSources(sourceFiles, opSys, install, addOptional=addOptional, limitSources=limitSources) return libs def getSkipCheckLibs(plugins=None): @@ -250,7 +244,7 @@ def getSkipCheckLibs(plugins=None): """ skipCheckLibs = OrderedDict() mainConfigFile = os.path.abspath(os.path.expanduser(os.path.join(os.path.dirname(__file__), - '..', 'dependencies.ini'))) + '..', 'dependencies.xml'))) config = _readDependencies(mainConfigFile) if config.has_section('skip-check'): _addLibsFromSection(config.items('skip-check'), skipCheckLibs) @@ -349,69 +343,125 @@ def _getInstallMethod(override=None): # no suggestion given, so we assume conda return 'conda' -def _parseLibs(config, opSys, install, addOptional=False, limit=None, plugins=None): +def _combineSources(sources, opSys, install, addOptional=False, limitSources=None): """ Parses config file to get libraries to install, using given options. - @ In, config, configparser.ConfigParser, read-in dependencies + @ In, sources, list(str), full-path dependency file locations @ In, opSys, str, operating system (not checked) @ In, install, str, installation method (not checked) @ In, addOptional, bool, optional, if True then include optional libraries - @ In, limit, list(str), optional, if provided then only read the given sections - @ In, plugins, list(tuple(str,configParser.configParser)), optional, plugins (name, config) that should be added to the parsing - @ Out, libs, dict, dictionary of libraries {name: version} + @ In, limitSources, list(str), optional, limit sections that are read in + @ Out, config, dict, dictionary of libraries {name: version} """ - libs = OrderedDict() - # get the main libraries, depending on request - for src in ['core', 'forge', 'pip']: - if config.has_section(src) and (True if limit is None else (src in limit)): - _addLibsFromSection(config.items(src), libs) - # os-specific are part of 'core' right now (if not explicitly reported in the pip section) - if config.has_section(opSys) and (True if limit is None else ('core' in limit)): - _addLibsFromSection(config.items(opSys), libs) - # os-specific of specific installer (e.g. pip) - if limit: - for lim in limit: - instSpecOp = "{opSys}-{lim}".format(lim=lim, opSys=opSys) - if config.has_section(instSpecOp): - _addLibsFromSection(config.items(instSpecOp), libs) - # optional are part of 'core' right now, but leave that up to the requester? - if addOptional and config.has_section('optional'): - _addLibsFromSection(config.items('optional'), libs) - if install == 'pip' and config.has_section('pip-install'): - _addLibsFromSection(config.items('pip-install'), libs) - instSpecOp = "{opSys}-pip".format(opSys=opSys) - if config.has_section(instSpecOp): - _addLibsFromSection(config.items(instSpecOp), libs) - return libs + config = {} + toRemove = [] + for source in sources: + requestor = os.path.basename(os.path.dirname(source)) + src = _readDependencies(source) + # always load main, if present + root = src.find('main') + if root is not None: + for libNode in root: + _readLibNode(libNode, config, toRemove, opSys, addOptional, limitSources, requestor) + # if using alternate install, load modifications + ## find matching install node, if any + for candidate in src.findall('alternate'): + if candidate.attrib['name'] == install: + altRoot = candidate + break + else: + altRoot = None + if altRoot is not None: + for libNode in altRoot: + _readLibNode(libNode, config, toRemove, opSys, addOptional, limitSources, requestor) + # remove stuff in toRemove + for entry in toRemove: + config.pop(entry, None) + return config -def _addLibsFromSection(configSection, libs): +def _readLibNode(libNode, config, toRemove, opSys, addOptional, limitSources, requestor): """ - Reads in libraries for a section of the config. - @ In, configSection, dict, libs: versions - @ In, libs, dict, libraries tracking dict - @ Out, None (changes libs in place) + Reads a single library request node into existing config + @ In, libNode, xml.etree.ElementTree.Element, node with library request + @ In, config, dict, mapping of existing configuration requests + @ In, toRemove, list, list of library names to be remeoved at the end + @ In, opSys, str, operating system (not checked) + @ In, install, str, installation method (not checked) + @ In, addOptional, bool, optional, if True then include optional libraries + @ In, limitSources, list(str), limit sources to those in this list (or defaults if None) + @ In, requestor, str, name of requesting plugin (for error verbosity) + @ Out, None """ - for lib, version in configSection: - #if lib not in configSection: - # return - if version == 'remove': - libs.pop(lib, None) - else: - # python 3 fix to work with python 2 syntax - if version is not None and version.strip() == '': - version = None - libs[lib] = version + tag = libNode.tag + # FIXME check if library already in the toRemove pile; if so, don't check it? + # check OS + ## note that None means "mac,os,linux" in this case + libOS = libNode.attrib.get('os', None) + # does library have a specified OS? + if libOS is not None: + # if this library's OS's don't match the requested OS, then we move on + if opSys not in [x.lower().strip() for x in libOS.split(',')]: + return # nothing to do + # check optional + ## note that None means "not optional" in this case + ## further note anything besides "True" is taken to mean "not optional" + libOptional = libNode.attrib.get('optional', None) + if libOptional is not None and libOptional.strip().lower() == 'true' and not addOptional: + return # nothing to do + # check limited sources + libSource = libNode.attrib.get('source', None) + if libSource is None: + libSource = 'conda' # DEFAULT + if limitSources is not None and libSource not in limitSources: + return # nothing to do + # otherwise, we have a valid request to handle + text = libNode.text + if text is not None: + text = text.strip().lower() + # check for removal + ## this says the library should be removed from the existing list, which we do at the end! + if text == 'remove': + toRemove.append(tag) + return + libVersion = text + libSkipCheck = libNode.attrib.get('skip_check', None) + request = {'skip_check': libSkipCheck, 'version': libVersion, 'requestor': requestor} + # does this entry already exist? + if tag in config: + existing = config[tag] + okay = True # tracks if duplicate entry is okay or error needs raising + # check if either existing or requested is default (None) for each of the dictionary entries + for entry, requestValue in request.items(): + if entry in ['requestor']: + continue + existValue = existing[entry] + # duplicates might be okay; for example, if the request/existing are identical + if requestValue != existValue: + # also okay if one of them is None (defaulting) + if None in [requestValue, existValue]: + # at least one is defaulting, so use the non-default one + if requestValue is None: + request[entry] = existValue + # if existValue is None, then keep the requestValue + else: + # there is a request conflict + print('ERROR: Dependency "{t}" has conflicting requirements for "{e}"'.format(t=tag, e=entry), + '({ev} in "{es}" vs {rv} in "{rs}")!'.format(ev=existValue, es=existing['requestor'], rv=requestValue, rs=requestor)) + okay = False + if not okay: + raise KeyError('There were errors resolving library handling requests; see above.') + # END if tag in config + config[tag] = request def _readDependencies(initFile): """ Reads in the library list using config parsing. @ In, None - @ Out, configparser.ConfigParser, configurations read in + @ Out, xml.etree.ElementTree.Element, configurations read in the most basic form """ - config = configparser.ConfigParser(allow_no_value=True) - config.read(initFile) - return config + root = ET.parse(initFile).getroot() + return root if __name__ == '__main__': mainParser = argparse.ArgumentParser(description='RAVEN Library Handler') @@ -482,7 +532,8 @@ def _readDependencies(initFile): libs = getRequiredLibs(useOS=args.useOS, installMethod='conda', addOptional=args.addOptional, plugins=plugins) msg = '\\begin{itemize}\n' - for lib, version in libs.items(): + for lib, request in libs.items(): + version = request['version'] msg += ' \\item {}{}\n'.format( lib.replace('_', '\\_'), ('' if version is None else '-'+version)) msg += '\\end{itemize}' @@ -499,7 +550,7 @@ def _readDependencies(initFile): # from defaults src = '-c defaults' addOptional = args.addOptional - limit = ['core'] + limit = ['conda'] elif args.subset == 'forge': # take libs from conda-forge src = '-c conda-forge ' @@ -514,7 +565,7 @@ def _readDependencies(initFile): libs = getRequiredLibs(useOS=args.useOS, installMethod='conda', addOptional=addOptional, - limit=limit, + limitSources=limit, plugins=plugins) # conda can create, install, or list if args.action == 'create': @@ -522,6 +573,7 @@ def _readDependencies(initFile): elif args.action == 'install': action = 'install' elif args.action == 'list': + action = 'list' preamble = '' actionArgs = actionArgs.format(env=envName, src=src) elif args.installer == 'pip': @@ -531,18 +583,19 @@ def _readDependencies(initFile): libs = getRequiredLibs(useOS=args.useOS, installMethod='pip', addOptional=args.addOptional, - limit=None, + limitSources=None, plugins=plugins) if args.action == 'install': action = 'install' elif args.action == 'list': + action = 'list' preamble = '' preamble = preamble.format(installer=installer, action=action, args=actionArgs) libTexts = ' '.join(['{lib}{ver}' .format(lib=lib, - ver=('{}{}'.format(equals, ver) if ver is not None else '')) - for lib, ver in libs.items()]) + ver=('{e}{r}'.format(e=equals, r=request['version']) if request['version'] is not None else '')) + for lib, request in libs.items()]) if len(libTexts) > 0: print(preamble + libTexts) else: