это вообще возможно? отправлять команды / объекты из одной оболочки python в другую? - PullRequest
1 голос
/ 22 апреля 2020

У меня есть вопрос, который я не смог решить после небольшой копки, и это тоже не моя область знаний, поэтому я даже не знаю, что ищу.

Мне интересно, можно ли "связать" вместе две python оболочки?

Это фактический вариант использования ...

Я работаю с программой, которая имеет свою собственную выделенная python оболочка, встроенная в GUI. Когда вы запускаете команды во внутренней оболочке python, GUI обновляется в режиме реального времени, отражая команды, которые вы выполняли.

Проблема в том, что среда сценариев ужасна. По сути, это текстовый блок рядом с оболочкой, и это просто постоянное копирование и вставка. Никаких серьезных разработок достичь невозможно.

Что я хочу сделать, так это открыть свою среду IDE (VSCode / Spyder), чтобы я мог получить надлежащая среда, но я могу запускать команды в моей среде IDE, которые каким-то образом отправляются во внутреннюю оболочку python программного обеспечения.

Можно ли как-то обнаружить открытую оболочку в программном обеспечении и подключиться / связать или создать канал между двумя python экземплярами? Таким образом, я могу передавать команды / python объектам между этими двумя объектами и в основном иметь одинаковое состояние переменных в каждой?

Самое близкое, что я увидел, - это модуль multiprocessing. , Или, возможно, socket или pexpect?

Передача данных между отдельно выполняющимися Python сценариями

Как совместно использовать переменные между сценариями в python ?

Даже если это может работать только односторонняя связь, просто захотите использовать это программное обеспечение в правильной среде разработки.

Честно говоря, на самом деле у меня нет Понятно, что я делаю и надеюсь на некоторую помощь здесь .. goal

Ответы [ 2 ]

2 голосов
/ 24 апреля 2020

TL.DR;

Это сложный запрос. Я не думаю, что может быть достигнуто с помощью трюка. Либо у вас есть процесс, в котором работает Blender (имеется в виду, что вы импортируете его API), либо вы подключаетесь к процессу (с помощью GDB, но я не знаю, сможете ли вы тогда использовать нужную вам IDE), или вы используете IDE с pydevd в комплекте. И даже в этом случае, я не знаю, сколько вы можете достичь.

Синхронизация двух python процессов не тривиальна. Ответ показывает немного об этом.


PyDev.Debugger

Вы хотите найти способ синхронизации двух python объектов, которые живут в разных python экземплярах. Я думаю, что единственное реальное решение вашей проблемы - настроить сервер pydevd и подключиться к нему. Проще использовать одну из поддерживаемых IDE, например PyDEV или PyCharm, поскольку у них есть все для этого:

Работа, выполняемая pydev, не тривиальна, репозиторий - это настоящий проект. Это ваш лучший выбор, но я не могу гарантировать, что он будет работать.


Обмен данными между процессами

Обычное решение для обмена данными не будет работать, поскольку они сериализуют и десериализуют (обрабатывают и распаковывают) данные за кулисами. Давайте рассмотрим пример реализации сервера в процессе blender, который получает произвольный код в виде строки и выполняет его, отправляя обратно последний результат кода. Результат будет получен клиентом в виде объекта Python, поэтому вы можете использовать свой интерфейс IDE для его проверки или даже запустить некоторый код на нем. Существуют ограничения:

  • не все может быть получено клиентом (например, определение класса должно существовать в клиенте)
  • только соединяемые объекты могут проходить по соединению
  • объект в клиенте и на сервере различен: изменения, сделанные на клиенте, не будут применены на сервере без дополнительных (довольно сложных) логи c

Это сервер, который должен запустите на вашем экземпляре Blender

from multiprocessing.connection import Listener
from threading import Thread
import pdb
import traceback

import ast
import copy


# This is your configuration, chose a strong password and only open
# on localhost, because you are opening an arbitrary code execution
# server. It is not much but at least we cover something.
port = 6000
address = ('127.0.0.1', port)
authkey = b'blender'
# If you want to run it from another machine, you must set the address
# to '0.0.0.0' (on Linux, on Windows is not accepted and you have to
# specify the interface that will accept the connection) 


# Credits: https://stackoverflow.com/a/52361938/2319299
# Awesome piece of code, it is a carbon copy from there

def convertExpr2Expression(Expr):
    r"""
    Convert a "subexpression" of a piece of code in an actual
    Expression in order to be handled by eval without a syntax error

    :param Expr: input expression
    :return: an ast.Expression object correctly initialized
    """
    Expr.lineno = 0
    Expr.col_offset = 0
    result = ast.Expression(Expr.value, lineno=0, col_offset=0)
    return result


