web-dev-qa-db-fra.com

Python avec entrée en temps réel et plusieurs consoles

Le principal problème

En bref: je veux deux consoles pour mon programme. Un pour l'entrée utilisateur active. Et l'autre pour la sortie du journal pur. (Le code de travail incluant la réponse acceptée est dans le texte de la question ci-dessous, dans la section "Edit-3". Et dans la section "Edit- 1 "et la section" Edit-2 "sont des solutions de contournement qui fonctionnent.)

Pour cela, j'ai une ligne de commande principale Python, qui est censé ouvrir une console supplémentaire pour la sortie du journal uniquement. Pour cela, j'ai l'intention de rediriger la sortie du journal, qui serait imprimée sur le principal la console du script, vers le stdin de la deuxième console, que je démarre en tant que sous-processus (j'utilise un sous-processus, car je n'ai trouvé aucun autre moyen d'ouvrir une deuxième console).

Le problème est qu'il semble que je peux envoyer au stdin de cette deuxième console - cependant, rien n'est imprimé sur cette deuxième console.

Voici le code que j'ai utilisé pour expérimenter (avec Python 3.4 sur PyDev sous Windows 10). La fonction writing(input, pipe, process) contient la partie, où la chaîne générée est copiée dans le as pipe passé stdin, de la console ouverte via le sous-processus. La fonction d'écriture (...) est exécutée via la classe writetest(Thread). (J'ai laissé du code, que j'ai commenté.)

import os
import sys
import io
import time
import threading
from cmd import Cmd
from queue import Queue
from subprocess import Popen, PIPE, CREATE_NEW_CONSOLE


REPETITIONS = 3


# Position of "The class" (Edit-2)


# Position of "The class" (Edit-1)


class generatetest(threading.Thread):

    def __init__(self, queue):
        self.output = queue
        threading.Thread.__init__(self)

    def run(self):
        print('run generatetest')
        generating(REPETITIONS, self.output)
        print('generatetest done')

    def getout(self):
        return self.output


class writetest(threading.Thread):

    def __init__(self, input=None, pipe=None, process=None):
        if (input == None):        # just in case
            self.input = Queue()
        else:
            self.input = input

        if (pipe == None):        # just in case
            self.pipe = PIPE
        else:
            self.pipe = pipe

        if (process == None):        # just in case
            self.process = subprocess.Popen('C:\Windows\System32\cmd.exe', universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)
        else:
            self.process = proc

        threading.Thread.__init__(self)

    def run(self):
        print('run writetest')
        writing(self.input, self.pipe, self.process)
        print('writetest done')


# Position of "The function" (Edit-2)


# Position of "The function" (Edit-1)


def generating(maxint, outline):
    print('def generating')
    for i in range(maxint):
        time.sleep(1)
        outline.put_nowait(i)


def writing(input, pipe, process):
    print('def writing')
    while(True):
        try:
            print('try')
            string = str(input.get(True, REPETITIONS)) + "\n"
            pipe = io.StringIO(string)
            pipe.flush()
            time.sleep(1)
            # print(pipe.readline())
        except:
            print('except')
            break
        finally:
            print('finally')
            pass


data_queue = Queue()
data_pipe = sys.stdin
# printer = sys.stdout
# data_pipe = os.pipe()[1]


# The code of 'C:\\Users\\Public\\Documents\\test\\test-cmd.py'
# can be found in the question's text further below under "More code"


exe = 'C:\Python34\python.exe'
# exe = 'C:\Windows\System32\cmd.exe'
arg = 'C:\\Users\\Public\\Documents\\test\\test-cmd.py'
arguments = [exe, arg]
# proc = Popen(arguments, universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)
proc = Popen(arguments, stdin=data_pipe, stdout=PIPE, stderr=PIPE,
             universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)


# Position of "The call" (Edit-2 & Edit-1) - file init (proxyfile)


# Position of "The call" (Edit-2) - thread = sockettest()
# Position of "The call" (Edit-1) - thread0 = logtest()
thread1 = generatetest(data_queue)
thread2 = writetest(data_queue, data_pipe, proc)
# time.sleep(5)


