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
), которая реализует полный протокол для преодоления этой проблемы.
Надеюсь, теперь мои комментарии стали более понятными.