2020-11-15

54: Import Any Module into Your LibreOffice Python Macro, Part 2

<The previous article in this series | The table of contents of this series | The next article in this series>

Title: 54: Import Any Module into Your LibreOffice Python Macro, Part 2

Also any in-document module can be imported into your macro or any module imported by your macro. Here is how.

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


  • The reader will know how to import any in-document module into his or her LibreOffice or Apache OpenOffice Python macro module.

Orientation


Using an external full Python has been addressed in a previous article.

Creating your user-owned or application-owned Python macro has been addressed in a previous article.

Creating your in-document Python macro has been addressed in a previous article.

Creating your in-extension Python macro has been addressed in a previous article.

Importing any non-in-document module has been addressed in the 1st part of this article.

The details of Python macro programming will be dug into in some future articles.


Main Body

Stage Direction
Here are Hypothesizer 7, Objector 54A, and Objector 54B in front of a computer.


1: Any In-Document Module Is Another Story Because . . .


Hypothesizer 7
If the module to be imported is not in-document, the 1st part of this article should be enough.

Any in-document module is another story because it is not in any operating system file.

Objector 54A
It IS in the document operating system file.

Hypothesizer 7
Certainly, sir, but what I mean is that the document operating system file is not the module file, but a ZIP file that contains the module file.

Objector 54A
So, I was right.

Hypothesizer 7
Well, to be exact, any in-document module is not DIRECTLY in any operating system file.

Anyway, the issue is that such a module location cannot be set in 'PYUNO_LOADER_PYTHONPATH' or 'sys.path'.


2: The Module Will Be Able to Be Imported, If the Module Contents Are Gotten


Hypothesizer 7
In fact, if the module contents are gotten, the module will be able to be imported all right.

Objector 54B
. . . So, I can just create the source loader?

Hypothesizer 7
If you can get the module contents, yes, madam.