# Position of "The call" (Edit-2) - thread.start()
# Position of "The call" (Edit-1) - thread0.start()
thread1.start()
thread2.start()


# Position of "The call" (Edit-2) - thread.join()
# Position of "The call" (Edit-1) - thread.join()
thread1.join(REPETITIONS * REPETITIONS)
thread2.join(REPETITIONS * REPETITIONS)

# data_queue.join()
# receiver = proc.communicate(stdin, 5)
# print('OUT:' + receiver[0])
# print('ERR:' + receiver[1])

print("1st part finished")

Une approche légèrement différente

L'extrait de code supplémentaire suivant fonctionne en ce qui concerne l'extraction de la sortie standard du sous-processus. Cependant, le stdin précédemment envoyé n'est toujours pas imprimé sur la deuxième console. De plus, la deuxième console est fermée immédiatement.

proc2 = Popen(['C:\Python34\python.exe', '-i'],
              stdin=PIPE,
              stdout=PIPE,
              stderr=PIPE,
              creationflags=CREATE_NEW_CONSOLE)
proc2.stdin.write(b'2+2\n')
proc2.stdin.flush()
print(proc2.stdout.readline())
proc2.stdin.write(b'len("foobar")\n')
proc2.stdin.flush()
print(proc2.stdout.readline())
time.sleep(1)
proc2.stdin.close()
proc2.terminate()
proc2.wait(timeout=0.2)

print("Exiting Main Thread")

Plus d'informations

Dès que j'utilise l'un des paramètres stdin=data_pipe, stdout=PIPE, stderr=PIPE Pour démarrer le sous-processus, la deuxième console résultante n'est pas active et n'accepte pas la saisie au clavier (ce qui n'est pas souhaité, mais peut-être des informations utiles ici).

La méthode de sous-processus communicate() ne peut pas être utilisée pour cela car elle attend la fin du processus.


Plus de code

Enfin le code du fichier, qui correspond à la deuxième console.

C:\Users\Public\Documents\test\test-cmd.py

from cmd import Cmd
from time import sleep
from datetime import datetime

INTRO = 'command line'
Prompt = '> '


class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, intro=INTRO, Prompt=PROMPT):
        Cmd.__init__(self)
        self.intro = intro
        self.Prompt = Prompt
        self.doc_header = intro
        self.running = False

    def do_dummy(self, args):
        """Runs a dummy method."""
        print("Do the dummy.")
        self.running = True
        while(self.running == True):
            print(datetime.now())
            sleep(5)

    def do_stop(self, args):
        """Stops the dummy method."""
        print("Stop the dummy, if you can.")
        self.running = False

    def do_exit(self, args):
        """Exits this console."""
        print("Do console exit.")
        exit()

if __name__ == '__main__':
    cl = CommandLine()
    cl.Prompt = Prompt
    cl.cmdloop(INTRO)

Pensées

Jusqu'à présent, je ne suis même pas certain que l'interface de ligne de commande Windows offre la possibilité d'accepter une autre entrée que celle du clavier (au lieu du canal stdin souhaité ou similaire). Cependant, avec un mode passif, je m'y attendais.

Pourquoi ça ne marche pas?


Edit-1: Solution de contournement via un fichier (preuve de concept)

L'utilisation d'un fichier comme solution de contournement pour afficher son nouveau contenu, comme suggéré dans la réponse de Travailler sur plusieurs consoles en python , fonctionne en général. Cependant, comme le fichier journal augmentera jusqu'à plusieurs Go, ce n'est pas une solution pratique dans ce cas. Cela nécessiterait au moins le fractionnement de fichiers et sa gestion appropriée.

La classe:

class logtest(threading.Thread):

    def __init__(self, file):
        self.file = file
        threading.Thread.__init__(self)

    def run(self):
        print('run logtest')
        logging(self.file)
        print('logtest done')

La fonction:

def logging(file):
    pexe = 'C:\Python34\python.exe '
    script = 'C:\\Users\\Public\\Documents\\test\\test-004.py'
    filek = '--file'
    filev = file

    file = open(file, 'a')
    file.close()
    time.sleep(1)

    print('LOG START (outer): ' + script + ' ' + filek + ' ' + filev)
    proc = Popen([pexe, script, filek, filev], universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)
    print('LOG FINISH (outer): ' + script + ' ' + filek + ' ' + filev)

    time.sleep(2)

