The GUI looks as though you cannot, but actually, you can. So, you do not need to use Basic just in order to locate your macro in-document.
Topics
About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: The Python programming language
The table of contents of this article
- Starting Context
- Target Context
- Orientation
- Main Body
- 1: You Can Locate Your Python Macro in Any 'odt', 'ods', 'odp', 'odb', 'odf', or 'odg' File
- 2: How to Locate Your Python Macro in Any 'odt', 'ods', 'odp', 'odb', 'odf', or 'odg' File
- 3: Testing
Starting Context
- The reader has a basic knowledge on LibreOffice or Apache OpenOffice.
- The reader has a basic knowledge on the Python programming language.
Target Context
- The reader will know how to create his or her in-document Python macro for LibreOffice or Apache OpenOffice.
Orientation
Creating your user-owned or application-owned Python macro has been addressed in the previous article.
Executing any macro (user-owned, application-owned, or in-document) function from your UNO program will be addressed in a future article.
The details of Python macro programming will be dug into in some future articles.
Using an external full Python has been addressed in a previous article.
Stage Direction
Here are Hypothesizer 7, Objector 49A, and Objector 49B in front of a computer.
Main Body
1: You Can Locate Your Python Macro in Any 'odt', 'ods', 'odp', 'odb', 'odf', or 'odg' File
Hypothesizer 7
You can locate your Python macro in any 'odt', 'ods', 'odp', 'odb', 'odf', or 'odg' file.
Objector 49A
You aren't serious, right?
Hypothesizer 7
I am very serious, sir.
Objector 49A
But I've heard I can't.
Hypothesizer 7
Then, the one who said so did not know how.
Objector 49A
Everyone said so!
Stage Direction
Hypothesizer 7 shakes his head plaintively.
Hypothesizer 7
I did not, sir . . .
Objector 49A
But the GUI doesn't let me.
Hypothesizer 7
Well, there seems to be a prevalent misconception that the GUI shows what can be done; actually, the GUI shows only what can be done via the GUI.
Objector 49A
. . . Seeing the GUI, everybody will think that any in-document Python macro is not allowed!
Stage Direction
Hypothesizer 7 shakes his head plaintively.
Hypothesizer 7
I will not, sir. You know, the word, "everybody", should not be used carelessly.
Objector 49B
So, you tell me to execute a command to put my macro in-document?
Hypothesizer 7
Madam, in fact, it is more than a single command.
Objector 49B
That sounds difficult!
Hypothesizer 7
Any one who attempts to create a macro should be able to do it all right, in my opinion.
Objector 49A
You shouldn't use "Any one" carelessly!
Hypothesizer 7
I have used it carefully, sir. I literally mean it: you should learn, if you really cannot now.
2: How to Locate Your Python Macro in Any 'odt', 'ods', 'odp', 'odb', 'odf', or 'odg' File
Hypothesizer 7
Any 'odt', 'ods', 'odp', 'odb', 'odf', or 'odg' file is really a ZIP file.
Objector 49B
Is that so? Can I just expand the file?
Hypothesizer 7
Yes, you can.
For example, let us expand this 'odt' file, 'MacroTests.odt', into a directory, 'fileIngredients', like this, in this Linux machine.
@bash Source Code
l_targetFileName="MacroTests.odt"; l_expansionDirectoryName="fileIngredients"; mkdir "./${l_expansionDirectoryName}"; cp "./${l_targetFileName}" "./${l_expansionDirectoryName}/."; cd "./${l_expansionDirectoryName}"; unzip "./${l_targetFileName}"; rm "./${l_targetFileName}"
Objector 49B
. . . Well, I don't use Linux, but I can just expand the file in whatever way, right?
Hypothesizer 7
Right, madam.
Then, we copy the Python macro file under the 'Scripts/python' directory (which you have to create) in the expansion directory, like this, where 'PythonEnvironmentChecker.py' is the Python macro file.
@bash Source Code
l_macroBasePath=~/"myData/development/pythonEnvironmentCheckerUnoExtension/source/python"; l_macroPackagePath="theBiasPlanet/pythonEnvironmentChecker/macros"; l_macroFileName="PythonEnvironmentChecker.py"; mkdir -p "./Scripts/python/${l_macroPackagePath}"; cp "${l_macroBasePath}/${l_macroPackagePath}/${l_macroFileName}" "./Scripts/python/${l_macroPackagePath}/."
Objector 49B
"theBiasPlanet/pythonEnvironmentChecker/macros"?
Hypothesizer 7
That is just in order to demonstrate that you can put the Python module in any package; of course, you can put the Python module file directly in the 'Scripts/python' directory, if you will.
Objector 49B
Hmm.
Hypothesizer 7
Then, we edit the 'META-INF/manifest.xml' file to add this in the 'manifest:manifest' node, after the existing child nodes.
@xml Source Code
<manifest:file-entry manifest:full-path="Scripts" manifest:media-type="application/binary">
</manifest:file-entry>
<manifest:file-entry manifest:full-path="Scripts/python" manifest:media-type="application/binary">
</manifest:file-entry>
<manifest:file-entry manifest:full-path="Scripts/python/theBiasPlanet" manifest:media-type="application/binary">
</manifest:file-entry>
<manifest:file-entry manifest:full-path="Scripts/python/theBiasPlanet/pythonEnvironmentChecker" manifest:media-type="application/binary">
</manifest:file-entry>
<manifest:file-entry manifest:full-path="Scripts/python/theBiasPlanet/pythonEnvironmentChecker/macros" manifest:media-type="application/binary">
</manifest:file-entry>
<manifest:file-entry manifest:full-path="Scripts/python/theBiasPlanet/pythonEnvironmentChecker/macros/PythonEnvironmentChecker.py" manifest:media-type="">
</manifest:file-entry>
Objector 49B
It's bothersome . . .
Hypothesizer 7
But you can do it, right?
Objector 49B
I don't say I can't, but it's bothersome.
Hypothesizer 7
The last step is to re-archive the expansion directory, like this.
@bash Source Code
zip -r "../${l_targetFileName}" *
Objector 49B
They are bothersome steps.
Hypothesizer 7
On the contrary, you can just execute a shell script or batch file like this, although you have to edit the manifest file still.
@bash Source Code
#!/bin/bash
# supposed to be executed at the target file directory
l_targetFileName="$1"
l_modeName="$2" # 'add' or 'remove'
l_macroBasePath="$3" # empty if the mode name is 'remove'
l_macroPackagePath="$4" # can be empty if the mode name is 'remove' (the 'Scripts' path will be removed)
l_macroFileName="$5" # can be empty if the mode name is 'remove'
l_expansionDirectoryName="fileIngredients"
set -x
cp "./${l_targetFileName}" "./${l_targetFileName}.save"
mkdir "./${l_expansionDirectoryName}"
mv "./${l_targetFileName}" "./${l_expansionDirectoryName}/."
cd "./${l_expansionDirectoryName}"
unzip "./${l_targetFileName}"
rm "./${l_targetFileName}"
if [ "${l_modeName}" == "add" ]; then
if [ ! -d "./Scripts/python/${l_macroPackagePath}" ]; then
mkdir -p "./Scripts/python/${l_macroPackagePath}"
fi
cp "${l_macroBasePath}/${l_macroPackagePath}/${l_macroFileName}" "./Scripts/python/${l_macroPackagePath}/."
else
if [ "${l_macroPackagePath}" != "" ]; then
rm -r "./Scripts/python/${l_macroPackagePath}/${l_macroFileName}"
else
rm -r "./Scripts"
fi
fi
vim "META-INF/manifest.xml"
zip -r "../${l_targetFileName}" *
cd ..
rm -r "./${l_expansionDirectoryName}"
Objector 49B
The editing part is bothersome.
Hypothesizer 7
You can create a Python program that do the editing, like this.
theBiasPlanet/inDocumentPythonMacrosRegistrar/programs/InDocumentPythonMacrosRegistrarConsoleProgram.py
@Python Source Code
from typing import List
from typing import Optional
from typing import TextIO
from typing import cast
from collections import OrderedDict
import os
from pathlib import Path
import shutil
import sys
import xml.sax
import xml.sax.handler
from xml.sax.handler import ContentHandler
from xml.sax.xmlreader import AttributesImpl
from xml.sax.xmlreader import XMLReader
from theBiasPlanet.coreUtilities.constantsGroups.DefaultValuesConstantsGroup import DefaultValuesConstantsGroup
from theBiasPlanet.coreUtilities.constantsGroups.FileNameSuffixesConstantsGroup import FileNameSuffixesConstantsGroup
from theBiasPlanet.coreUtilities.constantsGroups.FileOpenModeNamesConstantsGroup import FileOpenModeNamesConstantsGroup
from theBiasPlanet.coreUtilities.constantsGroups.GeneralConstantsConstantsGroup import GeneralConstantsConstantsGroup
from theBiasPlanet.coreUtilities.constantsGroups.XmlExpressionsConstantsGroup import XmlExpressionsConstantsGroup
from theBiasPlanet.coreUtilities.messagingHandling.Publisher import Publisher
from theBiasPlanet.coreUtilities.xmlDataHandling.XmlDatumHandler import XmlDatumHandler
"""
mode name: "add" or "remove"
"""
class InDocumentPythonMacrosRegistrarConsoleProgram:
c_manifestFileRelativePath: Path = Path ("META-INF/manifest.xml")
c_scriptsBaseDirectoryRelativePath: Path = Path ("Scripts")
c_pythonBaseDirectoryRelativePath: Path = c_scriptsBaseDirectoryRelativePath.joinpath ("python")
c_addModeName = "add"
c_removeModeName = "remove"
class ManifestDatumParseEventsHandler (ContentHandler):
c_rootElementName: str = "manifest:manifest"
c_filePathElementName: str = "manifest:file-entry"
c_pathAttributeName: str = "manifest:full-path"
c_mediaTypeAttributeName: str = "manifest:media-type"
c_binaryMediaTypeName = "application/binary"
c_emptyMediaTypeName = GeneralConstantsConstantsGroup.c_emptyString
c_pythonModuleFileGlobExpression = GeneralConstantsConstantsGroup.c_fileNameFormat.format (GeneralConstantsConstantsGroup.c_doubleAsterisks, FileNameSuffixesConstantsGroup.c_pythonModuleFileNameSuffix)
def __init__ (a_this: "InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler", a_modeName: str, a_concernedPathToIsFoundMap: "OrderedDict [Path, bool]", a_writer: TextIO) -> None:
a_this.i_modeName: str = a_modeName
a_this.i_concernedPathToIsFoundMap: "OrderedDict [Path, bool]" = a_concernedPathToIsFoundMap
a_this.i_writer: TextIO = a_writer
a_this.i_isForOneOfConcernedPaths: bool = False
def startDocument (a_this: "InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler") -> None:
a_this.i_writer.write (GeneralConstantsConstantsGroup.c_lineFormat.format (XmlExpressionsConstantsGroup.c_xml1_0Declaration))
def endDocument (a_this: "InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler") -> None:
None
def startElement (a_this: "InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler", a_elementName: str, a_attributes: AttributesImpl) -> None:
try:
l_pathString: Optional [str] = a_attributes.getValue (InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_pathAttributeName)
l_path: Path = Path (l_pathString)
if a_this.i_modeName == InDocumentPythonMacrosRegistrarConsoleProgram.c_addModeName:
if a_this.i_concernedPathToIsFoundMap.get (l_path) is not None:
a_this.i_isForOneOfConcernedPaths = True
a_this.i_concernedPathToIsFoundMap [l_path] = True
else:
l_concernedPath: Optional [Path] = None
l_isFound: Optional [bool] = None
# There is only one path in it.
for l_concernedPath, l_isFound in a_this.i_concernedPathToIsFoundMap.items ():
None
if l_path == l_concernedPath or l_path.match ("{0:s}{1:s}{2:s}".format (str (l_concernedPath), GeneralConstantsConstantsGroup.c_linuxDirectoriesDelimiter, GeneralConstantsConstantsGroup.c_doubleAsterisks)):
a_this.i_isForOneOfConcernedPaths = True
except (KeyError) as l_exception:
None
if not a_this.i_modeName == InDocumentPythonMacrosRegistrarConsoleProgram.c_removeModeName or not a_this.i_isForOneOfConcernedPaths:
a_this.i_writer.write (XmlDatumHandler.getElementOpenString (a_elementName, a_attributes))
def endElement (a_this: "InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler", a_elementName: str) -> None:
if a_elementName == InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_rootElementName and a_this.i_modeName == InDocumentPythonMacrosRegistrarConsoleProgram.c_addModeName:
l_path: Optional [Path] = None
l_isFound: Optional [bool] = None
for l_path, l_isFound in a_this.i_concernedPathToIsFoundMap.items ():
if not l_isFound:
l_mediaTypeName: Optional [str] = None
if not l_path.match (InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_pythonModuleFileGlobExpression):
l_mediaTypeName = InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_binaryMediaTypeName
else:
l_mediaTypeName = InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_emptyMediaTypeName
l_attributes: AttributesImpl = AttributesImpl ({InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_pathAttributeName: str (l_path), InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_mediaTypeAttributeName: l_mediaTypeName})
a_this.i_writer.write (" {0:s}".format (GeneralConstantsConstantsGroup.c_lineFormat).format (XmlDatumHandler.getElementOpenString (InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_filePathElementName, l_attributes)))
a_this.i_writer.write (" {0:s}".format (GeneralConstantsConstantsGroup.c_lineFormat).format (XmlDatumHandler.getElementCloseString (InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler.c_filePathElementName)))
if not a_this.i_modeName == InDocumentPythonMacrosRegistrarConsoleProgram.c_removeModeName or not a_this.i_isForOneOfConcernedPaths:
a_this.i_writer.write (XmlDatumHandler.getElementCloseString (a_elementName))
a_this.i_isForOneOfConcernedPaths = False
def characters (a_this: "InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler", a_contents: str) -> None:
if not a_this.i_modeName == InDocumentPythonMacrosRegistrarConsoleProgram.c_removeModeName or not a_this.i_isForOneOfConcernedPaths:
a_this.i_writer.write (a_contents)
"""
arguments:
1 -> the file ingredients base directory path string
2 -> the mode name: 'add' or 'remove'
3 -> the target path string: if the mode name is 'remove' and this is empty, the 'Scripts' directory will be removed.
"""
@staticmethod
def main (a_arguments: List [str]) -> None:
if len (a_arguments) != 4:
Publisher.logErrorInformation ("The arguments have to be these:\n1) the file ingredients base directory path string\n2) the mode name: 'add' or 'remove'\n3) the target path string: if the mode name is 'remove' and this is empty, the 'Scripts' directory will be removed.\n")
exit GeneralConstantsConstantsGroup.c_errorResult
l_fileIngredientsBaseDirectoryPath: Path = Path (a_arguments [1])
l_modeName: str = a_arguments [2]
if l_modeName != InDocumentPythonMacrosRegistrarConsoleProgram.c_addModeName and l_modeName != InDocumentPythonMacrosRegistrarConsoleProgram.c_removeModeName:
Publisher.logErrorInformation ("The mode name has to be 'add' or 'remove'\n")
exit GeneralConstantsConstantsGroup.c_errorResult
l_targetPath: Path = Path (a_arguments [3])
if l_modeName == InDocumentPythonMacrosRegistrarConsoleProgram.c_addModeName and l_targetPath == GeneralConstantsConstantsGroup.c_emptyString:
Publisher.logErrorInformation ("The target path string cannot be empty for the 'add' mode\n")
exit GeneralConstantsConstantsGroup.c_errorResult
l_manifestFilePath: Path = l_fileIngredientsBaseDirectoryPath.joinpath (InDocumentPythonMacrosRegistrarConsoleProgram.c_manifestFileRelativePath)
l_manifestModifiedFilePath: Path = l_fileIngredientsBaseDirectoryPath.joinpath (Path (GeneralConstantsConstantsGroup.c_fileNameFormat.format (str (InDocumentPythonMacrosRegistrarConsoleProgram.c_manifestFileRelativePath), FileNameSuffixesConstantsGroup.c_modifiedFileNameSuffix)))
l_concernedPathToIsFoundMap: "OrderedDict [Path, bool]" = OrderedDict ()
if l_modeName == InDocumentPythonMacrosRegistrarConsoleProgram.c_addModeName:
l_concernedPathToIsFoundMap.update ({InDocumentPythonMacrosRegistrarConsoleProgram.c_pythonBaseDirectoryRelativePath.parent: False, InDocumentPythonMacrosRegistrarConsoleProgram.c_pythonBaseDirectoryRelativePath: False})
l_path: Optional [Path] = None
for l_path in reversed (l_targetPath.parents):
if l_path != GeneralConstantsConstantsGroup.c_currentDirectoryPath:
l_concernedPathToIsFoundMap.update ({InDocumentPythonMacrosRegistrarConsoleProgram.c_pythonBaseDirectoryRelativePath.joinpath (l_path): False})
if l_targetPath != GeneralConstantsConstantsGroup.c_emptyString:
l_concernedPathToIsFoundMap.update ({InDocumentPythonMacrosRegistrarConsoleProgram.c_pythonBaseDirectoryRelativePath.joinpath (l_targetPath): False})
else:
l_concernedPathToIsFoundMap.update ({InDocumentPythonMacrosRegistrarConsoleProgram.c_scriptsBaseDirectoryRelativePath: False})
l_saxParser: XMLReader = xml.sax.make_parser ( [DefaultValuesConstantsGroup.c_saxParserModuleName])
l_saxParser.setFeature (xml.sax.handler.feature_namespaces, False)
l_manifestFileReader: Optional [TextIO] = None
l_manifestFileWriter: Optional [TextIO] = None
try:
l_manifestFileReader = cast (TextIO, open (l_manifestFilePath, FileOpenModeNamesConstantsGroup.c_readModeName))
l_manifestFileWriter = cast (TextIO, open (l_manifestModifiedFilePath, FileOpenModeNamesConstantsGroup.c_eraseAndWriteModeName))
l_manifestDatumParseEventsHandler: "InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler" = InDocumentPythonMacrosRegistrarConsoleProgram.ManifestDatumParseEventsHandler (l_modeName, l_concernedPathToIsFoundMap, l_manifestFileWriter)
l_saxParser.setContentHandler (l_manifestDatumParseEventsHandler)
l_saxParser.parse (l_manifestFileReader)
os.remove (l_manifestFilePath)
shutil.move (str (l_manifestModifiedFilePath), str (l_manifestFilePath))
except (Exception) as l_exception:
Publisher.logErrorInformation (l_exception)
if l_manifestFileWriter is not None:
l_manifestFileWriter.close ()
if l_manifestFileReader is not None:
l_manifestFileReader.close ()
if __name__ == GeneralConstantsConstantsGroup.c_pythonMainModuleName:
InDocumentPythonMacrosRegistrarConsoleProgram.main (sys.argv)
theBiasPlanet/coreUtilities/constantsGroups/DefaultValuesConstantsGroup.py
@Python Source Code
class DefaultValuesConstantsGroup:
~
c_saxParserModuleName: str = "xml.sax.expatreader"
theBiasPlanet/coreUtilities/constantsGroups/FileNameSuffixesConstantsGroup.py
@Python Source Code
class FileNameSuffixesConstantsGroup:
~
c_pythonModuleFileNameSuffix: str = "py"
~
c_modifiedFileNameSuffix: str = "modified"
theBiasPlanet/coreUtilities/constantsGroups/FileOpenModeNamesConstantsGroup.py
@Python Source Code
class FileOpenModeNamesConstantsGroup:
c_readModeName: str = "r"
c_eraseAndWriteModeName: str = "w"
~
theBiasPlanet/coreUtilities/constantsGroups/GeneralConstantsConstantsGroup.py
@Python Source Code
from pathlib import Path
import sys
from collections import OrderedDict
from theBiasPlanet.coreUtilities.constantsGroups.FileNameSuffixesConstantsGroup import FileNameSuffixesConstantsGroup
class GeneralConstantsConstantsGroup:
~
c_errorResult: int = -1
~
c_emptyString: str = ""
~
c_doubleAsterisks: str = "{0:s}{0:s}".format (c_asteriskCharacter)
~
c_linuxDirectoriesDelimiter: str = '/' # char
~
c_currentDirectoryPath: Path = Path (".")
~
c_fileNameFormat: str = "{{:s}}{:s}{{:s}}".format (c_fileNameElementsDelimiter)
c_lineFormat: str = "{{:s}}{:s}".format (c_newLineCharacter)
~
c_pythonMainModuleName: str = "__main__"
~
theBiasPlanet/coreUtilities/constantsGroups/XmlExpressionsConstantsGroup.py
@Python Source Code
class XmlExpressionsConstantsGroup:
c_xml1_0Declaration: str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
~
'theBiasPlanet/coreUtilities/messagingHandling/Publisher.py' is totally omitted because the used method just writes logs.
theBiasPlanet/coreUtilities/xmlDataHandling/XmlDatumHandler.py
@Python Source Code
from typing import List
from typing import Optional
import xml.sax.saxutils
from xml.sax.xmlreader import AttributesImpl
from theBiasPlanet.coreUtilities.constantsGroups.GeneralConstantsConstantsGroup import GeneralConstantsConstantsGroup
class XmlDatumHandler:
@staticmethod
def getElementOpenString (a_elementName: str, a_attributes: AttributesImpl) -> str:
l_attributeStrings: List [str] = []
l_attributeName: Optional [str] = None
for l_attributeName in a_attributes.getNames ():
l_attributeStrings.append ("{0:s}={1:s}".format (l_attributeName, xml.sax.saxutils.quoteattr (a_attributes.getValue (l_attributeName))))
l_attributesString = " ".join (l_attributeStrings)
return GeneralConstantsConstantsGroup.c_quotedByAngleBracketsFormat.format ("{0:s} {1:s}".format (a_elementName, l_attributesString))
@staticmethod
def getElementCloseString (a_elementName: str) -> str:
return GeneralConstantsConstantsGroup.c_quotedByAngleBracketsFormat.format ("/{0:s}".format (a_elementName))
Objector 49B
. . . Um? How can I use it?
Hypothesizer 7
In the above shell script, you can just replace 'vim "META-INF/manifest.xml"' with 'bash -c "export PYTHONPATH=\"${PYTHONPATH}:/home/%user name%/myData/development/inDocumentPythonMacrosRegistrar/target:/home/%user name%/myData/development/coreUtilities/target\"; python3 -m theBiasPlanet.inDocumentPythonMacrosRegistrar.programs.InDocumentPythonMacrosRegistrarConsoleProgram \".\" \"${l_modeName}\" \"./${l_macroPackagePath}/${l_macroFileName}\""', while, of course, the Python modules paths have to be configured appropriately.
3: Testing
Hypothesizer 7
Let us open the file and see that the Python macro is indeed recognized by LibreOffice or Apache OpenOffice, by selecting 'Tools' -> 'Macros' -> 'Run Macro...' -> '%the file name%' -> 'theBiasPlanet' -> 'pythonEnvironmentChecker' -> 'macros' -> 'PythonEnvironmentChecker' -> 'checkPythonEnvironment'.
Objector 49A
. . . Oh, it's there! Does it run?
Hypothesizer 7
If the macro is all right, of course.