def exec_with_return(code):
    r"""
    We need an evaluation with return value. The only two function 
    that are available are `eval` and `exec`, where the first evaluates
    an expression, returning the result and the latter evaluates arbitrary code
    but does not return.

    Those two functions intercept the commands coming from the client and checks
    if the last line is an expression. All the code is executed with an `exec`,
    if the last one is an expression (e.g. "a = 10"), then it will return the 
    result of the expression, if it is not an expression (e.g. "import os")
    then it will only `exec` it.

    It is bindend with the global context, thus it saves the variables there.

    :param code: string of code
    :return: object if the last line is an expression, None otherwise
    """
    code_ast = ast.parse(code)
    init_ast = copy.deepcopy(code_ast)
    init_ast.body = code_ast.body[:-1]
    last_ast = copy.deepcopy(code_ast)
    last_ast.body = code_ast.body[-1:]
    exec(compile(init_ast, "<ast>", "exec"), globals())
    if type(last_ast.body[0]) == ast.Expr:
        return eval(compile(convertExpr2Expression(last_ast.body[0]), "<ast>", "eval"), globals())
    else:
        exec(compile(last_ast, "<ast>", "exec"), globals())

# End of carbon copy code


class ArbitraryExecutionServer(Thread):
    r"""
    We create a server execute arbitrary piece of code (the most dangerous
    approach ever, but needed in this case) and it is capable of sending
    python object. There is an important thing to keep in mind. It cannot send
    **not pickable** objects, that probably **include blender objects**!

    This is a dirty server to be used as an example, the only way to close 
    it is by sending the "quit" string on the connection. You can envision
    your stopping approach as you wish

    It is a Thread object, remeber to initialize it and then call the
    start method on it.

    :param address: the tuple with address interface and port
    :param authkey: the connection "password"
    """

    QUIT = "quit" ## This is the string that closes the server

    def __init__(self, address, authkey):
        self.address = address
        self.authkey = authkey
        super().__init__()

    def run(self):
        last_input = ""
        with Listener(self.address, authkey=self.authkey) as server:
            with server.accept() as connection:
                while last_input != self.__class__.QUIT:
                    try:
                        last_input = connection.recv()
                        if last_input != self.__class__.QUIT:
                            result = exec_with_return(last_input) # Evaluating remote input                       
                            connection.send(result)
                    except:
                        # In case of an error we return a formatted string of the exception
                        # as a little plus to understand what's happening
                        connection.send(traceback.format_exc())


if __name__ == "__main__":
    server = ArbitraryExecutionServer(address, authkey)
    server.start() # You have to start the server thread
    pdb.set_trace() # I'm using a set_trace to get a repl in the server.
                    # You can start to interact with the server via the client
    server.join() # Remember to join the thread at the end, by sending quit

Пока это клиент в вашем VSCode

import time
from multiprocessing.connection import Client


# This is your configuration, should be coherent with 
# the one on the server to allow the connection
port = 6000
address = ('127.0.0.1', port)
authkey = b'blender'


class ArbitraryExecutionClient:
    QUIT = "quit"

    def __init__(self, address, authkey):
        self.address = address
        self.authkey = authkey
        self.connection = Client(address, authkey=authkey)

    def close(self):
        self.connection.send(self.__class__.QUIT)
        time.sleep(0.5)  # Gives some time before cutting connection
        self.connection.close()

    def send(self, code):
        r"""
        Run an arbitrary piece of code on the server. If the
        last line is an expression a python object will be returned.
        Otherwise nothing is returned
        """
        code = str(code)
        self.connection.send(code)
        result = self.connection.recv()
        return result

    def repl(self):
        r"""
        Run code in a repl loop fashion until user enter "quit". Closing
        the repl will not close the connection. It must be manually 
        closed.
        """
        last_input = ""
        last_result = None
        while last_input != self.__class__.QUIT:
            last_input = input("REMOTE >>> ")
            if last_input != self.__class__.QUIT:
                last_result = self.send(last_input)
                print(last_result)
        return last_result


if __name__ == "__main__":
    client = ArbitraryExecutionClient(address, authkey)
    import pdb; pdb.set_trace()
    client.close()

В нижней части скрипта также есть способ их запуска с pdb как «РЕПЛ». С этой конфигурацией вы можете запускать произвольный код с клиента на сервере (и на самом деле это чрезвычайно опасный сценарий , но для вашей конкретной ситуации c допустима, или лучше "основное требование").