L'appel:

# The file tempdata is filled with several strings of "0\n1\n2\n"
# Looking like this:
# 0
# 1
# 2
# 0
# 1
# 2

proxyfile = 'C:\\Users\\Public\\Documents\\test\\tempdata'
f = open(proxyfile, 'a')
f.close()
time.sleep(1)

thread0 = logtest(proxyfile)
thread0.start()
thread0.join(REPETITIONS * REPETITIONS)

Le script de queue ("test-004.py"):

Comme Windows n'offre pas la commande tail, j'ai plutôt utilisé le script suivant (basé sur la réponse pour Comment implémenter un équivalent Pythonic de tail -F? ), qui a fonctionné pour cela. La fonction supplémentaire [ mais non nécessaire class CommandLine(Cmd) était initialement une tentative de garder la deuxième console ouverte (car l'argument du fichier de script était manquant). Cependant, il s'est également révélé utile pour garder la console imprimant couramment le nouveau contenu du fichier journal. Sinon, la sortie n'était pas déterministe/prévisible.

import time
import sys
import os
import threading
from cmd import Cmd
from argparse import ArgumentParser


def main(args):
    parser = ArgumentParser(description="Parse arguments.")
    parser.add_argument("-f", "--file", type=str, default='', required=False)
    arguments = parser.parse_args(args)

    if not arguments.file:
        print('LOG PRE-START (inner): file argument not found. Creating new default entry.')
        arguments.file = 'C:\\Users\\Public\\Documents\\test\\tempdata'

    print('LOG START (inner): ' + os.path.abspath(os.path.dirname(__file__)) + ' ' + arguments.file)

    f = open(arguments.file, 'a')
    f.close()
    time.sleep(1)

    words = ['Word']
    console = CommandLine(arguments.file, words)
    console.Prompt = ''

    thread = threading.Thread(target=console.cmdloop, args=('', ))
    thread.start()
    print("\n")

    for hit_Word, hit_sentence in console.watch():
        print("Found %r in line: %r" % (hit_Word, hit_sentence))

    print('LOG FINISH (inner): ' + os.path.abspath(os.path.dirname(__file__)) + ' ' + arguments.file)


class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, fn, words):
        Cmd.__init__(self)
        self.fn = fn
        self.words = words

    def watch(self):
        fp = open(self.fn, 'r')
        while True:
            time.sleep(0.05)
            new = fp.readline()
            print(new)
            # Once all lines are read this just returns ''
            # until the file changes and a new line appears

            if new:
                for Word in self.words:
                    if Word in new:
                        yield (Word, new)

            else:
                time.sleep(0.5)


if __name__ == '__main__':
    print('LOG START (inner - as main).')
    main(sys.argv[1:])

Edit-1: Plus de réflexions

Trois solutions de contournement, que je n'ai pas encore essayées et qui pourraient fonctionner sont des sockets (également suggérées dans cette réponse Travailler plusieurs consoles en python ), obtenir un objet de processus via l'ID de processus pour plus de contrôle et utiliser le Bibliothèque ctypes pour accéder directement à l'API de la console Windows, permettant de définir le tampon d'écran, car la console peut avoir plusieurs tampons, mais un seul tampon actif (indiqué dans les remarques de la documentation pour le fonction CreateConsoleScreenBuffer ) .

Cependant, l'utilisation de sockets peut être la plus simple. Et au moins, la taille du journal n'a pas d'importance de cette façon. Cependant, les problèmes de connexion peuvent être un problème ici.


Edit-2: solution de contournement via les sockets (preuve de concept)

L'utilisation de sockets comme solution de contournement pour afficher de nouvelles entrées de journal, comme cela a également été suggéré dans la réponse de Travailler sur plusieurs consoles en python , fonctionne également en général. Cependant, cela semble être trop d'effort pour quelque chose, qui devrait simplement être envoyé au processus de la console de réception.

La classe:

class sockettest(threading.Thread):

    def __init__(self, Host, port, file):
        self.Host = Host
        self.port = port
        self.file = file
        threading.Thread.__init__(self)

    def run(self):
        print('run sockettest')
        socketing(self.Host, self.port, self.file)
        print('sockettest done')

La fonction:

def socketing(Host, port, file):
    pexe = 'C:\Python34\python.exe '
    script = 'C:\\Users\\Public\\Documents\\test\test-005.py'
    hostk = '--address'
    hostv = str(Host)
    portk = '--port'
    portv = str(port)
    filek = '--file'
    filev = file

    file = open(file, 'a')
    file.close()
    time.sleep(1)

    print('Host START (outer): ' + pexe + script + ' ' + hostk + ' ' + hostv + ' ' + portk + ' ' + portv + ' ' + filek + ' ' + filev)
    proc = Popen([pexe, script, hostk, hostv, portk, portv, filek, filev], universal_newlines=True, creationflags=CREATE_NEW_CONSOLE)

    print('Host FINISH (outer): ' + pexe + script + ' ' + hostk + ' ' + hostv + ' ' + portk + ' ' + portv + ' ' + filek + ' ' + filev)

    time.sleep(2)

L'appel:

# The file tempdata is filled with several strings of "0\n1\n2\n"
# Looking like this:
# 0
# 1
# 2
# 0
# 1
# 2

proxyfile = 'C:\\Users\\Public\\Documents\\test\\tempdata'
f = open(proxyfile, 'a')
f.close()
time.sleep(1)

thread = sockettest('127.0.0.1', 8888, proxyfile)
thread.start()
thread.join(REPETITIONS * REPETITIONS)

Le script de socket ("test-005.py"):

Le script suivant est basé sur Python: application serveur-client de programmation de socket utilisant des threads . Ici, je viens de conserver la class CommandLine(Cmd) comme générateur d'entrée de journal. À ce stade, cela ne devrait pas être un problème, de mettre le client dans le script principal, qui appelle la deuxième console, puis d'alimenter la file d'attente avec de véritables entrées de journal au lieu de (nouvelles) lignes de fichier. (Le serveur est l'imprimante.)

