Showing posts with label Let Me Understand the Python Programming Language. Show all posts
Showing posts with label Let Me Understand the Python Programming Language. Show all posts

2021-06-20

14: Not "Pythonic"? A Joke, Right?

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

I think that code should be less-buggy, more-responsive, higher-throughput, more-maintainable, and what else? Should it be "Pythonic"?

Topics


About: Python

The table of contents of this article


Starting Context


  • The reader knows what Python is and has heard the word, "Pythonic".

Target Context


  • The reader will understand that "Pythonic" is a joke and will become wary of going too far in a kind of jokes.

Orientation


There is an article on how "Duck Typing" is a bad idea except for some limited cases.

There are some articles on some inconveniences of Python (the prohibition of cyclic importing, the difficulty of interrupting a standard input wait).


Main Body

Stage Direction
Hypothesizer 7 soliloquies.


1: Not "Pythonic"? So What?


Hypothesizer 7
Does my code have a bug? Sorry, sir, I will rectify it right away.

Is my code responding badly? Sorry, madam, I will see to it immediately.

Is my code outputting slowly? Very well, boss, I will rethink the algorithm.

Is my code poorly maintainable? Well, fellows, let me see how I can refine it.

Is my code not "Pythonic"? . . . So what?

I aspire to write high-quality, high-performance, high-reasonability code, but I never aspire to write "Pythonic" code.


2: Do You Need Such a Word?


Hypothesizer 7
"Pythonic"? . . . Is my code bad because it is not "Pythonic"? . . . You sound like a religious cult.

Why do you not explain how my code will behave erroneously, work slowly, hamper maintenance, than declare it to be not "Pythonic"?

Do you need such a word?

"Pythonic"? . . . Ah, you are joking, right? Certainly, it is so absurd to be laughable.


3: Be Wary of Going Too Far in a Kind of Jokes


Hypothesizer 7
Many of the Python community seem to favor a kind of jokes.

"Duck Typing"? Is it a good thing? . . . A joke, right? I do not find it funny though.

The inability to allow cyclic importing is not any flaw? . . . Also a joke, right?

I see the pattern: pretending to be so self-righteous to be laughable, right?

But you should not go too far: many are not reading your true intentions.

It will be harmful if someone happens to be taken in to really believe that "Duck Typing" is generally a good idea.


References


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

2021-03-28

13: Python Subprocess: Asynchronously Relay Standard In-Out-Puts

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

That just-wait-for-completion 'subprocess.run' is not my option. A way of inter-processes communication. Interrupt the relays. A utility class.

Topics


About: Python

The table of contents of this article


Starting Context


  • The reader has a basic knowledge on Python.

Target Context


  • The reader will know how to invoke a process and relay the standard output, the standard error output, and the standard input of the process in the background, in Python.

Orientation


There is an article on how to interrupt any standard input wait in Python.

There is an article that introduces an objects pipe in Python.

There is an article that introduces a string pipe in Python.