Давайте углубимся в ожидаемое мной ограничение.

Вы можете определить класс Foo на сервере:

[client] >>> client = ArbitraryExecutionClient(address, authkey)
[client] >>> client.send("class Foo: pass")

[server] >>> Foo
[server] <class '__main__.Foo'>

и Вы можете определить объект с именем "foo" на сервере, но вы сразу получите сообщение об ошибке, потому что класс Foo не существует в локальном экземпляре (на этот раз с использованием repl):

[client] >>> client.repl()
[client] REMOTE >>> foo = Foo()
[client] None
[client] REMOTE >>> foo
[client] *** AttributeError: Can't get attribute 'Foo' on <module '__main__' from 'client.py'>

this ошибка возникает из-за отсутствия объявления класса Foo в локальном экземпляре, поэтому нет способа правильно распаковать полученный объект (эта проблема появится со всеми объектами Blender. Обратите внимание, если объект каким-либо образом импортируемый, он может все еще работает, мы увидим это позже).

Единственный способ не получить ошибку - это предварительно объявить c lass также на клиенте, но они не будут тем же объектом, как вы можете увидеть, посмотрев на их идентификаторы:

[client] >>> class Foo: pass
[client] >>> client.send("foo")
[client] <__main__.Foo object at 0x0000021E2F2F3488>

[server] >>> foo
[server] <__main__.Foo object at 0x00000203AE425308>

Их идентификаторы отличаются, потому что они живут в другом пространстве памяти: они полностью разные экземпляры, и вы должны вручную синхронизировать каждую операцию над ними!

Если определение класса каким-то образом импортируемо, а объект можно выбрать, вы можете избежать умножения определения класса, насколько я вижу он будет автоматически импортирован:

[client] >>> client.repl()
[client] REMOTE >>> import numpy as np
[client] None
[client] REMOTE >>> ary = np.array([1, 2, 3])
[client] None
[client] REMOTE >>> ary
[client] [1 2 3]
[client] REMOTE >>> quit
[client] array([1, 2, 3])
[client] >>> ary = client.send("ary")
[client] >>> ary
[client] array([1, 2, 3])
[client] >>> type(ary)
[client] <class 'numpy.ndarray'>

Мы никогда не импортировали на клиенте numpy, но мы правильно получили объект. Но что произойдет, если мы изменим локальный экземпляр на удаленный экземпляр?

[client] >>> ary[0] = 10
[client] >>> ary
[client] array([10,  2,  3])
[client] >>> client.send("ary")
[client] array([1, 2, 3])

[server] >>> ary
[server] array([1, 2, 3])

у нас нет синхронизации изменений внутри объекта.

Что произойдет, если объект не будет выбран? Мы можем протестировать с помощью переменной server, объект, который является Thread и содержит соединение, которое нельзя выбрать (это означает, что вы не можете дать им обратимое представление в виде списка байтов):

[server] >>> import pickle
[server] >>> pickle.dumps(server)
[server] *** TypeError: can't pickle _thread.lock objects

, а также мы можем увидеть ошибку на клиенте, пытаясь ее получить:

[client] >>> client.send("server")
[client] ... traceback for "TypeError: can't pickle _thread.lock objects" exception ...

Я не думаю, что есть «простое» решение этой проблемы, но я думаю, что есть какая-то библиотека (например, pydevd), которая реализует полный протокол для преодоления этой проблемы.

Надеюсь, теперь мои комментарии стали более понятными.

1 голос
/ 23 апреля 2020

Здесь собраны все части!

  • Многопоточность, чтобы и система обработки файлов, и интерактивная оболочка Python могли работать одновременно.
  • Переменные обновлено между интерактивной оболочкой и файлом. Другими словами, файл и интерактивная оболочка совместно используют переменные, функции, классы и т. Д. c.
  • Мгновенное обновление между оболочкой и файлом.

enter image description here

import threading
import platform
import textwrap
import traceback
import hashlib
import runpy
import code
import time
import sys
import os


def clear_console():
    """ Clear your console depending on OS. """

    if platform.system() == "Windows":
        os.system("cls")
    elif platform.system() in ("Darwin", "Linux"):
        os.system("clear")


def get_file_md5(file_name):
    """ Grabs the md5 hash of the file. """

    with open(file_name, "rb") as f:
        return hashlib.md5(f.read()).hexdigest()