import socket
import sys
import threading
import time
from cmd import Cmd
from argparse import ArgumentParser
from queue import Queue

BUFFER_SIZE = 5120

class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, fn, words, queue):
        Cmd.__init__(self)
        self.fn = fn
        self.words = words
        self.queue = queue

    def watch(self):
        fp = open(self.fn, 'r')
        while True:
            time.sleep(0.05)
            new = fp.readline()

            # Once all lines are read this just returns ''
            # until the file changes and a new line appears
            self.queue.put_nowait(new)


def main(args):
    parser = ArgumentParser(description="Parse arguments.")
    parser.add_argument("-a", "--address", type=str, default='127.0.0.1', required=False)
    parser.add_argument("-p", "--port", type=str, default='8888', required=False)
    parser.add_argument("-f", "--file", type=str, default='', required=False)
    arguments = parser.parse_args(args)

    if not arguments.address:
        print('Host PRE-START (inner): Host argument not found. Creating new default entry.')
        arguments.Host = '127.0.0.1'
    if not arguments.port:
        print('Host PRE-START (inner): port argument not found. Creating new default entry.')
        arguments.port = '8888'
    if not arguments.file:
        print('Host PRE-START (inner): file argument not found. Creating new default entry.')
        arguments.file = 'C:\\Users\\Public\\Documents\\test\\tempdata'

    file_queue = Queue()

    print('Host START (inner): ' + ' ' + arguments.address + ':' + arguments.port + ' --file ' + arguments.file)

    # Start server
    thread = threading.Thread(target=start_server, args=(arguments.address, arguments.port, ))
    thread.start()
    time.sleep(1)

    # Start client
    thread = threading.Thread(target=start_client, args=(arguments.address, arguments.port, file_queue, ))
    thread.start()

    # Start file reader
    f = open(arguments.file, 'a')
    f.close()
    time.sleep(1)

    words = ['Word']
    console = CommandLine(arguments.file, words, file_queue)
    console.Prompt = ''

    thread = threading.Thread(target=console.cmdloop, args=('', ))
    thread.start()
    print("\n")

    for hit_Word, hit_sentence in console.watch():
        print("Found %r in line: %r" % (hit_Word, hit_sentence))

    print('Host FINISH (inner): ' + ' ' + arguments.address + ':' + arguments.port)