Objector 54B
I can extract the module file from the ZIP file (the document file is a ZIP file, isn't it?), can't I?

Hypothesizer 7
That may be an option, if the document file is not encrypted.

Objector 54B
Well, what if?

Hypothesizer 7
The contents of the module file is encrypted.

Objector 54B
. . . I can decrypt the contents, can't I?

Hypothesizer 7
Can you?

Objector 54B
. . . Can't I?

Hypothesizer 7
Well, logically speaking, it should be possible if you learn the logic from the LibreOffice or Apache OpenOffice source files, and of course, you make the user provide the password.

Objector 54B
. . .


3: The Module Contents Can Be Gotten from the Opened Document, If the URL Is Known


Hypothesizer 7
In fact, the modules contents can be gotten from the opened document, like this, where 'l_remoteUnoObjectsContextInXComponentContext' is the UNO objects context to the LibreOffice or Apache OpenOffice instance, 'a_url' is the module URL, and the return is a 'bytes' datum.

@Python Source Code
from uno import ByteSequence
from com.sun.star.io import XInputStream
from com.sun.star.ucb import XSimpleFileAccess

		l_simpleFilesAccessUnoService: XSimpleFileAccess = cast (XSimpleFileAccess, l_remoteUnoObjectsContextInXComponentContext.getServiceManager ().createInstanceWithContext ("com.sun.star.ucb.SimpleFileAccess", l_remoteUnoObjectsContextInXComponentContext))
		try:
			l_inputStream: "XInputStream" = l_simpleFilesAccessUnoService.openFileRead (a_url)
			l_readLength: int = 0
			l_inputDatum: ByteSequence = ByteSequence (b"")
			l_inputBuffer: ByteSequence = None
			while True:
				l_readLength, l_inputBuffer = l_inputStream.readBytes (None, 1024)
				l_inputDatum = l_inputDatum + l_inputBuffer
				if l_readLength < 1024:
					break
		finally:
			if l_inputStream is not None:
				l_inputStream.closeInput
		return l_inputDatum.value

Objector 54A
. . . Huh? "UNO objects context"? What is that?

Hypothesizer 7
. . . Please do not try to do without learning the basics.

Objector 54A
"basics"? Are you kidding me? They are beneath me.

Hypothesizer 7
. . . If your program is a macro, 'XSCRIPTCONTEXT.getComponentContext ()' returns one.

Objector 54A
So, what is the URL?

Hypothesizer 7
The URL is like 'vnd.sun.star.tdoc:/1/Scripts/python/theBiasPlanet/pythonEnvironmentChecker/InDocumentModuleTest.py'.

Objector 54A
So, I can just replace the part after 'Scripts/python'?

Hypothesizer 7
In fact, that number, '1', is the document loading number.

Objector 54A
What do you mean?

Hypothesizer 7
The 1st loaded document is given '1'; the second '2'; etc., since the instance was started.

Objector 54A
How, the hell, am I supposed to know the number of a specific document?

Hypothesizer 7
Well, there can be some ways.


4: Supposing That the Module Is Imported into a Macro in the Same Document


Hypothesizer 7
Let us suppose that the module is imported into a macro in the same document, which is. I suppose, usual.

Objector 54B
How else can it be?

Hypothesizer 7
The module might be imported into a macro in another document, or a user-owned, application-owned, or in-extension macro.

Objector 54B
That's unlikely, I suppose.

Hypothesizer 7
If so, it is very convenient for me, and you.

Objector 54B
So, supposing so . . .

Hypothesizer 7
In fact, in a section of the 1st part of this article, I have already implemented a logic that sets the macro module URL into a variable ('s_sourceFileUrl') in the macro module.

Objector 54B
. . . Hmm, you mean the URL of the importing macro, not the URL of the imported module, which I really need?

Hypothesizer 7
Yes, but the document loading number, which you need, is the same.

Objector 54B
Certainly.


5: But If the Module Is Imported into a Module Imported into a Macro in the Same Document?


Objector 54B
But if the module is imported into a module (of course, in the same document) imported into a macro in the same document?

Hypothesizer 7
So, let us make the macro set the URL of the imported module into the imported module.

Objector 54B
Ah, of course, so, URLs are set by turns.

Hypothesizer 7
Implementation-wise, the source loader can set the URL into the imported module.


6: Supposing That the Module Is Imported into a Macro or Module Not in the Same Document


Objector 54A
Then, do suppose that the module is imported into a macro or module NOT in the same document.

Hypothesizer 7
Well, do you really need that, sir?

Objector 54A
I don't really, at least, for the time being, but do so anyway.

Hypothesizer 7
Why, sir?

Objector 54A
Because I won't let you get away with not fulfilling your promise that any module can be imported.

Hypothesizer 7
Fair enough. There can be some ways, but I would prepare a macro in the document, that (the macro) returns the document Python modules base URL.

Objector 54A
. . . How can I call the macro?

Hypothesizer 7
The way introduced in a previous article should do.

Objector 54A
. . . Well, what other ways?

Hypothesizer 7
Well, I do not know any more recommendable way, but maybe, you could create a document opening events handler that records document openings, or just do brute force tries, for example.


7: The Code of a Source Loader and a Module Importer


Hypothesizer 7
This is the code of a source loader and a module importer.

theBiasPlanet/unoUtilities/pythonSourceLoader/UnoExtendedPythonSourceLoader.py

@Python Source Code
from typing import Union
from typing import cast
from importlib.abc import SourceLoader
import uno
from uno import ByteSequence
from com.sun.star.io import XInputStream
from com.sun.star.ucb import XSimpleFileAccess
from com.sun.star.uno import XComponentContext

class UnoExtendedPythonSourceLoader (SourceLoader):
	c_readingBlockSize: int = 1024
	
	def __init__ (a_this: "UnoExtendedPythonSourceLoader", a_remoteUnoObjectsContextInXComponentContext: "XComponentContext", a_uriPrefix: str) -> None:
		a_this.i_simpleFilesAccessUnoService: XSimpleFileAccess = None
		a_this.i_uriPrefix: str = a_uriPrefix
		
		a_this.i_simpleFilesAccessUnoService = cast (XSimpleFileAccess, a_remoteUnoObjectsContextInXComponentContext.getServiceManager ().createInstanceWithContext ("com.sun.star.ucb.SimpleFileAccess", a_remoteUnoObjectsContextInXComponentContext))
	
	def get_filename (a_this: "UnoExtendedPythonSourceLoader", a_moduleName: str) -> str:
		return "{0:s}{1:s}.{2:s}".format (a_this.i_uriPrefix, a_moduleName.replace (".", "/"), "py")
	
	def get_data (a_this: "UnoExtendedPythonSourceLoader", a_url: Union [bytes, str]) -> bytes:
		try:
			l_inputStream: "XInputStream" = a_this.i_simpleFilesAccessUnoService.openFileRead (a_url)
			l_readLength: int = 0
			l_inputDatum: ByteSequence = ByteSequence (b"")
			l_inputBuffer: ByteSequence = None
			while True:
				l_readLength, l_inputBuffer = l_inputStream.readBytes (None, 1024)
				l_inputDatum = l_inputDatum + l_inputBuffer
				if l_readLength < 1024:
					break
		finally:
			if l_inputStream is not None:
				l_inputStream.closeInput
		return l_inputDatum.value

theBiasPlanet/unoUtilities/pythonModuleImporter/UnoExtendedPythonModuleImporter.py

@Python Source Code
from typing import Optional
from collections import OrderedDict
import sys
from types import ModuleType
from theBiasPlanet.unoUtilities.pythonSourceLoader.UnoExtendedPythonSourceLoader import UnoExtendedPythonSourceLoader

class UnoExtendedPythonModuleImporter:
	c_pythonModulesBaseDirectoryIndicator: str = "/Scripts/python/"
	c_sourceFileUrlModulePropertyName: str = "s_sourceFileUrl"
	
	@staticmethod
	def getUriPrefix (a_sourceFileUrl: str) -> str:
		l_uriPrefix: str = a_sourceFileUrl [0: a_sourceFileUrl.find (UnoExtendedPythonModuleImporter.c_pythonModulesBaseDirectoryIndicator) + len (UnoExtendedPythonModuleImporter.c_pythonModulesBaseDirectoryIndicator)]
		return l_uriPrefix
	
	@staticmethod
	def importModule (a_unoExtendedPythonSourceLoader: "UnoExtendedPythonSourceLoader", a_moduleName: str, a_setModuleProperties: "Optional [OrderedDict [str, object]]") -> ModuleType:
		l_pythonModule = ModuleType (a_moduleName)
		l_pythonModule.__dict__ [UnoExtendedPythonModuleImporter.c_sourceFileUrlModulePropertyName] = a_unoExtendedPythonSourceLoader.get_filename (a_moduleName)
		l_propertyName: str
		l_propertyValue: object
		for l_propertyName, l_propertyValue in a_setModuleProperties.items ():
			l_pythonModule.__dict__ [l_propertyName] = l_propertyValue
		a_unoExtendedPythonSourceLoader.exec_module (l_pythonModule)
		sys.modules [a_moduleName] = l_pythonModule
		return l_pythonModule

Objector 54B
Well . . .

Hypothesizer 7
The constructor of the source loader takes the UNO objects context to the LibreOffice or Apache OpenOffice instance and the URL prefix like 'vnd.sun.star.tdoc:/1/Scripts/python/'.

The importing method of the module importer takes the module name and the properties to be set into the imported module.


8: An Example Usage


Hypothesizer 7
This is an example usage that imports an in-the-same-document module, 'theBiasPlanet.pythonEnvironmentChecker.InDocumentModuleTest', into a module, which may be or not be a macro module.

@Python Source Code
from collections import OrderedDict
~
import sys
~
from types import ModuleType
~
from com.sun.star.script.provider import XScriptContext
~
from theBiasPlanet.unoUtilities.pythonModuleImporter.UnoExtendedPythonModuleImporter import UnoExtendedPythonModuleImporter
from theBiasPlanet.unoUtilities.pythonSourceLoader.UnoExtendedPythonSourceLoader import UnoExtendedPythonSourceLoader

XSCRIPTCONTEXT: XScriptContext

s_unoExtendedPythonSourceLoader: "UnoExtendedPythonSourceLoader" = UnoExtendedPythonSourceLoader (XSCRIPTCONTEXT.getComponentContext (), UnoExtendedPythonModuleImporter.getUriPrefix (s_sourceFileUrl))
s_pythonModule: ModuleType = UnoExtendedPythonModuleImporter.importModule (s_unoExtendedPythonSourceLoader, "theBiasPlanet.pythonEnvironmentChecker.InDocumentModuleTest", OrderedDict ( [ ("XSCRIPTCONTEXT", XSCRIPTCONTEXT)]))
InDocumentModuleTest = s_pythonModule.InDocumentModuleTest

~

def checkPythonEnvironment2 (a_message: str) -> str:
		return "The Python environment: version -> {0:s}, paths -> {1:s}\n".format (sys.version, str (sys.path)) + ", " + InDocumentModuleTest.c_test + ", " + s_sourceFileUrl

Objector 54A
. . . That's somehow fussy.

Hypothesizer 7
Is it? Whatever it looks like, the only thing you have to change is the module name.

Objector 54A
Well . . .

Hypothesizer 7
Note that that 's_sourceFileUrl' is there only because 'pythonscript.py' (modified as in the 1st part of this article) or the module importer has set it there.

Objector 54A
How about a not-in-the-same-document module?

Hypothesizer 7
Well, the only issue is that you have to use an appropriate value instead of 's_sourceFileUrl'.

Objector 54A
How appropriate?

Hypothesizer 7
Very appropriate.

Objector 54A
. . . I don't understand what exactly "a macro in the document, that (the macro) returns the document Python modules base URL" should be like?

Hypothesizer 7
Don't you? The macro will just return 'UnoExtendedPythonModuleImporter.getUriPrefix (s_sourceFileUrl)', like this.

@Python Source Code
def getBaseUrlOfThisDocumentPythonModules () -> str:
	return UnoExtendedPythonModuleImporter.getUriPrefix (s_sourceFileUrl)

As I said, how to call the macro is described in a previous article.


References


<The previous article in this series | The table of contents of this series | The next article in this series>