def track_file(file_name, one_way=False):
    """ Process external file. """

    # Grabs current md5 of file.
    md5 = get_file_md5(file_name)

    # Flag for the first run.
    first_run = True

    # If the event is set, thread gracefully quits by exiting loop.
    while not event_close_thread.is_set():

        time.sleep(0.1)

        # Gets updated (if any) md5 hash of file.
        md5_current = get_file_md5(file_name)
        if md5 != md5_current or first_run:
            md5 = md5_current

            # Executes the content of the file.
            try:
                # Gather the threads global scope to update the main thread's scope.
                thread_scope = runpy.run_path(file_name, init_globals=globals())

                if not one_way:
                    # Updates main thread's scope with other thread..
                    globals().update(thread_scope)

                # Prints updated only after first run.
                if not first_run:
                    print(f'\n{"="*20} File {file_name} updated! {"="*20}\n>>> ', end="")
                else:
                    first_run = False

            except:
                print(
                    f'\n{"="*20} File {file_name} threw error! {"="*20}\n {traceback.format_exc()}\n>>> ',
                    end="",
                )


def track(file_name):
    """ Initializes tracking thread (must be started with .start()). """

    print(f'{"="*20} File {file_name} being tracked! {"="*20}')
    return threading.Thread(target=track_file, args=(file_name,)).start()


if __name__ == "__main__":
    clear_console()

    # Creates a thread event for garbage collection, and file lock.
    event_close_thread = threading.Event()

    banner = textwrap.dedent(
        f"""\
        {"="*20} Entering Inception Shell {"="*20}\n
        This shell allows the sharing of the global scope between
        Python files and the Python interactive shell. To use:

        \t >>> track("script.py", one_way=False)

        On update of the file 'script.py' this shell will execute the
        file (passing the shells global variables to it), and then, if
        one_way is False, update its own global variables to that of the
        file's execution.
        """
    )

    # Begins interactive shell.
    code.interact(banner=banner, readfunc=None, local=globals(), exitmsg="")

    # Gracefully exits the thread.
    event_close_thread.set()

    # Exits shell.
    print(f'\n{"="*20} Exiting Inception Shell {"="*20}')
    exit()

Один вкладыш:

exec("""\nimport threading\nimport platform\nimport textwrap\nimport traceback\nimport hashlib\nimport runpy\nimport code\nimport time\nimport sys\nimport os\n\n\ndef clear_console():\n    \"\"\" Clear your console depending on OS. \"\"\"\n\n    if platform.system() == "Windows":\n        os.system("cls")\n    elif platform.system() in ("Darwin", "Linux"):\n        os.system("clear")\n\n\ndef get_file_md5(file_name):\n    \"\"\" Grabs the md5 hash of the file. \"\"\"\n\n    with open(file_name, "rb") as f:\n        return hashlib.md5(f.read()).hexdigest()\n\n\ndef track_file(file_name, one_way=False):\n    \"\"\" Process external file. \"\"\"\n\n    # Grabs current md5 of file.\n    md5 = get_file_md5(file_name)\n\n    # Flag for the first run.\n    first_run = True\n\n    # If the event is set, thread gracefully quits by exiting loop.\n    while not event_close_thread.is_set():\n\n        time.sleep(0.1)\n\n        # Gets updated (if any) md5 hash of file.\n        md5_current = get_file_md5(file_name)\n        if md5 != md5_current or first_run:\n            md5 = md5_current\n\n            # Executes the content of the file.\n            try:\n                # Gather the threads global scope to update the main thread's scope.\n                thread_scope = runpy.run_path(file_name, init_globals=globals())\n\n                if not one_way:\n                    # Updates main thread's scope with other thread..\n                    globals().update(thread_scope)\n\n                # Prints updated only after first run.\n                if not first_run:\n                    print(f'\\n{"="*20} File {file_name} updated! {"="*20}\\n>>> ', end="")\n                else:\n                    first_run = False\n\n            except:\n                print(\n                    f'\\n{"="*20} File {file_name} threw error! {"="*20}\\n {traceback.format_exc()}\\n>>> ',\n                    end="",\n                )\n\n\ndef track(file_name):\n    \"\"\" Initializes tracking thread (must be started with .start()). \"\"\"\n\n    print(f'{"="*20} File {file_name} being tracked! {"="*20}')\n    return threading.Thread(target=track_file, args=(file_name,)).start()\n\n\nif __name__ == "__main__":\n    clear_console()\n\n    # Creates a thread event for garbage collection, and file lock.\n    event_close_thread = threading.Event()\n\n    banner = textwrap.dedent(\n        f\"\"\"\\\n        {"="*20} Entering Inception Shell {"="*20}\\n\n        This shell allows the sharing of the global scope between\n        Python files and the Python interactive shell. To use:\n\n        \\t >>> track("script.py", one_way=False)\n\n        On update of the file 'script.py' this shell will execute the\n        file (passing the shells global variables to it), and then, if\n        one_way is False, update its own global variables to that of the\n        file's execution.\n        \"\"\"\n    )\n\n    # Begins interactive shell.\n    code.interact(banner=banner, readfunc=None, local=globals(), exitmsg="")\n\n    # Gracefully exits the thread.\n    event_close_thread.set()\n\n    # Exits shell.\n    print(f'\\n{"="*20} Exiting Inception Shell {"="*20}')\n    exit()\n""")