def start_client(Host, port, queue):
    Host = Host
    port = int(port)         # arbitrary non-privileged port
    queue = queue

    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        soc.connect((Host, port))
    except:
        print("Client connection error" + str(sys.exc_info()))
        sys.exit()

    print("Enter 'quit' to exit")
    message = ""

    while message != 'quit':
        time.sleep(0.05)
        if(message != ""):
            soc.sendall(message.encode("utf8"))
            if soc.recv(BUFFER_SIZE).decode("utf8") == "-":
                pass        # null operation

        string = ""
        if (not queue.empty()):
            string = str(queue.get_nowait()) + "\n"

        if(string == None or string == ""):
            message = ""
        else:
            message = string

    soc.send(b'--quit--')


def start_server(Host, port):
    Host = Host
    port = int(port)         # arbitrary non-privileged port

    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # SO_REUSEADDR flag tells the kernel to reuse a local socket in TIME_WAIT state, without waiting for its natural timeout to expire
    soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    print("Socket created")

    try:
        soc.bind((Host, port))
    except:
        print("Bind failed. Error : " + str(sys.exc_info()))
        sys.exit()

    soc.listen(5)       # queue up to 5 requests
    print("Socket now listening")

    # infinite loop- do not reset for every requests
    while True:
        connection, address = soc.accept()
        ip, port = str(address[0]), str(address[1])
        print("Connected with " + ip + ":" + port)

        try:
            threading.Thread(target=client_thread, args=(connection, ip, port)).start()
        except:
            print("Thread did not start.")
            traceback.print_exc()

    soc.close()


def client_thread(connection, ip, port, max_buffer_size=BUFFER_SIZE):
    is_active = True

    while is_active:
        client_input = receive_input(connection, max_buffer_size)

        if "--QUIT--" in client_input:
            print("Client is requesting to quit")
            connection.close()
            print("Connection " + ip + ":" + port + " closed")
            is_active = False
        Elif not client_input == "":
            print("{}".format(client_input))
            connection.sendall("-".encode("utf8"))
        else:
            connection.sendall("-".encode("utf8"))


def receive_input(connection, max_buffer_size):
    client_input = connection.recv(max_buffer_size)
    client_input_size = sys.getsizeof(client_input)

    if client_input_size > max_buffer_size:
        print("The input size is greater than expected {}".format(client_input_size))

    decoded_input = client_input.decode("utf8").rstrip()  # decode and strip end of line
    result = process_input(decoded_input)

    return result


def process_input(input_str):
    return str(input_str).upper()


if __name__ == '__main__':
    print('Host START (inner - as main).')
    main(sys.argv[1:])

Edit-2: En outre des pensées

Le contrôle direct du canal/tampon d'entrée de la console du sous-processus serait la solution préférable à ce problème. Car c'est la prime de 500 Réputation.

Malheureusement, je manque de temps. Par conséquent, je pourrais utiliser une de ces solutions de contournement pour l'instant et les remplacer par la solution appropriée plus tard. Ou peut-être que je dois utiliser l'option nucléaire, une seule console, où la sortie du journal en cours est interrompue pendant toute entrée au clavier de l'utilisateur et imprimée par la suite. Bien sûr, cela peut entraîner des problèmes de tampon, lorsque l'utilisateur décide de taper quelque chose à mi-chemin.


Edit-3: Code incluant la réponse acceptée (un fichier)

Avec la réponse de James Kent, j'obtiens le comportement souhaité, lorsque je démarre un script avec le code via la ligne de commande Windows (cmd) ou PowerShell. Cependant, lorsque je démarre ce même script via Eclipse/PyDev avec "Python run", la sortie est toujours imprimée sur la console principale Eclipse/PyDev, tandis que la deuxième console du sous-processus reste vide et reste inactive. Cependant, je suppose que c'est une autre spécialité système/environnement et un problème différent.

from sys import argv, stdin, stdout
from threading import Thread
from cmd import Cmd
from time import sleep
from datetime import datetime
from subprocess import Popen, PIPE, CREATE_NEW_CONSOLE