There are some article that introduce some objects pipes in some other programming languages (Java, C++, and C#).


Main Body

Stage Direction
Hypothesizer 7 soliloquies.


1: Why 'subprocess.run' Does Not Do, in General


Hypothesizer 7
'subprocess.run' inevitably waits for the invoked process to complete? . . . No kidding.

The process may take a long time and meanwhile may emit a large amount of standard output, and my program has to be idly waiting until the process ends, before it can begin to deal with the backlog? . . . Rejected.

The standard input data are able to be passed into the method, but they have to be determined a priori? . . . Unusable.

Oh, a correction: of course, the method may be usable in some limited cases, but it cannot be regarded as a general purpose method, which is what I mean.

Generally speaking, I think of interacting with the invoked process, with my program sending and receiving messages to-and-fro, an inter-processes communication.

The presumption of the method seems to be "You should a priori know what the invoked process will require in the standard input, and you should be OK if the standard outputs from the invoked process are read only after the process ends.", which I do not happen to share.


2: The Basics of Invoking a Process and Handling Its Standard Output, Standard Error Output, and Standard Input


Hypothesizer 7
In Python, a process can be invoked by instantiating the 'subprocess.Popen' class, and that instance contains a pointer to a 'TextIO' stream of each of the standard output, the standard error output, and the standard input of the process.

Of course, the constructor returns immediately without waiting for the invoked process to terminate, which is an indispensable feature for the class to be usable for general purposes.

This is an example of invoking a process and reading the standard output.

@Python Source Code
import subprocess
from subprocess import Popen
import sys

		l_process: Popen = Popen (args = ["ls", "-l"], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, cwd = None, env = None, encoding = "UTF-8")
		try:
			l_standardOutputData: str
			while True:
				l_standardOutputData = l_process.stdout.read (1)
				if l_standardOutputData == "":
					break
				sys.stdout.write (l_standardOutputData)
				sys.stdout.flush ()
		except (Exception) as l_exception:
			sys.stderr.write ("### A standard output error: {0:s}.\n".format (str (l_exception)))
			sys.stderr.flush ()
		l_process.communicate ()
		sys.stdout.write ("### the process return: {0:d}\n".format (l_process.returncode))
		sys.stdout.flush ()

The pointers to the streams of the standard error output and the standard input are 'l_process.stderr' and 'l_process.stdin'.

The command and the arguments are passed as a list into the 'args' argument of the constructor.

The working directory is passed as a string into the 'cwd' argument.

The environment variables can be passed as a map into the 'env' argument, but if 'None' is passed, the environment variables of the invoking process will be inherited to the invoked process.

Well, what is the relation between the lifetime of the invoked process and the duration of the accessibility to the standard output or something? . . . If the process finishes, will the standard output become inaccessible without the contents having been read yet? Or does yet-not-having-read the standard output block the process from terminating? . . . Let me test.

@Python Source Code
import subprocess
from subprocess import Popen
import sys
import time

		l_process: Popen = Popen (args = ["ls", "-l"], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, cwd = None, env = None, encoding = "UTF-8")
		sys.stdout.write ("### waiting 30 seconds before starting to read the standard output . . .\n")
		sys.stdout.flush ()
		time.sleep (30)
		try:
			l_standardOutputData: str
			while True:
				l_standardOutputData = l_process.stdout.read (1)
				if l_standardOutputData == "":
					break
				sys.stdout.write (l_standardOutputData)
				sys.stdout.flush ()
		except (Exception) as l_exception:
			sys.stderr.write ("### A standard output error: {0:s}.\n".format (str (l_exception)))
			sys.stderr.flush ()
		sys.stdout.write ("### waiting 30 seconds before calling 'communicate (input, timeout)' . . .\n")
		sys.stdout.flush ()
		time.sleep (30)
		l_processReturn: int = l_process.communicate ()
		sys.stdout.write ("### the process return: {0:d}\n".format (l_process.returncode))
		sys.stdout.flush ()

While the program was asleep, I looked up the invoked process with a 'ps' command in Linux.

Well, the invoked process became "defunct" even before the program began to read the standard output and survived until the 'communicate (input, timeout)' method was called; so I do not have to worry about failing to read the standard output or the standard error output because of not reading fast enough. On the other hand, I do not have to necessarily read the standard output or the standard error output: not having read them does not prevent the invoked process from terminating.

A lesson is that I have to call the 'communicate (input, timeout)' method somehow, even if my program does not want to wait for the invoked process to finish. . . . If the main thread does not want to wait, I can let a background thread wait.


3: Why the Relays Should Be Done in the Background


Hypothesizer 7
Obviously, if my program just reads the standard output in the foreground, the other 2 streams will not be able to be took cared of until the standard output is closed.

So, at least 2 of the 3 streams have to be handled in the background.

Preferably, all the 3 streams are handled in the background, because otherwise, the main thread may be unpredictably blocked by an invoked process that happens to keep silence.

A critical problem of Python 'TextIO' is that it cannot be interrupted or timed out, being stuck possibly for an indefinite time.

As 'Thtread.join (timeout)' and 'Popen.communicate (input, timeout)' can be timed out, this will prevent the main thread from being stuck.

@Python Source Code
import subprocess
from subprocess import Popen
from subprocess import TimeoutExpired
import sys
from threading import Thread

		l_process: Popen = Popen (args = ["ls", "-l"], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, cwd = None, env = None, encoding = "UTF-8")
		def l_printStandardOutputThreadFunction () -> None:
			try:
				l_standardOutputData: str
				while True:
					l_standardOutputData = l_process.stdout.read (1)
					if l_standardOutputData == "":
						break
					sys.stdout.write (l_standardOutputData)
					sys.stdout.flush ()
			except (Exception) as l_exception:
				sys.stderr.write ("### A standard output error: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
		l_printStandardOutputThread: Thread = Thread (target = l_printStandardOutputThreadFunction)
		l_printStandardOutputThread.start ()
		while True:
			l_printStandardOutputThread.join (0.1)
			if not l_printStandardOutputThread.is_alive ():
				break
		while True:
			try:
				l_process.communicate (None, 1.0)
				sys.stdout.write ("### the process return: {0:d}\n".format (l_process.returncode))
				sys.stdout.flush ()
				break
			except (TimeoutExpired) as l_exception:
				sys.stdout.write ("### The process has not terminated yet.\n")
				sys.stdout.flush ()

In fact, as the standard output has been already naturally closed, 'Popen.communicate (input, timeout)' will not particularly have to be timed out in that case.


4: Doing the Relays and Interrupting Them


Hypothesizer 7
Here, let me relay the standard output and the standard error output of the invoked process to those of the invoking program and relay the standard input of the invoking program to that of the invoked process.

In the 1st attempt, it has become like this.

@Python Source Code
import subprocess
from subprocess import Popen
from subprocess import TimeoutExpired
import sys
from threading import Thread

		l_process: Popen = Popen (args = ["ls", "-l"], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, cwd = None, env = None, encoding = "UTF-8")
		def l_printStandardOutputThreadFunction () -> None:
			try:
				l_standardOutputData: str
				while True:
					l_standardOutputData = l_process.stdout.read (1)
					if l_standardOutputData == "":
						break
					sys.stdout.write (l_standardOutputData)
					sys.stdout.flush ()
			except (Exception) as l_exception:
				sys.stderr.write ("### A standard output error: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
		l_printStandardOutputThread: Thread = Thread (target = l_printStandardOutputThreadFunction)
		l_printStandardOutputThread.start ()
		def l_printStandardErrorOutputThreadFunction () -> None:
			try:
				l_standardErrorOutputData: str
				while True:
					l_standardErrorOutputData = l_process.stderr.read (1)
					if l_standardErrorOutputData == "":
						break
					sys.stderr.write (l_standardErrorOutputData)
					sys.stderr.flush ()
			except (Exception) as l_exception:
				sys.stderr.write ("### A standard error output error: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
		l_printStandardErrorOutputThread: Thread = Thread (target = l_printStandardErrorOutputThreadFunction)
		l_printStandardErrorOutputThread.start ()
		def l_relayStandardInputThreadFunction () -> None:
			try:
				l_standardInputDatum: str = None
				while True:
					l_standardInputDatum = sys.stdin.read (1)
					l_process.stdin.write (l_standardInputDatum)
					l_process.stdin.flush ()
			except (Exception) as l_exception:
				sys.stderr.write ("### A standard input error: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
		l_relayStandardInputThread: Thread = Thread (target = l_relayStandardInputThreadFunction)
		l_relayStandardInputThread.start ()
		while True:
			l_printStandardOutputThread.join (0.1)
			if not l_printStandardOutputThread.is_alive ():
				break
		while True:
			l_printStandardErrorOutputThread.join (0.1)
			if not l_printStandardErrorOutputThread.is_alive ():
				break
		while True:
			try:
				l_process.communicate (None, 1.0)
				sys.stdout.write ("### the process return: {0:d}\n".format (l_process.returncode))
				sys.stdout.flush ()
				break
			except (TimeoutExpired) as l_exception:
				sys.stdout.write ("### The process has not terminated yet.\n")
				sys.stdout.flush ()
		while True:
			l_relayStandardInputThread.join (1.0)
			if not l_relayStandardInputThread.is_alive ():
				break
			else:
				sys.stdout.write ("### l_relayStandardInputThread has not terminated yet.\n")

That is a problem because the standard-input-relay thread will not end, because the thread will keep waiting for the next standard input datum of the invoking process. The standard input wait or the thread cannot be interrupted in any way, as far as I know (end runs like making the thread a daemon thread are not acceptable in general) . . .

In fact, the solution is here.

So, my improved code is this.

@Python Source Code
import subprocess
from subprocess import Popen
from subprocess import TimeoutExpired
import sys
from threading import Thread
from theBiasPlanet.coreUtilities.inputs.HaltableStandardInputReader import HaltableStandardInputReader
from theBiasPlanet.coreUtilities.inputsHandling.NoMoreDataException import NoMoreDataException
from theBiasPlanet.coreUtilities.timersHandling.TimeOutException import TimeOutException

		l_process: Popen = Popen (args = ["ls", "-l"], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, cwd = None, env = None, encoding = "UTF-8")
		def l_printStandardOutputThreadFunction () -> None:
			try:
				l_standardOutputData: str
				while True:
					l_standardOutputData = l_process.stdout.read (1)
					if l_standardOutputData == "":
						break
					sys.stdout.write (l_standardOutputData)
					sys.stdout.flush ()
			except (Exception) as l_exception:
				sys.stderr.write ("### A standard output error: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
		l_printStandardOutputThread: Thread = Thread (target = l_printStandardOutputThreadFunction)
		l_printStandardOutputThread.start ()
		def l_printStandardErrorOutputThreadFunction () -> None:
			try:
				l_standardErrorOutputData: str
				while True:
					l_standardErrorOutputData = l_process.stderr.read (1)
					if l_standardErrorOutputData == "":
						break
					sys.stderr.write (l_standardErrorOutputData)
					sys.stderr.flush ()
			except (Exception) as l_exception:
				sys.stderr.write ("### A standard error output error: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
		l_printStandardErrorOutputThread: Thread = Thread (target = l_printStandardErrorOutputThreadFunction)
		l_printStandardErrorOutputThread.start ()
		l_haltableStandardInputReader: "HaltableStandardInputReader" = HaltableStandardInputReader.getInstance ()
		def l_relayStandardInputThreadFunction () -> None:
			l_threadIdentification: str = "{0:d}".format (id (threading.current_thread ()))
			try:
				l_standardInputDatum: str = None
				while True:
					try:
						l_standardInputDatum = l_haltableStandardInputReader.read (l_threadIdentification, 1)
						if l_standardInputDatum == "":
							break
						l_process.stdin.write (l_standardInputDatum)
						l_process.stdin.flush ()
					except (NoMoreDataException) as l_exception:
						break
					except (TimeOutException) as l_exception:
						# impossible
						None
			except (Exception) as l_exception:
				sys.stderr.write ("### A standard input error: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
		l_relayStandardInputThread: Thread = Thread (target = l_relayStandardInputThreadFunction)
		l_haltableStandardInputReader.addSubscriber ("{0:d}".format (id (l_relayStandardInputThread)))
		sys.stdout.flush ()
		l_relayStandardInputThread.start ()
		sys.stdout.flush ()
		while True:
			l_printStandardOutputThread.join (0.1)
			if not l_printStandardOutputThread.is_alive ():
				break
		while True:
			l_printStandardErrorOutputThread.join (0.1)
			if not l_printStandardErrorOutputThread.is_alive ():
				break
		while True:
			try:
				l_process.communicate (None, 1.0)
				sys.stdout.write ("### the process return: {0:d}\n".format (l_process.returncode))
				sys.stdout.flush ()
				break
			except (TimeoutExpired) as l_exception:
				sys.stdout.write ("### The process has not terminated yet.\n")
				sys.stdout.flush ()
		l_haltableStandardInputReader.removeSubscriber ("{0:d}".format (id (l_relayStandardInputThread)))
		while True:
			l_relayStandardInputThread.join (1.0)
			if not l_relayStandardInputThread.is_alive ():
				break
			else:
				sys.stdout.write ("### l_relayStandardInputThread has not terminated yet.\n")

Well, how can the reads on the standard output and the standard error output be interrupted?

I think that they usually do not have to be interrupted, because why should the background threads not be left moving or waiting until the standard output is voluntarily closed by the invoked process? But if they happen to have to, I can close the outputs from my program.


5: Creating a Utility Class


Hypothesizer 7
As such code is a boilerplate, it is not wise to write that in multiple places.

So, let me create a utility class.

theBiasPlanet/coreUtilities/processesHandling/ProcessHandler.py

@Python Source Code
from typing import Dict
from typing import List
from typing import TextIO
from typing import cast
from io import StringIO
import os
import signal
import subprocess
from subprocess import Popen
import sys
import threading
from threading import Condition
from threading import Thread
from theBiasPlanet.coreUtilities.inputs.HaltableStandardInputReader import HaltableStandardInputReader
from theBiasPlanet.coreUtilities.inputsHandling.NoMoreDataException import NoMoreDataException
from theBiasPlanet.coreUtilities.messagingHandling.Publisher import Publisher
from theBiasPlanet.coreUtilities.timersHandling.TimeOutException import TimeOutException

class ProcessHandler ():
	s_haltableStandardInputReader: "HaltableStandardInputReader" = HaltableStandardInputReader.getInstance ()
	
	# the static initializer Start
	s_haltableStandardInputReader.startDispatchDataThread ()
	# the static initializer End
	
	@staticmethod
	def interruptRelayStandardInputThread (a_thread: Thread) -> None:
		ProcessHandler.s_haltableStandardInputReader.removeSubscriber ("{0:d}".format (id (a_thread)))
	
	class StandardInputAndOutputs:
		def __init__ (a_this: "ProcessHandler.StandardInputAndOutputs", a_process: Popen) -> None:
			a_this.i_process: Popen
			a_this.i_standardInputOutputStream: TextIO
			a_this.i_standardOutputInputStream: TextIO
			a_this.i_standardErrorOutputInputStream: TextIO
			a_this.i_relayStandardInputThread: Thread
			a_this.i_printStandardOutputThread: Thread
			a_this.i_printStandardErrorOutputThread: Thread
			a_this.i_thereWereStandardInputContents: bool
			a_this.i_thereWereStandardOutputContents: bool
			a_this.i_thereWereStandardErrorOutputContents: bool
			
			a_this.i_process = a_process
			a_this.i_standardInputOutputStream = cast (TextIO, a_this.i_process.stdin)
			a_this.i_standardOutputInputStream = cast (TextIO, a_this.i_process.stdout)
			a_this.i_standardErrorOutputInputStream = cast (TextIO, a_this.i_process.stderr)
			a_this.i_relayStandardInputThread = None
			a_this.i_printStandardOutputThread = None
			a_this.i_printStandardErrorOutputThread = None
			a_this.i_thereWereStandardInputContents = False
			a_this.i_thereWereStandardOutputContents = False
			a_this.i_thereWereStandardErrorOutputContents = False
		
		def getStandardInputOutputStream (a_this: "ProcessHandler.StandardInputAndOutputs", ) -> TextIO:
			return a_this.i_standardInputOutputStream
		
		def getStandardOutputInputStream (a_this: "ProcessHandler.StandardInputAndOutputs", ) -> TextIO:
			return a_this.i_standardOutputInputStream
		
		def getStandardErrorOutputInputStream (a_this: "ProcessHandler.StandardInputAndOutputs", ) -> TextIO:
			return a_this.i_standardErrorOutputInputStream
		
		def relayStandardInputAsynchronously (a_this: "ProcessHandler.StandardInputAndOutputs") -> None:
			def l_relayStandardInputThreadFunction () -> None:
				l_threadIdentification: str = "{0:d}".format (id (threading.current_thread ()))
				try:
					l_processStandardInputWriter: TextIO = a_this.i_standardInputOutputStream
					l_standardInputDatum: str = None
					while True:
						try:
							l_standardInputDatum = ProcessHandler.s_haltableStandardInputReader.read (l_threadIdentification, 1)
							a_this.i_thereWereStandardInputContents = True
							l_processStandardInputWriter.write (l_standardInputDatum)
							l_processStandardInputWriter.flush ()
						except (NoMoreDataException) as l_exception:
							break
						except (TimeOutException) as l_exception:
							# impossible
							None
				except (Exception) as l_exception:
					Publisher.logErrorInformation ("### A standard input error: {0:s}.".format (str (l_exception)))
			a_this.i_relayStandardInputThread = Thread (target = l_relayStandardInputThreadFunction)
			ProcessHandler.s_haltableStandardInputReader.addSubscriber ("{0:d}".format (id (a_this.i_relayStandardInputThread)))
			a_this.i_relayStandardInputThread.start ()
		
		def printStandardOutputAsynchronously (a_this: "ProcessHandler.StandardInputAndOutputs") -> None:
			def l_printStandardOutputThreadFunction () -> None:
				try:
					l_standardOutputReader: TextIO = a_this.i_standardOutputInputStream
					l_standardOutputData: str
					while True:
						l_standardOutputData = l_standardOutputReader.read (1)
						if l_standardOutputData == "":
							break
						a_this.i_thereWereStandardOutputContents = True
						sys.stdout.write (l_standardOutputData)
						sys.stdout.flush ()
				except (Exception) as l_exception:
					Publisher.logErrorInformation (l_exception)
					sys.stderr.write ("### A standard output error: {0:s}.\n".format (str (l_exception)))
					sys.stderr.flush ()
			a_this.i_printStandardOutputThread  = Thread (target = l_printStandardOutputThreadFunction)
			a_this.i_printStandardOutputThread.start ()
		
		def printStandardErrorOutputAsynchronously (a_this: "ProcessHandler.StandardInputAndOutputs") -> None:
			def l_printStandardErrorOutputThreadFunction () -> None:
				try:
					l_standardErrorOutputReader: TextIO = a_this.i_standardErrorOutputInputStream
					l_standardErrorOutputData: str
					while True:
						l_standardErrorOutputData = l_standardErrorOutputReader.read (1)
						if l_standardErrorOutputData == "":
							break
						a_this.i_thereWereStandardErrorOutputContents = True
						sys.stderr.write (l_standardErrorOutputData)
						sys.stderr.flush ()
				except (Exception) as l_exception:
					Publisher.logErrorInformation (l_exception)
					sys.stderr.write ("### A standard error output error: {0:s}.\n".format (str (l_exception)))
					sys.stderr.flush ()
			a_this.i_printStandardErrorOutputThread  = Thread (target = l_printStandardErrorOutputThreadFunction)
			a_this.i_printStandardErrorOutputThread .start ()
		
		def setStandardInputNextLine (a_this: "ProcessHandler.StandardInputAndOutputs", a_line: str) -> None:
			try:
				a_this.i_standardInputOutputStream.write ("{0:s}\n".format (a_line))
				a_this.i_standardInputOutputStream.flush ()
				a_this.i_thereWereStandardInputContents = True
			except (Exception) as l_exception:
				sys.stderr.write ("The standard input couldn't be written into: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
				a_this.i_standardInputOutputStream.close ()
		
		def getStandardOutputNextLine (a_this: "ProcessHandler.StandardInputAndOutputs") -> str:
			try:
				l_line: str = None
				l_line = a_this.i_standardOutputInputStream.readline ()
				if l_line != "":
					a_this.i_thereWereStandardOutputContents = True
					return l_line
				else:
					a_this.i_standardOutputInputStream.close ()
					return None
			except (Exception) as l_exception:
				sys.stderr.write ("The standard output couldn't be scanned: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
				a_this.i_standardOutputInputStream.close ()
				return None
		
		def getStandardErrorOutputNextLine (a_this: "ProcessHandler.StandardInputAndOutputs") -> str:
			try:
				l_line: str = None
				l_line = a_this.i_standardErrorOutputInputStream.readline ()
				if l_line != "":
					a_this.i_thereWereStandardErrorOutputContents = True
					return l_line
				else:
					a_this.i_standardErrorOutputInputStream.close ()
					return None
			except (Exception) as l_exception:
				sys.stderr.write ("The standard error output couldn't be scanned: {0:s}.\n".format (str (l_exception)))
				sys.stderr.flush ()
				a_this.i_standardErrorOutputInputStream.close ()
				return None
		
		def thereWereStandardInputContents (a_this: "ProcessHandler.StandardInputAndOutputs") -> bool:
			return a_this.i_thereWereStandardInputContents
		
		def thereWereStandardOutputContents (a_this: "ProcessHandler.StandardInputAndOutputs") -> bool:
			return a_this.i_thereWereStandardOutputContents
		
		def thereWereStandardErrorOutputContents (a_this: "ProcessHandler.StandardInputAndOutputs") -> bool:
			return a_this.i_thereWereStandardErrorOutputContents
		
		def waitUntillFinish (a_this: "ProcessHandler.StandardInputAndOutputs") -> int:
			if a_this.i_printStandardErrorOutputThread is not None:
				a_this.i_printStandardErrorOutputThread.join ()
				a_this.i_printStandardErrorOutputThread = None
			if a_this.i_printStandardOutputThread is not None:
				a_this.i_printStandardOutputThread.join ()
				a_this.i_printStandardOutputThread = None
			l_processReturn: int = a_this.i_process.wait ()
			if a_this.i_relayStandardInputThread is not None:
				ProcessHandler.interruptRelayStandardInputThread (a_this.i_relayStandardInputThread)
				a_this.i_relayStandardInputThread.join ()
				a_this.i_relayStandardInputThread = None
			return l_processReturn
	
	# The environment variables of the current process are automatically passed into the sub process.
	@staticmethod
	def executeAndReturnStandardInputAndOutputs (a_workingDirectoryPath: str, a_environmentVariableNameToValueMap: "Dict [str, str]", a_commandAndArguments: List [str]) -> "ProcessHandler.StandardInputAndOutputs":
		l_process: Popen = Popen (args = a_commandAndArguments, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, cwd = a_workingDirectoryPath, env = a_environmentVariableNameToValueMap, encoding = "UTF-8")
		return ProcessHandler.StandardInputAndOutputs (l_process)
	
	@staticmethod
	def execute (a_workingDirectory: str, a_environmentVariableNameToValueMap: "Dict [str, str]", a_commandAndArguments: List [str], a_waitsUntilFinish: bool) -> int:
		l_standardInputAndOutputs: "ProcessHandler.StandardInputAndOutputs" = ProcessHandler.executeAndReturnStandardInputAndOutputs (a_workingDirectory, a_environmentVariableNameToValueMap, a_commandAndArguments)
		l_childProcessReturn: int = 0
		if a_waitsUntilFinish:
			l_standardInputAndOutputs.printStandardOutputAsynchronously ()
			l_standardInputAndOutputs.printStandardErrorOutputAsynchronously ()
			l_standardInputAndOutputs.relayStandardInputAsynchronously ()
			l_childProcessReturn = l_standardInputAndOutputs.waitUntillFinish ()
		else:
			def l_waitForInvokedProcessToEndThreadFunction () -> None:
				try:
					l_standardInputAndOutputs.printStandardOutputAsynchronously ()
					l_standardInputAndOutputs.printStandardErrorOutputAsynchronously ()
					l_standardInputAndOutputs.relayStandardInputAsynchronously ()
					l_standardInputAndOutputs.waitUntillFinish ()
				except (Exception) as l_exception:
					None
			l_waitForInvokedProcessToEndThread = Thread (target = l_waitForInvokedProcessToEndThreadFunction)
			l_waitForInvokedProcessToEndThread.start ()
		return l_childProcessReturn


6: The Usage of the Utility Class


Hypothesizer 7
If I want to just do as I did in a section and wait until the invoked process finishes, I can just call the 'execute (a_workingDirectory: str, a_environmentVariableNameToValueMap: "Dict [str, str]", a_commandAndArguments: List [str], a_waitsUntilFinish: bool)' method like this.

@Python Source Code
from theBiasPlanet.coreUtilities.processesHandling.ProcessHandler import ProcessHandler

		l_executionResult: int = ProcessHandler.execute (None, None, ["ls", "-l"], False)

If not, I can call the 'executeAndReturnStandardInputAndOutputs (a_workingDirectoryPath: str, a_environmentVariableNameToValueMap: "Dict [str, str]", a_commandAndArguments: List [str])' method and afterward handle the returned 'ProcessHandler.StandardInputAndOutputs' object freely, for example, like this.

@Python Source Code
import sys
from theBiasPlanet.coreUtilities.processesHandling.ProcessHandler import ProcessHandler

		l_StandardInputAndOutputs: "ProcessHandler.StandardInputAndOutputs" = ProcessHandler.executeAndReturnStandardInputAndOutputs (None, None, ["ls", "-l"])
		l_StandardInputAndOutputs.printStandardErrorOutputAsynchronously ()
		l_StandardInputAndOutputs.relayStandardInputAsynchronously ()
		sys.stdout.write ("### The 1st line: {0:s}\n".format (l_StandardInputAndOutputs.getStandardOutputNextLine ()))
		l_StandardInputAndOutputs.waitUntillFinish ()


References


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

2020-12-20

12: Linux or Windows Clipboard with Python: in Multi-Formats

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

wxPython is used. Not just the text format. Many formats are supported.

Topics


About: the Python programming language

The table of contents of this article


Starting Context


  • The reader has a basic knowledge on the Python programming language.

Target Context


  • The reader will know how to get and set a composite of format-specific data from and to the operating system clipboard, in Python, with wxPython.

Orientation


Many, but not all the, formats are supported by this technique.

More , but still not all the, formats are supported by a technique in C# for Microsoft Windows, as will be introduced in a future article of another series.

All the formats are supported by a technique in C++ for Linux or Microsoft Windows, as will be introduced in a future article of another series.

The reason why any Python variable is a pointer (a prerequisite knowledge) has been explained in a previous article.


Main Body


1: Aim: to Implement Multiple Copy Buffers for a Word Processor


Hypothesizer 7
As I use Vim as a text editor, one of the benefits is that it has multiple copy buffers: I can copy a piece of text to the buffer 'a', another piece of text to the buffer 'b', etc.. That is huge help.

As I use LibreOffice Writer as a word processor, I want that functionality in it.

In fact, having been accustomed to the functionality, the lack of it is a considerable frustration.

A Python macro will implement the functionality, if it can get and set clipboard data.


2: The Text Format Is Not Enough


Hypothesizer 7
Getting and setting a text format clipboard datum is easy; for example, 'pyperclip' can do the work.

But, obviously, that is not enough for word processor: I need the fonts information to be copied, at least. And some image data and objects like tables may want to be copied to multiple buffers, too.

Ideally, the whole clipboard data should be copied as they are.


3: I Will Use wxPython


Hypothesizer 7
As I searched the internet, I could not find any tutorial for getting and setting the whole clipboard data in Python (ones that handle text data abound, though).

As I use wxPython, hoping that it may have the functionality, I searched the official wxPython document and found some information.

'wx.CustomDataObjec' sounded promising, but turned out to be for a datum of a single format, not for any composite of format-specific data.

'wx.DataObjectComposite' is for composites, but seems rather cumbersome unless I have a predetermined set of supported datum formats.

Well, 'wx.DataObject' (which is described as "for maximum flexibility and efficiency, but it is also the most difficult to implement" in the document) seems to be the one I should use, but even it seems to be supposing that I have a predetermined set of supported datum formats, which I do not.

After all, the concept of wxPython clipboard handling does not match my interest of getting and setting the whole clipboard data of whatever multiple datum formats, but I will see what I can do.


4: Some Basics of Clipboard Datum Formats


Hypothesizer 7
Although the clipboard mechanism of X Window system and that of Microsoft Windows are very different, at least wxPython-wise, each clipboard datum format always has a number as the identification of the format.

However, the number may be ephemeral, which means that a format may have '49161' at a time and '49162' at another; so, generally speaking, I cannot permanently identify a format with the number (although some usual formats like 'Text' have fixed numbers).

On the other hand, some formats do not have names (or at least the names cannot be retrieved API-wise).

However, such formats should have fixed numbers.

So, each of the formats that have names can be identified by the name, while each of the formats that have no name can be identified by the number.


5: The Specifications of 'wx.DataObject'


Hypothesizer 7
As the official document is not so clear on the specifications of the class (especially on when each method is called), I had to experiment to know necessary information.

An important point I have found is that any instance of 'wx.DataObject' becomes invalid after it has been passed into the 'SetData' method of 'wx.Clipboard', where 'invalid'.means that it cannot be passed into the 'SetData' method again

Well, that means that 'wx.DataObject' cannot be used as a clipboard data container for my purpose (as my clipboard data have to be able to be set into the clipboard repeatedly). So, 'wx.DataObject' is a clipboard events handler, for me.

These are what I have found out about the timings at which the methods are called.

MethodCalled When
GetFormatCountWhen 'wx.Clipboard.GetData' has been called
1st for the composite
1st per format
When 'wx.Clipboard.SetData' has been called
after 'GetPreferredFormat' for the composite
1st per format
GetAllFormatsWhen 'wx.Clipboard.GetData' has been called
after 'GetFormatCount' for the composite
after 'GetFormatCount' per format
When 'wx.Clipboard.SetData' has been called
after 'GetFormatCount' for the composite
after 'GetFormatCount' per format
GetDataHereWhen 'wx.Clipboard.SetData' has been called
after 'GetDataSize' per format
GetDataSizeWhen 'wx.Clipboard.SetData' has been called
twice after 'GetAllFormats' per format
GetPreferredFormatWhen 'wx.Clipboard.SetData' has been called
1st for the composite
IsSupportedNever
SetDataWhen 'wx.Clipboard.GetData' has been called
after 'GetAllFormats' per format

For 'wx.Clipboard.GetData', 'GetFormatCount' and 'GetAllFormats' have to respond with all the possible datum formats known a priori, because the existing datum formats in the clipboard are unknown to my program. But I can be assured that the methods that are called per format are called for only datum formats that exist in the clipboard (not all the datum formats returned by 'GetAllFormats').

Anyway, I find some things enigmatic. 1st, why 'GetFormatCount' and 'GetAllFormats' are called per format? 2nd, why is 'GetDataSize' called twice per format? 3rd, 'GetPreferredFormat' is called only for 'wx.Clipboard.SetData' in spite of the official document, and I do not see any effect whatever it returns.

The 'buf' arguments of 'GetDataHere' and 'SetData' are 'memoryview' instances.


6: How to Know the Possible Datum Formats a Priori


Hypothesizer 7
How can I know possible datum formats a priori?

In Linux, this command shows the datum formats that exist in the clipboard.

@bash Source Code
xclip -t TARGETS -o

In Microsoft Windows, well, I have created a C++ Win32 API program, as will be seen in a future article of another series.

For example, when a text piece in LibreOffice Writer is copied in Linux, the available formats are these.

@Output
application/x-openoffice-embed-source-xml;windows_formatname="Star Embed Source (XML)"
text/rtf
text/richtext
text/html
text/plain;charset=utf-16
application/x-openoffice-objectdescriptor-xml;windows_formatname="Star Object Descriptor (XML)";classname="8BC6B165-B1B2-4EDD-aa47-dae2ee689dd6";typename="LibreOffice 6.4 Text Document";viewaspect="1";width="16999";height="2995";posx="0";posy="0"
text/plain;charset=utf-8
text/plain
UTF8_STRING
STRING
TEXT
TARGETS
MULTIPLE
TIMESTAMP
SAVE_TARGETS

For Microsoft Windows, the available formats are these.

@Output
DataObject
Star Embed Source (XML)
Rich Text Format
Richtext Format
HTML (HyperText Markup Language)
HTML Format
UnicodeText
Text
Link
Star Object Descriptor (XML)
Ole Private Data
Locale
OEMText


7: My Code


Hypothesizer 7
As I noticed earlier, 'wx.DataObject' cannot be used as a data container for my purpose. So, I have created a data container class, 'theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDataComposite' (and 'theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDatum' as its component) and a data history class, 'theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDataCompositesHistory', which is the multi buffers container.

theBiasPlanet/coreUtilities/clipboardHandling/ClipboardFormatSpecificDatum.py

@Python Source Code

class ClipboardFormatSpecificDatum:
	def __init__ (a_this: "ClipboardFormatSpecificDatum", a_formatName: str, a_formatNumber: int, a_datum: bytearray) -> None:
		a_this.i_formatName: str = a_formatName
		a_this.i_formatNumber: int = a_formatNumber
		a_this.i_datum: bytearray = a_datum
	
	def __del__ (a_this: "ClipboardFormatSpecificDatum") -> None:
		None
	
	def getFormatName (a_this: "ClipboardFormatSpecificDatum") -> str:
		return a_this.i_formatName
	
	def getFormatNumber (a_this: "ClipboardFormatSpecificDatum") -> int:
		return a_this.i_formatNumber
	
	def getDatumSize (a_this: "ClipboardFormatSpecificDatum") -> int:
		return len (a_this.i_datum)
	
	def getDatum (a_this: "ClipboardFormatSpecificDatum") -> bytearray:
		return a_this.i_datum


theBiasPlanet/coreUtilities/clipboardHandling/ClipboardFormatSpecificDataComposite.py

@Python Source Code
from typing import List
from collections import OrderedDict
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDatum import ClipboardFormatSpecificDatum

class ClipboardFormatSpecificDataComposite:
	def __init__ (a_this: "ClipboardFormatSpecificDataComposite") -> None:
		a_this.i_formatNameToDatumMap: "OrderedDict [str, ClipboardFormatSpecificDatum]" = OrderedDict ()
	
	def __del__ (a_this: "ClipboardFormatSpecificDataComposite") -> None:
		None
	
	def addFormatSpecificDatum (a_this: "ClipboardFormatSpecificDataComposite", a_formatSpecificDatum: "ClipboardFormatSpecificDatum") -> bool:
		a_this.i_formatNameToDatumMap [a_formatSpecificDatum.getFormatName ()] = a_formatSpecificDatum
		return True
	
	def getFormatNames (a_this: "ClipboardFormatSpecificDataComposite") -> List [str]:
		l_formatNames: List [str] = []
		l_formatName: str = None
		for l_formatName in a_this.i_formatNameToDatumMap:
			l_formatNames.append (l_formatName)
		return l_formatNames
	
	def getFormatSpecificDatum (a_this: "ClipboardFormatSpecificDataComposite", a_formatName: str) -> "ClipboardFormatSpecificDatum":
		l_formatSpecificDatum: "ClipboardFormatSpecificDatum" = None
		try:
			l_formatSpecificDatum = a_this.i_formatNameToDatumMap [a_formatName]
		except (KeyError) as l_exception:
			None
		return l_formatSpecificDatum


theBiasPlanet/coreUtilities/clipboardHandling/ClipboardFormatSpecificDataCompositesHistory.py

@Python Source Code
from collections import OrderedDict
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDataComposite import ClipboardFormatSpecificDataComposite

class ClipboardFormatSpecificDataCompositesHistory:
	def __init__ (a_this: "ClipboardFormatSpecificDataCompositesHistory") -> None:
		a_this.i_dataCompositeKeyToDataCompositeMap: "OrderedDict [str, ClipboardFormatSpecificDataComposite]" = OrderedDict ()
	
	def __del__ (a_this: "ClipboardFormatSpecificDataCompositesHistory") -> None:
		None
	
	def addDataComposite (a_this: "ClipboardFormatSpecificDataCompositesHistory", a_dataCompositeKey: str, a_dataComposite: "ClipboardFormatSpecificDataComposite") -> bool:
		a_this.i_dataCompositeKeyToDataCompositeMap [a_dataCompositeKey] = a_dataComposite
		return True
	
	def removeDataComposite (a_this: "ClipboardFormatSpecificDataCompositesHistory", a_dataCompositeKey: str) -> bool:
		try:
			a_this.i_dataCompositeKeyToDataCompositeMap.pop (a_dataCompositeKey)
			return True
		except (KeyError) as l_exception:
			return False
	
	def getDataComposite (a_this: "ClipboardFormatSpecificDataCompositesHistory", a_dataCompositeKey: str) -> "ClipboardFormatSpecificDataComposite":
		l_dataComposite: "ClipboardFormatSpecificDataComposite" = None
		try:
			l_dataComposite = a_this.i_dataCompositeKeyToDataCompositeMap [a_dataCompositeKey]
		except (KeyError) as l_exception:
			None
		return l_dataComposite


The 'wx.DataObject' extension class is this.

theBiasPlanet/coreUtilities/clipboardHandling/WxPythonClipboardEventsHandler.py

@Python Source Code
from typing import List
from typing import cast
from collections import OrderedDict
import sys
from wx import DataFormat as wx_DataFormat
from wx import DataObject as wx_DataObject
import wx
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDataComposite import ClipboardFormatSpecificDataComposite
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDatum import ClipboardFormatSpecificDatum

class WxPythonClipboardEventsHandler (wx_DataObject):
	s_possibleDatumFormatNameToFormatMap: "OrderedDict [str, wx_DataFormat]" = None
	s_preferedDatumFormatName: str = None
	s_possibleDatumFormats: List [wx_DataFormat] = None
	
	@staticmethod
	def getDatumFormatName (a_datumFormat: wx_DataFormat) -> str:
		l_datumFormatNumber: int = a_datumFormat.GetType ()
		l_datumFormatName: str = None
		if l_datumFormatNumber == wx.DF_TEXT:
			l_datumFormatName = "Text"
		elif l_datumFormatNumber == wx.DF_BITMAP:
			l_datumFormatName = "Bitmap"
		elif l_datumFormatNumber == wx.DF_METAFILE:
			l_datumFormatName = "Metafile"
		elif l_datumFormatNumber == wx.DF_FILENAME:
			l_datumFormatName = "Filename"
		elif l_datumFormatNumber == wx.DF_HTML:
			l_datumFormatName = "HTML Format"
		else:
			l_datumFormatName = a_datumFormat.GetId ()
		return l_datumFormatName
	
	def __init__ (a_this: "WxPythonClipboardEventsHandler", a_formatSpecificDataComposite: "ClipboardFormatSpecificDataComposite") -> None:
		a_this.i_formatSpecificDataComposite: "ClipboardFormatSpecificDataComposite" = a_formatSpecificDataComposite
		a_this.i_supportedDatumFormats: List [wx_DataFormat] = None
		
		super ().__init__ ()
	l_availableDatumFormatNames: List [str] = a_this.i_formatSpecificDataComposite.getFormatNames ()
		if len (l_availableDatumFormatNames) > 0:
			a_this.i_supportedDatumFormats = []
		l_datumFormatName: str = None
			for l_datumFormatName in l_availableDatumFormatNames:
				a_this.i_supportedDatumFormats.append (WxPythonClipboardEventsHandler.s_possibleDatumFormatNameToFormatMap [l_datumFormatName])
		else:
			a_this.i_supportedDatumFormats = WxPythonClipboardEventsHandler.s_possibleDatumFormats
	
	def getFormatSpecificDataComposite (a_this: "WxPythonClipboardEventsHandler") -> "ClipboardFormatSpecificDataComposite":
		return a_this.i_formatSpecificDataComposite
	
	def GetFormatCount (a_this: "WxPythonClipboardEventsHandler", a_datumTransferDirection: wx_DataObject.Direction = wx_DataObject.Direction.Get) -> int:
		sys.stdout.write ("### GetFormatCount: {0:d}\n".format (len (a_this.i_supportedDatumFormats)))
		return len (a_this.i_supportedDatumFormats)
	
	def GetAllFormats (a_this: "WxPythonClipboardEventsHandler", a_datumTransferDirection: wx_DataObject.Direction = wx_DataObject.Direction.Get) -> List [wx_DataFormat]:
		sys.stdout.write ("### GetAllFormats:\n")
		return a_this.i_supportedDatumFormats
	
	# 'a_formatSpecificDatum' is not really 'bytearray', but 'memoryview', but the stub of 'memoryview' seems to be mistaken, which makes me use 'bytearray' instead.
	def GetDataHere (a_this: "WxPythonClipboardEventsHandler", a_datumFormat: wx_DataFormat, a_formatSpecificDatum: bytearray) -> bool:
		sys.stdout.write ("### GetDataHere: {0:s}\n".format (WxPythonClipboardEventsHandler.getDatumFormatName (a_datumFormat)))
		l_formatSpecificDatumIsFound: bool = False
		if a_formatSpecificDatum is not None:
			l_formatSpecificDatum: "ClipboardFormatSpecificDatum" = a_this.i_formatSpecificDataComposite.getFormatSpecificDatum (WxPythonClipboardEventsHandler.getDatumFormatName (a_datumFormat))
			if l_formatSpecificDatum is not None:
				l_copiedFormatSpecificDatum: bytearray = l_formatSpecificDatum.getDatum ()
				l_byteIndex: int = 0
				for l_byteIndex in range (0, len (a_formatSpecificDatum), 1):
					#a_formatSpecificDatum [l_byteIndex:l_byteIndex + 1] = cast (Sequence [bytes], bytes ([l_copiedFormatSpecificDatum [l_byteIndex]])) # This is if I rather use 'memoryview'.
					a_formatSpecificDatum [l_byteIndex] = l_copiedFormatSpecificDatum [l_byteIndex]
				l_formatSpecificDatumIsFound = True
		return l_formatSpecificDatumIsFound
	
	def GetDataSize (a_this: "WxPythonClipboardEventsHandler", a_datumFormat: wx_DataFormat) -> int:
		sys.stdout.write ("### GetDataSize:\n")
		l_size: int = 0
		l_formatSpecificDatum: "ClipboardFormatSpecificDatum" = a_this.i_formatSpecificDataComposite.getFormatSpecificDatum (WxPythonClipboardEventsHandler.getDatumFormatName (a_datumFormat))
		if l_formatSpecificDatum is not None:
			l_size = len (l_formatSpecificDatum.getDatum ())
		return l_size
	
	def GetPreferredFormat (a_this: "WxPythonClipboardEventsHandler", a_datumTransferDirection: wx_DataObject.Direction = wx_DataObject.Direction.Get) -> wx_DataFormat:
		sys.stdout.write ("### GetPreferredFormat:\n")
		return WxPythonClipboardEventsHandler.s_possibleDatumFormatNameToFormatMap [WxPythonClipboardEventsHandler.s_preferedDatumFormatName]
	
	def IsSupported (a_this: "WxPythonClipboardEventsHandler", a_datumFormat: wx_DataFormat, a_datumTransferDirection: wx_DataObject.Direction = wx_DataObject.Direction.Get) -> bool:
		sys.stdout.write ("### IsSupported:\n")
		try:
			WxPythonClipboardEventsHandler.s_possibleDatumFormatNameToFormatMap [WxPythonClipboardEventsHandler.getDatumFormatName (a_datumFormat)]
			return True
		except (KeyError) as l_exception:
			return False
	
	def SetData (a_this: "WxPythonClipboardEventsHandler", a_datumFormat: wx_DataFormat, a_formatSpecificDatum: memoryview) -> bool:
		sys.stdout.write ("### SetData: {0:s}\n".format (WxPythonClipboardEventsHandler.getDatumFormatName (a_datumFormat)))
		l_datumFormatName: str = WxPythonClipboardEventsHandler.getDatumFormatName (a_datumFormat)
		if a_formatSpecificDatum is not None:
			l_datumSize = len (a_formatSpecificDatum)
			l_copiedFormatSpecificDatum: bytearray = None
			if l_datumFormatName == "Text" or l_datumFormatName == "HTML Format":
				l_copiedFormatSpecificDatum = bytearray (l_datumSize + 1)
				l_copiedFormatSpecificDatum [l_datumSize] = 0
			else:
				l_copiedFormatSpecificDatum = bytearray (l_datumSize)
			l_byteIndex: int = 0
			for l_byteIndex in range (0, len (a_formatSpecificDatum), 1):
				l_copiedFormatSpecificDatum [l_byteIndex] = a_formatSpecificDatum [l_byteIndex]
			a_this.i_formatSpecificDataComposite.addFormatSpecificDatum (ClipboardFormatSpecificDatum (l_datumFormatName, a_datumFormat.GetType (), l_copiedFormatSpecificDatum))
		return False

Of course, you cannot do like 'a_formatSpecificDatum = l_formatSpecificDatum.getDatum ()' for 'GetDataHere' (if you think you can, you do not understand that any Python variable is a pointer).

The clipboard handling class is this.

theBiasPlanet/coreUtilities/clipboardHandling/WxPythonClipboard.py

@Python Source Code
from typing import List
from collections import OrderedDict
from wx import DataFormat as wx_DataFormat
import wx
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDataComposite import ClipboardFormatSpecificDataComposite
from theBiasPlanet.coreUtilities.clipboardHandling.WxPythonClipboardEventsHandler import WxPythonClipboardEventsHandler

class WxPythonClipboard:
	@staticmethod
	def setUp (a_datumFormatNameToFormatMap: "OrderedDict [str, wx_DataFormat]", a_preferedDatumFormatName: str) -> bool:
		WxPythonClipboardEventsHandler.s_possibleDatumFormatNameToFormatMap = a_datumFormatNameToFormatMap
		WxPythonClipboardEventsHandler.s_preferedDatumFormatName = a_preferedDatumFormatName
		
		WxPythonClipboardEventsHandler.s_possibleDatumFormats = []
		l_datumFormatName: str = None
		for l_datumFormatName in WxPythonClipboardEventsHandler.s_possibleDatumFormatNameToFormatMap:
			WxPythonClipboardEventsHandler.s_possibleDatumFormats.append (WxPythonClipboardEventsHandler.s_possibleDatumFormatNameToFormatMap [l_datumFormatName])
		return True
	
	@staticmethod
	def openClipboard () -> bool:
		wx.TheClipboard.Open ()
		return True
	
	@staticmethod
	def closeClipboard () -> bool:
		wx.TheClipboard.Close ()
		return True
	
	@staticmethod
	def clearClipboard () -> bool:
		wx.TheClipboard.Clear ()
		return True
	
	@staticmethod
	def getFormatSpecificDataComposite () -> "ClipboardFormatSpecificDataComposite":
		l_wxPythonClipboardEventsHandler: "WxPythonClipboardEventsHandler" = WxPythonClipboardEventsHandler (ClipboardFormatSpecificDataComposite ())
		wx.TheClipboard.GetData (l_wxPythonClipboardEventsHandler)
		return l_wxPythonClipboardEventsHandler.getFormatSpecificDataComposite ()
	
	@staticmethod
	def setFormatSpecificDataComposite (a_formatSpecificDataComposite: "ClipboardFormatSpecificDataComposite") -> bool:
		l_wxPythonClipboardEventsHandler: "WxPythonClipboardEventsHandler" = WxPythonClipboardEventsHandler (a_formatSpecificDataComposite)
		wx.TheClipboard.SetData (l_wxPythonClipboardEventsHandler)
		wx.TheClipboard.Flush ()
		return True



8: The Limitation


Hypothesizer 7
In fact, not the data of all the formats can be gotten or set in this technique.

For example, in Microsoft Windows, for a LibreOffice Writer clipboard data, the 'UnicodeText', 'Locale', and 'OEMText' data cannot be gotten.

Why? Well, in Microsoft Windows, it is not that the datum of any format can be just gotten in a uniform way as a bytes array; the data of a format have to be gotten in a certain way, but wxPython has not made the effort of supporting all the possible formats.

If the supported formats are not satisfactory, I will have to go to another technique (there will be an article for C# and an article for C++)


9: Testing


Hypothesizer 7
My test code is this for Microsoft Windows.

@Python Source Code
from typing import List
rom typing import TextIO
import sys
import wx
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDataComposite import ClipboardFormatSpecificDataComposite
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDataCompositesHistory import ClipboardFormatSpecificDataCompositesHistory
from theBiasPlanet.coreUtilities.clipboardHandling.ClipboardFormatSpecificDatum import ClipboardFormatSpecificDatum
from theBiasPlanet.coreUtilities.clipboardHandling.WxPythonClipboard import WxPythonClipboard
from theBiasPlanet.coreUtilities.clipboardHandling.WxPythonClipboardEventsHandler import WxPythonClipboardEventsHandler
from theBiasPlanet.coreUtilities.constantsGroups.ClipboardDatumFormatNameToFormatMapsConstantsGroup import ClipboardDatumFormatNameToFormatMapsConstantsGroup

class Test1Test:
	@staticmethod
	def main (a_arguments: List [str]) -> None:
		Test1Test.test ()
	
	@staticmethod
	def test () -> None:
		l_clipbardFormatSpecificDataCompositesHistory: "ClipboardFormatSpecificDataCompositesHistory" = ClipboardFormatSpecificDataCompositesHistory ()
		l_standardInputReader: TextIO = sys.stdin
		WxPythonClipboard.setUp (ClipboardDatumFormatNameToFormatMapsConstantsGroup.c_windowsDatumFormatNameToFormatMap, "Text")
		while True:
			sys.stdout.write ("### Input 'S' (Set), 'G' (Get), 'D (Display)', or 'Q' (Quit):\n")
			sys.stdout.flush ()
			l_userInput: str = l_standardInputReader.read (2)
			l_clipboardFormatSpecificDataCompositeKey: str
			if l_userInput.startswith ("S") or l_userInput.startswith ("G") or l_userInput.startswith ("D"):
				sys.stdout.write ("### Input the key:\n")
				sys.stdout.flush ()
				l_clipboardFormatSpecificDataCompositeKey = l_standardInputReader.read (2) [0]
				if l_userInput.startswith ("S"):
					WxPythonClipboard.openClipboard ()
					WxPythonClipboard.clearClipboard ()
					WxPythonClipboard.setFormatSpecificDataComposite (l_clipbardFormatSpecificDataCompositesHistory.getDataComposite (l_clipboardFormatSpecificDataCompositeKey))
					WxPythonClipboard.closeClipboard ()
				elif l_userInput.startswith ("G"):
					WxPythonClipboard.openClipboard ()
					l_clipbardFormatSpecificDataCompositesHistory.addDataComposite (l_clipboardFormatSpecificDataCompositeKey, WxPythonClipboard.getFormatSpecificDataComposite ())
					WxPythonClipboard.closeClipboard ()
				elif l_userInput.startswith ("D"):
					l_clipboardFormatSpecificDataComposite: "ClipboardFormatSpecificDataComposite " = l_clipbardFormatSpecificDataCompositesHistory.getDataComposite (l_clipboardFormatSpecificDataCompositeKey)
					l_clipboardDatumFormatNames: List [str] = l_clipboardFormatSpecificDataComposite.getFormatNames ()
					l_clipboardDatumFormatName: str = None
					for l_clipboardDatumFormatName in l_clipboardDatumFormatNames:
						l_clipboardFormatSpecificDatum: "ClipboardFormatSpecificDatum " = l_clipboardFormatSpecificDataComposite.getFormatSpecificDatum (l_clipboardDatumFormatName)
						sys.stdout.write ("### clipboard datum format number, format name, size: {0:d}, {1:s}, {2:d}\n".format (l_clipboardFormatSpecificDatum.getFormatNumber (), l_clipboardDatumFormatName, l_clipboardFormatSpecificDatum.getDatumSize ()))
						sys.stdout.flush ()
			else:
				break

if __name__ == "__main__":
	l_application = wx.App ()
	Test1Test.main (sys.argv)


theBiasPlanet/coreUtilities/constantsGroups/ClipboardDatumFormatNameToFormatMapsConstantsGroup.py

@Python Source Code
from collections import OrderedDict
from wx import DataFormat as wx_DataFormat
import wx

class ClipboardDatumFormatNameToFormatMapsConstantsGroup:
	c_linuxDatumFormatNameToFormatMap: "OrderedDict [str, wx_DataFormat]" = OrderedDict ({"Text": wx_DataFormat (wx.DF_TEXT), "Bitmap": wx_DataFormat (wx.DF_BITMAP), "Metafile": wx_DataFormat (wx.DF_METAFILE), "Filename": wx_DataFormat (wx.DF_FILENAME), "HTML Format": wx_DataFormat (wx.DF_HTML), "application/x-openoffice-embed-source-xml;windows_formatname=\"Star Embed Source (XML)\"": wx_DataFormat ("application/x-openoffice-embed-source-xml;windows_formatname=\"Star Embed Source (XML)\""), "text/rtf": wx_DataFormat ("text/rtf"), "text/richtext": wx_DataFormat ("text/richtext"), "text/html": wx_DataFormat ("text/html"), "text/plain;charset=utf-16": wx_DataFormat ("text/plain;charset=utf-16"), "application/x-openoffice-objectdescriptor-xml;windows_formatname=\"Star Object Descriptor (XML)\";classname=\"8BC6B165-B1B2-4EDD-aa47-dae2ee689dd6\";typename=\"LibreOffice 6.4 Text Document\";viewaspect=\"1\";width=\"16999\";height=\"2995\";posx=\"0\";posy=\"0\"": wx_DataFormat ("application/x-openoffice-objectdescriptor-xml;windows_formatname=\"Star Object Descriptor (XML)\";classname=\"8BC6B165-B1B2-4EDD-aa47-dae2ee689dd6\";typename=\"LibreOffice 6.4 Text Document\";viewaspect=\"1\";width=\"16999\";height=\"2995\";posx=\"0\";posy=\"0\""), "text/plain;charset=utf-8": wx_DataFormat ("text/plain;charset=utf-8"), "text/plain": wx_DataFormat ("text/plain"), "UTF8_STRING": wx_DataFormat ("UTF8_STRING"), "STRING": wx_DataFormat ("STRING"), "TEXT": wx_DataFormat ("TEXT"), "MULTIPLE": wx_DataFormat ("MULTIPLE"), "TIMESTAMP": wx_DataFormat ("TIMESTAMP")})
	c_windowsDatumFormatNameToFormatMap: "OrderedDict [str, wx_DataFormat]" = OrderedDict ({"Text": wx_DataFormat (wx.DF_TEXT), "Metafile": wx_DataFormat (wx.DF_METAFILE), "Filename": wx_DataFormat (wx.DF_FILENAME), "UnicodeText": wx_DataFormat ("UnicodeText"), "Locale": wx_DataFormat ("Locale"), "OEMText": wx_DataFormat ("OEMText"), "DataObject": wx_DataFormat ("DataObject"), "Preferred DropEffect": wx_DataFormat ("Preferred DropEffect"), "Ole Private Data": wx_DataFormat ("Ole Private Data"), "HTML Format": wx_DataFormat (wx.DF_HTML), "HTML (HyperText Markup Language)": wx_DataFormat ("HTML (HyperText Markup Language)"), "Star Embed Source (XML)": wx_DataFormat ("Star Embed Source (XML)"), "Rich Text Format": wx_DataFormat ("Rich Text Format"), "Richtext Format": wx_DataFormat ("Richtext Format"), "Link": wx_DataFormat ("Link"), "Star Object Descriptor (XML)": wx_DataFormat ("Star Object Descriptor (XML)"), "GDIMetaFile": wx_DataFormat ("GDIMetaFile"), "EnhancedMetafile": wx_DataFormat ("EnhancedMetafile"), "MetaFilePict": wx_DataFormat ("MetaFilePict"), "PNG": wx_DataFormat ("PNG"), "DeviceIndependentBitmap": wx_DataFormat ("DeviceIndependentBitmap"), "Windows Bitmap": wx_DataFormat ("Windows Bitmap"), "SymbolicLink": wx_DataFormat ("SymbolicLink"), "DataInterchangeFormat": wx_DataFormat ("DataInterchangeFormat"), "EditEngine ODF": wx_DataFormat ("EditEngine ODF")})


After I have copied a text piece on a 'cmd' terminal, I get an output like this for the 'G' -> 'A' -> 'D' -> 'A' inputs.

@Output
### clipboard datum format number, format name, size: 1, Text, 8

After I have copied a text piece on a LibreOffice Writer instance, I get an output like this for the 'G' -> 'A' -> 'D' -> 'A' inputs.

@Output
### clipboard datum format number, format name, size: 1, Text, 37
### clipboard datum format number, format name, size: 49161, DataObject, 8
### clipboard datum format number, format name, size: 49171, Ole Private Data, 552
### clipboard datum format number, format name, size: 30, HTML Format, 772
### clipboard datum format number, format name, size: 49819, HTML (HyperText Markup Language), 667
### clipboard datum format number, format name, size: 49817, Star Embed Source (XML), 6279
### clipboard datum format number, format name, size: 49285, Rich Text Format, 2675
### clipboard datum format number, format name, size: 49790, Richtext Format, 2675
### clipboard datum format number, format name, size: 49852, Link, 111
### clipboard datum format number, format name, size: 49853, Star Object Descriptor (XML), 164

For Linux, I have to change the expected clipboard datum formats.


References


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