Попробуйте следующее для вашей оболочки Blender:

import threading
import traceback
import hashlib
import runpy
import time


def get_file_md5(file_name):
    """ Grabs the md5 hash of the file. """

    with open(file_name, "rb") as f:
        return hashlib.md5(f.read()).hexdigest()


def track_file(file_name, one_way=False):
    """ Process external file. """

    # Grabs current md5 of file.
    md5 = get_file_md5(file_name)

    # Flag for the first run.
    first_run = True

    # If the event is set, thread gracefully quits by exiting loop.
    while not event_close_thread.is_set():

        time.sleep(0.1)

        # Gets updated (if any) md5 hash of file.
        md5_current = get_file_md5(file_name)
        if md5 != md5_current or first_run:
            md5 = md5_current

            # Executes the content of the file.
            try:
                # Gather the threads global scope to update the main thread's scope.
                thread_scope = runpy.run_path(file_name, init_globals=globals())

                if not one_way:
                    # Updates main thread's scope with other thread..
                    globals().update(thread_scope)

                # Prints updated only after first run.
                if not first_run:
                    print(
                        f'\n{"="*20} File {file_name} updated! {"="*20}\n>>> ', end=""
                    )
                else:
                    first_run = False

            except:
                print(
                    f'\n{"="*20} File {file_name} threw error! {"="*20}\n {traceback.format_exc()}\n>>> ',
                    end="",
                )


def track(file_name):
    """ Initializes tracking thread (must be started with .start()). """

    print(f'{"="*20} File {file_name} being tracked! {"="*20}')
    return threading.Thread(target=track_file, args=(file_name,)).start()


if __name__ == "__main__":
    # Creates a thread event for garbage collection, and file lock.
    event_close_thread = threading.Event()

    # Gracefully exits the thread.
    event_close_thread.set()

Один вкладыш:

exec("""\nimport threading\nimport traceback\nimport hashlib\nimport runpy\nimport time\n\n\ndef get_file_md5(file_name):\n    \"\"\" Grabs the md5 hash of the file. \"\"\"\n\n    with open(file_name, "rb") as f:\n        return hashlib.md5(f.read()).hexdigest()\n\n\ndef track_file(file_name, one_way=False):\n    \"\"\" Process external file. \"\"\"\n\n    # Grabs current md5 of file.\n    md5 = get_file_md5(file_name)\n\n    # Flag for the first run.\n    first_run = True\n\n    # If the event is set, thread gracefully quits by exiting loop.\n    while not event_close_thread.is_set():\n\n        time.sleep(0.1)\n\n        # Gets updated (if any) md5 hash of file.\n        md5_current = get_file_md5(file_name)\n        if md5 != md5_current or first_run:\n            md5 = md5_current\n\n            # Executes the content of the file.\n            try:\n                # Gather the threads global scope to update the main thread's scope.\n                thread_scope = runpy.run_path(file_name, init_globals=globals())\n\n                if not one_way:\n                    # Updates main thread's scope with other thread..\n                    globals().update(thread_scope)\n\n                # Prints updated only after first run.\n                if not first_run:\n                    print(\n                        f'\\n{"="*20} File {file_name} updated! {"="*20}\\n>>> ', end=""\n                    )\n                else:\n                    first_run = False\n\n            except:\n                print(\n                    f'\\n{"="*20} File {file_name} threw error! {"="*20}\\n {traceback.format_exc()}\\n>>> ',\n                    end="",\n                )\n\n\ndef track(file_name):\n    \"\"\" Initializes tracking thread (must be started with .start()). \"\"\"\n\n    print(f'{"="*20} File {file_name} being tracked! {"="*20}')\n    return threading.Thread(target=track_file, args=(file_name,)).start()\n\n\nif __name__ == "__main__":\n    # Creates a thread event for garbage collection, and file lock.\n    event_close_thread = threading.Event()\n\n    # Gracefully exits the thread.\n    event_close_thread.set()\n""")
...