INTRO = 'command line'
Prompt = '> '


class CommandLine(Cmd):
    """Custom console"""

    def __init__(self, subprocess, intro=INTRO, Prompt=PROMPT):
        Cmd.__init__(self)
        self.subprocess = subprocess
        self.intro = intro
        self.Prompt = Prompt
        self.doc_header = intro
        self.running = False

    def do_date(self, args):
        """Prints the current date and time."""
        print(datetime.now())
        sleep(1)

    def do_exit(self, args):
        """Exits this command line application."""
        print("Exit by user command.")
        if self.subprocess is not None:
            try:
                self.subprocess.terminate()
            except:
                self.subprocess.kill()
        exit()


class Console():

    def __init__(self):
        if '-r' not in argv:
            self.p = Popen(
                ['python.exe', __file__, '-r'],
                stdin=PIPE,
                creationflags=CREATE_NEW_CONSOLE
            )
        else:
            while True:
                data = stdin.read(1)
                if not data:
                    #                     break
                    sleep(1)
                    continue
                stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

    def getSubprocess(self):
        if self.p:
            return self.p
        else:
            return None


class Feeder (Thread):

    def __init__(self, console):
        self.console = console
        Thread.__init__(self)

    def run(self):
        feeding(self.console)


def feeding(console):
    for i in range(0, 100):
        console.write('test %i\n' % i)
        sleep(1)


if __name__ == '__main__':
    p = Console()
    if '-r' not in argv:
        thread = Feeder(p)
        thread.setDaemon(True)
        thread.start()

        cl = CommandLine(subprocess=p.getSubprocess())
        cl.use_rawinput = False
        cl.Prompt = Prompt
        cl.cmdloop('\nCommand line is waiting for user input (e.g. help).')

Edit-3: Mentions honorables

Dans le texte des questions ci-dessus, j'ai mentionné l'utilisation de la bibliothèque ctypes pour accéder directement à l'API de la console Windows comme autre solution (sous "Édition-1: Autres réflexions"). Ou en utilisant une seule console d'une manière, l'invite d'entrée reste toujours en bas comme option nucléaire pour tout ce problème. (sous "Edit-2: En outre des pensées")

Pour utiliser la bibliothèque ctypes, je me serais orienté sur la réponse suivante à Changer la police de la console sous Windows . Et pour utiliser une seule console, j'aurais essayé la réponse suivante à Conserver la ligne d'entrée de la console sous la sortie . Je pense que ces deux réponses peuvent offrir des erreurs potentielles en ce qui concerne ce problème et peut-être qu'elles sont utiles aux autres sur la manière de traverser ce message. Aussi, si je trouve le temps, j'essaierai s'ils fonctionnent d'une manière ou d'une autre.

20
Jonathan Root

Le problème auquel vous êtes confronté est l'architecture du sous-système de console sous Windows, la fenêtre de console que vous voyez normalement n'est pas hébergée par cmd.exe mais par conhost.exe, un processus enfant d'une fenêtre conhost ne peut se connecter qu'à un instance de conhost unique, ce qui signifie que vous êtes limité à une seule fenêtre par processus.

Cela conduit ensuite à avoir un processus supplémentaire pour chaque fenêtre de console que vous souhaitez avoir, puis afin de regarder afficher quoi que ce soit dans cette fenêtre, vous devez regarder comment stdin et stdout sont normalement traités, en ce sens qu'ils sont écrits et lus à partir de par l'instance conhost, sauf si vous transformez stdin en pipe (afin que vous puissiez écrire dans le processus) il ne provient plus de conhost mais plutôt de votre processus parent et en tant que tel, conhost n'a aucune visibilité. Cela signifie que tout ce qui est écrit dans stdin n'est lu que par le processus enfant et n'est donc pas affiché par conhost.

Pour autant que je sache, il n'y a pas de moyen de partager la pipe comme ça.

En tant qu'effet secondaire, si vous faites de stdin un canal, toutes les entrées de clavier envoyées à la nouvelle fenêtre de console ne vont nulle part, car stdin n'est pas connecté à cette fenêtre.

Pour une fonction de sortie uniquement, cela signifie que vous pouvez générer un nouveau processus qui communique avec le parent via un canal vers stdin et renvoie tout à stdout.

