mirror of
https://github.com/Relintai/gdnative_python.git
synced 2024-11-14 10:27:22 +01:00
229 lines
8.2 KiB
Cython
229 lines
8.2 KiB
Cython
# cython: c_string_type=unicode, c_string_encoding=utf8
|
|
|
|
import importlib
|
|
|
|
from cpython.ref cimport PyObject
|
|
|
|
from pandemonium._hazmat.gdnative_api_struct cimport (
|
|
pandemonium_pluginscript_language_data,
|
|
pandemonium_string,
|
|
pandemonium_bool,
|
|
pandemonium_array,
|
|
pandemonium_pool_string_array,
|
|
pandemonium_object,
|
|
pandemonium_variant,
|
|
pandemonium_error,
|
|
pandemonium_string_name,
|
|
pandemonium_pluginscript_script_data,
|
|
pandemonium_pluginscript_script_manifest,
|
|
PANDEMONIUM_OK,
|
|
PANDEMONIUM_ERR_UNAVAILABLE,
|
|
PANDEMONIUM_ERR_FILE_BAD_PATH,
|
|
PANDEMONIUM_ERR_PARSE_ERROR,
|
|
PANDEMONIUM_METHOD_FLAG_FROM_SCRIPT,
|
|
PANDEMONIUM_METHOD_RPC_MODE_DISABLED,
|
|
)
|
|
from pandemonium._hazmat.gdapi cimport pythonscript_gdapi10 as gdapi10
|
|
from pandemonium._hazmat.conversion cimport (
|
|
pandemonium_string_to_pyobj,
|
|
pyobj_to_pandemonium_string,
|
|
pyobj_to_pandemonium_string_name,
|
|
pytype_to_pandemonium_type,
|
|
)
|
|
from pandemonium._hazmat.internal cimport (
|
|
get_pythonscript_verbose,
|
|
get_exposed_class,
|
|
set_exposed_class,
|
|
destroy_exposed_class,
|
|
)
|
|
from pandemonium.bindings cimport _initialize_bindings, Object
|
|
from pandemonium.builtins cimport Array, Dictionary
|
|
|
|
import inspect
|
|
import traceback
|
|
|
|
from pandemonium.tags import ExportedField, SignalField
|
|
|
|
|
|
cdef inline pandemonium_pluginscript_script_manifest _build_empty_script_manifest():
|
|
cdef pandemonium_pluginscript_script_manifest manifest
|
|
manifest.data = NULL
|
|
gdapi10.pandemonium_string_name_new_data_char(&manifest.name, "")
|
|
manifest.is_tool = False
|
|
gdapi10.pandemonium_string_name_new_data_char(&manifest.base, "")
|
|
gdapi10.pandemonium_dictionary_new(&manifest.member_lines)
|
|
gdapi10.pandemonium_array_new(&manifest.methods)
|
|
gdapi10.pandemonium_array_new(&manifest.signals)
|
|
gdapi10.pandemonium_array_new(&manifest.properties)
|
|
return manifest
|
|
|
|
|
|
cdef Dictionary _build_signal_info(object signal):
|
|
cdef Dictionary methinfo = Dictionary()
|
|
methinfo["name"] = signal.name
|
|
# Dummy data, only name is important here
|
|
methinfo["args"] = Array()
|
|
methinfo["default_args"] = Array()
|
|
methinfo["return"] = None
|
|
methinfo["flags"] = PANDEMONIUM_METHOD_FLAG_FROM_SCRIPT
|
|
return methinfo
|
|
|
|
|
|
cdef Dictionary _build_method_info(object meth, object methname):
|
|
cdef Dictionary methinfo = Dictionary()
|
|
spec = inspect.getfullargspec(meth)
|
|
methinfo["name"] = methname
|
|
# TODO: Handle classmethod/staticmethod
|
|
methinfo["args"] = Array(spec.args)
|
|
methinfo["default_args"] = Array() # TODO
|
|
# TODO: use annotation to determine return type ?
|
|
methinfo["return"] = None
|
|
methinfo["flags"] = PANDEMONIUM_METHOD_FLAG_FROM_SCRIPT
|
|
return methinfo
|
|
|
|
|
|
cdef Dictionary _build_property_info(object prop):
|
|
cdef Dictionary propinfo = Dictionary()
|
|
propinfo["name"] = prop.name
|
|
propinfo["type"] = pytype_to_pandemonium_type(prop.type)
|
|
propinfo["hint"] = prop.hint
|
|
propinfo["hint_string"] = prop.hint_string
|
|
propinfo["usage"] = prop.usage
|
|
propinfo["default_value"] = prop.default
|
|
return propinfo
|
|
|
|
cdef inline object is_method(object meth):
|
|
if inspect.isfunction(meth):
|
|
return True
|
|
|
|
if 'cython_function' in type(meth).__name__:
|
|
return True
|
|
|
|
return False
|
|
|
|
cdef pandemonium_pluginscript_script_manifest _build_script_manifest(object cls):
|
|
cdef pandemonium_pluginscript_script_manifest manifest
|
|
# No need to increase refcount here given `cls` is guaranteed to be kept
|
|
# until we call `destroy_exposed_class`
|
|
manifest.data = <PyObject*>cls
|
|
pyobj_to_pandemonium_string_name(cls.__name__, &manifest.name)
|
|
manifest.is_tool = cls.__tool
|
|
gdapi10.pandemonium_dictionary_new(&manifest.member_lines)
|
|
|
|
if cls.__bases__:
|
|
# Only one Pandemonium parent class (checked at class definition time)
|
|
pandemonium_parent_class = next(
|
|
(b for b in cls.__bases__ if issubclass(b, Object))
|
|
)
|
|
if not pandemonium_parent_class.__dict__.get("__exposed_python_class"):
|
|
base = pandemonium_parent_class.__name__
|
|
else:
|
|
# Pluginscript wants us to return the parent as a path
|
|
base = f"res://{pandemonium_parent_class.__module__.replace('.', '/')}.py"
|
|
pyobj_to_pandemonium_string_name(base, &manifest.base)
|
|
|
|
methods = Array()
|
|
signals = Array()
|
|
properties = Array()
|
|
for k, v in cls.__exported.items():
|
|
if isinstance(v, ExportedField):
|
|
properties.append(_build_property_info(v))
|
|
elif isinstance(v, SignalField):
|
|
signals.append(_build_signal_info(v))
|
|
else:
|
|
assert is_method(v)
|
|
methods.append(_build_method_info(v, k))
|
|
gdapi10.pandemonium_array_new_copy(&manifest.methods, &methods._gd_data)
|
|
gdapi10.pandemonium_array_new_copy(&manifest.signals, &signals._gd_data)
|
|
gdapi10.pandemonium_array_new_copy(&manifest.properties, &properties._gd_data)
|
|
|
|
return manifest
|
|
|
|
|
|
cdef api pandemonium_pluginscript_script_manifest pythonscript_script_init(
|
|
pandemonium_pluginscript_language_data *p_data,
|
|
const pandemonium_string *p_path,
|
|
const pandemonium_string *p_source,
|
|
pandemonium_error *r_error
|
|
) with gil:
|
|
# Pandemonium class&singleton are not all available at Pythonscript bootstrap.
|
|
# Hence we wait until the Pythonscript start being actually used (i.e. until
|
|
# the first Python script is loaded) before initializing the bindings.
|
|
_initialize_bindings()
|
|
|
|
cdef object path = pandemonium_string_to_pyobj(p_path)
|
|
if get_pythonscript_verbose():
|
|
print(f"Loading python script from {path}")
|
|
|
|
if not path.startswith("res://") or not path.rsplit(".", 1)[-1] in (
|
|
"py",
|
|
"pyc",
|
|
"pyo",
|
|
"pyd",
|
|
):
|
|
print(
|
|
f"Bad python script path `{path}`, must starts by `res://` and ends with `.py/pyc/pyo/pyd`"
|
|
)
|
|
r_error[0] = PANDEMONIUM_ERR_FILE_BAD_PATH
|
|
return _build_empty_script_manifest()
|
|
|
|
# TODO: possible bug if res:// is not part of PYTHONPATH
|
|
# Remove `res://`, `.py` and replace / by .
|
|
modname = path[6:].rsplit(".", 1)[0].replace("/", ".")
|
|
|
|
is_reload = modname in sys.modules
|
|
if is_reload:
|
|
# Reloading is done in two steps: remove the exported class,
|
|
# then do module reloading through importlib.
|
|
cls = get_exposed_class(modname)
|
|
|
|
# If the module has no exported class, it has no real connection with
|
|
# Pandemonium and doesn't need to be reloaded
|
|
if cls:
|
|
if get_pythonscript_verbose():
|
|
print(f"Reloading python script from {path} ({modname})")
|
|
destroy_exposed_class(cls)
|
|
importlib.reload(sys.modules[modname])
|
|
|
|
try:
|
|
importlib.import_module(modname) # Force lazy loading of the module
|
|
cls = get_exposed_class(modname)
|
|
|
|
except BaseException:
|
|
# If we are here it could be because the file doesn't exists
|
|
# or (more possibly) the file content is not valid python (or
|
|
# doesn't provide an exposed class)
|
|
print(
|
|
f"Got exception loading {path} ({modname}): {traceback.format_exc()}"
|
|
)
|
|
r_error[0] = PANDEMONIUM_ERR_PARSE_ERROR
|
|
return _build_empty_script_manifest()
|
|
|
|
if cls is None:
|
|
print(
|
|
f"Cannot load {path} ({modname}) because it doesn't expose any class to Pandemonium"
|
|
)
|
|
r_error[0] = PANDEMONIUM_ERR_PARSE_ERROR
|
|
return _build_empty_script_manifest()
|
|
|
|
if is_reload:
|
|
# During reloading, Pandemonium calls the new class init before the old class finish (so
|
|
# `pythonscript_script_finish` is going to be called after this function returns).
|
|
# Hence we must manually increase the refcount to prevent finish to remove
|
|
# the class.
|
|
# Apparently multiple PluginScript instances can exist at the same time for the same script.
|
|
set_exposed_class(cls)
|
|
|
|
r_error[0] = PANDEMONIUM_OK
|
|
return _build_script_manifest(cls)
|
|
|
|
|
|
cdef api void pythonscript_script_finish(
|
|
pandemonium_pluginscript_script_data *p_data
|
|
) with gil:
|
|
cdef object cls = <object>p_data
|
|
if get_pythonscript_verbose():
|
|
# Using print here will cause a crash on editor/game shutdown
|
|
sys.__stdout__.write(f"Destroying python script {cls.__name__}\n")
|
|
destroy_exposed_class(cls)
|