diff --git a/JsonPreprocessor/JsonPreprocessor.pdf b/JsonPreprocessor/JsonPreprocessor.pdf new file mode 100644 index 00000000..f089a8c7 Binary files /dev/null and b/JsonPreprocessor/JsonPreprocessor.pdf differ diff --git a/JsonPreprocessor/version.py b/JsonPreprocessor/version.py new file mode 100644 index 00000000..bf715f22 --- /dev/null +++ b/JsonPreprocessor/version.py @@ -0,0 +1,23 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# Version and date of JsonPreprocessor +# +VERSION = "0.1.2" +VERSION_DATE = "01.07.2022" + diff --git a/README.rst b/README.rst index 3527fa21..488d2017 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ package documentation is located in 'doc/_build/'. Package Documentation --------------------- -A detailed documentation of the Json Preprocessor's package can be found here: `Json-Preprocessor.pdf `_ +A detailed documentation of the Json Preprocessor's package can be found here: `JsonPreprocessor.pdf `_ Feedback -------- diff --git a/config/CExtendedSetup.py b/additions/CExtendedSetup.py similarity index 54% rename from config/CExtendedSetup.py rename to additions/CExtendedSetup.py index 1c1a7867..00d770da 100644 --- a/config/CExtendedSetup.py +++ b/additions/CExtendedSetup.py @@ -18,23 +18,18 @@ # # CExtendedSetup.py # -# CM-CI1/ECA3-Queckenstedt +# XC-CT/ECA3-Queckenstedt # # Contains all functions to support the extended setup process. # # -------------------------------------------------------------------------------------------------------------- # -# 21.02.2022 / XC-CT/ECA3-Queckenstedt -# Added add_htmldoc_to_wheel() to support wheel based distribution -# -# 30.09.2021 / XC-CI1/ECA3-Queckenstedt -# Added wrapper for error messages -# -# Initial version 08/2021 +# 10.05.2022 # # -------------------------------------------------------------------------------------------------------------- import os, sys, platform, shlex, subprocess, shutil +import pypandoc import colorama as col col.init(autoreset=True) @@ -70,11 +65,11 @@ def __del__(self): # -------------------------------------------------------------------------------------------------------------- - def gen_doc(self): - """Executes sphinx-makeall.py + def genpackagedoc(self): + """Executes genpackagedoc.py """ - sPython = self.__oRepositoryConfig.Get('sPython') - sDocumentationBuilder = self.__oRepositoryConfig.Get('sDocumentationBuilder') + sPython = self.__oRepositoryConfig.Get('PYTHON') + sDocumentationBuilder = self.__oRepositoryConfig.Get('DOCUMENTATIONBUILDER') listCmdLineParts = [] listCmdLineParts.append(f"\"{sPython}\"") listCmdLineParts.append(f"\"{sDocumentationBuilder}\"") @@ -96,16 +91,58 @@ def gen_doc(self): return ERROR print() return nReturn - # eof def gen_doc(): + # eof def genpackagedoc(): + + # -------------------------------------------------------------------------------------------------------------- + + def convert_repo_readme(self): + """Converts the main repository README from 'rst' to 'md' format. + """ + + sReadMe_rst = self.__oRepositoryConfig.Get("README_RST") + if sReadMe_rst is None: + print() + printerror(f"'sReadMe_rst' is None") + print() + return ERROR + + sReadMe_md = self.__oRepositoryConfig.Get("README_MD") + if sReadMe_md is None: + print() + printerror(f"'sReadMe_md' is None") + print() + return ERROR + + if os.path.isfile(sReadMe_rst) is False: + print() + printerror(f"Missing readme file '{sReadMe_rst}'") + print() + return ERROR + + sFileContent = pypandoc.convert_file(sReadMe_rst, 'md') + hFile_md = open(sReadMe_md, "w", encoding="utf-8") + listFileContent = sFileContent.splitlines() + for sLine in listFileContent: + hFile_md.write(sLine + "\n") + hFile_md.close() + + print(f"File '{sReadMe_rst}'") + print("converted to") + print(f"'{sReadMe_md}'") + print() + + return SUCCESS + + # eof def convert_repo_readme(self): # -------------------------------------------------------------------------------------------------------------- def delete_previous_build(self): """Deletes folder containing previous builds of setup.py within the repository """ - sSetupBuildFolder = self.__oRepositoryConfig.Get('sSetupBuildFolder') - sSetupDistFolder = self.__oRepositoryConfig.Get('sSetupDistFolder') - sEggInfoFolder = self.__oRepositoryConfig.Get('sEggInfoFolder') + sSetupBuildFolder = self.__oRepositoryConfig.Get('SETUPBUILDFOLDER') + sSetupDistFolder = self.__oRepositoryConfig.Get('SETUPDISTFOLDER') + sEggInfoFolder = self.__oRepositoryConfig.Get('EGGINFOFOLDER') if os.path.isdir(sSetupBuildFolder) is True: print(f"* Deleting '{sSetupBuildFolder}'") try: @@ -141,7 +178,7 @@ def delete_previous_build(self): def delete_previous_installation(self): """Deletes previous package installation folder within the Python installation """ - sInstalledPackageFolder = self.__oRepositoryConfig.Get('sInstalledPackageFolder') + sInstalledPackageFolder = self.__oRepositoryConfig.Get('INSTALLEDPACKAGEFOLDER') if os.path.isdir(sInstalledPackageFolder) is True: print(f"* Deleting '{sInstalledPackageFolder}'") try: @@ -151,102 +188,11 @@ def delete_previous_installation(self): printexception(str(ex)) print() return ERROR - sInstalledPackageDocFolder = self.__oRepositoryConfig.Get('sInstalledPackageDocFolder') - if os.path.isdir(sInstalledPackageDocFolder) is True: - print(f"* Deleting '{sInstalledPackageDocFolder}'") - try: - shutil.rmtree(sInstalledPackageDocFolder) - except Exception as ex: - print() - printexception(str(ex)) - print() - return ERROR print() return SUCCESS # eof def delete_previous_installation(): - # -------------------------------------------------------------------------------------------------------------- - - def add_htmldoc_to_installation(self): - """Adds the package documentation in HTML format to the Python onstallation - """ - sHTMLOutputFolder = self.__oRepositoryConfig.Get('sHTMLOutputFolder') - sInstalledPackageDocFolder = self.__oRepositoryConfig.Get('sInstalledPackageDocFolder') - if os.path.isdir(sHTMLOutputFolder) is False: - print() - printerror(f"Error: Missing html output folder '{sHTMLOutputFolder}'") - print() - return ERROR - shutil.copytree(sHTMLOutputFolder, sInstalledPackageDocFolder) - if os.path.isdir(sInstalledPackageDocFolder) is False: - print() - printerror(f"Error: html documentation not copied to package installation folder '{sInstalledPackageDocFolder}'") - print() - return ERROR - print(COLBY + f"Folder '{sHTMLOutputFolder}'") - print(COLBY + "copied to") - print(COLBY + f"'{sInstalledPackageDocFolder}'") - print() - return SUCCESS - # eof def add_htmldoc_to_installation(): - - # -------------------------------------------------------------------------------------------------------------- - - def add_htmldoc_to_wheel(self): - """Adds the package documentation in HTML format to the wheel folder inside build - """ - sHTMLOutputFolder = self.__oRepositoryConfig.Get('sHTMLOutputFolder') - sSetupBuildFolder = self.__oRepositoryConfig.Get('sSetupBuildFolder') - sPackageName = self.__oRepositoryConfig.Get('sPackageName') - if os.path.isdir(sHTMLOutputFolder) is False: - print() - printerror(f"Error: Missing html output folder '{sHTMLOutputFolder}'") - print() - return ERROR - - # The desired destination path for the documentation is: - # \bdist.win-amd64\wheel\\doc - # with is already available by 'sSetupBuildFolder' in CConfig. - # I am not convinced that it's a good idea to have hard coded parts like 'bdist.win-amd64' within a path here. - # Therefore we search recursively the file system for a subfolder with name 'wheel/'. And that's it! - sTargetFolder = f"wheel/{sPackageName}" - sWheelDocDestPath = None - bBreak = False - for sRootFolder, listFolders, listFiles in os.walk(sSetupBuildFolder): - for sFolder in listFolders: - sPath = os.path.join(sRootFolder, sFolder) - sPathMod = sPath.replace("\\", "/") - if sPathMod.endswith(sTargetFolder): - sWheelDocDestPath = f"{sPathMod}/doc" - bBreak = True - break # for sFolder in listFolders: - # eof if sPathMod.endswith(sTargetFolder): - # eof for sFolder in listFolders: - if bBreak is True: - break # walk - # eof for sRootFolder, listFolders, listFiles in os.walk(sSetupBuildFolder): - - if sWheelDocDestPath is None: - print() - printerror(f"Error: Not able to find '{sTargetFolder}' inside {sSetupBuildFolder}") - print() - return ERROR - - shutil.copytree(sHTMLOutputFolder, sWheelDocDestPath) - if os.path.isdir(sWheelDocDestPath) is False: - print() - printerror(f"Error: html documentation not copied to local wheel folder '{sWheelDocDestPath}'") - print() - return ERROR - - print(COLBY + f"Folder '{sHTMLOutputFolder}'") - print(COLBY + "copied to") - print(COLBY + f"'{sWheelDocDestPath}'") - print() - return SUCCESS - # eof def add_htmldoc_to_wheel(): - # eof class CExtendedSetup(): # -------------------------------------------------------------------------------------------------------------- diff --git a/additions/PythonExtensionsCollection/File/CFile.py b/additions/PythonExtensionsCollection/File/CFile.py new file mode 100644 index 00000000..320424c6 --- /dev/null +++ b/additions/PythonExtensionsCollection/File/CFile.py @@ -0,0 +1,1072 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# CFile.py +# +# XC-CT/ECA3-Queckenstedt +# +# 27.06.2022 +# +# ************************************************************************************************************** + +# -- import standard Python modules +import os, shutil, platform + +# -- import Bosch Python modules +from PythonExtensionsCollection.String.CString import CString + +# ************************************************************************************************************** + +class enFileStatiType: + """ +The class ``enFileStatiType`` defines the sollowing file states: + +* ``closed`` +* ``openedforwriting`` +* ``openedforappending`` +* ``openedforreading`` + """ + closed = "closed" + openedforwriting = "openedforwriting" + openedforappending = "openedforappending" + openedforreading = "openedforreading" + +# -------------------------------------------------------------------------------------------------------------- + +class CFile(object): + """ +The class ``CFile`` provides a small set of file functions with extended parametrization (like switches +defining if a file is allowed to be overwritten or not). + +Most of the functions at least returns ``bSuccess`` and ``sResult``. + +* ``bSuccess`` is ``True`` in case of no error occurred. +* ``bSuccess`` is ``False`` in case of an error occurred. +* ``bSuccess`` is ``None`` in case of a very fatal error occurred (exceptions). + +* ``sResult`` contains details about what happens during computation. + +Every instance of CFile handles one single file only and forces exclusive access to this file. + +It is not possible to create an instance of this class with a file that is already in use by another instance. + +It is also not possible to use ``CopyTo`` or ``MoveTo`` to overwrite files that are already in use by another instance. +This makes the file handling more save against access violations. + """ + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __init__(self, sFile=None): + self.__sFile = CString.NormalizePath(sFile) + self.__oFileHandle = None + self.__oFileStatus = enFileStatiType.closed + self.__sLastDestination = None + + try: + CFile.__listFilesInUse + except: + CFile.__listFilesInUse = [] + + # exclusive access is required (checked by self.__bIsFreeToUse; relevant for destination in CopyTo and MoveTo) + if self.__sFile in CFile.__listFilesInUse: + raise Exception(f"The file '{self.__sFile}' is already in use by another CFile instance.") + else: + CFile.__listFilesInUse.append(self.__sFile) + + # eof def __init__(self, sFile=None): + + def __del__(self): + self.Close() + if self.__sFile in CFile.__listFilesInUse: + CFile.__listFilesInUse.remove(self.__sFile) + + # eof def __del__(self): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __bIsFreeToUse(self, sFile=None): + """ +Checks if the file ``sFile`` is free to use, that means: not used by another instance of ``CFile``. + """ + + bIsFreeToUse = False # init + if sFile is None: + bIsFreeToUse = False # error handling + else: + if sFile in CFile.__listFilesInUse: + bIsFreeToUse = False + else: + bIsFreeToUse = True + return bIsFreeToUse + + # eof def __bIsFreeToUse(self, sFile=None): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __OpenForWriting(self): + """ +Opens a text file for writing. + +Returns ``bSuccess`` and ``sResult`` (feedback). + """ + + sMethod = "CFile.__OpenForWriting" + + if self.__sFile is None: + bSuccess = False + sResult = "self.__sFile is None; please provide path and name of a file when creating a CFile object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + bSuccess, sResult = self.Close() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + try: + self.__oFileHandle = open(self.__sFile, "w", encoding="utf-8") + self.__oFileStatus = enFileStatiType.openedforwriting + bSuccess = True + sResult = f"File '{self.__sFile}' is open for writing" + except Exception as reason: + self.Close() + bSuccess = None + sResult = f"Not possible to open file '{self.__sFile}' for writing.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def __OpenForWriting(self): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __OpenForAppending(self): + """ +Opens a text file for appending. + +Returns ``bSuccess`` and ``sResult`` (feedback). + """ + + sMethod = "CFile.__OpenForAppending" + + if self.__sFile is None: + bSuccess = False + sResult = "self.__sFile is None; please provide path and name of a file when creating a CFile object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + bSuccess, sResult = self.Close() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + try: + self.__oFileHandle = open(self.__sFile, "a", encoding="utf-8") + self.__oFileStatus = enFileStatiType.openedforappending + bSuccess = True + sResult = f"File '{self.__sFile}' is open for appending" + except Exception as reason: + self.Close() + bSuccess = None + sResult = f"Not possible to open file '{self.__sFile}' for appending.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def __OpenForAppending(self): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __OpenForReading(self): + """ +Opens a text file for reading. + +Returns ``bSuccess`` and ``sResult`` (feedback). + """ + + sMethod = "CFile.__OpenForReading" + + if self.__sFile is None: + bSuccess = False + sResult = "self.__sFile is None; please provide path and name of a file when creating a CFile object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + bSuccess, sResult = self.Close() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + try: + self.__oFileHandle = open(self.__sFile, "r", encoding="utf-8") + self.__oFileStatus = enFileStatiType.openedforreading + bSuccess = True + sResult = f"File '{self.__sFile}' is open for reading" + except Exception as reason: + self.Close() + bSuccess = None + sResult = f"Not possible to open file '{self.__sFile}' for reading.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def __OpenForReading(self): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def Close(self): + """ +Closes the opened file. + +**Arguments:** + +(no args) + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + sMethod = "CFile.Close" + + if self.__oFileHandle is not None: + try: + self.__oFileHandle.flush() + self.__oFileHandle.close() + bSuccess = True + sResult = f"File '{self.__sFile}' closed" + except Exception as reason: + bSuccess = None + sResult = f"Exception while closing file '{self.__sFile}'.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + self.__oFileHandle = None + else: + bSuccess = True + sResult = "Done" + + self.__oFileStatus = enFileStatiType.closed + + return bSuccess, sResult + + # eof def Close(self): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def Delete(self, bConfirmDelete=True): + """ +Deletes the current file. + +**Arguments:** + +* ``bConfirmDelete`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + Defines if it will be handled as error if the file does not exist. + + If ``True``: If the file does not exist, the method indicates an error (``bSuccess = False``). + + If ``False``: It doesn't matter if the file exists or not. + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + + sMethod = "CFile.Delete" + + if self.__sFile is None: + bSuccess = False + sResult = "self.__sFile is None; please provide path and name of a file when creating a CFile object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if os.path.isfile(self.__sFile) is False: + if bConfirmDelete is True: + bSuccess = False + else: + bSuccess = True + sResult = f"Nothing to delete. The file '{self.__sFile}' does not exist." + return bSuccess, sResult + + bSuccess, sResult = self.Close() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + try: + os.remove(self.__sFile) + bSuccess = True + sResult = f"File '{self.__sFile}' deleted." + except Exception as reason: + bSuccess = None + sResult = f"Exception while deleting file '{self.__sFile}'.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def Delete(self, bConfirmDelete=True): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __PrepareOutput(self, Content=""): + """ +Helper for ``Write`` and ``Append`` (consideration of composite data types). + +Returns a list of strings (that will be written to file). + """ + + listOut = [] + + if type(Content) == list: + for element in Content: + listOut.append(str(element)) + elif type(Content) == tuple: + for element in Content: + listOut.append(str(element)) + elif type(Content) == set: + for element in Content: + listOut.append(str(element)) + elif type(Content) == dict: + listKeys = Content.keys() + nRJust = 0 + for key in listKeys: + sKey = str(key) # because also numerical values can be keys + if len(sKey) > nRJust: + nRJust = len(sKey) + for key in listKeys: + sKey = str(key) # because also numerical values can be keys + sOut = sKey.rjust(nRJust, ' ') + " : " + str(Content[key]) + listOut.append(sOut) + elif str(type(Content)).lower().find('dotdict') >=0: + try: + listKeys = Content.keys() + nRJust = 0 + for key in listKeys: + sKey = str(key) # because also numerical values can be keys + if len(sKey) > nRJust: + nRJust = len(sKey) + for key in listKeys: + sKey = str(key) # because also numerical values can be keys + sOut = sKey.rjust(nRJust, ' ') + " : " + str(Content[key]) + listOut.append(sOut) + except Exception as reason: + listOut.append(str(Content)) + else: + listOut.append(str(Content)) + + return listOut + + # eof def __PrepareOutput(self, Content=""): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def Write(self, Content="", nVSpaceAfter=0, sPrefix=None, bToScreen=False): + """ +Writes the content of a variable ``Content`` to file. + +**Arguments:** + +* ``Content`` + + / *Condition*: required / *Type*: one of: str, list, tuple, set, dict, dotdict / + + If ``Content`` is not a string, the ``Write`` method resolves the data structure before writing the content to file. + +* ``nVSpaceAfter`` + + / *Condition*: optional / *Type*: int / *Default*: 0 / + + Adds vertical space ``nVSpaceAfter`` (= number of blank lines) after ``Content``. + +* ``sPrefix`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + `sPrefix`` is added to every line of output (in case of ``sPrefix`` is not ``None``). + +* ``bToScreen`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + Prints ``Content`` also to screen (in case of ``bToScreen`` is ``True``). + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + + sMethod = "CFile.Write" + + if self.__oFileStatus != enFileStatiType.openedforwriting: + bSuccess, sResult = self.__OpenForWriting() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + listOut = self.__PrepareOutput(Content) + + for nCnt in range(nVSpaceAfter): + listOut.append("") + + if bToScreen is True: + for sOut in listOut: + if ( (sPrefix is not None) and (sOut != '') ): + sOut = f"{sPrefix}{sOut}" + print(sOut) + + bSuccess = True + sResult = "Done" + try: + for sOut in listOut: + if ( (sPrefix is not None) and (sOut != '') ): + sOut = f"{sPrefix}{sOut}" + self.__oFileHandle.write(sOut + "\n") + except Exception as reason: + bSuccess = None + sResult = f"Not possible to write to file '{self.__sFile}'.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def Write(self, Content="", nVSpaceAfter=0, sPrefix=None, bToScreen=False): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def Append(self, Content="", nVSpaceAfter=0, sPrefix=None, bToScreen=False): + """ +Appends the content of a variable ``Content`` to file. + +**Arguments:** + +* ``Content`` + + / *Condition*: required / *Type*: one of: str, list, tuple, set, dict, dotdict / + + If ``Content`` is not a string, the ``Write`` method resolves the data structure before writing the content to file. + +* ``nVSpaceAfter`` + + / *Condition*: optional / *Type*: int / *Default*: 0 / + + Adds vertical space ``nVSpaceAfter`` (= number of blank lines) after ``Content``. + +* ``sPrefix`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + `sPrefix`` is added to every line of output (in case of ``sPrefix`` is not ``None``). + +* ``bToScreen`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + Prints ``Content`` also to screen (in case of ``bToScreen`` is ``True``). + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + sMethod = "CFile.Append" + + if self.__oFileStatus != enFileStatiType.openedforappending: + bSuccess, sResult = self.__OpenForAppending() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + listOut = self.__PrepareOutput(Content) + + for nCnt in range(nVSpaceAfter): + listOut.append("") + + if bToScreen is True: + for sOut in listOut: + if ( (sPrefix is not None) and (sOut != '') ): + sOut = f"{sPrefix}{sOut}" + print(sOut) + + bSuccess = True + sResult = "Done" + try: + for sOut in listOut: + if ( (sPrefix is not None) and (sOut != '') ): + sOut = f"{sPrefix}{sOut}" + self.__oFileHandle.write(sOut + "\n") + except Exception as reason: + bSuccess = None + sResult = f"Not possible to append to file '{self.__sFile}'.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def Append(self, Content="", nVSpaceAfter=0, sPrefix=None, bToScreen=False): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def ReadLines(self, + bCaseSensitive = True, + bSkipBlankLines = False, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = None, + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None, + bLStrip = False, + bRStrip = True, + bToScreen = False): + """ +Reads content from current file. Returns an array of lines together with ``bSuccess`` and ``sResult`` (feedback). + +The method takes care of opening and closing the file. The complete file content is read by ``ReadLines`` in one step, +but with the help of further parameters it is possible to reduce the content by including and excluding lines. + +The logical join of all filter is: ``AND``. + +**Arguments:** + +* ``bCaseSensitive`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + * If ``True``, the standard filters work case sensitive, otherwise not. + * This has no effect to the regular expression based filters ``sInclRegEx`` and ``sExclRegEx``. + +* ``bSkipBlankLines`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True``, blank lines will be skipped, otherwise not. + +* ``sComment`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + In case of a line starts with the string ``sComment``, this line is skipped. + +* ``sStartsWith`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * The criterion of this filter is fulfilled in case of the input string starts with the string ``sStartsWith`` + * More than one string can be provided (semicolon separated; logical join: ``OR``) + +* ``sEndsWith`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * The criterion of this filter is fulfilled in case of the input string ends with the string ``sEndsWith`` + * More than one string can be provided (semicolon separated; logical join: ``OR``) + +* ``sStartsNotWith`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * The criterion of this filter is fulfilled in case of the input string starts not with the string ``sStartsNotWith`` + * More than one string can be provided (semicolon separated; logical join: ``AND``) + +* ``sEndsNotWith`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * The criterion of this filter is fulfilled in case of the input string ends not with the string ``sEndsNotWith`` + * More than one string can be provided (semicolon separated; logical join: ``AND``) + +* ``sContains`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * The criterion of this filter is fulfilled in case of the input string contains the string ``sContains`` at any position + * More than one string can be provided (semicolon separated; logical join: ``OR``) + +* ``sContainsNot`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * The criterion of this filter is fulfilled in case of the input string does **not** contain the string ``sContainsNot`` at any position + * More than one string can be provided (semicolon separated; logical join: ``AND``) + +* ``sInclRegEx`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * *Include* filter based on regular expressions (consider the syntax of regular expressions!) + * The criterion of this filter is fulfilled in case of the regular expression ``sInclRegEx`` matches the input string + * Leading and trailing blanks within the input string are considered + * ``bCaseSensitive`` has no effect + * A semicolon separated list of several regular expressions is **not** supported + +* ``sExclRegEx`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + * *Exclude* filter based on regular expressions (consider the syntax of regular expressions!) + * The criterion of this filter is fulfilled in case of the regular expression ``sExclRegEx`` does **not** match the input string + * Leading and trailing blanks within the input string are considered + * ``bCaseSensitive`` has no effect + * A semicolon separated list of several regular expressions is **not** supported + +* ``bLStrip`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True``, leading spaces are removed from line before the filters are used, otherwise not. + +* ``bRStrip`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + If ``True``, trailing spaces are removed from line before the filters are used, otherwise not. + +* ``bToScreen`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True``, the content read from file is also printed to screen, otherwise not. + """ + + sMethod = "CFile.ReadLines" + + listLines = [] + + if os.path.isfile(self.__sFile) is False: + bSuccess = False + sResult = f"The file '{self.__sFile}' does not exist." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return listLines, bSuccess, sResult + + # !!! independend from: self.__oFileStatus != enFileStatiType.openedforreading: !!! + # Reason: Repeated call of ReadLines needs to have the read pointer at the beginning of the file. + bSuccess, sResult = self.__OpenForReading() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return listLines, bSuccess, sResult + + try: + sFileContent = self.__oFileHandle.read() + except Exception as reason: + bSuccess = None + sResult = f"Not possible to read from file '{self.__sFile}'.\nReason: " + str(reason) + return listLines, bSuccess, sResult + + bSuccess, sResult = self.Close() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return listLines, bSuccess, sResult + + listFileContent = sFileContent.splitlines() # in opposite to readlines this is OS independend! + + for sLine in listFileContent: + if CString.StringFilter(sString = sLine, + bCaseSensitive = bCaseSensitive, + bSkipBlankStrings = bSkipBlankLines, + sComment = sComment, + sStartsWith = sStartsWith, + sEndsWith = sEndsWith, + sStartsNotWith = sStartsNotWith, + sEndsNotWith = sEndsNotWith, + sContains = sContains, + sContainsNot = sContainsNot, + sInclRegEx = sInclRegEx, + sExclRegEx = sExclRegEx, + bDebug = False) is True: + if bLStrip is True: + sLine = sLine.lstrip(" \t\r\n") + + if bRStrip is True: + sLine = sLine.rstrip(" \t\r\n") + + if bToScreen is True: + print(sLine) + + listLines.append(sLine) + + # eof for sLine in listFileContent: + + del listFileContent + + nNrOfLines = len(listLines) + + bSuccess = True + sResult = f"Read {nNrOfLines} lines from '{self.__sFile}'." + return listLines, bSuccess, sResult + + # eof def ReadLines(...) + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def GetFileInfo(self): + """ +Returns the following informations about the file (encapsulated within a dictionary ``dFileInfo``): + +**Returns:** + +* Key ``sFile`` + + / *Type*: str / + + Path and name of current file + + +* Key ``bFileIsExisting`` + + / *Type*: bool / + + ``True`` if file is existing, otherwise ``False`` + +* Key ``sFileName`` + + / *Type*: str / + + The name of the current file (incl. extension) + +* Key ``sFileExtension`` + + / *Type*: str / + + The extension of the current file + +* Key ``sFileNameOnly`` + + / *Type*: str / + + The pure name of the current file (without extension) + +* Key ``sFilePath`` + + / *Type*: str / + + The the path to current file + +* Key ``bFilePathIsExisting`` + + / *Type*: bool / + + ``True`` if file path is existing, otherwise ``False`` + """ + + sMethod = "CFile.GetFileInfo" + + dFileInfo = {} + dFileInfo['sFile'] = None + dFileInfo['bFileIsExisting'] = None + dFileInfo['sFileName'] = None + dFileInfo['sFileExtension'] = None + dFileInfo['sFileNameOnly'] = None + dFileInfo['sFilePath'] = None + dFileInfo['bFilePathIsExisting'] = None + + if self.__sFile is None: + return None + + dFileInfo['sFile'] = self.__sFile + dFileInfo['bFileIsExisting'] = os.path.isfile(self.__sFile) + + sFileName = os.path.basename(self.__sFile) + dFileInfo['sFileName'] = sFileName + + sFileExtension = "" + sFileNameOnly = "" + listParts = sFileName.split('.') + if len(listParts) > 1: + sFileExtension = listParts[len(listParts)-1] + sFileNameOnly = sFileName[:-len(sFileExtension)-1] + else: + sFileExtension = "" + sFileNameOnly = sFileName + + dFileInfo['sFileExtension'] = sFileExtension + dFileInfo['sFileNameOnly'] = sFileNameOnly + dFileInfo['sFilePath'] = os.path.dirname(self.__sFile) + dFileInfo['bFilePathIsExisting'] = os.path.isdir(dFileInfo['sFilePath']) + + return dFileInfo + + # eof def GetFileInfo(self): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def CopyTo(self, sDestination=None, bOverwrite=False): + """ +Copies the current file to ``sDestination``, that can either be a path without file name or a path together with a file name. + +In case of the destination file already exists and ``bOverwrite`` is ``True``, than the destination file will be overwritten. + +In case of the destination file already exists and ``bOverwrite`` is ``False`` (default), than the destination file will not be overwritten +and ``CopyTo`` returns ``bSuccess = False``. + +**Arguments:** + +* ``sDestination`` + + / *Condition*: required / *Type*: string / + + The path to destination file (either incl. file name or without file name) + +* ``bOverwrite`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + * In case of the destination file already exists and ``bOverwrite`` is ``True``, than the destination file will be overwritten. + * In case of the destination file already exists and ``bOverwrite`` is ``False`` (default), than the destination file will not be overwritten + and ``CopyTo`` returns ``bSuccess = False``. + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + sMethod = "CFile.CopyTo" + + if self.__sFile is None: + bSuccess = False + sResult = "self.__sFile is None; please provide path and name of a file when creating a CFile object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if os.path.isfile(self.__sFile) is False: + bSuccess = False + sResult = f"The file '{self.__sFile}' does not exist, therefore nothing can be copied." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if sDestination is None: + bSuccess = False + sResult = "sDestination is None; please provide path and name of destination file. Or at least the destination path. In this case the file name will be taken over." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + sDestination = CString.NormalizePath(sDestination) + + bDeleteDestFile = False + + sDestFile = sDestination # default + + if os.path.isdir(sDestination) is True: + sFileName = os.path.basename(self.__sFile) + sDestFile = f"{sDestination}/{sFileName}" # file name in destination is required for: shutil.copyfile + + if self.__bIsFreeToUse(sDestFile) is False: + bSuccess = False + sResult = f"The destination file '{sDestFile}' is already in use by another CFile instance." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + self.__sLastDestination = sDestFile + + if os.path.isfile(sDestFile) is True: + # destination file already exists + if sDestFile == self.__sFile: + bSuccess = False + sResult = f"Source file and destination file are the same: '{self.__sFile}'. Therefore nothing to do." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if bOverwrite is True: + bDeleteDestFile = True + else: + bSuccess = False + sResult = f"Not allowed to overwrite existing destination file '{sDestFile}'. Therefore nothing to do." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + else: + # destination file not yet exists + # (we assume here that the destination shall be a file because we already have figured out that the destination is not a folder) + # => we have to check if the path to the file exists + sDestFilePath = os.path.dirname(sDestFile) + if os.path.isdir(sDestFilePath) is True: + bDeleteDestFile = False + else: + bSuccess = False + sResult = f"The destination path '{sDestFilePath}' does not exist. The file '{self.__sFile}' cannot be copied." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + # eof else - if os.path.isfile(sDestFile) is True: + + # analysis done, now the action + + bSuccess, sResult = self.Close() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if bDeleteDestFile is True: + # To delete the destination file explicitely before executing any copy-function is an addon here in this library. + # The purpose is to be independend from the way the used copy function is handling existing destination files. + # But this makes only sense under Windows and not under Linux, because Windows is much more strict with access + # violations than Linux. Therefore we avoid such kind of additional steps in case of the platform is not Windows. + if platform.system() == "Windows": + try: + os.remove(sDestFile) + bSuccess = True + sResult = f"File '{sDestFile}' deleted." + except Exception as reason: + bSuccess = None + sResult = f"Exception while deleting destination file '{sDestFile}'.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + # eof if bDeleteDestFile is True: + + try: + shutil.copyfile(self.__sFile, sDestFile) + bSuccess = True + sResult = f"File '{self.__sFile}' copied to '{sDestFile}'." + except Exception as reason: + bSuccess = None + sResult = f"Exception while copying file '{self.__sFile}' to '{sDestFile}'.\nReason: " + str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def CopyTo(self, sDestination=None, bOverwrite=False): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def MoveTo(self, sDestination=None, bOverwrite=False): + """ +Moves the current file to ``sDestination``, that can either be a path without file name or a path together with a file name. + +**Arguments:** + +* ``sDestination`` + + / *Condition*: required / *Type*: string / + + The path to destination file (either incl. file name or without file name) + +* ``bOverwrite`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + * In case of the destination file already exists and ``bOverwrite`` is ``True``, than the destination file will be overwritten. + * In case of the destination file already exists and ``bOverwrite`` is ``False`` (default), than the destination file will not be overwritten + and ``MoveTo`` returns ``bSuccess = False``. + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation was successful or not + +* ``sResult`` + + / *Type*: str / + + Contains details about what happens during computation + """ + sMethod = "CFile.MoveTo" + + bSuccess, sResult = self.CopyTo(sDestination, bOverwrite) + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if os.path.isfile(self.__sLastDestination) is False: + # the copied file should exist at new location + bSuccess = None + sResult = f"Someting went wrong while copying the file '{self.__sFile}' to '{self.__sLastDestination}'. Aborting." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + else: + bSuccess, sResult = self.Delete() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + bSuccess = True + sResult = f"File moved from '{self.__sFile}' to '{self.__sLastDestination}'" + return bSuccess, sResult + + # eof def MoveTo(self, sDestination=None, bOverwrite=False): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + +# eof class CFile(object): + +# ************************************************************************************************************** + + diff --git a/additions/PythonExtensionsCollection/File/__init__.py b/additions/PythonExtensionsCollection/File/__init__.py new file mode 100644 index 00000000..958420af --- /dev/null +++ b/additions/PythonExtensionsCollection/File/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/additions/PythonExtensionsCollection/Folder/CFolder.py b/additions/PythonExtensionsCollection/Folder/CFolder.py new file mode 100644 index 00000000..451c32f9 --- /dev/null +++ b/additions/PythonExtensionsCollection/Folder/CFolder.py @@ -0,0 +1,460 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# CFolder.py +# +# XC-CT/ECA3-Queckenstedt +# +# 28.06.2022 +# +# ************************************************************************************************************** + +# -- import standard Python modules +import os, shutil, time, stat + +# -- import Bosch Python modules +from PythonExtensionsCollection.String.CString import CString + +# -------------------------------------------------------------------------------------------------------------- + +# little helper to delete folders containing files that are write protected +def rm_dir_readonly(func, path, excinfo): + """ +Calls ``os.chmod`` in case of ``shutil.rmtree`` (within ``Delete()``) throws an exception (making files writable). + """ + # print(f"{excinfo}") # debug only + os.chmod(path, stat.S_IWRITE) + func(path) + +# -------------------------------------------------------------------------------------------------------------- + +class CFolder(object): + """ +The class ``CFolder`` provides a small set of folder functions with extended parametrization (like switches +defining if a folder is allowed to be overwritten or not). + +Most of the functions at least returns ``bSuccess`` and ``sResult``. + +* ``bSuccess`` is ``True`` in case of no error occurred. +* ``bSuccess`` is ``False`` in case of an error occurred. +* ``bSuccess`` is ``None`` in case of a very fatal error occurred (exceptions). + +* ``sResult`` contains details about what happens during computation. + +Every instance of CFolder handles one single folder only and forces exclusive access to this folder. + +It is not possible to create an instance of this class with a folder that is already in use by another instance. + +The constructor of ``CFolder`` requires the input parameter ``sFolder``, that is the path and the name of a folder +that is handled by the current class instance. + """ + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __init__(self, sFolder=None): + self.__sFolder = CString.NormalizePath(sFolder) + + try: + CFolder.__listFoldersInUse + except: + CFolder.__listFoldersInUse = [] + + # exclusive access is required (checked by self.__bIsFreeToUse; relevant for destination in CopyTo and MoveTo) + if self.__sFolder in CFolder.__listFoldersInUse: + raise Exception(f"The folder '{self.__sFolder}' is already in use by another CFolder instance.") + else: + CFolder.__listFoldersInUse.append(self.__sFolder) + + # eof def __init__(self, sFolder=None): + + def __del__(self): + if self.__sFolder in CFolder.__listFoldersInUse: + CFolder.__listFoldersInUse.remove(self.__sFolder) + + # eof def __del__(self): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __bIsFreeToUse(self, sFolder=None): + """ +Checks if the folder ``sFolder`` is free to use, that means: not used by another instance of ``CFolder``. + """ + + bIsFreeToUse = False # init + if sFolder is None: + bIsFreeToUse = False # error handling + else: + if sFolder in CFolder.__listFoldersInUse: + bIsFreeToUse = False + else: + bIsFreeToUse = True + return bIsFreeToUse + + # eof def __bIsFreeToUse(self, sFolder=None): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def __Delete(self, sFolder=None, bConfirmDelete=True): + """ +Deletes the folder ``sFolder``. + +**Arguments:** + +* ``sFolder`` + + / *Condition*: required / *Type*: str / + + Path and name of folder to be deleted + +* ``bConfirmDelete`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + Defines if it will be handled as error if the folder does not exist. + + If ``True``: If the folder does not exist, the method indicates an error (``bSuccess = False``). + + If ``False``: It doesn't matter if the folder exists or not. + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + sMethod = "CFolder.__Delete" + + if sFolder is None: + bSuccess = False + sResult = "sFolder is None; please provide path and name of a folder when creating a CFolder object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if os.path.isdir(sFolder) is False: + sResult = f"Nothing to delete. The folder '{sFolder}' does not exist." + if bConfirmDelete is True: + bSuccess = False + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + else: + bSuccess = True + return bSuccess, sResult + # eof if os.path.isdir(sFolder) is False: + + bSuccess = False + sResult = "UNKNOWN" + nCntTries = 1 + nTriesMax = 4 + nDelay = 2 # sec + listResults = [] + while nCntTries <= nTriesMax: + try: + print(f"Trying to delete '{sFolder}'") + print() + shutil.rmtree(sFolder, ignore_errors=False, onerror=rm_dir_readonly) + except Exception as reason: + listResults.append(str(reason)) + if os.path.isdir(sFolder) is True: + sResult = f"({nCntTries}/{nTriesMax}) Problem with deleting the folder '{sFolder}'. Folder still present." + listResults.append(sResult) + time.sleep(nDelay) # delay before next try + else: + bSuccess = True + sResult = f"Folder '{sFolder}' deleted." + break + nCntTries = nCntTries + 1 + # eof while nCntTries <= nTriesMax: + + if bSuccess is False: + sResult = "\n".join(listResults) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def __Delete(self, sFolder=None, bConfirmDelete=True): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def Delete(self, bConfirmDelete=True): + """ +Deletes the folder the current class instance contains. + +**Arguments:** + +* ``bConfirmDelete`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + Defines if it will be handled as error if the folder does not exist. + + If ``True``: If the folder does not exist, the method indicates an error (``bSuccess = False``). + + If ``False``: It doesn't matter if the folder exists or not. + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + sMethod = "CFolder.Delete" + bSuccess, sResult = self.__Delete(self.__sFolder, bConfirmDelete) + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + # eof def Delete(self, bConfirmDelete=True): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def Create(self, bOverwrite=False, bRecursive=False): + """ +Creates the current folder ``sFolder``. + +**Arguments:** + +* ``bOverwrite`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + * In case of the folder already exists and ``bOverwrite`` is ``True``, than the folder will be deleted before creation. + * In case of the folder already exists and ``bOverwrite`` is ``False`` (default), than the folder will not be touched. + + In both cases the return value ``bSuccess`` is ``True`` - because the folder exists. + +* ``bRecursive`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + * In case of ``bRecursive`` is ``True``, than the complete destination path will be created (including all intermediate subfolders). + * In case of ``bRecursive`` is ``False``, than it is expected that the parent folder of the new folder already exists. + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + sMethod = "CFolder.Create" + + if self.__sFolder is None: + bSuccess = False + sResult = "self.__sFolder is None; please provide path and name of a folder when creating a CFolder object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + bCreateFolder = False + if os.path.isdir(self.__sFolder) is True: + if bOverwrite is True: + bSuccess, sResult = self.Delete() + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + bCreateFolder = True + else: + bSuccess = True + sResult = f"Folder '{self.__sFolder}' already exists." + return bSuccess, sResult + else: + bCreateFolder = True + + bSuccess = False + sResult = "UNKNOWN" + + if bCreateFolder is True: + nCntTries = 1 + nTriesMax = 3 + nDelay = 2 # sec + listResults = [] + while nCntTries <= nTriesMax: + try: + print(f"Trying to create '{self.__sFolder}'") + print() + if bRecursive is True: + os.makedirs(self.__sFolder) + else: + os.mkdir(self.__sFolder) + except Exception as reason: + listResults.append(str(reason)) + if os.path.isdir(self.__sFolder) is False: + sResult = f"({nCntTries}/{nTriesMax}) Problem with creating the folder '{self.__sFolder}'." + listResults.append(sResult) + time.sleep(nDelay) # delay before next try + else: + bSuccess = True + sResult = f"Folder '{self.__sFolder}' created." + break + nCntTries = nCntTries + 1 + # eof while nCntTries <= nTriesMax: + + if bSuccess is False: + sResult = "\n".join(listResults) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + # eof if bCreateFolder is True: + + return bSuccess, sResult + + # eof def Create(self, bOverwrite=False, bRecursive=False): + + # -------------------------------------------------------------------------------------------------------------- + # TM*** + + def CopyTo(self, sDestination=None, bOverwrite=False): + """ +Copies the current folder to ``sDestination``, that has to be a path to a folder **within** the source folder will be copied to +(with it's original name), + +In case of the destination folder already exists and ``bOverwrite`` is ``True``, than the destination folder will be overwritten. + +In case of the destination folder already exists and ``bOverwrite`` is ``False`` (default), than the destination folder will not be overwritten +and ``CopyTo`` returns ``bSuccess = False``. + +**Arguments:** + +* ``sDestination`` + + / *Condition*: required / *Type*: string / + + The path to destination folder + +* ``bOverwrite`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + * In case of the destination folder already exists and ``bOverwrite`` is ``True``, than the destination folder will be overwritten. + * In case of the destination folder already exists and ``bOverwrite`` is ``False`` (default), than the destination folder will not be overwritten + and ``CopyTo`` returns ``bSuccess = False``. + +**Returns:** + +* ``bSuccess`` + + / *Type*: bool / + + Indicates if the computation of the method was successful or not. + +* ``sResult`` + + / *Type*: str / + + The result of the computation of the method. + """ + sMethod = "CFolder.CopyTo" + + if self.__sFolder is None: + bSuccess = False + sResult = "self.__sFolder is None; please provide path and name of a folder when creating a CFolder object." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if os.path.isdir(self.__sFolder) is False: + bSuccess = False + sResult = f"The folder '{self.__sFolder}' does not exist, therefore nothing can be copied." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if sDestination is None: + bSuccess = False + sResult = "sDestination is None; please provide a path to a destination folder." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + sDestination = CString.NormalizePath(sDestination) + + if os.path.isdir(sDestination) is False: + # the folder to be copied will be created within the destination folder, therefore we expect that the destination folder already exists + bSuccess = False + sResult = f"The destination folder '{sDestination}' does not exist." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + sSourceFolderName = os.path.basename(self.__sFolder) + sDestFolder = f"{sDestination}/{sSourceFolderName}" + + if sDestFolder == self.__sFolder: + bSuccess = False + sResult = f"Source folder and destination folder are the same: '{self.__sFolder}'. Therefore nothing to do." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if self.__bIsFreeToUse(sDestFolder) is False: + bSuccess = False + sResult = f"The destination folder '{sDestFolder}' is already in use by another CFolder instance." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + + if os.path.isdir(sDestFolder) is True: + # destination folder already exists + if bOverwrite is True: + bSuccess, sResult = self.__Delete(sDestFolder) + if bSuccess is not True: + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + else: + bSuccess = False + sResult = f"Not allowed to overwrite existing destination folder '{sDestFolder}'. Therefore nothing to do." + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + return bSuccess, sResult + # eof if os.path.isdir(sDestFolder) is True: + + # analysis and preconditions done, now the action + + try: + shutil.copytree(self.__sFolder, sDestFolder) + bSuccess = True + sResult = "Folder copied from\n> '" + self.__sFolder + "'\nto\n> '" + sDestFolder + "'" + except Exception as reason: + bSuccess = None + sResult = str(reason) + sResult = CString.FormatResult(sMethod, bSuccess, sResult) + + return bSuccess, sResult + + # eof def CopyTo(self, sDestination=None, bOverwrite=False): + +# -------------------------------------------------------------------------------------------------------------- + + diff --git a/additions/PythonExtensionsCollection/Folder/__init__.py b/additions/PythonExtensionsCollection/Folder/__init__.py new file mode 100644 index 00000000..958420af --- /dev/null +++ b/additions/PythonExtensionsCollection/Folder/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/additions/PythonExtensionsCollection/String/CString.py b/additions/PythonExtensionsCollection/String/CString.py new file mode 100644 index 00000000..82aeb204 --- /dev/null +++ b/additions/PythonExtensionsCollection/String/CString.py @@ -0,0 +1,1165 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# CString.py +# +# XC-CT/ECA3-Queckenstedt +# +# 02.06.2022 +# +# ************************************************************************************************************** + +# -- import standard Python modules +import os, ntpath, re + +# ************************************************************************************************************** + +class CString(object): + """ +The class ``CString`` contains some string computation methods like e.g. normalizing a path. + """ + + # -------------------------------------------------------------------------------------------------------------- + #TM*** + + def NormalizePath(sPath=None, bWin=False, sReferencePathAbs=None, bConsiderBlanks=False, bExpandEnvVars=True, bMask=True): + """ +Normalizes local paths, paths to local network resources and internet addresses + +**Arguments:** + +* ``sPath`` + + / *Condition*: required / *Type*: str / + + The path to be normalized + +* ``bWin`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True`` then returned path contains masked backslashes as separator, otherwise slashes + +* ``sReferencePathAbs`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + In case of ``sPath`` is relative and ``sReferencePathAbs`` (expected to be absolute) is given, then + the returned absolute path is a join of both input paths + +* ``bConsiderBlanks`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True`` then the returned path is encapsulated in quotes - in case of the path contains blanks + +* ``bExpandEnvVars`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + If ``True`` then in the returned path environment variables are resolved, otherwise not. + +* ``bMask`` + + / *Condition*: optional / *Type*: bool / *Default*: True (requires ``bWin=True``)/ + + * If ``bWin`` is ``True`` and ``bMask`` is ``True`` then the returned path contains masked backslashes as separator. + * If ``bWin`` is ``True`` and ``bMask`` is ``False`` then the returned path contains single backslashes only - this might be + required for applications, that are not able to handle masked backslashes. + * In case of ``bWin`` is ``False`` ``bMask`` has no effect. + +**Returns:** + +* ``sPath`` + + / *Type*: str / + + The normalized path (is ``None`` in case of ``sPath`` is ``None``) + """ + + if sPath is not None: + + # -- expand Windows environment variables + if bExpandEnvVars is True: + sPath = os.path.expandvars(sPath) + + # - remove leading and trailing horizontal space + sPath = sPath.strip(" \t\r\n") + + # - remove leading and trailing quotes + sPath = sPath.strip("\"'") + + # - remove once more leading and trailing horizontal space + # (after the removal of leading and trailing quotes further horizontal space might be there, that has to be removed; + # but further levels of nesting are not considered) + sPath = sPath.strip(" \t") + + if sPath == "": + return sPath + + # - remove trailing slash or backslash (maybe at end of path to folder) + sPath = sPath.rstrip("/\\") + + # -------------------------------------------------------------------------------------------------------------- + # consider internet addresses and local network resources + # -------------------------------------------------------------------------------------------------------------- + # -- local network resource / file server + # (prepare for Windows explorer) + # either (default) + # //server.com/abc/xyz + # or (with bWin=True); bMask must be False because \\server.com\\abc\\xyz is not allowed + # \\server.com\abc\xyz + # (=> user is allowed to select bWin but not bMask) + # + # -- local network resource / file server + # (prepare for web browser) + # after 'file://///' only single slashes allowed; bWin and bMask must be False + # file://///server.com/abc/xyz + # (=> user is NOT allowed to select bWin and bMask) + # + # -- internet address + # after server name only single slashes allowed; bWin and bMask must be False + # http://server.com/abc/xyz + # https://server.com/abc/xyz + # (=> user is NOT allowed to select bWin and bMask) + # + # - not allowed (=> this method must not return this format): + # http:\\server.com + # https:\\server.com + # -------------------------------------------------------------------------------------------------------------- + + sPathPrefix = None + + # In case there is any prefix, we remove this prefix, we compute the remaining part of the path separately, + # we also modify this prefix manually, and at the end we put the new prefix back to the path. + + if ( (sPath[:2] == "\\\\") or (sPath[:2] == "//") ): + sPath = sPath[2:] + if bWin is True: + sPathPrefix = "\\\\" + else: + sPathPrefix = "//" + bMask = False # !!! this overrules the input parameter value, because masked backslashes are not allowed in remaining path !!! + elif sPath[:10] == "file://///": # exactly this must be given; all other combinations of slashes and backslashes are not handled + sPath = sPath[10:] + sPathPrefix = "file://///" + bWin = False # !!! this overrules the input parameter value, because only single slashes allowed in remaining path !!! + bMask = False # !!! this overrules the input parameter value, because only single slashes allowed in remaining path !!! + elif ( (sPath[:7] == "http://") or (sPath[:7] == "http:\\\\") ): + sPath = sPath[7:] + sPathPrefix = "http://" + bWin = False # !!! this overrules the input parameter value, because only single slashes allowed in remaining path !!! + bMask = False # !!! this overrules the input parameter value, because only single slashes allowed in remaining path !!! + elif ( (sPath[:8] == "https://") or (sPath[:8] == "https:\\\\") ): + sPath = sPath[8:] + sPathPrefix = "https://" + bWin = False # !!! this overrules the input parameter value, because only single slashes allowed in remaining path !!! + bMask = False # !!! this overrules the input parameter value, because only single slashes allowed in remaining path !!! + else: + # Internet addresses and local network resources handled, now checking for relative paths: + # In case of sPath is a relative path AND an absolute reference path is provided + # merge them to an absolute path; without reference path use standard function to + # convert relative path to absolute path + if ( (sPath[0] != "%") and (sPath[0] != "$") ): + # If sPath starts with '%' or with '$' it is assumed that the path starts with an environment variable (Windows or Linux). + # But in this case 'os.path.isabs(sPath)' will not detect this to be an absolute path and will call + # 'sPath = os.path.abspath(sPath)' (depending on sReferencePathAbs). This will accidently merge + # the root path together with the path starting with the environment variable and cause invalid results. + if os.path.isabs(sPath) is False: + if sReferencePathAbs is not None: + sPath = os.path.join(sReferencePathAbs, sPath) + else: + sPath = os.path.abspath(sPath) + + # eof computation of sPathPrefix + + # - normalize the path (collapse redundant separators and up-level references) + # on Windows this converts slashes to backward slashes + # sPath = os.path.normpath(sPath) # under Linux this unfortunately keeps redundant separators (in opposite to Windows) + # -- alternative + sPath = ntpath.normpath(sPath) + + # - exchange single backslashes by single slashes (= partly we have to repair the outcome of normpath) + if bWin is False: + sPath = sPath.replace("\\", "/") + else: + if bMask is True: + sPath = sPath.replace("\\", "\\\\") + + # - restore the path prefix + if sPathPrefix is not None: + sPath = f"{sPathPrefix}{sPath}" + + # - consider blanks (prepare path for usage in Windows command line) + if bConsiderBlanks is True: + if sPath.find(" ") >= 0: + sPath = f"\"{sPath}\"" + + # eof if sPath is not None: + + return sPath + + # eof NormalizePath(sPath=None, bWin=False, sReferencePathAbs=None, bConsiderBlanks=False, bExpandEnvVars=True, bMask=True) + + # -------------------------------------------------------------------------------------------------------------- + #TM*** + + def DetectParentPath(sStartPath=None, sFolderName=None, sFileName=None): + """ +Computes the path to any parent folder inside a given path. Optionally DetectParentPath is able +to search for files inside the parent folder. + +**Arguments:** + +* ``sStartPath`` + + / *Condition*: required / *Type*: str / + + The path in which to search for a parent folder + +* ``sFolderName`` + + / *Condition*: required / *Type*: str / + + The name of the folder to search for within ``sStartPath``. It is possible to provide more than one folder name separated by semicolon + +* ``sFileName`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + The name of a file to search within the detected parent folder + +**Returns:** + +* ``sDestPath`` + + / *Type*: str / + + Path and name of parent folder found inside ``sStartPath``, ``None`` in case of ``sFolderName`` is not found inside ``sStartPath``. + In case of more than one parent folder is found ``sDestPath`` contains the first result and ``listDestPaths`` contains all results. + +* ``listDestPaths`` + + / *Type*: list / + + If ``sFolderName`` contains a single folder name this list contains only one element that is ``sDestPath``. + In case of ``FolderName`` contains a semicolon separated list of several folder names this list contains all found paths of the given folder names. + ``listDestPaths`` is ``None`` (and not an empty list!) in case of ``sFolderName`` is not found inside ``sStartPath``. + +* ``sDestFile`` + + / *Type*: str / + + Path and name of ``sFileName``, in case of ``sFileName`` is given and found inside ``listDestPaths``. + In case of more than one file is found ``sDestFile`` contains the first result and ``listDestFiles`` contains all results. + ``sDestFile`` is ``None`` in case of ``sFileName`` is ``None`` and also in case of ``sFileName`` is not found inside ``listDestPaths`` + (and therefore also in case of ``sFolderName`` is not found inside ``sStartPath``). + +* ``listDestFiles`` + + / *Type*: list / + + Contains all positions of ``sFileName`` found inside ``listDestPaths``. + + ``listDestFiles`` is ``None`` (and not an empty list!) in case of ``sFileName`` is ``None`` and also in case of ``sFileName`` + is not found inside ``listDestPaths`` (and therefore also in case of ``sFolderName`` is not found inside ``sStartPath``). + +* ``sDestPathParent`` + + / *Type*: str / + + The parent folder of ``sDestPath``, ``None`` in case of ``sFolderName`` is not found inside ``sStartPath`` (``sDestPath`` is ``None``). + """ + + sDestPath = None + listDestPaths = None + sDestFile = None + listDestFiles = None + sDestPathParent = None + + if sStartPath is None: + return sDestPath, listDestPaths, sDestFile, listDestFiles, sDestPathParent + + if sFolderName is None: + return sDestPath, listDestPaths, sDestFile, listDestFiles, sDestPathParent + + sStartPath = sStartPath.strip() + if sStartPath == "": + return sDestPath, listDestPaths, sDestFile, listDestFiles, sDestPathParent + + sFolderName = sFolderName.strip() + if sFolderName == "": + return sDestPath, listDestPaths, sDestFile, listDestFiles, sDestPathParent + + listSplit = sFolderName.split(';') + + listTopLevelFolders = [] + for sFolder in listSplit: + # removing duplicates + sFolder = sFolder.strip() + if sFolder != "": + if sFolder not in listTopLevelFolders: + listTopLevelFolders.append(sFolder) + # eof for sFolder in listSplit: + + nNrOfFolders = len(listTopLevelFolders) + sStartPath = CString.NormalizePath(sStartPath) + listLevels = sStartPath.split("/") + + listDestPaths = [] + + while len(listLevels) > 0: + # -- merging paths with folder names and search for existing combinations + sPathParent = "/".join(listLevels) + for sTLFolder in listTopLevelFolders: + sSubPath = sPathParent + "/" + sTLFolder + if os.path.isdir(sSubPath) is True: + listDestPaths.append(sSubPath) + if len(listTopLevelFolders) == len(listDestPaths): + # all folders found + break + else: + listLevels.pop() + # eof while len(listLevels) > 0: + + sDestPath = None + sDestPathParent = None + if len(listDestPaths) > 0: + # -- returning sDestPath and sDestPathParent related to first entry in list; just to return anything else than None + sDestPath = listDestPaths[0] + sDestPathParent = CString.NormalizePath(os.path.dirname(sDestPath)) + + # -- optionally searching also for a single file + # Input: file name + # Output: full path of file and list of full paths of files (!!! limited to 'listDestPaths' !!!) + + listDestFiles = [] + + if ( (sFileName is not None) and (len(listDestPaths) > 0) ): + for sDestPathToWalk in listDestPaths: + for sLocalRootPath, listFolderNames, listFileNames in os.walk(sDestPathToWalk): + for sFileNameTmp in listFileNames: + if sFileNameTmp == sFileName: + sFile = CString.NormalizePath(os.path.join(sLocalRootPath, sFileName)) + listDestFiles.append(sFile) + # eof for sLocalRootPath, listFolderNames, listFileNames in os.walk(sDestPathToWalk): + # eof for sDestPathToWalk in listDestPaths: + # eof if ( (sFileName is not None) and (len(listDestPaths) > 0) ): + + if len(listDestFiles) > 0: + listDestFiles.sort() + sDestFile = listDestFiles[0] # just to return anything else than None + + # -- preparing output (setting empty lists to None, to have unique criteria for results not available) + if listDestPaths is not None: + if len(listDestPaths) == 0: + listDestPaths = None + if listDestFiles is not None: + if len(listDestFiles) == 0: + listDestFiles = None + + return sDestPath, listDestPaths, sDestFile, listDestFiles, sDestPathParent + + # eof def DetectParentPath(sStartPath=None, sFolderName=None, sFileName=None): + + # -------------------------------------------------------------------------------------------------------------- + #TM*** + + def StringFilter(sString = None, + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = None, + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None, + bDebug = False): + """ +During the computation of strings there might occur the need to get to know if this string fulfils certain criteria or not. +Such a criterion can e.g. be that the string contains a certain substring. Also an inverse logic might be required: +In this case the criterion is that the string does **not** contain this substring. + +It might also be required to combine several criteria to a final conclusion if in total the criterion for a string is fulfilled or not. +For example: The string must start with the string *prefix* and must also contain either the string *substring1* or the string *substring2* +but must also **not** end with the string *suffix*. + +This method provides a bunch of predefined filters that can be used singly or combined to come to a final conclusion if the string fulfils all criteria or not. + +The filters are divided into three different types: + +1. Filters that are interpreted as raw strings (called 'standard filters'; no wild cards supported) +2. Filters that are interpreted as regular expressions (called 'regular expression based filters'; the syntax of regular expressions has to be considered) +3. Boolean switches (e.g. indicating if also an empty string is accepted or not) + +The input string might contain leading and trailing blanks and tabs. This kind of horizontal space is removed from the input string +before the standard filters start their work (except the regular expression based filters). + +The regular expression based filters consider the original input string (including the leading and trailing space). + +The outcome is that in case of the leading and trailing space shall be part of the criterion, the regular expression based filters can be used only. + +It is possible to decide if the standard filters shall work case sensitive or not. This decision has no effect on the regular expression based filters. + +The regular expression based filters always work with the original input string that is not modified in any way. + +Except the regular expression based filters it is possible to provide more than one string for every standard filter (must be a semikolon separated list in this case). +A semicolon that shall be part of the search string, has to be masked in this way: ``\;``. + +This method returns a boolean value that is ``True`` in case of all criteria are fulfilled, and ``False`` in case of some or all of them are not fulfilled. + +The default value for all filters is ``None`` (except ``bSkipBlankStrings``). In case of a filter value is ``None`` this filter has no influence on the result. + +In case of all filters are ``None`` (default) the return value is ``True`` (except the string itself is ``None`` +or the string is empty and ``bSkipBlankStrings`` is ``True``). + +In case of the string is ``None``, the return value is ``False``, because nothing concrete can be done with ``None`` strings. + +Internally every filter has his own individual acknowledge that indicates if the criterion of this filter is fulfilled or not. + +The meaning of *criterion fulfilled* of a filter is that the filter supports the final return value ``bAck`` of this method with ``True``. + +The final return value ``bAck`` of this method is a logical join (``AND``) of all individual acknowledges (except ``bSkipBlankStrings`` and ``sComment``; +in case of their criteria are **not** fulfilled, immediately ``False`` is returned). + +Summarized: + +* Filters are used to define *criteria* +* The return value of this method provides the *conclusion* - indicating if all criteria are fulfilled or not + +The following filters are available: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**bSkipBlankStrings** + + * Like already mentioned above leading and trailing spaces are removed from the input string at the beginning + * In case of the result is an empty string and ``bSkipBlankStrings`` is ``True``, the method immediately returns ``False`` + and all other filters are ignored + +**sComment** + + * In case of the input string starts with the string ``sComment``, the method immediately returns ``False`` + and all other filters are ignored + * Leading blanks within the input string have no effect + * The decision also depends on ``bCaseSensitive`` + * The idea behind this decision is: Ignore a string that is commented out + +**sStartsWith** + + * The criterion of this filter is fulfilled in case of the input string starts with the string ``sStartsWith`` + * Leading blanks within the input string have no effect + * The decision also depends on ``bCaseSensitive`` + * More than one string can be provided (semicolon separated; logical join: ``OR``) + +**sEndsWith** + + * The criterion of this filter is fulfilled in case of the input string ends with the string ``sEndsWith`` + * Trailing blanks within the input string have no effect + * The decision also depends on ``bCaseSensitive`` + * More than one string can be provided (semicolon separated; logical join: ``OR``) + +**sStartsNotWith** + + * The criterion of this filter is fulfilled in case of the input string does **not** start with the string ``sStartsNotWith`` + * Leading blanks within the input string have no effect + * The decision also depends on ``bCaseSensitive`` + * More than one string can be provided (semicolon separated; logical join: ``AND``) + +**sEndsNotWith** + + * The criterion of this filter is fulfilled in case of the input string does **not** end with the string ``sEndsNotWith`` + * Trailing blanks within the input string have no effect + * The decision also depends on ``bCaseSensitive`` + * More than one string can be provided (semicolon separated; logical join: ``AND``) + +**sContains** + + * The criterion of this filter is fulfilled in case of the input string contains the string ``sContains`` at any position + * Leading and trailing blanks within the input string have no effect + * The decision also depends on ``bCaseSensitive`` + * More than one string can be provided (semicolon separated; logical join: ``OR``) + +**sContainsNot** + + * The criterion of this filter is fulfilled in case of the input string does **not** contain the string ``sContainsNot`` at any position + * Leading and trailing blanks within the input string have no effect + * The decision also depends on ``bCaseSensitive`` + * More than one string can be provided (semicolon separated; logical join: ``AND``) + +**sInclRegEx** + + * *Include* filter based on regular expressions (consider the syntax of regular expressions!) + * The criterion of this filter is fulfilled in case of the regular expression ``sInclRegEx`` matches the input string + * Leading and trailing blanks within the input string are considered + * ``bCaseSensitive`` has no effect + * A semicolon separated list of several regular expressions is **not** supported + +**sExclRegEx** + + * *Exclude* filter based on regular expressions (consider the syntax of regular expressions!) + * The criterion of this filter is fulfilled in case of the regular expression ``sExclRegEx`` does **not** match the input string + * Leading and trailing blanks within the input string are considered + * ``bCaseSensitive`` has no effect + * A semicolon separated list of several regular expressions is **not** supported + +**Further arguments:** + +* ``sString`` + + / *Condition*: required / *Type*: str / + + The input string that has to be investigated. + +* ``bCaseSensitive`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + If ``True``, the standard filters work case sensitive, otherwise not. + +* ``bDebug`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True``, additional output is printed to console (e.g. the decision of every single filter), otherwise not. + +**Returns:** + +* ``bAck`` + + / *Type*: bool / + + Final statement about the input string ``sString`` after filter computation + +Examples: +~~~~~~~~~ + +1. Returns ``True``: + +.. code:: python + + StringFilter(sString = "Speed is 25 beats per minute", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = "Sp", + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = "beats", + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None) + +2. Returns ``False``: + +.. code:: python + + StringFilter(sString = "Speed is 25 beats per minute", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = "Sp", + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = "minute", + sContains = "beats", + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None) + +3. Returns ``True``: + +.. code:: python + + StringFilter(sString = "Speed is 25 beats per minute", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = None, + sContainsNot = "Beats", + sInclRegEx = None, + sExclRegEx = None) + +4. Returns ``True``: + +.. code:: python + + StringFilter(sString = "Speed is 25 beats per minute", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = None, + sContainsNot = None, + sInclRegEx = r"\d{2}", + sExclRegEx = None) + +5. Returns ``False``: + +.. code:: python + + StringFilter(sString = "Speed is 25 beats per minute", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = "Speed", + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = None, + sContainsNot = None, + sInclRegEx = r"\d{3}", + sExclRegEx = None) + +6. Returns ``False``: + +.. code:: python + + StringFilter(sString = "Speed is 25 beats per minute", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = "Speed", + sEndsWith = "minute", + sStartsNotWith = "speed", + sEndsNotWith = None, + sContains = "beats", + sContainsNot = None, + sInclRegEx = r"\d{2}", + sExclRegEx = r"\d{2}") + +7. Returns ``False``: + +.. code:: python + + StringFilter(sString = " ", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = None, + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None) + +8. Returns ``False``: + +.. code:: python + + StringFilter(sString = "# Speed is 25 beats per minute", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = "#", + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = "beats", + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None) + + +9. Returns ``False``: + +.. code:: python + + StringFilter(sString = " Alpha is not beta; and beta is not gamma ", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = " Alpha ", + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None) + +Because blanks around search strings (here ``" Alpha "``) are considered, whereas the blanks around the input string are removed before computation. +Therefore ``" Alpha "`` cannot be found within the (shortened) input string. + + +10. This alternative solution returns ``True``: + +.. code:: python + + StringFilter(sString = " Alpha is not beta; and beta is not gamma ", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = None, + sContainsNot = None, + sInclRegEx = r"\s{3}Alpha", + sExclRegEx = None) + + +11. Returns ``True``: + +.. code:: python + + StringFilter(sString = "Alpha is not beta; and beta is not gamma", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = "beta; and", + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None) + +The meaning of ``"beta; and"`` is: The criterion is fulfilled in case of either ``"beta"`` or ``" and"`` can be found. That's ``True`` in this example - but this +has nothing to do with the fact, that also this string ``"beta; and"`` can be found. Here the semikolon is a separator character and therefore part of the syntax. + +A semicolon that shall be part of the search string, has to be masked with '``\;``'! + +The meaning of ``"beta\; not"`` in the following example is: The criterion is fulfilled in case of ``"beta; not"`` can be found. + +That's **not** ``True``. Therefore the method returns ``False``: + +.. code:: python + + StringFilter(sString = "Alpha is not beta; and beta is not gamma", + bCaseSensitive = True, + bSkipBlankStrings = True, + sComment = None, + sStartsWith = None, + sEndsWith = None, + sStartsNotWith = None, + sEndsNotWith = None, + sContains = r"beta\; not", + sContainsNot = None, + sInclRegEx = None, + sExclRegEx = None) + """ + + if sString is None: + return False # hard coded here; no separate filter for that decision + + # The original string 'sString' is used by regular expression filters sInclRegEx and sExclRegEx. + # The stripped string 'sStringStripped' is used by all other filters. + sStringStripped = sString.strip(" \t\r\n") + + # -- skipping blank strings or strings commented out; other filters will not be considered any more in this case + + if bSkipBlankStrings is True: + if sStringStripped == "": + return False + + if sComment is not None: + if sComment != "": + if bCaseSensitive is True: + if sStringStripped.startswith(sComment) is True: + return False + else: + if sStringStripped.upper().startswith(sComment.upper()) is True: + return False + + # -- consider further filters + # + # No filter set (= no criteria defined) => use this string (bAck is True). + # + # At least one filter set (except sExclRegEx), at least one set filter fits (except sExclRegEx) => use this string. + # Filter sExclRegEx is set and fits => skip this string (final veto). + # At least one filter does not fit (except sExclRegEx) => skip this string. + # + # All filters (except sExclRegEx) are include filter (bAck is True in case of all set filters fit, also the 'not' filters) + # The filter sExclRegEx is an exclude filter and has final veto right (can revoke the True from other filters). + # + # All filters (except sInclRegEx and sExclRegEx) are handled as 'raw strings': no wild cards, just strings, considering bCaseSensitive. + # The filters sInclRegEx and sExclRegEx are handled as regular expressions; bCaseSensitive is not considered here. + + # -- filter specific flags (containing the names of the criteria within their names) + bStartsWith = None + bEndsWith = None + bStartsNotWith = None + bEndsNotWith = None + bContains = None + bContainsNot = None + bInclRegEx = None + bExclRegEx = None + + # Meaning: + # - Flag is None : filter not set => filter has no effect + # - Flag is True : filter set => result: use the input string (from this single filter flag point of view) + # - Flag is False: filter set => result: do not use the input string (from this single filter flag point of view) + # The results of all flags will be merged at the end of this function to one final conclusion to use the input string + # (bAck is True) or not (bAck is False). + # Logical join between all set filters: AND + + # substitute for the masked filter separator '\n' (hopefully the input string does not contain this substitute) + sSeparatorSubstitute = "#|S#|E#|P#|A#|R#|A#|T#|O#|R#" + + # -- filter: starts with + # > several filter strings possible (separated by semicolon; logical join: OR) + if sStartsWith is not None: + if sStartsWith != "": + sStartsWithModified = sStartsWith.replace(r"\;", sSeparatorSubstitute) # replace the masked separator by a substitute separator + listStartsWith = [] + if sStartsWith.find(";") >= 0: + listParts = sStartsWithModified.split(";") + for sPart in listParts: + sPart = sPart.replace(sSeparatorSubstitute , ";") # recover the original version + listStartsWith.append(sPart) + else: + sStartsWithModified = sStartsWith.replace(r"\;", ";") # convert to unmasked version + listStartsWith.append(sStartsWithModified) + + bStartsWith = False + for sStartsWith in listStartsWith: + if bCaseSensitive is True: + if sStringStripped.startswith(sStartsWith) is True: + bStartsWith = True + break + else: + if sStringStripped.upper().startswith(sStartsWith.upper()) is True: + bStartsWith = True + break + + # -- filter: ends with + # > several filter strings possible (separated by semicolon; logical join: OR) + if sEndsWith is not None: + if sEndsWith != "": + sEndsWithModified = sEndsWith.replace(r"\;", sSeparatorSubstitute) # replace the masked separator by a substitute separator + listEndsWith = [] + if sEndsWith.find(";") >= 0: + listParts = sEndsWithModified.split(";") + for sPart in listParts: + sPart = sPart.replace(sSeparatorSubstitute , ";") # recover the original version + listEndsWith.append(sPart) + else: + sEndsWithModified = sEndsWith.replace(r"\;", ";") # convert to unmasked version + listEndsWith.append(sEndsWithModified) + + bEndsWith = False + for sEndsWith in listEndsWith: + if bCaseSensitive is True: + if sStringStripped.endswith(sEndsWith) is True: + bEndsWith = True + break + else: + if sStringStripped.upper().endswith(sEndsWith.upper()) is True: + bEndsWith = True + break + + # -- filter: starts not with + # > several filter strings possible (separated by semicolon; logical join: AND) + if sStartsNotWith is not None: + if sStartsNotWith != "": + sStartsNotWithModified = sStartsNotWith.replace(r"\;", sSeparatorSubstitute) # replace the masked separator by a substitute separator + listStartsNotWith = [] + if sStartsNotWith.find(";") >= 0: + listParts = sStartsNotWithModified.split(";") + for sPart in listParts: + sPart = sPart.replace(sSeparatorSubstitute , ";") # recover the original version + listStartsNotWith.append(sPart) + else: + sStartsNotWithModified = sStartsNotWith.replace(r"\;", ";") # convert to unmasked version + listStartsNotWith.append(sStartsNotWithModified) + + bStartsNotWith = True + for sStartsNotWith in listStartsNotWith: + if bCaseSensitive is True: + if sStringStripped.startswith(sStartsNotWith) is True: + bStartsNotWith = False + break + else: + if sStringStripped.upper().startswith(sStartsNotWith.upper()) is True: + bStartsNotWith = False + break + + # -- filter: ends not with + # > several filter strings possible (separated by semicolon; logical join: AND) + if sEndsNotWith is not None: + if sEndsNotWith != "": + sEndsNotWithModified = sEndsNotWith.replace(r"\;", sSeparatorSubstitute) # replace the masked separator by a substitute separator + listEndsNotWith = [] + if sEndsNotWith.find(";") >= 0: + listParts = sEndsNotWithModified.split(";") + for sPart in listParts: + sPart = sPart.replace(sSeparatorSubstitute , ";") # recover the original version + listEndsNotWith.append(sPart) + else: + sEndsNotWithModified = sEndsNotWith.replace(r"\;", ";") # convert to unmasked version + listEndsNotWith.append(sEndsNotWithModified) + + bEndsNotWith = True + for sEndsNotWith in listEndsNotWith: + if bCaseSensitive is True: + if sStringStripped.endswith(sEndsNotWith) is True: + bEndsNotWith = False + break + else: + if sStringStripped.upper().endswith(sEndsNotWith.upper()) is True: + bEndsNotWith = False + break + + # -- filter: contains + # > several filter strings possible (separated by semicolon; logical join: OR) + if sContains is not None: + if sContains != "": + sContainsModified = sContains.replace(r"\;", sSeparatorSubstitute) # replace the masked separator by a substitute separator + listContains = [] + if sContainsModified.find(";") >= 0: + listParts = sContainsModified.split(";") + for sPart in listParts: + sPart = sPart.replace(sSeparatorSubstitute , ";") # recover the original version + print(f"- Part: '{sPart}'") + listContains.append(sPart) + else: + sContainsModified = sContains.replace(r"\;", ";") # convert to unmasked version + listContains.append(sContainsModified) + + bContains = False + for sContains in listContains: + if bCaseSensitive is True: + if sStringStripped.find(sContains) >= 0: + bContains = True + break + else: + if sStringStripped.upper().find(sContains.upper()) >= 0: + bContains = True + break + + # -- filter: contains not + # > several filter strings possible (separated by semicolon; logical join: AND) + if sContainsNot is not None: + if sContainsNot != "": + sContainsNotModified = sContainsNot.replace(r"\;", sSeparatorSubstitute) # replace the masked separator by a substitute separator + listContainsNot = [] + if sContainsNot.find(";") >= 0: + listParts = sContainsNotModified.split(";") + for sPart in listParts: + sPart = sPart.replace(sSeparatorSubstitute , ";") # recover the original version + listContainsNot.append(sPart) + else: + sContainsNotModified = sContainsNot.replace(r"\;", ";") # convert to unmasked version + listContainsNot.append(sContainsNotModified) + + bContainsNot = True + for sContainsNot in listContainsNot: + if bCaseSensitive is True: + if sStringStripped.find(sContainsNot) >= 0: + bContainsNot = False + break + else: + if sStringStripped.upper().find(sContainsNot.upper()) >= 0: + bContainsNot = False + break + + # -- filter: sInclRegEx + # > (take care to mask special characters that are part of the syntax of regular expressions!) + # > bCaseSensitive not considered here + if sInclRegEx is not None: + if sInclRegEx != "": + bInclRegEx = False + if re.search(sInclRegEx, sString) is not None: + bInclRegEx = True + + # -- last filter: sExclRegEx (final veto right) + # > (take care to mask special characters that are part of the syntax of regular expressions!) + # > bCaseSensitive not considered here + if sExclRegEx is not None: + if sExclRegEx != "": + bExclRegEx = True + if re.search(sExclRegEx, sString) is not None: + bExclRegEx = False + + # -- debug info + if bDebug is True: + print("\n* [sString] : '" + str(sString) + "'\n") + print(" -> [bStartsWith] : '" + str(bStartsWith) + "'") + print(" -> [bEndsWith] : '" + str(bEndsWith) + "'") + print(" -> [bStartsNotWith] : '" + str(bStartsNotWith) + "'") + print(" -> [bEndsNotWith] : '" + str(bEndsNotWith) + "'") + print(" -> [bContains] : '" + str(bContains) + "'") + print(" -> [bContainsNot] : '" + str(bContainsNot) + "'") + print(" -> [bInclRegEx] : '" + str(bInclRegEx) + "'") + print(" -> [bExclRegEx] : '" + str(bExclRegEx) + "'\n") + + # -- final conclusion (AND condition between filters) + + listDecisions = [] + listDecisions.append(bStartsWith) + listDecisions.append(bEndsWith) + listDecisions.append(bStartsNotWith) + listDecisions.append(bEndsNotWith) + listDecisions.append(bContains) + listDecisions.append(bContainsNot) + listDecisions.append(bInclRegEx) + listDecisions.append(bExclRegEx) + + bAck = False # initial + + # -- 1.) no filter set (all None) + nCntDecisions = 0 + for bDecision in listDecisions: + if bDecision is None: + nCntDecisions = nCntDecisions + 1 + if nCntDecisions == len(listDecisions): + bAck = True + if bDebug is True: + print(" > case [1] - bAck: " + str(bAck)) + + # -- 2.) final veto from exclude filter + if bExclRegEx is False: + bAck = False + if bDebug is True: + print(" > case [2] - bAck: " + str(bAck)) + + # -- 3.) exclude filter not set; decision only made by other filters (include) + if bExclRegEx is None: + bAck = True + for bDecision in listDecisions: + if bDecision is False: + bAck = False + break + if bDebug is True: + print(" > case [3] - bAck: " + str(bAck)) + + # -- 4.) exclude filter is True (only relevant in case of all other filters are not set; otherwise decision only made by other filters (include)) + if bExclRegEx is True: + if ( (bStartsWith is None) and + (bEndsWith is None) and + (bStartsNotWith is None) and + (bEndsNotWith is None) and + (bContains is None) and + (bContainsNot is None) and + (bInclRegEx is None) ): + bAck = True + if bDebug is True: + print(" > case [4.1] - bAck: " + str(bAck)) + else: + bAck = True + for bDecision in listDecisions: + if bDecision is False: + bAck = False + break + if bDebug is True: + print(" > case [4.2] - bAck: " + str(bAck)) + + if bDebug is True: + print() + + return bAck + + # eof def StringFilter(...) + + # -------------------------------------------------------------------------------------------------------------- + #TM*** + + def FormatResult(sMethod="", bSuccess=True, sResult=""): + """ +Formats the result string ``sResult`` depending on ``bSuccess``: + +* ``bSuccess`` is ``True`` indicates *success* +* ``bSuccess`` is ``False`` indicates an *error* +* ``bSuccess`` is ``None`` indicates an *exception* + +Additionally the name of the method that causes the result, can be provided (*optional*). +This is useful for debugging. + +**Arguments:** + +* ``sMethod`` + + / *Condition*: optional / *Type*: str / *Default*: (empty string) / + + Name of the method that causes the result. + +* ``bSuccess`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + Indicates if the computation of the method ``sMethod`` was successful or not. + +* ``sResult`` + + / *Condition*: optional / *Type*: str / *Default*: (empty string) / + + The result of the computation of the method ``sMethod``. + +**Returns:** + +* ``sResult`` + + / *Type*: str / + + The formatted result string. + """ + + if sMethod is None: + sMethod = str(sMethod) + if sResult is None: + sResult = str(sResult) + if bSuccess is True: + if sMethod != "": + sResult = f"[{sMethod}] : {sResult}" + elif bSuccess is False: + sError = "!!! ERROR !!!" + if sMethod != "": + sResult = f"{sError}\n[{sMethod}] : {sResult}" + else: + sResult = f"{sError}\n{sResult}" + else: + sException = "!!! EXCEPTION !!!" + if sMethod != "": + sResult = f"{sException}\n[{sMethod}] : {sResult}" + else: + sResult = f"{sException}\n{sResult}" + return sResult + + # eof def FormatResult(sMethod="", bSuccess=True, sResult=""): + + # -------------------------------------------------------------------------------------------------------------- + #TM*** + + # - make the methods static + + NormalizePath = staticmethod(NormalizePath) + DetectParentPath = staticmethod(DetectParentPath) + StringFilter = staticmethod(StringFilter) + FormatResult = staticmethod(FormatResult) + +# eof class CString(object): + +# ************************************************************************************************************** + + + diff --git a/additions/PythonExtensionsCollection/String/__init__.py b/additions/PythonExtensionsCollection/String/__init__.py new file mode 100644 index 00000000..958420af --- /dev/null +++ b/additions/PythonExtensionsCollection/String/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/additions/PythonExtensionsCollection/Utils/CUtils.py b/additions/PythonExtensionsCollection/Utils/CUtils.py new file mode 100644 index 00000000..20447501 --- /dev/null +++ b/additions/PythonExtensionsCollection/Utils/CUtils.py @@ -0,0 +1,374 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# CUtils.py +# +# XC-CT/ECA3-Queckenstedt +# +# 20.05.2022 +# +# ************************************************************************************************************** + +# -- import standard Python modules +from dotdict import dotdict + +# ************************************************************************************************************** +# wrapper +# ************************************************************************************************************** + +def PrettyPrint(oData=None, hOutputFile=None, bToConsole=True, nIndent=0, sPrefix=None, bHexFormat=False): + """ +Wrapper function to create and use a ``CTypePrint`` object. This wrapper function is responsible for +printing out the content to console and to a file (depending on input parameter). + +The content itself is prepared by the method ``TypePrint`` of class ``CTypePrint``. This happens ``PrettyPrint`` internally. + +The idea behind the ``PrettyPrint`` function is to resolve also the content of composite data types and provide for every parameter inside: + +* the type +* the total number of elements inside (e.g. the number of keys inside a dictionary) +* the counter number of the current element +* the value + +Example call: + +.. code:: python + + PrettyPrint(oData) + +(*with oData is a Python variable of any type*) + +The output can e.g. look like this: + +.. code:: python + + [DICT] (3/1) > {K1} [STR] : 'Val1' + [DICT] (3/2) > {K2} [LIST] (4/1) > [INT] : 1 + [DICT] (3/2) > {K2} [LIST] (4/2) > [STR] : 'A' + [DICT] (3/2) > {K2} [LIST] (4/3) > [INT] : 2 + [DICT] (3/2) > {K2} [LIST] (4/4) > [TUPLE] (2/1) > [INT] : 9 + [DICT] (3/2) > {K2} [LIST] (4/4) > [TUPLE] (2/2) > [STR] : 'Z' + [DICT] (3/3) > {K3} [INT] : 5 + +Every line of output has to be interpreted strictly from left to right. + +For example the meaning of the fifth line of output + +.. code:: python + + [DICT] (3/2) > {K2} [LIST] (4/4) > [TUPLE] (2/1) > [INT] : 9 + +is: + +* The type of input parameter (``oData``) is ``dict`` +* The dictionary contains 3 keys +* The current line gives information about the second key of the dictionary +* The name of the second key is 'K2' +* The value of the second key is of type ``list`` +* The list contains 4 elements +* The current line gives information about the fourth element of the list +* The fourth element of the list is of type ``tuple`` +* The tuple contains 2 elements +* The current line gives information about the first element of the tuple +* The first element of the tuple is of type ``int`` and has the value 9 + +Types are encapsulated in square brackets, counter in round brackets and key names are encapsulated in curly brackets. + +**Arguments:** + +* ``oData`` + + / *Condition*: required / *Type*: (*any Python data type*) / + + A variable of any Python data type. + +* ``hOutputFile`` + + / *Condition*: optional / *Type*: file handle / *Default*: None / + + If handle is not ``None`` the content is written to this file, otherwise not. + +* ``bToConsole`` + + / *Condition*: optional / *Type*: bool / *Default*: True / + + If ``True`` the content is written to console, otherwise not. + +* ``nIndent`` + + / *Condition*: optional / *Type*: int / *Default*: 0 / + + Sets the number of additional blanks at the beginning of every line of output (indentation). + +* ``sPrefix`` + + / *Condition*: optional / *Type*: str / *Default*: None / + + Sets a prefix string that is added at the beginning of every line of output. + +* ``bHexFormat`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True`` the output is printed in hexadecimal format (but valid for strings only). + +**Returns:** + +* ``listOutLines`` (*list*) + + / *Type*: list / + + List of lines containing the prepared output + """ + + oTypePrint = CTypePrint() + listOutLines = oTypePrint.TypePrint(oData, bHexFormat) + + listReturned = [] + for sLine in listOutLines: + # if requested add indentation and prefix + sLineOut = "" + if sPrefix is not None: + sLineOut = nIndent*" " + sPrefix + " " + sLine + else: + sLineOut = nIndent*" " + sLine + listReturned.append(sLineOut) + + if hOutputFile is not None: + hOutputFile.write(sLineOut + "\n") + if bToConsole is True: + print(sLineOut) + + return listReturned + +# eof def PrettyPrint(oData=None, hOutputFile=None, bToConsole=True, nIndent=0, sPrefix=None, bHexFormat=False): + +# -------------------------------------------------------------------------------------------------------------- +# TM*** + +class CTypePrint(object): + """ +The class ``CTypePrint`` provides a method (``TypePrint``) to compute the following data: + +* the type +* the total number of elements inside (e.g. the number of keys inside a dictionary) +* the counter number of the current element +* the value + +of simple and composite data types. + +The call of this method is encapsulated within the function ``PrettyPrint`` inside this module. + """ + def __init__(self): + self.listGlobalPrefixes = [] + self.listOutLines = [] + + def __del__(self): + pass + + def _ToHex(self, sString=None): + if ( (sString is None) or (sString == "") ): + return sString + listHex = [] + for sChar in sString: + listHex.append(hex(ord(sChar))) + sStringHex = " ".join(listHex) + return sStringHex + + def TypePrint(self, oData=None, bHexFormat=False): + """ +The method ``TypePrint`` computes details about the input variable ``oData``. + +**Arguments:** + +* ``oData`` + + / *Condition*: required / *Type*: any Python data type / + + Python variable of any data type. + +* ``bHexFormat`` + + / *Condition*: optional / *Type*: bool / *Default*: False / + + If ``True`` the output is provide in hexadecimal format. + +**Returns:** + +* ``listOutLines`` + + / *Type*: list / + + List of lines containing the resolved content of ``oData``. + """ + + if oData is None: + sLocalPrefix = "[NONE]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : " + str(oData) + self.listOutLines.append(sOut.strip()) + + elif type(oData) == int: + sLocalPrefix = "[INT]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : " + str(oData) + self.listOutLines.append(sOut.strip()) + + elif type(oData) == float: + sLocalPrefix = "[FLOAT]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : " + str(oData) + self.listOutLines.append(sOut.strip()) + + elif type(oData) == bool: + sLocalPrefix = "[BOOL]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : " + str(oData) + self.listOutLines.append(sOut.strip()) + + elif type(oData) == str: + sLocalPrefix = "[STR]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sData = str(oData) + if bHexFormat is True: + sData = self._ToHex(sData) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : '" + sData + "'" + self.listOutLines.append(sOut.strip()) + + elif type(oData) == list: + nNrOfElements = len(oData) + if nNrOfElements == 0: + # -- indicate empty list + sLocalPrefix = "[LIST]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : []" + self.listOutLines.append(sOut.strip()) + else: + # -- list elements of list + self.listGlobalPrefixes.append("[LIST]") + nCnt = 0 + for oElement in oData: + nCnt = nCnt + 1 + sCnt = "(" + str(nNrOfElements) + "/" + str(nCnt) + ") >" + self.listGlobalPrefixes.append(sCnt) + self.TypePrint(oElement, bHexFormat) # >>>> recursion + del self.listGlobalPrefixes[-1] # remove prefix count + del self.listGlobalPrefixes[-1] # remove prefix name + + elif type(oData) == tuple: + nNrOfElements = len(oData) + if nNrOfElements == 0: + # -- indicate empty tuple + sLocalPrefix = "[TUPLE]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : ()" + self.listOutLines.append(sOut.strip()) + else: + # -- list elements of tuple + self.listGlobalPrefixes.append("[TUPLE]") + nCnt = 0 + for oElement in oData: + nCnt = nCnt + 1 + sCnt = "(" + str(nNrOfElements) + "/" + str(nCnt) + ") >" + self.listGlobalPrefixes.append(sCnt) + self.TypePrint(oElement, bHexFormat) # >>>> recursion + del self.listGlobalPrefixes[-1] # remove prefix count + del self.listGlobalPrefixes[-1] # remove prefix name + + elif type(oData) == set: + nNrOfElements = len(oData) + if nNrOfElements == 0: + # -- indicate empty set + sLocalPrefix = "[SET]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : ()" + self.listOutLines.append(sOut.strip()) + else: + # -- list elements of set + self.listGlobalPrefixes.append("[SET]") + nCnt = 0 + for oElement in oData: + nCnt = nCnt + 1 + sCnt = "(" + str(nNrOfElements) + "/" + str(nCnt) + ") >" + self.listGlobalPrefixes.append(sCnt) + self.TypePrint(oElement, bHexFormat) # >>>> recursion + del self.listGlobalPrefixes[-1] # remove prefix count + del self.listGlobalPrefixes[-1] # remove prefix name + + elif type(oData) == dict: + nNrOfElements = len(oData) + if nNrOfElements == 0: + # -- indicate empty dictionary + sLocalPrefix = "[DICT]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : {}" + self.listOutLines.append(sOut.strip()) + else: + # -- list elements of dictionary + self.listGlobalPrefixes.append("[DICT]") + nCnt = 0 + listKeys = list(oData.keys()) + for sKey in listKeys: + nCnt = nCnt + 1 + oValue = oData[sKey] + sCntAndKey = "(" + str(nNrOfElements) + "/" + str(nCnt) + ") > {" + str(sKey) + "}" + self.listGlobalPrefixes.append(sCntAndKey) + self.TypePrint(oValue, bHexFormat) # >>>> recursion + del self.listGlobalPrefixes[-1] # remove prefix count + del self.listGlobalPrefixes[-1] # remove prefix name + + # elif type(oData) == dotdict: + elif ( (type(oData) == dotdict) or (str(type(oData)) == "") ): + nNrOfElements = len(oData) + if nNrOfElements == 0: + # -- indicate empty dot dictionary + sLocalPrefix = "[DOTDICT]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : {}" + self.listOutLines.append(sOut.strip()) + else: + # -- list elements of dot dictionary + self.listGlobalPrefixes.append("[DOTDICT]") + nCnt = 0 + listKeys = list(oData.keys()) + for sKey in listKeys: + nCnt = nCnt + 1 + oValue = oData[sKey] + sCntAndKey = "(" + str(nNrOfElements) + "/" + str(nCnt) + ") > {" + str(sKey) + "}" + self.listGlobalPrefixes.append(sCntAndKey) + self.TypePrint(oValue, bHexFormat) # >>>> recursion + del self.listGlobalPrefixes[-1] # remove prefix count + del self.listGlobalPrefixes[-1] # remove prefix name + + else: + sLocalPrefix = "[" + str(type(oData)) + "]" + sGlobalPrefix = " ".join(self.listGlobalPrefixes) + sData = str(oData) + if bHexFormat is True: + sData = self._ToHex(sData) + sOut = sGlobalPrefix + " " + sLocalPrefix + " : '" + sData + "'" + self.listOutLines.append(sOut.strip()) + + return self.listOutLines + + # eof def TypePrint(...): + +# eof class CTypePrint(): + +# ************************************************************************************************************** + diff --git a/additions/PythonExtensionsCollection/Utils/__init__.py b/additions/PythonExtensionsCollection/Utils/__init__.py new file mode 100644 index 00000000..958420af --- /dev/null +++ b/additions/PythonExtensionsCollection/Utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/additions/PythonExtensionsCollection/__init__.py b/additions/PythonExtensionsCollection/__init__.py new file mode 100644 index 00000000..958420af --- /dev/null +++ b/additions/PythonExtensionsCollection/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/additions/PythonExtensionsCollection/version.py b/additions/PythonExtensionsCollection/version.py new file mode 100644 index 00000000..b5b224a4 --- /dev/null +++ b/additions/PythonExtensionsCollection/version.py @@ -0,0 +1,23 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# Version and date of PythonExtensionsCollection +# +VERSION = "0.8.0" +VERSION_DATE = "28.06.2022" + diff --git a/additions/__init__.py b/additions/__init__.py new file mode 100644 index 00000000..85a18b76 --- /dev/null +++ b/additions/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/config/CConfig.py b/config/CConfig.py deleted file mode 100644 index e040ea49..00000000 --- a/config/CConfig.py +++ /dev/null @@ -1,240 +0,0 @@ -# ************************************************************************************************************** -# -# Copyright 2020-2022 Robert Bosch GmbH -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ************************************************************************************************************** -# -# CConfig.py -# -# XC-CT/ECA3-Queckenstedt -# -# Purpose: -# - Compute and store all repository specific information, like the repository name, -# paths to repository subfolder, paths to interpreter and so on ... -# -# - All paths to subfolder depends on the repository root path that has to be provided to constructor of CConfig -# -# Additional hints: -# - Variable names like SPHINXBUILD, SOURCEDIR and BUILDDIR are taken over from original output of Sphinx -# (when documentation project files like make.bat are generated by Sphinx; for better understanding -# no new names here). -# -# - Output in PDF format requires LaTeX compiler and depends on %ROBOTLATEXPATH%/miktex/bin/x64/pdflatex.exe -# -# - Don't be confused: We have 'doc/_build' containing the documentation builder output -# and we have 'build' containing the build of the setup tools. These are different things. -# -# -------------------------------------------------------------------------------------------------------------- -# -# 18.02.2022 / XC-CT/ECA3-Queckenstedt -# Usage of %RobotPythonPath% exchanged by sys.executable -# -# 11.10.2021 / XC-CI1/ECA3-Queckenstedt -# Fixed path within site-packages (Linux) -# -# 06.10.2021 / XC-CI1/ECA3-Queckenstedt -# Added Linux support -# -# 01.10.2021 / XC-CI1/ECA3-Queckenstedt -# Added environment check -# -# 01.10.2021 / XC-CI1/ECA3-Queckenstedt -# Added wrapper for error messages -# -# Initial version 08/2021 -# -# -------------------------------------------------------------------------------------------------------------- - -import os, sys, platform, shlex, subprocess -import colorama as col -import pypandoc - -col.init(autoreset=True) -COLBR = col.Style.BRIGHT + col.Fore.RED -COLBG = col.Style.BRIGHT + col.Fore.GREEN - -# -------------------------------------------------------------------------------------------------------------- - -def printerror(sMsg): - sys.stderr.write(COLBR + f"Error: {sMsg}!\n") - -def printexception(sMsg): - sys.stderr.write(COLBR + f"Exception: {sMsg}!\n") - -# -------------------------------------------------------------------------------------------------------------- - -class CConfig(): - - def __init__(self, sReferencePath="."): - - self.__dictConfig = {} - - self.__sReferencePath = os.path.normpath(os.path.abspath(sReferencePath)) - self.__dictConfig['sReferencePath'] = self.__sReferencePath # only to have the possibility to print out all values only with help of 'self.__dictConfig' - - # 1. basic setup stuff - self.__dictConfig['sPackageName'] = "JsonPreprocessor" - self.__dictConfig['sVersion'] = "0.2.0" - self.__dictConfig['sAuthor'] = "Mai Dinh Nam Son" - self.__dictConfig['sAuthorEMail'] = "son.maidinhnam@vn.bosch.com" - self.__dictConfig['sDescription'] = "This package provides a preprocessor for json files" - self.__dictConfig['sLongDescriptionContentType'] = "text/markdown" - self.__dictConfig['sURL'] = "https://github.com/test-fullautomation/python-jsonpreprocessor" - self.__dictConfig['sProgrammingLanguage'] = "Programming Language :: Python :: 3" - self.__dictConfig['sLicence'] = "License :: OSI Approved :: Apache Software License" - self.__dictConfig['sOperatingSystem'] = "Operating System :: OS Independent" - self.__dictConfig['sPythonRequires'] = ">=3.0" - self.__dictConfig['sDevelopmentStatus'] = "Development Status :: 4 - Beta" - self.__dictConfig['sIntendedAudience'] = "Intended Audience :: Developers" - self.__dictConfig['sTopic'] = "Topic :: Software Development" - self.__dictConfig['arInstallRequires'] = ['sphinx','pypandoc','colorama'] - - - # 2. certain folder and executables (things that requires computation) - bSuccess, sResult = self.__InitConfig() - if bSuccess != True: - raise Exception(sResult) - print(COLBG + sResult) - print() - - - def __del__(self): - del self.__dictConfig - - - def __InitConfig(self): - - sOSName = os.name - sPlatformSystem = platform.system() - sPythonPath = os.path.dirname(sys.executable) - sPython = sys.executable - sPythonVersion = sys.version - - SPHINXBUILD = None - sLaTeXInterpreter = None - sInstalledPackageFolder = None - sInstalledPackageDocFolder = None - - try: - self.__dictConfig['sPandoc'] = pypandoc.get_pandoc_path() - except Exception as ex: - bSuccess = False - sResult = str(ex) - return bSuccess, sResult - - if sPlatformSystem == "Windows": - SPHINXBUILD = f"{sPythonPath}/Scripts/sphinx-build.exe" - sInstalledPackageFolder = f"{sPythonPath}/Lib/site-packages/" + self.__dictConfig['sPackageName'] - sInstalledPackageDocFolder = f"{sPythonPath}/Lib/site-packages/" + self.__dictConfig['sPackageName'] + "_doc" - sLaTeXInterpreter = os.path.normpath(os.path.expandvars("%ROBOTLATEXPATH%/miktex/bin/x64/pdflatex.exe")) - - elif sPlatformSystem == "Linux": - SPHINXBUILD = f"{sPythonPath}/sphinx-build" - sInstalledPackageFolder = f"{sPythonPath}/../lib/python3.9/site-packages/" + self.__dictConfig['sPackageName'] - sInstalledPackageDocFolder = f"{sPythonPath}/../lib/python3.9/site-packages/" + self.__dictConfig['sPackageName'] + "_doc" - sLaTeXInterpreter = os.path.normpath(os.path.expandvars("${ROBOTLATEXPATH}/miktex/bin/x64/pdflatex")) - - else: - bSuccess = False - sResult = f"Operating system {sPlatformSystem} ({sOSName}) not supported" - return bSuccess, sResult - - if os.path.isfile(sLaTeXInterpreter) is False: - sLaTeXInterpreter = None # not an error; PDF generation is an option - - if os.path.isfile(SPHINXBUILD) is False: - bSuccess = False - sResult = f"Missing Sphinx '{SPHINXBUILD}'" - return bSuccess, sResult - - self.__dictConfig['SPHINXBUILD'] = SPHINXBUILD - self.__dictConfig['sPython'] = sPython - self.__dictConfig['sLaTeXInterpreter'] = sLaTeXInterpreter - self.__dictConfig['sInstalledPackageFolder'] = sInstalledPackageFolder - self.__dictConfig['sInstalledPackageDocFolder'] = sInstalledPackageDocFolder - - - # ---- paths relative to repository root folder (where the srcipts are located that use this module) - - # ====== 1. documentation - - # This doesn't matter in case of the documentation builder itself is using this CConfig. - # But if the documentation builder is called by other apps like setup_ext.py, they need to know where to find. - sDocumentationBuilder = os.path.normpath(self.__sReferencePath + "/sphinx-makeall.py") - self.__dictConfig['sDocumentationBuilder'] = sDocumentationBuilder - - # - documentation project source dir (relative to reference path (= position of executing script) - SOURCEDIR = os.path.normpath(self.__sReferencePath + "/doc") - self.__dictConfig['SOURCEDIR'] = SOURCEDIR - - # - documentation project build dir - BUILDDIR = os.path.normpath(SOURCEDIR + "/_build") - self.__dictConfig['BUILDDIR'] = BUILDDIR - - # - documentation project html output folder - sHTMLOutputFolder = os.path.normpath(BUILDDIR + "/html") - self.__dictConfig['sHTMLOutputFolder'] = sHTMLOutputFolder - - # - README - sReadMe_rst = os.path.normpath(self.__sReferencePath + "/README.rst") - self.__dictConfig['sReadMe_rst'] = sReadMe_rst - sReadMe_md = os.path.normpath(self.__sReferencePath + "/README.md") - self.__dictConfig['sReadMe_md'] = sReadMe_md - - - # ====== 2. setuptools - - self.__dictConfig['sSetupBuildFolder'] = os.path.normpath(self.__sReferencePath + "/build") - self.__dictConfig['sSetupBuildLibFolder'] = os.path.normpath(self.__sReferencePath + "/build/lib") - self.__dictConfig['sSetupBuildLibDocFolder'] = os.path.normpath(self.__sReferencePath + "/build/lib/" + self.__dictConfig['sPackageName'] + "_doc") - self.__dictConfig['sSetupDistFolder'] = os.path.normpath(self.__sReferencePath + "/dist") - self.__dictConfig['sEggInfoFolder'] = os.path.normpath(self.__sReferencePath + "/" + self.__dictConfig['sPackageName'] + ".egg-info") - - print() - print(f"Running under {sPlatformSystem} ({sOSName})") - self.PrintConfig() - - bSuccess = True - sResult = "Repository setup done" - return bSuccess, sResult - - # eof def __InitConfig(self): - - - def PrintConfig(self): - # -- printing configuration to console - nJust = 30 - print() - for sKey in self.__dictConfig: - print(sKey.rjust(nJust, ' ') + " : " + str(self.__dictConfig[sKey])) - print() - # eof def PrintConfig(self): - - - def Get(self, sName=None): - if ( (sName is None) or (sName not in self.__dictConfig) ): - print() - printerror(f"Error: Configuration parameter '{sName}' not existing!") - # from here it's standard output: - print("Use instead one of:") - self.PrintConfig() - return None # returning 'None' in case of key is not existing !!! - else: - return self.__dictConfig[sName] - # eof def Get(self, sName=None): - -# eof class CConfig(): - -# -------------------------------------------------------------------------------------------------------------- diff --git a/config/CRepositoryConfig.py b/config/CRepositoryConfig.py new file mode 100644 index 00000000..3da68162 --- /dev/null +++ b/config/CRepositoryConfig.py @@ -0,0 +1,198 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# CRepositoryConfig.py +# +# XC-CT/ECA3-Queckenstedt +# +# Purpose: +# - Compute and store all repository specific information, like the repository name, +# paths to repository subfolder, paths to interpreter and so on ... +# +# - All paths to subfolder depends on the repository root path that has to be provided +# to constructor of CRepositoryConfig +# +# -------------------------------------------------------------------------------------------------------------- +# +# 01.07.2022 +# +# -------------------------------------------------------------------------------------------------------------- + +import os, sys, platform, shlex, subprocess, json +import colorama as col +import pypandoc + +from PythonExtensionsCollection.String.CString import CString + +from JsonPreprocessor.version import VERSION +from JsonPreprocessor.version import VERSION_DATE + +col.init(autoreset=True) +COLBR = col.Style.BRIGHT + col.Fore.RED +COLBG = col.Style.BRIGHT + col.Fore.GREEN + +# -------------------------------------------------------------------------------------------------------------- + +def printerror(sMsg): + sys.stderr.write(COLBR + f"Error: {sMsg}!\n") + +def printexception(sMsg): + sys.stderr.write(COLBR + f"Exception: {sMsg}!\n") + +# -------------------------------------------------------------------------------------------------------------- + +class CRepositoryConfig(): + + def __init__(self, sCalledBy=None): + + # TODO: error handling sCalledBy=None + sCalledBy = CString.NormalizePath(sCalledBy) + self.__sReferencePath = os.path.dirname(sCalledBy) + + self.__dictRepositoryConfig = None # initialized below by json.load() + + # load static configuration values (name of json file is fix) + sRepositoryConfigurationFile = CString.NormalizePath(f"{self.__sReferencePath}/config/repository_config.json") + hRepositoryConfigurationFile = open(sRepositoryConfigurationFile) + self.__dictRepositoryConfig = json.load(hRepositoryConfigurationFile) + hRepositoryConfigurationFile.close() + + # add further infos + # (to have the possibility to print out all values with help of 'PrintConfig()') + self.__dictRepositoryConfig['CALLEDBY'] = sCalledBy + self.__dictRepositoryConfig['CWD'] = os.getcwd() + self.__dictRepositoryConfig['REFERENCEPATH'] = self.__sReferencePath + self.__dictRepositoryConfig['REPOSITORYCONFIGURATIONFILE'] = sRepositoryConfigurationFile + + # add version and date of the package this repository configuration belongs to + self.__dictRepositoryConfig['PACKAGEVERSION'] = VERSION + self.__dictRepositoryConfig['PACKAGEDATE'] = VERSION_DATE + + # make absolute path to package documentation + self.__dictRepositoryConfig['PACKAGEDOC'] = CString.NormalizePath(sPath=self.__dictRepositoryConfig['PACKAGEDOC'], sReferencePathAbs=self.__sReferencePath) + + # compute dynamic configuration values + bSuccess, sResult = self.__InitConfig() + if bSuccess != True: + raise Exception(sResult) + print(COLBG + sResult) + print() + + + def __del__(self): + del self.__dictRepositoryConfig + + + def __InitConfig(self): + + sOSName = os.name + sPlatformSystem = platform.system() + sPythonPath = CString.NormalizePath(os.path.dirname(sys.executable)) + sPython = CString.NormalizePath(sys.executable) + sPythonVersion = sys.version + + sInstalledPackageFolder = None + + try: + # try to access pandoc; if not installed we detect this already here as early as possible + pypandoc.get_pandoc_path() + except Exception as ex: + bSuccess = False + sResult = str(ex) + return bSuccess, sResult + + if sPlatformSystem == "Windows": + sInstalledPackageFolder = f"{sPythonPath}/Lib/site-packages/" + self.__dictRepositoryConfig['PACKAGENAME'] + elif sPlatformSystem == "Linux": + sInstalledPackageFolder = f"{sPythonPath}/../lib/python3.9/site-packages/" + self.__dictRepositoryConfig['PACKAGENAME'] + else: + bSuccess = False + sResult = f"Operating system {sPlatformSystem} ({sOSName}) not supported" + return bSuccess, sResult + + self.__dictRepositoryConfig['OSNAME'] = sOSName + self.__dictRepositoryConfig['PLATFORMSYSTEM'] = sPlatformSystem + self.__dictRepositoryConfig['PYTHON'] = sPython + self.__dictRepositoryConfig['PYTHONVERSION'] = sPythonVersion + self.__dictRepositoryConfig['INSTALLEDPACKAGEFOLDER'] = sInstalledPackageFolder + + # ---- paths relative to repository root folder (where the scripts are located that use this module) + + # ====== 1. documentation + + # - README + self.__dictRepositoryConfig['README_RST'] = CString.NormalizePath(f"{self.__sReferencePath}/README.rst") + self.__dictRepositoryConfig['README_MD'] = CString.NormalizePath(f"{self.__sReferencePath}/README.md") + + # The following key doesn't matter in case of the documentation builder itself is using this CRepositoryConfig. + # But if the documentation builder is called by other apps like setup.py, they need to know where to find. + self.__dictRepositoryConfig['DOCUMENTATIONBUILDER'] = CString.NormalizePath(f"{self.__sReferencePath}/genpackagedoc.py") + + # - folder containing the package source files (will also contain the PDF documentation) + self.__dictRepositoryConfig['PACKAGESOURCEFOLDER'] = CString.NormalizePath(f"{self.__sReferencePath}/{self.__dictRepositoryConfig['PACKAGENAME']}") + + # ====== 2. setuptools + + self.__dictRepositoryConfig['SETUPBUILDFOLDER'] = CString.NormalizePath(f"{self.__sReferencePath}/build") + self.__dictRepositoryConfig['SETUPBUILDLIBFOLDER'] = CString.NormalizePath(f"{self.__sReferencePath}/build/lib") + self.__dictRepositoryConfig['SETUPBUILDLIBPACKAGEFOLDER'] = CString.NormalizePath(f"{self.__sReferencePath}/build/lib/{self.__dictRepositoryConfig['PACKAGENAME']}") + self.__dictRepositoryConfig['SETUPDISTFOLDER'] = CString.NormalizePath(f"{self.__sReferencePath}/dist") + EGGINFOFOLDER = self.__dictRepositoryConfig['PACKAGENAME'].replace('-', '_') + self.__dictRepositoryConfig['EGGINFOFOLDER'] = CString.NormalizePath(f"{self.__sReferencePath}/{EGGINFOFOLDER}.egg-info") + + print() + print(f"Running under {sPlatformSystem} ({sOSName})") + self.PrintConfig() + + bSuccess = True + sResult = "Repository setup done" + return bSuccess, sResult + + # eof def __InitConfig(self): + + + def PrintConfig(self): + # -- printing configuration to console + nJust = 30 + print() + for sKey in self.__dictRepositoryConfig: + print(sKey.rjust(nJust, ' ') + " : " + str(self.__dictRepositoryConfig[sKey])) + print() + # eof def PrintConfig(self): + + + def Get(self, sName=None): + if ( (sName is None) or (sName not in self.__dictRepositoryConfig) ): + print() + printerror(f"Error: Configuration parameter '{sName}' not existing!") + # from here it's standard output: + print("Use instead one of:") + self.PrintConfig() + return None # returning 'None' in case of key is not existing !!! + else: + return self.__dictRepositoryConfig[sName] + # eof def Get(self, sName=None): + + + def GetConfig(self): + return self.__dictRepositoryConfig + # eof def GetConfig(self): + +# eof class CRepositoryConfig(): + +# -------------------------------------------------------------------------------------------------------------- diff --git a/config/repository_config.json b/config/repository_config.json new file mode 100644 index 00000000..5c378414 --- /dev/null +++ b/config/repository_config.json @@ -0,0 +1,19 @@ +{ + "REPOSITORYNAME" : "python-jsonpreprocessor", + "PACKAGENAME" : "JsonPreprocessor", + "AUTHOR" : "Mai Dinh Nam Son", + "AUTHOREMAIL" : "son.maidinhnam@vn.bosch.com", + "DESCRIPTION" : "Preprocessor for json files", + "LONGDESCRIPTIONCONTENTTYPE" : "text/markdown", + "URL" : "https://github.com/test-fullautomation/python-jsonpreprocessor", + "PROGRAMMINGLANGUAGE" : "Programming Language :: Python :: 3", + "LICENCE" : "License :: OSI Approved :: Apache Software License", + "OPERATINGSYSTEM" : "Operating System :: OS Independent", + "PYTHONREQUIRES" : ">=3.0", + "DEVELOPMENTSTATUS" : "Development Status :: 4 - Beta", + "INTENDEDAUDIENCE" : "Intended Audience :: Developers", + "TOPIC" : "Topic :: Software Development", + "INSTALLREQUIRES" : ["pypandoc","colorama","GenPackageDoc","PythonExtensionsCollection"], + "PACKAGEDATA" : ["*.pdf"], + "PACKAGEDOC" : "./packagedoc" +} diff --git a/dump_repository_config.py b/dump_repository_config.py new file mode 100644 index 00000000..fcc051b8 --- /dev/null +++ b/dump_repository_config.py @@ -0,0 +1,75 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# dump_repository_config.py +# +# XC-CT/ECA3-Queckenstedt +# +# dump_repository_config.py is a little helper to get deeper knowledge about the environment under which +# the software in this repository is being executed. +# +# The script prints all repository configuration values to console: +# Reference: +# - config\repository_config.json +# - config\CRepositoryConfig.py +# +# -------------------------------------------------------------------------------------------------------------- +# +import os, sys + +import colorama as col + +from config.CRepositoryConfig import CRepositoryConfig + +col.init(autoreset=True) + +COLBR = col.Style.BRIGHT + col.Fore.RED +COLBY = col.Style.BRIGHT + col.Fore.YELLOW +COLBG = col.Style.BRIGHT + col.Fore.GREEN + +SUCCESS = 0 +ERROR = 1 + +# -------------------------------------------------------------------------------------------------------------- + +def printerror(sMsg): + sys.stderr.write(COLBR + f"Error: {sMsg}!\n") + +def printexception(sMsg): + sys.stderr.write(COLBR + f"Exception: {sMsg}!\n") + +# -------------------------------------------------------------------------------------------------------------- + +# -- setting up the repository configuration (relative to the path of this script) +oRepositoryConfig = None +try: + oRepositoryConfig = CRepositoryConfig(os.path.abspath(sys.argv[0])) +except Exception as ex: + print() + printexception(str(ex)) + print() + sys.exit(ERROR) + +# -------------------------------------------------------------------------------------------------------------- + +print(COLBG + "Repository configuration dump done") +print() +sys.exit(SUCCESS) + +# -------------------------------------------------------------------------------------------------------------- + diff --git a/genpackagedoc.py b/genpackagedoc.py new file mode 100644 index 00000000..f948d6de --- /dev/null +++ b/genpackagedoc.py @@ -0,0 +1,104 @@ +# ************************************************************************************************************** +# +# Copyright 2020-2022 Robert Bosch GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ************************************************************************************************************** +# +# genpackagedoc.py +# +# XC-CT/ECA3-Queckenstedt +# +# -------------------------------------------------------------------------------------------------------------- +# +# 29.06.2022 +# +# -------------------------------------------------------------------------------------------------------------- + +import os, sys + +import colorama as col + +from config.CRepositoryConfig import CRepositoryConfig # providing repository and environment specific information +from GenPackageDoc.CPackageDocConfig import CPackageDocConfig +from GenPackageDoc.CDocBuilder import CDocBuilder + +col.init(autoreset=True) + +COLBR = col.Style.BRIGHT + col.Fore.RED +COLBY = col.Style.BRIGHT + col.Fore.YELLOW +COLBG = col.Style.BRIGHT + col.Fore.GREEN + +SUCCESS = 0 +ERROR = 1 + +# -------------------------------------------------------------------------------------------------------------- + +def printerror(sMsg): + sys.stderr.write(COLBR + f"Error: {sMsg}!\n") + +def printexception(sMsg): + sys.stderr.write(COLBR + f"Exception: {sMsg}!\n") + +# -------------------------------------------------------------------------------------------------------------- + +# -- setting up the repository configuration (relative to the path of this script) +oRepositoryConfig = None +try: + oRepositoryConfig = CRepositoryConfig(os.path.abspath(sys.argv[0])) +except Exception as ex: + print() + printexception(str(ex)) + print() + sys.exit(ERROR) + +# -- setting up the GenPackageDoc configuration +oGenPackageDocConfig = None +try: + oPackageDocConfig = CPackageDocConfig(oRepositoryConfig) +except Exception as ex: + print() + printexception(str(ex)) + print() + sys.exit(ERROR) + +# -- setting up and calling the doc builder +try: + oDocBuilder = CDocBuilder(oPackageDocConfig) +except Exception as ex: + print() + printexception(str(ex)) + print() + sys.exit(ERROR) + +bSuccess, sResult = oDocBuilder.Build() +if bSuccess is None: + print() + printexception(sResult) + print() + sys.exit(ERROR) +elif bSuccess is False: + print() + printerror(sResult) + print() + sys.exit(ERROR) +else: + print(COLBY + sResult) + print() + print(COLBG + "genpackagedoc done") + print() + sys.exit(SUCCESS) + +# -------------------------------------------------------------------------------------------------------------- + diff --git a/packagedoc/additional_docs/Appendix.rst b/packagedoc/additional_docs/Appendix.rst new file mode 100644 index 00000000..d70d0c0b --- /dev/null +++ b/packagedoc/additional_docs/Appendix.rst @@ -0,0 +1,37 @@ +.. Copyright 2020-2022 Robert Bosch GmbH + +.. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + +.. http://www.apache.org/licenses/LICENSE-2.0 + +.. Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +**About this package:** + +.. table:: Package setup + :widths: auto + + ================== ========================= + Setup parameter Value + ================== ========================= + Name ###PACKAGENAME### + Version ###PACKAGEVERSION### + Date ###PACKAGEDATE### + Description ###DESCRIPTION### + Package URL `###REPOSITORYNAME### <###URL###>`_ + Author ###AUTHOR### + Email ###AUTHOREMAIL### + Language ###PROGRAMMINGLANGUAGE### + License ###LICENCE### + OS ###OPERATINGSYSTEM### + Python required ###PYTHONREQUIRES### + Development status ###DEVELOPMENTSTATUS### + Intended audience ###INTENDEDAUDIENCE### + Topic ###TOPIC### + ================== ========================= diff --git a/packagedoc/additional_docs/Description.rst b/packagedoc/additional_docs/Description.rst new file mode 100644 index 00000000..66b4088c --- /dev/null +++ b/packagedoc/additional_docs/Description.rst @@ -0,0 +1,24 @@ +.. Copyright 2020-2022 Robert Bosch GmbH + +.. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + +.. http://www.apache.org/licenses/LICENSE-2.0 + +.. Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The json preprocessor: + +.. image:: ./pictures/python3-jsonpreprocessor.png + + + +TO BE CONTINUED + + + diff --git a/packagedoc/additional_docs/Description_orig.rst b/packagedoc/additional_docs/Description_orig.rst new file mode 100644 index 00000000..7651ae7a --- /dev/null +++ b/packagedoc/additional_docs/Description_orig.rst @@ -0,0 +1,420 @@ +.. Copyright 2020-2022 Robert Bosch GmbH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Json Preprocessor's Feature Documentation +========================================= + +Introduction: +------------- + +.. image:: /images/python3-jsonpreprocessor.png + +The JsonPreprocessor is a Python3 package which allows programmers to handle +additional features in json files such as + +* add comments +* import other json files +* overwrite already existing parameters with new values + +These json files will be handled by the JsonPreprocessor package which returns as result +a dictionary object of the deserialized data. + +New features +~~~~~~~~~~~~ + +`Adding comments to Json file`_ + +`Import the contents from other json files`_ + +`Overwrite existing and add new parameters`_ + +`Nested parameters`_ + +Features in details +------------------- + +Adding comments to Json file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Every line starting with **"//"**, is commented out. Therefore a comment is valid for singles lines only. + +Comment out a block of several lines with only one start and one end comment string, is currently not supported. + +Adding comments to json files is useful in case of more and more content is added, e.g. because of a json file +has to hold a huge number of configuration parameters for different features. Comments can be used here +to clarify the meaning of these parameters or the differences between them. + +**Example:** + +.. code-block:: + + //***************************************************************************** + // Author: ROBFW-AIO Team + // + // This file defines all common global parameters and will be included to all + // test config files + //***************************************************************************** + { + "Project": "G3g", + "WelcomeString": "Hello... ROBFW is running now!", + // Version control information. + "version": { + "majorversion": "0", + "minorversion": "1", + "patchversion": "1" + }, + "params": { + // Global parameters + "global": { + "gGlobalIntParam" : 1, + "gGlobalFloatParam" : 1.332, // This parameter is used to configure for .... + "gGlobalString" : "This is a string", + "gGlobalStructure": { + "general": "general" + } + } + }, + "preprocessor": { + "definitions": { + // FEATURE switches + "gPreprolIntParam" : 1, + "gPreproFloatParam" : 1.332, + // The parameter for feature ABC + "gPreproString" : "This is a string", + "gPreproStructure": { + "general": "general" + } + } + }, + "TargetName" : "gen3flex@dlt" + } + +Import the contents from other json files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This import feature enables developers to take over the content of other json files into the +current json file. A json file that is imported into another json file, can contain imports also +(allows nested imports). + +A possible usecase for nested imports is to handle similar configuration parameters of different variants of a feature +or a component within a bunch of several smaller files, instead of putting all parameter into only one large json file. + +**Example:** + +Suppose we have the json file ``params_global.json`` with the content: + +.. code-block:: + + //***************************************************************************** + // Author: ROBFW-AIO Team + // + // This file defines all common global parameters and will be included to all + // test config files + //***************************************************************************** + // + // This is to distinguish the different types of resets + { + "gGlobalIntParam" : 1, + + "gGlobalFloatParam" : 1.332, // This parameter is used to configure for .... + + "gGlobalString" : "This is a string", + + "gGlobalStructure": { + "general": "general" + } + } + +And other json file ``preprocessor_definitions.json`` with content: + +.. code-block:: + + //***************************************************************************** + // Author: ROBFW-AIO Team + // + // This file defines all common global parameters and will be included to all + // test config files + //***************************************************************************** + { + "gPreprolIntParam" : 1, + + "gPreproFloatParam" : 1.332, + // The parameter for feature ABC + "gPreproString" : "This is a string", + + "gPreproStructure": { + "general": "general" + } + } + +Then we can import these 2 files above to the json file ``config.json`` with content: + +.. code-block:: + + //***************************************************************************** + // Author: ROBFW-AIO Team + // + // This file defines all common global parameters and will be included to all + // test config files + //***************************************************************************** + { + "Project": "G3g", + "WelcomeString": "Hello... ROBFW is running now!", + // Version control information. + "version": { + "majorversion": "0", + "minorversion": "1", + "patchversion": "1" + }, + "params": { + // Global parameters + "global": { + "[import]": "/params_global.json" + } + }, + "preprocessor": { + "definitions": { + // FEATURE switches + "[import]": "/preprocessor_definitions.json" + } + }, + "TargetName" : "gen3flex@dlt" + } + +The ``config.json`` file is handled by JsonPreprocessor package, then return the dictionary object for a program like below: + +.. code-block:: + + { + "Project": "G3g", + "WelcomeString": "Hello... ROBFW is running now!", + "version": { + "majorversion": "0", + "minorversion": "1", + "patchversion": "1" + }, + "params": { + "global": { + "gGlobalIntParam" : 1, + "gGlobalFloatParam" : 1.332, + "gGlobalString" : "This is a string", + "gGlobalStructure": { + "general": "general" + } + } + }, + "preprocessor": { + "definitions": { + "gPreprolIntParam" : 1, + "gPreproFloatParam" : 1.332, + "gPreproString" : "This is a string", + "gPreproStructure": { + "general": "general" + } + } + }, + "TargetName" : "gen3flex@dlt" + } + +Overwrite existing and add new parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This package also provides user ability to overwrite or update as well as add new +parameters. User can update parameters which are already declared and add new parameters +or new element into existing parameters. The below example will show the way to do +these features. + +In case we have many different variants, and each variant requires a different value +assigned to the parameter. This feature could help us update new value for existing +parameters, it also supports to add new parameters to existing configuation object. + +**Example:** + +Suppose we have the json file ``params_global.json`` with the content: + +.. code-block:: + + { + "gGlobalIntParam" : 1, + + "gGlobalFloatParam" : 1.332, // This parameter is used to configure for .... + + "gGlobalString" : "This is a string", + + "gGlobalStructure": { + "general": "general" + } + } + +Then we import ``params_global.json`` to json file ``config.json`` with content: + +.. code-block:: + + { + "Project": "G3g", + "WelcomeString": "Hello... ROBFW is running now!", + // Version control information. + "version": { + "majorversion": "0", + "minorversion": "1", + "patchversion": "1" + }, + "params": { + // Global parameters + "global": { + "[import]": "/params_global.json" + } + }, + "TargetName" : "gen3flex@dlt", + // Overwrite parameters + "${params}['global']['gGlobalFloatParam']": 9.999, + "${version}['patchversion']": "2", + "${params}['global']['gGlobalString']": "This is the new value for the already existing parameter.", + // Add new parameters + "${newParam}": { + "abc": 9, + "xyz": "new param" + }, + "${params}['global']['gGlobalStructure']['newGlobalParam']": 123 + } + +The ``config.json`` file is handled by JsonPreprocessor package, then return the dictionary object for a program like below: + +.. code-block:: + + { + "Project": "G3g", + "WelcomeString": "Hello... ROBFW is running now!", + "version": { + "majorversion": "0", + "minorversion": "1", + "patchversion": "2" + }, + "params": { + "global": { + "gGlobalIntParam" : 1, + "gGlobalFloatParam" : 9.999, + "gGlobalString" : "This is the new value for the already existing parameter.", + "gGlobalStructure": { + "general": "general", + "newGlobalParam": 123 + } + } + }, + "TargetName": "gen3flex@dlt", + "newParam": { + "abc": 9, + "xyz": "new param" + } + } + +Nested parameters +~~~~~~~~~~~~~~~~~ + +With JsonPreprocessor package, user can also use nested parameters as example below: + +**Example:** + +Suppose we have the json file ``config.json`` with the content: + +.. code-block:: + + { + "Project": "G3g", + "WelcomeString": "Hello... ROBFW is running now!", + // Version control information. + "version": { + "majorversion": "0", + "minorversion": "1", + "patchversion": "1" + }, + "params": { + // Global parameters + "global": { + "gGlobalIntParam" : 1, + "gGlobalFloatParam" : 1.332, // This parameter is used to configure for .... + "gGlobalString" : "This is a string", + "gGlobalStructure": { + "general": "general" + } + } + }, + "preprocessor": { + "definitions": { + "gPreprolIntParam" : 1, + "gPreproFloatParam" : 9.664, + "ABC": "checkABC", + "gPreproString" : "This is a string", + "gPreproStructure": { + "general": "general" + } + } + }, + "TargetName" : "gen3flex@dlt", + // Nested parameter + "${params}['global'][${preprocessor}['definitions']['ABC']]": true, + "${params}['global']['gGlobalFloatParam']": "${preprocessor}['definitions']['gPreproFloatParam']" + } + +The ``config.json`` file is handled by JsonPreprocessor package, then return the dictionary object for a program like below: + +.. code-block:: + + { + "Project": "G3g", + "WelcomeString": "Hello... ROBFW is running now!", + "version": { + "majorversion": "0", + "minorversion": "1", + "patchversion": "1" + }, + "params": { + "global": { + "gGlobalIntParam" : 1, + "gGlobalFloatParam" : 9.664, + "gGlobalString" : "This is a string", + "gGlobalStructure": { + "general": "general" + }, + "checkABC": true + } + }, + "preprocessor": { + "definitions": { + "gPreprolIntParam" : 1, + "gPreproFloatParam" : 9.664, + "ABC": "checkABC", + "gPreproString" : "This is a string", + "gPreproStructure": { + "general": "general" + } + } + }, + "TargetName" : "gen3flex@dlt" + } + +Feedback +-------- + +To give us a feedback, you can send an email to `Thomas Pollerspöck `_ or +`RBVH-ECM-Automation_Test_Framework-Associates `_ + +In case you want to report a bug or request any interesting feature, please don't hesitate to raise a ticket on +`our Jira `_ + +References +---------- + +For more information please refer to our `Bosch Connect Community `_ + diff --git a/packagedoc/additional_docs/History.tex b/packagedoc/additional_docs/History.tex new file mode 100644 index 00000000..d529af95 --- /dev/null +++ b/packagedoc/additional_docs/History.tex @@ -0,0 +1,20 @@ +% Copyright 2020-2022 Robert Bosch GmbH + +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at + +% http://www.apache.org/licenses/LICENSE-2.0 + +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. + +\begin{center} +\begin{tabular}{| m{6em} | m{26em} |}\hline + \textbf{0.1.0} & 01/2022\\ \hline + \multicolumn{2}{| m{32em} |}{\textit{Initial version}}\\ \hline +\end{tabular} +\end{center} diff --git a/packagedoc/additional_docs/Introduction.rst b/packagedoc/additional_docs/Introduction.rst new file mode 100644 index 00000000..7e72e6c3 --- /dev/null +++ b/packagedoc/additional_docs/Introduction.rst @@ -0,0 +1,17 @@ +.. Copyright 2020-2022 Robert Bosch GmbH + +.. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + +.. http://www.apache.org/licenses/LICENSE-2.0 + +.. Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*Json Preprocessor* + +!!! add introduction - if wanted !! diff --git a/packagedoc/additional_docs/pictures/python3-jsonpreprocessor.png b/packagedoc/additional_docs/pictures/python3-jsonpreprocessor.png new file mode 100644 index 00000000..a691ae94 Binary files /dev/null and b/packagedoc/additional_docs/pictures/python3-jsonpreprocessor.png differ diff --git a/packagedoc/packagedoc_config.json b/packagedoc/packagedoc_config.json new file mode 100644 index 00000000..c927d22c --- /dev/null +++ b/packagedoc/packagedoc_config.json @@ -0,0 +1,127 @@ +# ************************************************************************************************************** +# GenPackageDoc configuration file +# +# GenPackageDoc creates tex files out of the docstrings of Python modules and out of separate rst files. +# With the help of a LaTeX compiler the tex files can be converted into a PDF file. +# This json file contains the required settings. +# +# 29.06.2022 +# +# ************************************************************************************************************** +# Format: extended json format with the possibility to comment out lines (by '#' at the beginning of the line). +# All fixed terms (keys) have to be written in capital letters, all user defined keys in small letters. +# Paths to files and folders within this file must be relative (except "TEX"). The reference for relative paths +# is the position of this file. +# -------------------------------------------------------------------------------------------------------------- + +{ + +# Section "CONTROL": +# ------------------ +# Contains parameter to control the application behavior in general. + + "CONTROL" : { + # if 'INCLUDEPRIVATE' is false: private methods are skipped, otherwise they are included in documentation + "INCLUDEPRIVATE" : false, + # if 'INCLUDEUNDOCUMENTED' is true: also classes and methods without docstring are listed in the documentation; + # otherwise they are skipped + "INCLUDEUNDOCUMENTED" : true, + # if 'STRICT' is true: missing LaTeX compiler causes an error; otherwise PDF generation is handled as option + # and a missing LaTeX compiler does not cause an error + "STRICT" : false + }, + +# Section "TOC": +# -------------- +# Defines the content of the PDF document (= the order of the chapters). +# A chapter can be represented by an additional text file in rst format. Every rst file is a separate chapter +# within the resulting PDF document. +# A chapter can also be represented by an additional tex file. Every tex file is a separate chapter +# within the resulting PDF document. +# In case of Python modules are involved, every Python module is a chapter. +# Class and function definitions within a Python module are subchapters. Methods of classes are subsubchapters. +# In this section every additional rst file is represented by a short name (key) and a path incl. file name (key value). +# This is completely under the responsibility of the user and therefore the names have to be written in small letters only. +# The keys with name starting with "INTERFACE" points to folder containing Python modules that have to be documented. +# In case of a repository contains several separate folder with Python modules, it is possible to use more than one +# key starting with "INTERFACE", e.g. "INTERFACE_part1" and "INTERFACE_part2". +# The key "DOCUMENTPARTS" contains a list of all defined keys in the order of their desired appearances within the resulting PDF document. +# It is strongly recommended to place all additional rst files flat into one single folder (and not in any further sub folder). +# Reason is that currently the tex files generated out of the rst files are also placed flat into only one single output folder +# (see section "OUTPUT") and the relative paths to possibly imported pictures (see section "PICTURES") must be kept valid! + + "TOC" : { + "introduction" : "./additional_docs/Introduction.rst", + "description" : "./additional_docs/Description.rst", + "INTERFACE" : "../###PACKAGENAME###", + "appendix" : "./additional_docs/Appendix.rst", + "history" : "./additional_docs/History.tex", + "DOCUMENTPARTS" : ["introduction","description","INTERFACE","appendix","history"] + }, + +# Section "PARAMS": +# ----------------- +# In this section a user has the ability to define own runtime variables, that are accessible within the doctrings of involved Python modules +# and also within separate rst files. Key names have to be written in small letters to indicate that they are user defined. +# Values can be either hard coded or taken over from the repository configuration (see config/repository_config.json). +# In second case the key name has to be encapsulated within three hashes '#'. The same notation can be used in rst files and doctrings +# of Python modules (for example adding an additional hint within a doctring of a class: ###additional_hint###). +# This mechanism is limited to strings! +# This key is optional. In case of own parameters are not needed, this key can be removed or set to null. + + # "PARAMS" : { + # "" : "", + # }, + +# Section "DOCUMENT": +# ------------------- +# This section contains basic details about the output document. Like in the previous section, values can be defined +# either hard coded or taken over from the repository configuration (with '#' notation). +# The keys in this section also are runtime variables. + + "DOCUMENT" : { + "OUTPUTFILENAME" : "###PACKAGENAME###.tex", + "AUTHOR" : "###AUTHOR###", + "TITLE" : "###PACKAGENAME###", + "DATE" : "###PACKAGEDATE###", + "VERSION" : "###PACKAGEVERSION###" + }, + +# Section "PICTURES": +# ------------------- +# Additional rst files may import pictures. GenPackageDoc needs to know where to find them. +# To keep the relative paths to possibly imported pictures valid also within the created tex files, +# this pictures folder will be copied into the output folder defined by key "OUTPUT". +# This key is optional. In case of there are no pictures needed in this package description, +# this key can be removed or set to null. + + "PICTURES" : "./additional_docs/pictures", + +# Section "OUTPUT": +# ----------------- +# Defines the path to the output folder containing the generated tex files and the PDF file. + + "OUTPUT" : "./build", + +# Section "PDFDEST": +# ----------------- +# Defines the path to a folder in which the PDF file will be copied after creation +# (the "OUTPUT" folder in which the LaTeX compiler creates the PDF file, is not really a proper destination finally) +# This key is optional. In case of it is not required to copy the PDF file to another location, +# this key can be removed or set to null. + + "PDFDEST" : "../###PACKAGENAME###", + +# Section "TEX": +# ----------------- +# Converting the generated text source files to a PDF document requires a LaTeX distribution. +# GenPackageDoc needs to know where to find LaTeX. Because the path to the LaTeX interpreter depends +# on the operating system, this path has to be defined separately for every supported operating system +# (currently "WINDOWS" and "LINUX"). +# Example: + + "TEX" : { + "WINDOWS" : "%ROBOTLATEXPATH%/miktex/bin/x64/pdflatex.exe", + "LINUX" : "${ROBOTLATEXPATH}/miktex/bin/x64/pdflatex" + } +} diff --git a/setup.py b/setup.py index 22358c59..33ea18d8 100644 --- a/setup.py +++ b/setup.py @@ -20,12 +20,12 @@ # # XC-CT/ECA3-Queckenstedt # -# Extends the standard setuptools installation by adding the documentation in HTML format +# Extends the standard setuptools installation by adding the documentation in PDF format # (requires installation mode) and tidying up some folders. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # -# This script deletes folders (as defined in config.CConfig, depending on the position of this script): +# This script deletes folders (as defined in config.CRepositoryConfig, depending on the position of this script): # - previous builds within this repository # - previous installations within # * \Lib\site-packages (Windows) @@ -44,9 +44,9 @@ # The usual # packages = setuptools.find_packages(), # is replaced by -# packages = [str(oRepositoryConfig.Get('sPackageName')), ], -# to avoid that also config.CConfig() and config.CExtendedSetup() are part of the distribution. -# CConfig and CExtendedSetup() are only repository internal helper. +# packages = [str(oRepositoryConfig.Get('PACKAGENAME')), ], +# to avoid that also config.CRepositoryConfig() and additions.CExtendedSetup() are part of the distribution. +# CRepositoryConfig and CExtendedSetup() are only repository internal helper. # # * Known issues: # @@ -60,21 +60,7 @@ # # -------------------------------------------------------------------------------------------------------------- # -# 21.02.2022 / XC-CT/ECA3-Queckenstedt -# -# "sdist bdist_wheel" maintenance: some steps moved from inside 'ExtendedInstallCommand' to outside -# -# 09.02.2022 / XC-CT/ECA3-Queckenstedt -# Suppressed generation of documents and installations in case of command line -# parameter is not 'install' and not 'build' (this enables printing the help only). (10.02.2022: and not 'sdist') -# -# 11.10.2021 / XC-CI1/ECA3-Queckenstedt -# Fixed computation order of readme files together with long_description -# -# 30.09.2021 / XC-CI1/ECA3-Queckenstedt -# Added wrapper for error messages -# -# Initial version 08/2021 +# 30.06.2022 # # -------------------------------------------------------------------------------------------------------------- @@ -82,8 +68,11 @@ import setuptools from setuptools.command.install import install -from config.CConfig import CConfig # providing repository and environment specific information -from config.CExtendedSetup import CExtendedSetup # providing functions to support the extended setup process +# prefer the repository local version of all additional libraries (instead of the installed version under site-packages) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "./additions"))) + +from config.CRepositoryConfig import CRepositoryConfig # providing repository and environment specific information +from additions.CExtendedSetup import CExtendedSetup # providing functions to support the extended setup process import colorama as col @@ -113,28 +102,7 @@ def run(self): listCmdArgs = sys.argv if ( ('install' in listCmdArgs) or ('build' in listCmdArgs) or ('sdist' in listCmdArgs) or ('bdist_wheel' in listCmdArgs) ): - print() - print(COLBY + "Extended setup step 4/5: install.run(self)") # creates the build folder .\build - print() - install.run(self) # TODO: What does install.run(self) return? How to realize error handling? - print() - if 'bdist_wheel' in listCmdArgs: - print(COLBY + "Extended setup step 5/5: Add html documentation to local wheel folder inside build") - print() - nReturn = oExtendedSetup.add_htmldoc_to_wheel() - if nReturn != SUCCESS: - return nReturn - print() - else: - print(COLBY + "Extended setup step 5/5: Add html documentation to package installation folder") # (./doc/_build/html to \Lib\site-packages\_doc) - print() - nReturn = oExtendedSetup.add_htmldoc_to_installation() - if nReturn != SUCCESS: - return nReturn - print() - print(COLBG + "Extended installation done") - print() - + install.run(self) return SUCCESS # eof class ExtendedInstallCommand(install): @@ -146,9 +114,8 @@ def run(self): # -- setting up the repository configuration oRepositoryConfig = None -sReferencePath = os.path.dirname(os.path.abspath(sys.argv[0])) try: - oRepositoryConfig = CConfig(sReferencePath) + oRepositoryConfig = CRepositoryConfig(os.path.abspath(sys.argv[0])) except Exception as ex: print() printexception(str(ex)) @@ -176,59 +143,79 @@ def run(self): print() print(COLBY + "Extended setup step 1/5: Calling the documentation builder") - # (previously called inside ExtendedInstallCommand - but this is too late, because the content of the initially - # generated or updated README file is already needed for the long_description below.) print() - nReturn = oExtendedSetup.gen_doc() + + nReturn = oExtendedSetup.genpackagedoc() + if nReturn != SUCCESS: + sys.exit(nReturn) + + print(COLBY + "Extended setup step 2/5: Converting the repository README") + print() + + nReturn = oExtendedSetup.convert_repo_readme() if nReturn != SUCCESS: sys.exit(nReturn) - print(COLBY + "Extended setup step 2/5: Deleting previous setup outputs (build, dist, .egg-info within repository)") + print(COLBY + "Extended setup step 3/5: Deleting previous setup outputs (build, dist, .egg-info within repository)") print() nReturn = oExtendedSetup.delete_previous_build() if nReturn != SUCCESS: sys.exit(nReturn) - if not 'bdist_wheel' in listCmdArgs: + if ( ('bdist_wheel' in listCmdArgs) or ('build' in listCmdArgs) ): + print() + print(COLBY + "Skipping extended setup step 4/5: Deleting previous package installation folder within site-packages") + print() + else: print() - print(COLBY + "Extended setup step 3/5: Deleting previous package installation folder within site-packages") # ( and _doc under \Lib\site-packages + print(COLBY + "Extended setup step 4/5: Deleting previous package installation folder within site-packages") # ( and _doc under \Lib\site-packages print() nReturn = oExtendedSetup.delete_previous_installation() if nReturn != SUCCESS: sys.exit(nReturn) - with open("README.md", "r", encoding="utf-8") as fh: + README_MD = str(oRepositoryConfig.Get('README_MD')) + with open(README_MD, "r", encoding="utf-8") as fh: long_description = fh.read() - print() - + fh.close() # -------------------------------------------------------------------------------------------------------------- -# This also handles the printing of help to console and therefore must be called in every case. -# And therefore all variables and objects must exist (even in case of the values are not used). +# -- the 'setup' itself + +print(COLBY + "Extended setup step 5/5: install.run(self)") +print() + setuptools.setup( - name = str(oRepositoryConfig.Get('sPackageName')), - version = str(oRepositoryConfig.Get('sVersion')), - author = str(oRepositoryConfig.Get('sAuthor')), - author_email = str(oRepositoryConfig.Get('sAuthorEMail')), - description = str(oRepositoryConfig.Get('sDescription')), + name = str(oRepositoryConfig.Get('PACKAGENAME')), + version = str(oRepositoryConfig.Get('PACKAGEVERSION')), + author = str(oRepositoryConfig.Get('AUTHOR')), + author_email = str(oRepositoryConfig.Get('AUTHOREMAIL')), + description = str(oRepositoryConfig.Get('DESCRIPTION')), long_description = long_description, - long_description_content_type = str(oRepositoryConfig.Get('sLongDescriptionContentType')), - url = str(oRepositoryConfig.Get('sURL')), - packages = [str(oRepositoryConfig.Get('sPackageName')), ], + long_description_content_type = str(oRepositoryConfig.Get('LONGDESCRIPTIONCONTENTTYPE')), + url = str(oRepositoryConfig.Get('URL')), + packages = [str(oRepositoryConfig.Get('PACKAGENAME')),], classifiers = [ - str(oRepositoryConfig.Get('sProgrammingLanguage')), - str(oRepositoryConfig.Get('sLicence')), - str(oRepositoryConfig.Get('sOperatingSystem')), - str(oRepositoryConfig.Get('sDevelopmentStatus')), - str(oRepositoryConfig.Get('sIntendedAudience')), - str(oRepositoryConfig.Get('sTopic')), + str(oRepositoryConfig.Get('PROGRAMMINGLANGUAGE')), + str(oRepositoryConfig.Get('LICENCE')), + str(oRepositoryConfig.Get('OPERATINGSYSTEM')), + str(oRepositoryConfig.Get('DEVELOPMENTSTATUS')), + str(oRepositoryConfig.Get('INTENDEDAUDIENCE')), + str(oRepositoryConfig.Get('TOPIC')), ], - python_requires = str(oRepositoryConfig.Get('sPythonRequires')), + python_requires = str(oRepositoryConfig.Get('PYTHONREQUIRES')), cmdclass={ 'install': ExtendedInstallCommand, }, - install_requires = oRepositoryConfig.Get('arInstallRequires'), + install_requires = oRepositoryConfig.Get('INSTALLREQUIRES'), + package_data={f"{oRepositoryConfig.Get('PACKAGENAME')}" : oRepositoryConfig.Get('PACKAGEDATA')}, ) # -------------------------------------------------------------------------------------------------------------- +print() +print(COLBG + "Extended installation done") +print() + +# -------------------------------------------------------------------------------------------------------------- + diff --git a/sphinx-makeall.py b/sphinx-makeall.py deleted file mode 100644 index 8b5d87d8..00000000 --- a/sphinx-makeall.py +++ /dev/null @@ -1,344 +0,0 @@ -# ************************************************************************************************************** -# -# Copyright 2020-2022 Robert Bosch GmbH -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ************************************************************************************************************** -# -# sphinx-makeall.py -# -# XC-CT/ECA3-Queckenstedt -# -# Uses the Python documentation tool Sphinx to generate the documentation of Python modules -# - based on the docstrings inside the Python modules and -# - based on additionally included separate text files (containing usually more common information -# than the docstrings of the Python modules). -# -# The docstrings and the additionally included text files have to be written in reStructureText syntax. -# The output is available in HTML format and in LaTeX format (.tex), optionally also in PDF. -# -# Preconditions: -# - Sphinx available as part of the Python installation. -# - Sphinx documentation project files already created and adapted to the local repository -# (see conf.py and index.rst). -# - LaTeX compiler (in case of output in PDF format is wanted) -# - pandoc and pypandoc for conversions between markdown formats -# -# Known issues: -# - LaTeX compiler throws some warnings when computing Sphinx output. -# -# -------------------------------------------------------------------------------------------------------------- -# -# 02.11.2021 / XC-CT/ECA3-Queckenstedt -# gen_doc_pdf called twice (to get also a table of content added to PDF file) -# -# 30.09.2021 / XC-CI1/ECA3-Queckenstedt -# Added wrapper for error messages -# -# Initial version 08/2021 -# -# -------------------------------------------------------------------------------------------------------------- - -import os, sys, platform, shlex, subprocess - -import pypandoc - -from config.CConfig import CConfig # providing repository and environment specific information - -import colorama as col - -col.init(autoreset=True) - -COLBR = col.Style.BRIGHT + col.Fore.RED -COLBY = col.Style.BRIGHT + col.Fore.YELLOW -COLBG = col.Style.BRIGHT + col.Fore.GREEN - -SUCCESS = 0 -ERROR = 1 - -# -------------------------------------------------------------------------------------------------------------- - -def printerror(sMsg): - sys.stderr.write(COLBR + f"Error: {sMsg}!\n") - -def printexception(sMsg): - sys.stderr.write(COLBR + f"Exception: {sMsg}!\n") - -# -------------------------------------------------------------------------------------------------------------- - -def convert_repo_readme(oRepositoryConfig=None): - """Converts the main repository README from 'rst' to 'md' format. - """ - - if oRepositoryConfig is None: - print() - printerror("oRepositoryConfig is None") - print() - return ERROR - - sReadMe_rst = oRepositoryConfig.Get("sReadMe_rst") - if sReadMe_rst is None: - return ERROR - - sReadMe_md = oRepositoryConfig.Get("sReadMe_md") - if sReadMe_md is None: - return ERROR - - if os.path.isfile(sReadMe_rst) is False: - print() - printerror(f"Missing readme file '{sReadMe_rst}'") - print() - return ERROR - - sFileContent = pypandoc.convert_file(sReadMe_rst, 'md') - hFile_md = open(sReadMe_md, "w", encoding="utf-8") - listFileContent = sFileContent.splitlines() - for sLine in listFileContent: - hFile_md.write(sLine + "\n") - hFile_md.close() - - print() - print(COLBY + f"File '{sReadMe_rst}'") - print(COLBY + "converted to") - print(COLBY + f"'{sReadMe_md}'") - print() - - return SUCCESS - -# eof def convert_repo_readme(oRepositoryConfig=None): - -# -------------------------------------------------------------------------------------------------------------- - -def sphinx_build(sFormat=None, oRepositoryConfig=None): - """Executes Sphinx to generate the documentation in format 'sFormat'. - """ - - if sFormat is None: - print() - printerror("sFormat is None") - print() - return ERROR - - if oRepositoryConfig is None: - print() - printerror("oRepositoryConfig is None") - print() - return ERROR - - SPHINXBUILD = oRepositoryConfig.Get("SPHINXBUILD") - if SPHINXBUILD is None: - return ERROR - - SOURCEDIR = oRepositoryConfig.Get("SOURCEDIR") - if SOURCEDIR is None: - return ERROR - - BUILDDIR = oRepositoryConfig.Get("BUILDDIR") - if BUILDDIR is None: - return ERROR - - sPython = oRepositoryConfig.Get("sPython") - if sPython is None: - return ERROR - - listCmdLineParts = [] - listCmdLineParts.append(f"\"{sPython}\"") - listCmdLineParts.append(f"\"{SPHINXBUILD}\"") - listCmdLineParts.append(f"-M {sFormat}") - listCmdLineParts.append(f"\"{SOURCEDIR}\"") - listCmdLineParts.append(f"\"{BUILDDIR}\"") - - sCmdLine = " ".join(listCmdLineParts) - del listCmdLineParts - listCmdLineParts = shlex.split(sCmdLine) - - # -- debug - sCmdLine = " ".join(listCmdLineParts) - print() - print("Now executing command line:\n" + sCmdLine) - print() - - nReturn = ERROR - try: - nReturn = subprocess.call(listCmdLineParts) - except Exception as ex: - printexception(str(ex)) - print() - return ERROR - - print() - - return nReturn - -# eof def sphinx_build(sFormat=None, oRepositoryConfig=None): - -# -------------------------------------------------------------------------------------------------------------- - -def gen_doc_pdf(oRepositoryConfig=None): - """Executes LaTeX to generate the documentation in PDF format (based on previously generated LaTeX format). - """ - - if oRepositoryConfig is None: - print() - printerror("oRepositoryConfig is None") - print() - return ERROR - - print() # empty line after Sphinx console output - for better readibility - - BUILDDIR = oRepositoryConfig.Get("BUILDDIR") - if BUILDDIR is None: - return ERROR - - sLaTeXInterpreter = oRepositoryConfig.Get("sLaTeXInterpreter") - if sLaTeXInterpreter is None: - return ERROR - - # LaTeX sources are placed by Sphinx within subfolder 'latex' of folder 'BUILDDIR' - sLaTeXRoot = os.path.normpath(f"{BUILDDIR}/latex") # not part of oRepositoryConfig; only needed here! - if os.path.isdir(sLaTeXRoot) is False: - print() - printerror(f"Missing LaTeX documentation folder '{sLaTeXRoot}'") - print() - return ERROR - - # Not really sure which name of main tex file we can expect here; therefore scanning for tex files and compute all of them - # (but usually only one tex file is expected) - listTeXFiles = [] - for root, dirs, files in os.walk(sLaTeXRoot): - for name in files: - if name.lower().endswith(".tex"): - sTeXFile = os.path.join(root, name) - listTeXFiles.append(sTeXFile) - - if len(listTeXFiles) == 0: - print() - printerror(f"Missing LaTeX source files (.tex) within '{sLaTeXRoot}'") - print() - return ERROR - - for sTeXFile in listTeXFiles: - print(COLBY + f"* Rendering file '{sTeXFile}'") - print() - - listCmdLineParts = [] - listCmdLineParts.append(f"\"{sLaTeXInterpreter}\"") - listCmdLineParts.append(f"\"{sTeXFile}\"") - - sCmdLine = " ".join(listCmdLineParts) - del listCmdLineParts - listCmdLineParts = shlex.split(sCmdLine) - - # -- debug - # sCmdLine = " ".join(listCmdLineParts) - # print("Now executing command line:\n" + sCmdLine) - # print() - - nReturn = ERROR - cwd = os.getcwd() # we have to save cwd because later we have to change - try: - os.chdir(sLaTeXRoot) # otherwise LaTeX compiler is not able to find files inside - nReturn = subprocess.call(listCmdLineParts) - print() - print(f"LaTeX compiler returned {nReturn}") - print() - os.chdir(cwd) # restore original value - except Exception as ex: - printexception(str(ex)) - print() - os.chdir(cwd) # restore original value - return ERROR - - if nReturn != SUCCESS: - printerror(f"LaTeX compiler not returned expected value {SUCCESS}") - print() - return nReturn - - # finally let's see what has been generated - for root, dirs, files in os.walk(sLaTeXRoot): - for name in files: - if name.lower().endswith(".pdf"): - sPDFFile = os.path.join(root, name) - print(COLBY + f"* Created '{sPDFFile}'") - print() - - return nReturn - -# eof def gen_doc_pdf(oRepositoryConfig=None): - -# -------------------------------------------------------------------------------------------------------------- - -# -- setting up the repository configuration (relative to the path of this script) -oRepositoryConfig = None -sReferencePath = os.path.dirname(os.path.abspath(sys.argv[0])) -try: - oRepositoryConfig = CConfig(sReferencePath) -except Exception as ex: - print() - printexception(str(ex)) - print() - sys.exit(ERROR) - -# -- converting the main repository README from 'rst' to 'md' format -nReturn = convert_repo_readme(oRepositoryConfig) -if nReturn != SUCCESS: - printerror("convert_repo_readme with 'README.rst' failed") - print() - sys.exit(nReturn) - -# -- removing previous output in documentation build folder -nReturn = sphinx_build("clean", oRepositoryConfig) -if nReturn != SUCCESS: - printerror("sphinx_build 'clean' failed") - print() - sys.exit(nReturn) - -# -- generating new documentation in HTML format -nReturn = sphinx_build("html", oRepositoryConfig) -if nReturn != SUCCESS: - printerror("sphinx_build 'html' failed") - print() - sys.exit(nReturn) - -# -- generating new documentation in LaTeX format -nReturn = sphinx_build("latex", oRepositoryConfig) -if nReturn != SUCCESS: - printerror("sphinx_build 'latex' failed") - print() - sys.exit(nReturn) - -# -- generating new documentation in PDF format (requires configured LaTeX) -sLaTeXInterpreter = oRepositoryConfig.Get('sLaTeXInterpreter') -if sLaTeXInterpreter is not None: - print("Calling LaTeX PDF renderer (1/2)") - nReturn = gen_doc_pdf(oRepositoryConfig) - if nReturn != SUCCESS: - printerror("PDF generation failed") - print() - sys.exit(nReturn) - print("Calling LaTeX PDF renderer (2/2) - to get referencs and table of content updated") - nReturn = gen_doc_pdf(oRepositoryConfig) - if nReturn != SUCCESS: - printerror("PDF generation failed") - print() - sys.exit(nReturn) - -# -------------------------------------------------------------------------------------------------------------- - -print(COLBG + "sphinx-makeall done") -print() -sys.exit(SUCCESS) - -# -------------------------------------------------------------------------------------------------------------- -