Voici une tentative:

#!python3

import sys, subprocess, time

class Console():
    def __init__(self):
        if '-r' not in sys.argv:
            self.p = subprocess.Popen(
                ['python.exe', __file__, '-r'],
                stdin=subprocess.PIPE,
                creationflags=subprocess.CREATE_NEW_CONSOLE
                )
        else:
            while True:
                data = sys.stdin.read(1)
                if not data:
                    break
                sys.stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

if (__name__ == '__main__'):
    p = Console()
    if '-r' not in sys.argv:
        for i in range(0, 100):
            p.write('test %i\n' % i)
            time.sleep(1)

Donc, un simple tuyau sympa entre deux processus et faisant écho de l'entrée à la sortie si c'est le sous-processus, j'ai utilisé un -r pour signifier si l'instance est un processus mais il existe d'autres façons selon la façon dont vous l'implémentez.

Plusieurs choses à noter:

  • le vidage après l'écriture dans stdin est nécessaire car python utilise normalement la mise en mémoire tampon.
  • la façon dont cette approche est écrite vise à être dans son propre module d'où l'utilisation de __file__
  • en raison de l'utilisation de __file__ cette approche peut nécessiter une modification si elle est gelée à l'aide de cx_Freeze ou similaire.

EDIT 1

pour une version qui peut être gelée avec cx_Freeze:

Console.py

import sys, subprocess

class Console():
    def __init__(self, ischild=True):
        if not ischild:
            if hasattr(sys, 'frozen'):
                args = ['Console.exe']
            else:
                args = [sys.executable, __file__]
            self.p = subprocess.Popen(
                args,
                stdin=subprocess.PIPE,
                creationflags=subprocess.CREATE_NEW_CONSOLE
                )
        else:
            while True:
                data = sys.stdin.read(1)
                if not data:
                    break
                sys.stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

if (__name__ == '__main__'):
    p = Console()

test.py

from Console import Console
import sys, time

if (__name__ == '__main__'):
    p = Console(False)
    for i in range(0, 100):
        p.write('test %i\n' % i)
        time.sleep(1)

setup.py

from cx_Freeze import setup, Executable

setup(
    name = 'Console-test',
    executables = [
        Executable(
            'Console.py',
            base=None,
            ),
        Executable(
            'test.py',
            base=None,
            )
        ]
)

EDIT 2

Nouvelle version qui devrait fonctionner avec des outils de développement comme IDLE

Console.py

#!python3

import ctypes, sys, subprocess

Kernel32 = ctypes.windll.Kernel32

class Console():
    def __init__(self, ischild=True):
        if ischild:
            # try allocate new console
            result = Kernel32.AllocConsole()
            if result > 0:
                # if we succeed open handle to the console output
                sys.stdout = open('CONOUT$', mode='w')
        else:
            # if frozen we assume its names Console.exe
            # note that when frozen 'Win32GUI' must be used as a base
            if hasattr(sys, 'frozen'):
                args = ['Console.exe']
            else:
                # otherwise we use the console free version of python
                args = ['pythonw.exe', __file__]
            self.p = subprocess.Popen(
                args,
                stdin=subprocess.PIPE
                )
            return
        while True:
            data = sys.stdin.read(1)
            if not data:
                break
            sys.stdout.write(data)

    def write(self, data):
        self.p.stdin.write(data.encode('utf8'))
        self.p.stdin.flush()

if (__name__ == '__main__'):
    p = Console()

test.py

from Console import Console
import sys, time

if (__name__ == '__main__'):
    p = Console(False)
    for i in range(0, 100):
        p.write('test %i\n' % i)
        time.sleep(1)

setup.py

from cx_Freeze import setup, Executable

setup(
    name = 'Console-test',
    executables = [
        Executable(
            'Console.py',
            base='Win32GUI',
            ),
        Executable(
            'test.py',
            base=None,
            )
        ]
)

Cela pourrait être rendu plus robuste, c'est-à-dire toujours vérifier une console existante et la détacher si elle est trouvée avant de créer une nouvelle console, et éventuellement une meilleure gestion des erreurs.

14
James Kent