Integrating latest 47acbe8

This commit is contained in:
alexpete
2021-03-25 13:57:57 -07:00
parent 448c549698
commit 75dc720198
10312 changed files with 2711566 additions and 671451 deletions
@@ -0,0 +1,61 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Module/Module.h>
#include <PythonSystemComponent.h>
#include <PythonReflectionComponent.h>
#include <PythonMarshalComponent.h>
#include <PythonLogSymbolsComponent.h>
namespace EditorPythonBindings
{
class EditorPythonBindingsModule
: public AZ::Module
{
public:
AZ_RTTI(EditorPythonBindingsModule, "{851B9E35-4FD5-49B1-8207-E40D4BBA36CC}", AZ::Module);
AZ_CLASS_ALLOCATOR(EditorPythonBindingsModule, AZ::SystemAllocator, 0);
EditorPythonBindingsModule()
: AZ::Module()
{
m_descriptors.insert(m_descriptors.end(),
{
PythonSystemComponent::CreateDescriptor(),
PythonReflectionComponent::CreateDescriptor(),
PythonMarshalComponent::CreateDescriptor(),
PythonLogSymbolsComponent::CreateDescriptor(),
});
}
/**
* Add required SystemComponents to the SystemEntity.
*/
AZ::ComponentTypeList GetRequiredSystemComponents() const override
{
return AZ::ComponentTypeList
{
azrtti_typeid<PythonSystemComponent>(),
azrtti_typeid<PythonReflectionComponent>(),
azrtti_typeid<PythonMarshalComponent>(),
azrtti_typeid<PythonLogSymbolsComponent>()
};
}
};
}
// DO NOT MODIFY THIS LINE UNLESS YOU RENAME THE GEM
// The first parameter should be GemName_GemIdLower
// The second should be the fully qualified name of the class above
AZ_DECLARE_MODULE_CLASS(Gem_EditorPythonBindings, EditorPythonBindings::EditorPythonBindingsModule)
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(LY_COMPILE_OPTIONS
PRIVATE
-fexceptions # The macro PYBIND11_EMBEDDED_MODULE uses a try catch block
)
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(LY_COMPILE_OPTIONS
PRIVATE
-fexceptions # The macro PYBIND11_EMBEDDED_MODULE uses a try catch block
)
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(LY_COMPILE_OPTIONS
PRIVATE
/EHsc # The macro PYBIND11_EMBEDDED_MODULE uses a try catch block
)
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(LY_COMPILE_OPTIONS
PRIVATE
/EHsc # The macro PYBIND11_EMBEDDED_MODULE uses a try catch block
)
@@ -0,0 +1,63 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/PlatformDef.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/IO/Path/Path.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace Platform
{
bool InsertPythonLibraryPath(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot, const char* subPath)
{
// append lib path to Python paths
AZ::IO::FixedMaxPath libPath = engineRoot;
libPath /= AZ::IO::FixedMaxPathString::format(subPath, pythonPackage);
libPath = libPath.LexicallyNormal();
if (AZ::IO::SystemFile::Exists(libPath.c_str()))
{
paths.insert(libPath.c_str());
return true;
}
AZ_Warning("python", false, "Python library path should exist! path:%s", libPath.c_str());
return false;
}
bool InsertPythonBinaryLibraryPaths(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot)
{
bool succeeded = true;
// append lib path to Python paths
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python/lib");
// append lib-dynload path
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python/lib/python3.7/lib-dynload");
// append base path to dynamic link libraries
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python/lib/python3.7");
// append path to site-packages
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python/lib/python3.7/site-packages");
return succeeded;
}
AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot)
{
// append lib path to Python paths
AZ::IO::FixedMaxPath libPath = engineRoot;
libPath /= AZ::IO::FixedMaxPathString::format("python/runtime/%s/python", pythonPackage);
libPath = libPath.LexicallyNormal();
return libPath.String();
}
}
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(LY_COMPILE_OPTIONS
PRIVATE
-fexceptions # The macro PYBIND11_EMBEDDED_MODULE uses a try catch block
)
@@ -0,0 +1,14 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../Linux/PythonSystemComponent_linux.cpp
)
@@ -0,0 +1,63 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/PlatformDef.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/IO/Path/Path.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace Platform
{
bool InsertPythonLibraryPath(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot, const char* subPath)
{
// append lib path to Python paths
AZ::IO::FixedMaxPath libPath = engineRoot;
libPath /= AZ::IO::FixedMaxPathString::format(subPath, pythonPackage);
libPath = libPath.LexicallyNormal();
if (AZ::IO::SystemFile::Exists(libPath.c_str()))
{
paths.insert(libPath.c_str());
return true;
}
AZ_Warning("python", false, "Python library path should exist! path:%s", libPath.c_str());
return false;
}
bool InsertPythonBinaryLibraryPaths(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot)
{
// append lib path to Python paths
bool succeeded = true;
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/Python.framework/Versions/3.7/lib");
// append lib-dynload path
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/Python.framework/Versions/3.7/lib/python3.7/lib-dynload");
// append base path to dynamic link libraries
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/Python.framework/Versions/3.7/lib/python3.7");
// append path to site-packages
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/Python.framework/Versions/3.7/lib/python3.7/site-packages");
return succeeded;
}
AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot)
{
// append lib path to Python paths
AZ::IO::FixedMaxPath libPath = engineRoot;
libPath /= AZ::IO::FixedMaxPathString::format("python/runtime/%s/Python.framework/Versions/3.7", pythonPackage);
libPath = libPath.LexicallyNormal();
return libPath.String();
}
}
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../Mac/PythonSystemComponent_mac.cpp
)
@@ -0,0 +1,57 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/PlatformDef.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/IO/Path/Path.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace Platform
{
bool InsertPythonLibraryPath(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot, const char* subPath)
{
// append lib path to Python paths
AZ::IO::FixedMaxPath libPath = engineRoot;
libPath /= AZ::IO::FixedMaxPathString::format(subPath, pythonPackage);
libPath = libPath.LexicallyNormal();
if (AZ::IO::SystemFile::Exists(libPath.c_str()))
{
paths.insert(libPath.c_str());
return true;
}
AZ_Warning("python", false, "Python library path should exist! path:%s", libPath.c_str());
return false;
}
bool InsertPythonBinaryLibraryPaths(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot)
{
// append lib path to Python paths
bool succeeded = true;
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python");
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python/lib");
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python/lib/site-packages");
succeeded = succeeded && InsertPythonLibraryPath(paths, pythonPackage, engineRoot, "python/runtime/%s/python/DLLs");
return succeeded;
}
AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot)
{
// append lib path to Python paths
AZ::IO::FixedMaxPath libPath = engineRoot;
libPath /= AZ::IO::FixedMaxPathString::format("python/runtime/%s/python", pythonPackage);
libPath = libPath.LexicallyNormal();
return libPath.String();
}
}
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(LY_COMPILE_DEFINITIONS
PRIVATE
HAVE_ROUND # defined for Windows since http://p-nand-q.com/python/building-python-33-with-vs2013.html
)
@@ -0,0 +1,14 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../Windows/PythonSystemComponent_windows.cpp
)
@@ -0,0 +1,20 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
// 'The problem is that with this line, "slots" is a keyword by default in Qt.' https://stackoverflow.com/questions/23068700/embedding-python3-in-qt-5
#pragma push_macro("slots")
#undef slots
#include <Python.h>
#pragma pop_macro("slots")
@@ -0,0 +1,766 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <PythonLogSymbolsComponent.h>
#include <Source/PythonCommon.h>
#include <Source/PythonUtility.h>
#include <Source/PythonTypeCasters.h>
#include <Source/PythonProxyBus.h>
#include <Source/PythonProxyObject.h>
#include <pybind11/embed.h>
#include <AzCore/PlatformDef.h>
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/std/sort.h>
#include <AzCore/Serialization/Utils.h>
#include <AzFramework/CommandLine/CommandRegistrationBus.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace EditorPythonBindings
{
namespace Internal
{
struct FileHandle final
{
explicit FileHandle(AZ::IO::HandleType handle)
: m_handle(handle)
{}
~FileHandle()
{
Close();
}
void Close()
{
if (IsValid())
{
AZ::IO::FileIOBase::GetInstance()->Close(m_handle);
}
m_handle = AZ::IO::InvalidHandle;
}
bool IsValid() const
{
return m_handle != AZ::IO::InvalidHandle;
}
operator AZ::IO::HandleType() const { return m_handle; }
AZ::IO::HandleType m_handle;
};
void Indent(int level, AZStd::string& buffer)
{
buffer.append(level * 4, ' ');
}
void AddCommentBlock(int level, const AZStd::string& comment, AZStd::string& buffer)
{
Indent(level, buffer);
AzFramework::StringFunc::Append(buffer, "\"\"\"\n");
Indent(level, buffer);
AzFramework::StringFunc::Append(buffer, comment.c_str());
Indent(level, buffer);
AzFramework::StringFunc::Append(buffer, "\"\"\"\n");
}
}
void PythonLogSymbolsComponent::Reflect(AZ::ReflectContext* context)
{
if (auto&& serialize = azrtti_cast<AZ::SerializeContext*>(context))
{
serialize->Class<PythonLogSymbolsComponent, AZ::Component>()
->Version(0);
}
}
void PythonLogSymbolsComponent::Activate()
{
PythonSymbolEventBus::Handler::BusConnect();
EditorPythonBindingsNotificationBus::Handler::BusConnect();
AZ::Interface<AzToolsFramework::EditorPythonConsoleInterface>::Register(this);
}
void PythonLogSymbolsComponent::Deactivate()
{
AZ::Interface<AzToolsFramework::EditorPythonConsoleInterface>::Unregister(this);
PythonSymbolEventBus::Handler::BusDisconnect();
EditorPythonBindingsNotificationBus::Handler::BusDisconnect();
}
void PythonLogSymbolsComponent::OnPostInitialize()
{
m_basePath.clear();
if (AZ::IO::FileIOBase::GetInstance()->GetAlias("@user@"))
{
// clear out the previous symbols path
char pythonSymbolsPath[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/python_symbols", pythonSymbolsPath, AZ_MAX_PATH_LEN);
AZ::IO::FileIOBase::GetInstance()->CreatePath(pythonSymbolsPath);
m_basePath = pythonSymbolsPath;
}
EditorPythonBindingsNotificationBus::Handler::BusDisconnect();
}
void PythonLogSymbolsComponent::WriteMethod(AZ::IO::HandleType handle, AZStd::string_view methodName, const AZ::BehaviorMethod& behaviorMethod, const AZ::BehaviorClass* behaviorClass)
{
AZStd::string buffer;
int indentLevel = 0;
AZStd::vector<AZStd::string> pythonArgs;
const bool isMemberLike = behaviorClass ? PythonProxyObjectManagement::IsMemberLike(behaviorMethod, behaviorClass->m_typeId) : false;
if (isMemberLike)
{
indentLevel = 1;
Internal::Indent(indentLevel, buffer);
pythonArgs.emplace_back("self");
}
else
{
indentLevel = 0;
}
AzFramework::StringFunc::Append(buffer, "def ");
if (isMemberLike || !behaviorClass)
{
AzFramework::StringFunc::Append(buffer, methodName.data());
}
else
{
AzFramework::StringFunc::Append(buffer, behaviorClass->m_name.c_str());
AzFramework::StringFunc::Append(buffer, "_");
AzFramework::StringFunc::Append(buffer, methodName.data());
}
AzFramework::StringFunc::Append(buffer, "(");
AZStd::string bufferArg;
for (size_t argIndex = 0; argIndex < behaviorMethod.GetNumArguments(); ++argIndex)
{
const AZStd::string* name = behaviorMethod.GetArgumentName(argIndex);
if (!name || name->empty())
{
bufferArg = AZStd::string::format(" arg%zu", argIndex);
}
else
{
bufferArg = *name;
}
AZStd::string_view type = FetchPythonType(*behaviorMethod.GetArgument(argIndex));
if (!type.empty())
{
AzFramework::StringFunc::Append(bufferArg, ": ");
AzFramework::StringFunc::Append(bufferArg, type.data());
}
pythonArgs.push_back(bufferArg);
bufferArg.clear();
}
AZStd::string argsList;
AzFramework::StringFunc::Join(buffer, pythonArgs.begin(), pythonArgs.end(), ",");
AzFramework::StringFunc::Append(buffer, ") -> None:\n");
Internal::Indent(indentLevel + 1, buffer);
AzFramework::StringFunc::Append(buffer, "pass\n\n");
AZ::IO::FileIOBase::GetInstance()->Write(handle, buffer.c_str(), buffer.size());
}
void PythonLogSymbolsComponent::WriteProperty(AZ::IO::HandleType handle, int level, AZStd::string_view propertyName, const AZ::BehaviorProperty& property, [[maybe_unused]] const AZ::BehaviorClass * behaviorClass)
{
AZStd::string buffer;
// property declaration
Internal::Indent(level, buffer);
AzFramework::StringFunc::Append(buffer, "@property\n");
Internal::Indent(level, buffer);
AzFramework::StringFunc::Append(buffer, "def ");
AzFramework::StringFunc::Append(buffer, propertyName.data());
AzFramework::StringFunc::Append(buffer, "(self) -> ");
AZStd::string_view type = FetchPythonTypeAndTraits(property.GetTypeId(), AZ::BehaviorParameter::TR_NONE);
if (type.empty())
{
AzFramework::StringFunc::Append(buffer, "Any");
}
else
{
AzFramework::StringFunc::Append(buffer, type.data());
}
AzFramework::StringFunc::Append(buffer, ":\n");
Internal::Indent(level + 1, buffer);
AzFramework::StringFunc::Append(buffer, "pass\n\n");
AZ::IO::FileIOBase::GetInstance()->Write(handle, buffer.c_str(), buffer.size());
}
void PythonLogSymbolsComponent::LogClass(AZStd::string_view moduleName, AZ::BehaviorClass* behaviorClass)
{
LogClassWithName(moduleName, behaviorClass, behaviorClass->m_name.c_str());
}
void PythonLogSymbolsComponent::LogClassWithName(AZStd::string_view moduleName, AZ::BehaviorClass* behaviorClass, AZStd::string_view className)
{
Internal::FileHandle fileHandle(OpenModuleAt(moduleName));
if (fileHandle.IsValid())
{
// Behavior Class types with member methods and properties
AZStd::string buffer;
AzFramework::StringFunc::Append(buffer, "class ");
AzFramework::StringFunc::Append(buffer, className.data());
AzFramework::StringFunc::Append(buffer, ":\n");
AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, buffer.c_str(), buffer.size());
buffer.clear();
if (behaviorClass->m_methods.empty() && behaviorClass->m_properties.empty())
{
AZStd::string body{ " # behavior class type with no methods or properties \n" };
Internal::Indent(1, body);
AzFramework::StringFunc::Append(body, "pass\n\n");
AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, body.c_str(), body.size());
}
else
{
for (const auto& properyEntry : behaviorClass->m_properties)
{
AZ::BehaviorProperty* property = properyEntry.second;
AZStd::string propertyName{ properyEntry.first };
Scope::FetchScriptName(property->m_attributes, propertyName);
WriteProperty(fileHandle, 1, propertyName, *property, behaviorClass);
}
for (const auto& methodEntry : behaviorClass->m_methods)
{
AZ::BehaviorMethod* method = methodEntry.second;
if (method && PythonProxyObjectManagement::IsMemberLike(*method, behaviorClass->m_typeId))
{
AZStd::string baseMethodName{ methodEntry.first };
Scope::FetchScriptName(method->m_attributes, baseMethodName);
WriteMethod(fileHandle, baseMethodName, *method, behaviorClass);
}
}
}
}
}
void PythonLogSymbolsComponent::LogClassMethod(AZStd::string_view moduleName, AZStd::string_view globalMethodName, AZ::BehaviorClass* behaviorClass, AZ::BehaviorMethod* behaviorMethod)
{
AZ_UNUSED(behaviorClass);
Internal::FileHandle fileHandle(OpenModuleAt(moduleName));
if (fileHandle.IsValid())
{
WriteMethod(fileHandle, globalMethodName, *behaviorMethod, nullptr);
}
}
void PythonLogSymbolsComponent::LogBus(AZStd::string_view moduleName, AZStd::string_view busName, AZ::BehaviorEBus* behaviorEBus)
{
if (behaviorEBus->m_events.empty())
{
return;
}
Internal::FileHandle fileHandle(OpenModuleAt(moduleName));
if (fileHandle.IsValid())
{
AZStd::string buffer;
const auto& eventSenderEntry = behaviorEBus->m_events.begin();
const AZ::BehaviorEBusEventSender& sender = eventSenderEntry->second;
AzFramework::StringFunc::Append(buffer, "def ");
AzFramework::StringFunc::Append(buffer, busName.data());
bool isBroadcast = false;
if (sender.m_event)
{
AZStd::string_view addressType = FetchPythonType(behaviorEBus->m_idParam);
if (addressType.empty())
{
AzFramework::StringFunc::Append(buffer, "(busCallType: int, busEventName: str, address: Any, args: Tuple[Any])");
}
else
{
AzFramework::StringFunc::Append(buffer, "(busCallType: int, busEventName: str, address: ");
AzFramework::StringFunc::Append(buffer, AZStd::string::format(AZ_STRING_FORMAT, AZ_STRING_ARG(addressType)).c_str());
AzFramework::StringFunc::Append(buffer, ", args: Tuple[Any])");
}
}
else
{
AzFramework::StringFunc::Append(buffer, "(busCallType: int, busEventName: str, args: Tuple[Any])");
isBroadcast = true;
}
AzFramework::StringFunc::Append(buffer, " -> Any:\n");
AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, buffer.c_str(), buffer.size());
buffer.clear();
auto eventInfoBuilder = [this](const AZ::BehaviorMethod* behaviorMethod, AZStd::string& inOutStrBuffer, [[maybe_unused]] TypeMap& typeCache)
{
AzFramework::StringFunc::Append(inOutStrBuffer, "(");
size_t numArguments = behaviorMethod->GetNumArguments();
const AZ::BehaviorParameter* busIdArg = behaviorMethod->GetBusIdArgument();
for (size_t i = 0; i < numArguments; ++i)
{
const AZ::BehaviorParameter* argParam = behaviorMethod->GetArgument(i);
if (argParam == busIdArg)
{
// address argument is part of the bus call, skip from event argument list
continue;
}
AZStd::string_view argType = FetchPythonTypeAndTraits(argParam->m_typeId, argParam->m_traits);
AzFramework::StringFunc::Append(inOutStrBuffer, argType.data());
if (i < (numArguments - 1))
{
AzFramework::StringFunc::Append(inOutStrBuffer, ", ");
}
}
const AZ::BehaviorParameter* resultParam = behaviorMethod->GetResult();
AZStd::string_view returnType = FetchPythonType(*resultParam);
AZStd::string returnTypeStr = AZStd::string::format(") -> " AZ_STRING_FORMAT" \n", AZ_STRING_ARG(returnType));
AzFramework::StringFunc::Append(inOutStrBuffer, returnTypeStr.c_str());
};
// record the event names the behavior can send, their parameters and return type
AZStd::string comment = behaviorEBus->m_toolTip;
if (!behaviorEBus->m_events.empty())
{
AzFramework::StringFunc::Append(comment, "The following bus Call types, Event names and Argument types are supported by this bus:\n");
AZStd::vector<AZStd::string> events;
for (const auto& eventSenderEntry2 : behaviorEBus->m_events)
{
const AZStd::string& eventName = eventSenderEntry2.first;
AZStd::string eventNameStr = AZStd::string::format("'%s', ", eventName.c_str());
// prefer m_event info over m_broadcast
if (!isBroadcast && eventSenderEntry2.second.m_event != nullptr)
{
AZStd::string eventInfo;
AzFramework::StringFunc::Append(eventInfo, "bus.Event, ");
AzFramework::StringFunc::Append(eventInfo, eventNameStr.c_str());
eventInfoBuilder(eventSenderEntry2.second.m_event, eventInfo, m_typeCache);
events.push_back(eventInfo);
}
else if (isBroadcast && eventSenderEntry2.second.m_broadcast != nullptr)
{
AZStd::string eventInfo;
AzFramework::StringFunc::Append(eventInfo, "bus.Broadcast, ");
AzFramework::StringFunc::Append(eventInfo, eventNameStr.c_str());
eventInfoBuilder(eventSenderEntry2.second.m_broadcast, eventInfo, m_typeCache);
events.push_back(eventInfo);
}
else
{
AZ_Warning("python", false, "Event %s is expected to have valid event information.", eventName.c_str());
}
}
AZStd::sort(events.begin(), events.end());
for (auto& eventInfo : events)
{
Internal::Indent(1, comment);
AzFramework::StringFunc::Append(comment, eventInfo.c_str());
}
}
Internal::AddCommentBlock(1, comment, buffer);
Internal::Indent(1, buffer);
AzFramework::StringFunc::Append(buffer, "pass\n\n");
AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, buffer.c_str(), buffer.size());
// can the EBus create & destroy a handler?
if (behaviorEBus->m_createHandler && behaviorEBus->m_destroyHandler)
{
buffer.clear();
AzFramework::StringFunc::Append(buffer, "def ");
AzFramework::StringFunc::Append(buffer, busName.data());
AzFramework::StringFunc::Append(buffer, "Handler() -> None:\n");
Internal::Indent(1, buffer);
AzFramework::StringFunc::Append(buffer, "pass\n\n");
AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, buffer.c_str(), buffer.size());
}
}
}
void PythonLogSymbolsComponent::LogGlobalMethod(AZStd::string_view moduleName, AZStd::string_view methodName, AZ::BehaviorMethod* behaviorMethod)
{
Internal::FileHandle fileHandle(OpenModuleAt(moduleName));
if (fileHandle.IsValid())
{
WriteMethod(fileHandle, methodName, *behaviorMethod, nullptr);
}
auto functionMapIt = m_globalFunctionMap.find(moduleName);
if (functionMapIt == m_globalFunctionMap.end())
{
auto moduleSetIt = m_moduleSet.find(moduleName);
if (moduleSetIt != m_moduleSet.end())
{
m_globalFunctionMap[*moduleSetIt] = { AZStd::make_pair(behaviorMethod, methodName) };
}
}
else
{
GlobalFunctionList& globalFunctionList = functionMapIt->second;
globalFunctionList.emplace_back(AZStd::make_pair(behaviorMethod, methodName));
}
}
void PythonLogSymbolsComponent::LogGlobalProperty(AZStd::string_view moduleName, AZStd::string_view propertyName, AZ::BehaviorProperty* behaviorProperty)
{
if (!behaviorProperty->m_getter || !behaviorProperty->m_getter->GetResult())
{
return;
}
Internal::FileHandle fileHandle(OpenModuleAt(moduleName));
if (fileHandle.IsValid())
{
AZStd::string buffer;
// add header
AZ::u64 filesize = 0;
AZ::IO::FileIOBase::GetInstance()->Size(fileHandle, filesize);
if (filesize == 0)
{
AzFramework::StringFunc::Append(buffer, "class property():\n");
}
Internal::Indent(1, buffer);
AzFramework::StringFunc::Append(buffer, propertyName.data());
AzFramework::StringFunc::Append(buffer, ": ClassVar[");
const AZ::BehaviorParameter* resultParam = behaviorProperty->m_getter->GetResult();
AZStd::string_view type = FetchPythonTypeAndTraits(resultParam->m_typeId, resultParam->m_traits);
if (type.empty())
{
AzFramework::StringFunc::Append(buffer, "Any");
}
else
{
AzFramework::StringFunc::Append(buffer, type.data());
}
AzFramework::StringFunc::Append(buffer, "] = None");
if (behaviorProperty->m_getter && !behaviorProperty->m_setter)
{
AzFramework::StringFunc::Append(buffer, " # read only");
}
AzFramework::StringFunc::Append(buffer, "\n");
AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, buffer.c_str(), buffer.size());
}
}
void PythonLogSymbolsComponent::Finalize()
{
Internal::FileHandle fileHandle(OpenInitFileAt("azlmbr.bus"));
if (fileHandle)
{
AZStd::string buffer;
AzFramework::StringFunc::Append(buffer, "# Bus dispatch types:\n");
AzFramework::StringFunc::Append(buffer, "from typing_extensions import Final\n");
AzFramework::StringFunc::Append(buffer, "Broadcast: Final[int] = 0\n");
AzFramework::StringFunc::Append(buffer, "Event: Final[int] = 1\n");
AzFramework::StringFunc::Append(buffer, "QueueBroadcast: Final[int] = 2\n");
AzFramework::StringFunc::Append(buffer, "QueueEvent: Final[int] = 3\n");
AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, buffer.c_str(), buffer.size());
}
fileHandle.Close();
}
void PythonLogSymbolsComponent::GetModuleList(AZStd::vector<AZStd::string_view>& moduleList) const
{
moduleList.clear();
moduleList.reserve(m_moduleSet.size());
AZStd::copy(m_moduleSet.begin(), m_moduleSet.end(), AZStd::back_inserter(moduleList));
}
void PythonLogSymbolsComponent::GetGlobalFunctionList(GlobalFunctionCollection& globalFunctionCollection) const
{
globalFunctionCollection.clear();
for (const auto& globalFunctionMapEntry : m_globalFunctionMap)
{
const AZStd::string_view moduleName{ globalFunctionMapEntry.first };
const GlobalFunctionList& moduleFunctionList = globalFunctionMapEntry.second;
AZStd::transform(moduleFunctionList.begin(), moduleFunctionList.end(), AZStd::back_inserter(globalFunctionCollection), [moduleName](auto& entry) -> auto
{
const GlobalFunctionEntry& globalFunctionEntry = entry;
const AZ::BehaviorMethod* behaviorMethod = entry.first;
return AzToolsFramework::EditorPythonConsoleInterface::GlobalFunction({ moduleName, globalFunctionEntry.second, behaviorMethod->m_debugDescription });
});
}
}
AZStd::string PythonLogSymbolsComponent::FetchListType(const AZ::TypeId& typeId)
{
AZStd::string type = "list";
AZStd::vector<AZ::Uuid> typeList = AZ::Utils::GetContainedTypes(typeId);
if (!typeList.empty())
{
// trait info not available, so defaulting to TR_NONE
AZStd::string_view itemType = FetchPythonTypeAndTraits(typeList[0], AZ::BehaviorParameter::TR_NONE);
if (!itemType.empty())
{
type = AZStd::string::format("List[" AZ_STRING_FORMAT "]", AZ_STRING_ARG(itemType));
}
}
return type;
}
AZStd::string PythonLogSymbolsComponent::FetchMapType(const AZ::TypeId& typeId)
{
AZStd::string type = "dict";
AZStd::vector<AZ::Uuid> typeList = AZ::Utils::GetContainedTypes(typeId);
if (!typeList.empty())
{
// trait info not available, so defaulting to TR_NONE
AZStd::string_view kType = FetchPythonTypeAndTraits(typeList[0], AZ::BehaviorParameter::TR_NONE);
AZStd::string_view vType = FetchPythonTypeAndTraits(typeList[1], AZ::BehaviorParameter::TR_NONE);
if (!kType.empty() && !vType.empty())
{
type = AZStd::string::format("Dict[" AZ_STRING_FORMAT ", " AZ_STRING_FORMAT "]",
AZ_STRING_ARG(kType), AZ_STRING_ARG(vType));
}
}
return type;
}
AZStd::string PythonLogSymbolsComponent::FetchOutcomeType(const AZ::TypeId& typeId)
{
AZStd::string type = "Outcome";
AZStd::pair<AZ::Uuid, AZ::Uuid> outcomeTypes = AZ::Utils::GetOutcomeTypes(typeId);
// trait info not available, so defaulting to TR_NONE
AZStd::string_view valueT = FetchPythonTypeAndTraits(outcomeTypes.first, AZ::BehaviorParameter::TR_NONE);
AZStd::string_view errorT = FetchPythonTypeAndTraits(outcomeTypes.second, AZ::BehaviorParameter::TR_NONE);
if (!valueT.empty() && !errorT.empty())
{
type = AZStd::string::format("Outcome[" AZ_STRING_FORMAT ", " AZ_STRING_FORMAT "]",
AZ_STRING_ARG(valueT), AZ_STRING_ARG(errorT));
}
return type;
}
AZStd::string PythonLogSymbolsComponent::TypeNameFallback(const AZ::TypeId& typeId)
{
// fall back to class data m_name
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
if (serializeContext)
{
auto classData = serializeContext->FindClassData(typeId);
if (classData)
{
return classData->m_name;
}
}
return "";
}
AZStd::string_view PythonLogSymbolsComponent::FetchPythonTypeAndTraits(const AZ::TypeId& typeId, AZ::u32 traits)
{
if (m_typeCache.find(typeId) == m_typeCache.end())
{
AZStd::string type;
if (AZ::AzTypeInfo<AZStd::string_view>::Uuid() == typeId ||
AZ::AzTypeInfo<AZStd::string>::Uuid() == typeId)
{
type = "str";
}
else if (AZ::AzTypeInfo<char>::Uuid() == typeId &&
traits & AZ::BehaviorParameter::TR_POINTER &&
traits & AZ::BehaviorParameter::TR_CONST)
{
type = "str";
}
else if (AZ::AzTypeInfo<float>::Uuid() == typeId ||
AZ::AzTypeInfo<double>::Uuid() == typeId)
{
type = "float";
}
else if (AZ::AzTypeInfo<bool>::Uuid() == typeId)
{
type = "bool";
}
else if (AZ::AzTypeInfo<AZ::s8>::Uuid() == typeId ||
AZ::AzTypeInfo<AZ::u8>::Uuid() == typeId ||
AZ::AzTypeInfo<AZ::s16>::Uuid() == typeId ||
AZ::AzTypeInfo<AZ::u16>::Uuid() == typeId ||
AZ::AzTypeInfo<AZ::s32>::Uuid() == typeId ||
AZ::AzTypeInfo<AZ::u32>::Uuid() == typeId ||
AZ::AzTypeInfo<AZ::s64>::Uuid() == typeId ||
AZ::AzTypeInfo<AZ::u64>::Uuid() == typeId)
{
type = "int";
}
else if (AZ::AzTypeInfo<AZStd::vector<AZ::u8>>::Uuid() == typeId)
{
type = "bytes";
}
else if (AZ::AzTypeInfo<AZStd::any>::Uuid() == typeId)
{
type = "object";
}
else if (AZ::AzTypeInfo<void>::Uuid() == typeId)
{
type = "None";
}
else if (AZ::Utils::IsVectorContainerType(typeId))
{
type = FetchListType(typeId);
}
else if (AZ::Utils::IsMapContainerType(typeId))
{
type = FetchMapType(typeId);
}
else if (AZ::Utils::IsOutcomeType(typeId))
{
type = FetchOutcomeType(typeId);
}
else
{
type = TypeNameFallback(typeId);
}
m_typeCache[typeId] = type;
}
return m_typeCache[typeId];
}
AZStd::string_view PythonLogSymbolsComponent::FetchPythonType(const AZ::BehaviorParameter& param)
{
AZStd::string_view pythonType = FetchPythonTypeAndTraits(param.m_typeId, param.m_traits);
if (pythonType.empty())
{
if (AZ::StringFunc::Equal(param.m_name, "void"))
{
return "None";
}
return param.m_name;
}
return pythonType;
}
AZ::IO::HandleType PythonLogSymbolsComponent::OpenInitFileAt(AZStd::string_view moduleName)
{
if (m_basePath.empty())
{
return AZ::IO::InvalidHandle;
}
// creates the __init__.py file in this path
AZStd::string modulePath(moduleName);
AzFramework::StringFunc::Replace(modulePath, ".", AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING);
AZStd::string initFile;
AzFramework::StringFunc::Path::Join(m_basePath.c_str(), modulePath.c_str(), initFile);
AzFramework::StringFunc::Append(initFile, AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING);
AzFramework::StringFunc::Append(initFile, "__init__.pyi");
AZ::IO::OpenMode openMode = AZ::IO::OpenMode::ModeText | AZ::IO::OpenMode::ModeWrite;
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
AZ::IO::Result result = AZ::IO::FileIOBase::GetInstance()->Open(initFile.c_str(), openMode, fileHandle);
AZ_Warning("python", result, "Could not open %s to write Python symbols.", initFile.c_str());
if (result)
{
return fileHandle;
}
return AZ::IO::InvalidHandle;
}
AZ::IO::HandleType PythonLogSymbolsComponent::OpenModuleAt(AZStd::string_view moduleName)
{
if (m_basePath.empty())
{
return AZ::IO::InvalidHandle;
}
bool resetFile = false;
if (m_moduleSet.find(moduleName) == m_moduleSet.end())
{
m_moduleSet.insert(moduleName);
resetFile = true;
}
AZStd::vector<AZStd::string> moduleParts;
AzFramework::StringFunc::Tokenize(moduleName.data(), moduleParts, '.');
// prepare target PYI file
AZStd::string targetModule = moduleParts.back();
moduleParts.pop_back();
AzFramework::StringFunc::Append(targetModule, ".pyi");
AZStd::string modulePath;
AzFramework::StringFunc::Append(modulePath, m_basePath.c_str());
AzFramework::StringFunc::Append(modulePath, AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING);
AzFramework::StringFunc::Join(modulePath, moduleParts.begin(), moduleParts.end(), AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING);
// prepare the path
AZ::IO::FileIOBase::GetInstance()->CreatePath(modulePath.c_str());
// assemble the file path
AzFramework::StringFunc::Append(modulePath, AZ_CORRECT_FILESYSTEM_SEPARATOR_STRING);
AzFramework::StringFunc::Append(modulePath, targetModule.c_str());
AzFramework::StringFunc::AssetDatabasePath::Normalize(modulePath);
AZ::IO::OpenMode openMode = AZ::IO::OpenMode::ModeText;
if (AZ::IO::SystemFile::Exists(modulePath.c_str()))
{
openMode |= (resetFile) ? AZ::IO::OpenMode::ModeWrite : AZ::IO::OpenMode::ModeAppend;
}
else
{
openMode |= AZ::IO::OpenMode::ModeWrite;
}
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
AZ::IO::Result result = AZ::IO::FileIOBase::GetInstance()->Open(modulePath.c_str(), openMode, fileHandle);
AZ_Warning("python", result, "Could not open %s to write Python module symbols.", modulePath.c_str());
if (result)
{
return fileHandle;
}
return AZ::IO::InvalidHandle;
}
}
@@ -0,0 +1,97 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/IO/FileIO.h>
#include <Source/PythonCommon.h>
#include <Source/PythonUtility.h>
#include <Source/PythonSymbolsBus.h>
#include <EditorPythonBindings/EditorPythonBindingsBus.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
namespace AZ
{
class BehaviorClass;
class BehaviorMethod;
class BehaviorProperty;
}
namespace EditorPythonBindings
{
//! Exports Python symbols to the log folder for Python script developers to include into their local projects
class PythonLogSymbolsComponent
: public AZ::Component
, private EditorPythonBindingsNotificationBus::Handler
, private PythonSymbolEventBus::Handler
, private AzToolsFramework::EditorPythonConsoleInterface
{
public:
AZ_COMPONENT(PythonLogSymbolsComponent, "{F1873D04-C472-41A2-8AA4-48B0CE4A5979}", AZ::Component);
static void Reflect(AZ::ReflectContext* context);
protected:
////////////////////////////////////////////////////////////////////////
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override;
////////////////////////////////////////////////////////////////////////
// EditorPythonBindingsNotificationBus::Handler
void OnPostInitialize() override;
////////////////////////////////////////////////////////////////////////
// PythonSymbolEventBus::Handler
void LogClass(AZStd::string_view moduleName, AZ::BehaviorClass* behaviorClass) override;
void LogClassWithName(AZStd::string_view moduleName, AZ::BehaviorClass* behaviorClass, AZStd::string_view className) override;
void LogClassMethod(AZStd::string_view moduleName, AZStd::string_view globalMethodName, AZ::BehaviorClass* behaviorClass, AZ::BehaviorMethod* behaviorMethod) override;
void LogBus(AZStd::string_view moduleName, AZStd::string_view busName, AZ::BehaviorEBus* behaviorEBus) override;
void LogGlobalMethod(AZStd::string_view moduleName, AZStd::string_view methodName, AZ::BehaviorMethod* behaviorMethod) override;
void LogGlobalProperty(AZStd::string_view moduleName, AZStd::string_view propertyName, AZ::BehaviorProperty* behaviorProperty) override;
void Finalize() override;
////////////////////////////////////////////////////////////////////////
// EditorPythonConsoleInterface
void GetModuleList(AZStd::vector<AZStd::string_view>& moduleList) const override;
void GetGlobalFunctionList(GlobalFunctionCollection& globalFunctionCollection) const override;
////////////////////////////////////////////////////////////////////////
// Python type deduction
AZStd::string_view FetchPythonTypeAndTraits(const AZ::TypeId& typeId, AZ::u32 traits);
AZStd::string_view FetchPythonType(const AZ::BehaviorParameter& param);
private:
using ModuleSet = AZStd::unordered_set<AZStd::string>;
using GlobalFunctionEntry = AZStd::pair<const AZ::BehaviorMethod*, AZStd::string>;
using GlobalFunctionList = AZStd::vector<GlobalFunctionEntry>;
using GlobalFunctionMap = AZStd::unordered_map<AZStd::string_view, GlobalFunctionList>;
using TypeMap = AZStd::unordered_map<AZ::TypeId, AZStd::string>;
AZStd::string m_basePath;
ModuleSet m_moduleSet;
GlobalFunctionMap m_globalFunctionMap;
TypeMap m_typeCache;
AZStd::string FetchListType(const AZ::TypeId& typeId);
AZStd::string FetchMapType(const AZ::TypeId& typeId);
AZStd::string FetchOutcomeType(const AZ::TypeId& typeId);
AZStd::string TypeNameFallback(const AZ::TypeId& typeId);
AZ::IO::HandleType OpenInitFileAt(AZStd::string_view moduleName);
AZ::IO::HandleType OpenModuleAt(AZStd::string_view moduleName);
void WriteMethod(AZ::IO::HandleType handle, AZStd::string_view methodName, const AZ::BehaviorMethod& behaviorMethod, const AZ::BehaviorClass* behaviorClass);
void WriteProperty(AZ::IO::HandleType handle, int level, AZStd::string_view propertyName, const AZ::BehaviorProperty& property, const AZ::BehaviorClass* behaviorClass);
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,101 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <EditorPythonBindings/EditorPythonBindingsSymbols.h>
#include <Source/PythonUtility.h>
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <AzCore/Component/Component.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/optional.h>
#include <AzCore/std/string/string_view.h>
namespace EditorPythonBindings
{
//! An abstract to marshal between Behavior and Python type values
class PythonMarshalTypeRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = AZ::TypeId;
//////////////////////////////////////////////////////////////////////////
using DeallocateFunction = Convert::VariableDeleter;
using BehaviorTraits = AZ::BehaviorParameter::Traits;
//! Marshals a Python value to a BehaviorValueParameter plus an optional function to deallocate it after usage
//! @return returns a pair of a flag to indicate success and an function to deallocate
using BehaviorValueResult = AZStd::pair<bool, DeallocateFunction>;
virtual AZStd::optional<BehaviorValueResult> PythonToBehaviorValueParameter(BehaviorTraits traits, pybind11::object pyObj, AZ::BehaviorValueParameter& outValue) = 0;
//! Marshals a BehaviorValueParameter to a Python value object
//! @return returns a pair of a valid Python object and an optional function to deallocate after sent to Python
using PythonValueResult = AZStd::pair<pybind11::object, DeallocateFunction>;
virtual AZStd::optional<PythonValueResult> BehaviorValueParameterToPython(AZ::BehaviorValueParameter& behaviorValue) = 0;
//! Validates that a particular Python object can convert into a Behavior value parameter type
virtual bool CanConvertPythonToBehaviorValue(BehaviorTraits traits, pybind11::object pyObj) const = 0;
};
using PythonMarshalTypeRequestBus = AZ::EBus<PythonMarshalTypeRequests>;
//! Handles marshaling of built-in Behavior types like numbers, strings, and lists
class PythonMarshalComponent
: public AZ::Component
, protected PythonMarshalTypeRequestBus::MultiHandler
{
public:
AZ_COMPONENT(PythonMarshalComponent, PythonMarshalComponentTypeId, AZ::Component);
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
class TypeConverter
{
public:
virtual AZStd::optional<PythonMarshalComponent::BehaviorValueResult> PythonToBehaviorValueParameter(PythonMarshalTypeRequests::BehaviorTraits traits, pybind11::object pyObj, AZ::BehaviorValueParameter& outValue) = 0;
virtual AZStd::optional<PythonMarshalComponent::PythonValueResult> BehaviorValueParameterToPython(AZ::BehaviorValueParameter& behaviorValue) = 0;
virtual bool CanConvertPythonToBehaviorValue(BehaviorTraits traits, pybind11::object pyObj) const = 0;
virtual ~TypeConverter() = default;
};
using TypeConverterPointer = AZStd::shared_ptr<TypeConverter>;
void RegisterTypeConverter(const AZ::TypeId& typeId, TypeConverterPointer typeConverterPointer);
protected:
////////////////////////////////////////////////////////////////////////
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override;
////////////////////////////////////////////////////////////////////////
// PythonMarshalTypeRequestBus interface implementation
AZStd::optional<PythonMarshalTypeRequests::BehaviorValueResult> PythonToBehaviorValueParameter(PythonMarshalTypeRequests::BehaviorTraits traits, pybind11::object pyObj, AZ::BehaviorValueParameter& outValue) override;
AZStd::optional<PythonMarshalTypeRequests::PythonValueResult> BehaviorValueParameterToPython(AZ::BehaviorValueParameter& behaviorValue) override;
bool CanConvertPythonToBehaviorValue(BehaviorTraits traits, pybind11::object pyObj) const override;
private:
using TypeConverterMap = AZStd::unordered_map<AZ::TypeId, TypeConverterPointer>;
TypeConverterMap m_typeConverterMap;
};
}
@@ -0,0 +1,408 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <PythonProxyBus.h>
#include <Source/PythonUtility.h>
#include <Source/PythonTypeCasters.h>
#include <Source/PythonCommon.h>
#include <Source/PythonSymbolsBus.h>
#include <pybind11/embed.h>
#include <AzCore/PlatformDef.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/std/optional.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace EditorPythonBindings
{
namespace Internal
{
enum class EventType
{
Broadcast,
Event,
QueueBroadcast,
QueueEvent
};
pybind11::object InvokeEbus(AZ::BehaviorEBus& behaviorEBus, EventType eventType, AZStd::string_view eventName, pybind11::args pythonArgs)
{
auto eventIterator = behaviorEBus.m_events.find(eventName);
AZ_Warning("python", eventIterator != behaviorEBus.m_events.end(), "Event %.*s does not exist in EBus %s", aznumeric_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
if (eventIterator == behaviorEBus.m_events.end())
{
return pybind11::cast<pybind11::none>(Py_None);
}
auto& behaviorEBusEventSender = eventIterator->second;
switch (eventType)
{
case EventType::Broadcast:
{
AZ_Warning("python", behaviorEBusEventSender.m_broadcast, "EventSender: function %.*s in EBus %s does not support the bus.Broadcast event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
if (behaviorEBusEventSender.m_broadcast)
{
return Call::StaticMethod(behaviorEBusEventSender.m_broadcast, pythonArgs);
}
break;
}
case EventType::Event:
{
AZ_Warning("python", behaviorEBusEventSender.m_event, "EventSender: function %.*s in EBus %s does not support the bus.Event event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
if (behaviorEBusEventSender.m_event)
{
return Call::StaticMethod(behaviorEBusEventSender.m_event, pythonArgs);
}
break;
}
case EventType::QueueBroadcast:
{
AZ_Warning("python", behaviorEBusEventSender.m_queueBroadcast, "EventSender: function %.*s in EBus %s does not support the bus.QueueBroadcast event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
if (behaviorEBusEventSender.m_queueBroadcast)
{
return Call::StaticMethod(behaviorEBusEventSender.m_queueBroadcast, pythonArgs);
}
break;
}
case EventType::QueueEvent:
{
AZ_Warning("python", behaviorEBusEventSender.m_queueEvent, "EventSender: function %.*s in EBus %s does not support the bus.QueueEvent event type.", static_cast<int>(eventName.size()), eventName.data(), behaviorEBus.m_name.c_str());
if (behaviorEBusEventSender.m_queueEvent)
{
return Call::StaticMethod(behaviorEBusEventSender.m_queueEvent, pythonArgs);
}
break;
}
default:
AZ_Error("python", false, "Unknown EBus call type %d", eventType);
break;
}
return pybind11::cast<pybind11::none>(Py_None);
}
class PythonProxyNotificationHandler final
{
public:
AZ_CLASS_ALLOCATOR(PythonProxyNotificationHandler, AZ::SystemAllocator, 0);
PythonProxyNotificationHandler(AZStd::string_view busName)
{
AZ::BehaviorContext* behaviorContext(nullptr);
AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
if (!behaviorContext)
{
AZ_Error("python", false, "A behavior context is required to bind the buses!");
return;
}
auto behaviorEBusEntry = behaviorContext->m_ebuses.find(busName);
if (behaviorEBusEntry == behaviorContext->m_ebuses.end())
{
AZ_Error("python", false, "There is no EBus by the name of %.*s", static_cast<int>(busName.size()), busName.data());
return;
}
AZ_Assert(behaviorEBusEntry->second, "A null EBus:%s is in the Behavior Context!", behaviorEBusEntry->first.c_str());
m_ebus = behaviorEBusEntry->second;
}
~PythonProxyNotificationHandler()
{
Disconnect();
}
bool IsConnected() const
{
if (m_handler)
{
return m_handler->IsConnected();
}
return false;
}
bool Connect(pybind11::object busId)
{
if (!m_ebus)
{
AZ_Error("python", false, "EBus not set.");
return false;
}
if (!CreateHandler(*m_ebus))
{
AZ_Error("python", false, "Could not create a handler for ebus");
return false;
}
// does the EBus require an address to connect?
if (m_ebus->m_idParam.m_typeId.IsNull())
{
AZ_Warning("python", busId.is_none(), "Connecting to an singleton EBus but was given a non-None busId(%s)", pybind11::cast<AZStd::string>(busId).c_str());
return m_handler->Connect();
}
else if (busId.is_none())
{
AZ_Warning("python", busId.is_none(), "Connecting to an EBus that requires an address but was given a None busId");
return false;
}
Convert::StackVariableAllocator stackVariableAllocator;
AZ::BehaviorValueParameter busAddress;
if (!Convert::PythonToBehaviorValueParameter(m_ebus->m_idParam, busId, busAddress, stackVariableAllocator))
{
AZ_Warning("python", busId.is_none(), "Could not convert busId(%s) to address type (%s)",
pybind11::cast<AZStd::string>(busId).c_str(), m_ebus->m_idParam.m_typeId.ToString<AZStd::string>().c_str());
return false;
}
return m_handler->Connect(&busAddress);
}
bool Disconnect()
{
if (!m_handler)
{
return false;
}
m_handler->Disconnect();
if (m_ebus)
{
DestroyHandler(*m_ebus);
}
return true;
}
bool AddCallback(AZStd::string_view eventName, pybind11::function callback)
{
if (!PyCallable_Check(callback.ptr()))
{
AZ_Error("python", false, "The callback needs to be a callable python function.");
return false;
}
if (!m_handler)
{
AZ_Error("python", false, "No EBus connection deteced; missing call or failed call to connect()?");
return false;
}
const AZ::BehaviorEBusHandler::EventArray& events = m_handler->GetEvents();
for (int iEvent = 0; iEvent < static_cast<int>(events.size()); ++iEvent)
{
const AZ::BehaviorEBusHandler::BusForwarderEvent& e = events[iEvent];
if (eventName == e.m_name)
{
AZStd::string eventNameValue{ eventName };
const auto& callbackIt = m_callbackMap.find(eventNameValue);
AZ_Warning("python", m_callbackMap.end() == callbackIt, "Replacing callback for eventName:%s", eventNameValue.c_str());
m_callbackMap[eventNameValue] = callback;
return true;
}
}
return false;
}
protected:
void DestroyHandler(const AZ::BehaviorEBus& ebus)
{
if (m_handler)
{
AZ_Warning("python", ebus.m_destroyHandler, "Ebus (%s) does not have a handler destroyer.", ebus.m_name.c_str());
if (ebus.m_destroyHandler)
{
ebus.m_destroyHandler->Invoke(m_handler);
}
}
m_handler = nullptr;
m_callbackMap.clear();
}
bool CreateHandler(const AZ::BehaviorEBus& ebus)
{
DestroyHandler(ebus);
AZ_Warning("python", ebus.m_createHandler, "Ebus (%s) does not have a handler creator.", ebus.m_name.c_str());
if (!ebus.m_createHandler)
{
return false;
}
if (!ebus.m_createHandler->InvokeResult(m_handler))
{
AZ_Warning("python", ebus.m_createHandler, "Ebus (%s) failed to create a handler.", ebus.m_name.c_str());
return false;
}
if (m_handler)
{
const AZ::BehaviorEBusHandler::EventArray& events = m_handler->GetEvents();
for (int iEvent = 0; iEvent < static_cast<int>(events.size()); ++iEvent)
{
const AZ::BehaviorEBusHandler::BusForwarderEvent& e = events[iEvent];
m_handler->InstallGenericHook(iEvent, &PythonProxyNotificationHandler::OnEventGenericHook, this);
}
}
return true;
}
static void OnEventGenericHook(void* userData, const char* eventName, int eventIndex, AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
reinterpret_cast<PythonProxyNotificationHandler*>(userData)->OnEventGenericHook(eventName, eventIndex, result, numParameters, parameters);
}
void OnEventGenericHook(const char* eventName, [[maybe_unused]] int eventIndex, [[maybe_unused]] AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
// find the callback for the event
const auto& callbackEntry = m_callbackMap.find(eventName);
if (callbackEntry == m_callbackMap.end())
{
return;
}
pybind11::function callback = callbackEntry->second;
// build the parameters to send to callback
Convert::StackVariableAllocator stackVariableAllocator;
pybind11::tuple pythonParamters(numParameters);
for (int index = 0; index < numParameters; ++index)
{
AZ::BehaviorValueParameter& behaviorValueParameter{ *(parameters + index) };
pythonParamters[index] = Convert::BehaviorValueParameterToPython(behaviorValueParameter, stackVariableAllocator);
if (pythonParamters[index].is_none())
{
AZ_Warning("python", false, "Ebus(%s) event(%s) failed to convert parameter at index(%d)", m_ebus->m_name.c_str(), eventName, index);
return;
}
}
try
{
pybind11::object pyResult = callback(pythonParamters);
// store the result
if (result && pyResult.is_none() == false)
{
// reset/prepare the stack allocator
m_stackVariableAllocator = {};
AZ::BehaviorValueParameter coverted;
const AZ::u32 traits = result->m_traits;
if (Convert::PythonToBehaviorValueParameter(*result, pyResult, coverted, m_stackVariableAllocator))
{
result->Set(coverted);
result->m_value = coverted.GetValueAddress();
if ((traits & AZ::BehaviorParameter::TR_POINTER) == AZ::BehaviorParameter::TR_POINTER)
{
result->m_value = &result->m_value;
}
}
}
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("python", false, "Python callback threw an exception %s", e.what());
}
}
private:
const AZ::BehaviorEBus* m_ebus = nullptr;
AZ::BehaviorEBusHandler* m_handler = nullptr;
AZStd::unordered_map<AZStd::string, pybind11::function> m_callbackMap;
Convert::StackVariableAllocator m_stackVariableAllocator;
};
}
namespace PythonProxyBusManagement
{
void CreateSubmodule(pybind11::module baseModule)
{
AZ::BehaviorContext* behaviorContext(nullptr);
AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
if (!behaviorContext)
{
AZ_Error("python", false, "A behavior context is required to bind the buses!");
return;
}
auto busModule = baseModule.def_submodule("bus");
Module::PackageMapType modulePackageMap;
// export possible ways an EBus can be invoked
pybind11::enum_<Internal::EventType>(busModule, "EventType")
.value("Event", Internal::EventType::Event)
.value("Broadcast", Internal::EventType::Broadcast)
.value("QueueEvent", Internal::EventType::QueueEvent)
.value("QueueBroadcast", Internal::EventType::QueueBroadcast)
.export_values();
// export the EBuses flagged for Automation or Common scope
for (auto&& busEntry : behaviorContext->m_ebuses)
{
AZStd::string& ebusName = busEntry.first;
AZ::BehaviorEBus* behaviorEBus = busEntry.second;
if (Scope::IsBehaviorFlaggedForEditor(behaviorEBus->m_attributes))
{
auto busCaller = pybind11::cpp_function([behaviorEBus](Internal::EventType eventType, AZStd::string_view eventName, pybind11::args pythonArgs)
{
return Internal::InvokeEbus(*behaviorEBus, eventType, eventName, pythonArgs);
});
auto createPythonProxyNotificationHandler = pybind11::cpp_function([behaviorEBus]()
{
return aznew Internal::PythonProxyNotificationHandler(behaviorEBus->m_name.c_str());
});
pybind11::module thisBusModule = busModule;
auto moduleName = Module::GetName(behaviorEBus->m_attributes);
if (moduleName)
{
// this will place the bus into either:
// 1) if the module is valid, then azlmbr.<module name>.<ebus name>
// 2) or, then azlmbr.bus.<ebus name>
thisBusModule = Module::DeterminePackageModule(modulePackageMap, *moduleName, baseModule, busModule, true);
}
// for each notification handler type, make a convenient Python type to make the script more Python-ic
if (behaviorEBus->m_createHandler && behaviorEBus->m_destroyHandler)
{
AZStd::string ebusNotificationName{ AZStd::string::format("%sHandler", ebusName.c_str()) };
thisBusModule.attr(ebusNotificationName.c_str()) = createPythonProxyNotificationHandler;
}
// is a request EBus
thisBusModule.attr(ebusName.c_str()) = busCaller;
// log the bus symbol
AZStd::string subModuleName = pybind11::cast<AZStd::string>(thisBusModule.attr("__name__"));
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogBus, subModuleName, ebusName, behaviorEBus);
}
}
// export possible ways an EBus can be invoked
pybind11::class_<Internal::PythonProxyNotificationHandler>(busModule, "NotificationHandler")
.def(pybind11::init<AZStd::string_view>())
.def("is_connected", &Internal::PythonProxyNotificationHandler::IsConnected)
.def("connect", &Internal::PythonProxyNotificationHandler::Connect, pybind11::arg("busId") = pybind11::none())
.def("disconnect", &Internal::PythonProxyNotificationHandler::Disconnect)
.def("add_callback", &Internal::PythonProxyNotificationHandler::AddCallback)
;
}
}
}
@@ -0,0 +1,24 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
namespace EditorPythonBindings
{
namespace PythonProxyBusManagement
{
//! Creates the 'azlmbr.bus' module so that Python script can use Lumberyard buses
void CreateSubmodule(pybind11::module module);
}
}
@@ -0,0 +1,956 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <PythonProxyObject.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <Source/PythonCommon.h>
#include <Source/PythonUtility.h>
#include <Source/PythonMarshalComponent.h>
#include <Source/PythonTypeCasters.h>
#include <Source/PythonSymbolsBus.h>
#include <pybind11/embed.h>
#include <AzCore/PlatformDef.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/RTTI/AttributeReader.h>
namespace EditorPythonBindings
{
namespace Operator
{
constexpr const char s_isEqual[] = "__eq__";
constexpr const char s_notEqual[] = "__ne__";
constexpr const char s_greaterThan[] = "__gt__";
constexpr const char s_greaterThanOrEqual[] = "__ge__";
constexpr const char s_lessThan[] = "__lt__";
constexpr const char s_lessThanOrEqual[] = "__le__";
}
namespace Builtins
{
constexpr const char s_repr[] = "__repr__";
constexpr const char s_str[] = "__str__";
}
namespace Naming
{
void StripReplace(AZStd::string& inout, AZStd::string_view prefix, char bracketIn, char bracketOut, AZStd::string_view replacement)
{
size_t pos = inout.find(prefix);
while (pos != AZStd::string::npos)
{
const char* const start = &inout[pos];
pos += prefix.size();
const char* end = &inout[pos];
int bracketCount = 1;
do
{
if (pos == inout.size())
{
break;
}
else if (inout[pos] == bracketIn)
{
bracketCount++;
}
else if (inout[pos] == bracketOut)
{
bracketCount--;
}
end++;
pos++;
}
while (bracketCount > 0);
AZStd::string target{ start, end };
AZ::StringFunc::Replace(inout, target.c_str(), replacement.data());
pos = inout.find(prefix);
}
}
AZStd::optional<AZStd::string> GetPythonSyntax(const AZ::BehaviorClass& behaviorClass)
{
constexpr const char* invalidCharacters = " :<>,*&";
if (behaviorClass.m_name.find_first_of(invalidCharacters) == AZStd::string::npos)
{
// this class name is not using invalid characters
return AZStd::nullopt;
}
AZStd::string syntaxName = behaviorClass.m_name;
// replace common core template types and name spaces like AZStd
StripReplace(syntaxName, "AZStd::basic_string<", '<', '>', "string");
AZ::StringFunc::Replace(syntaxName, "AZStd", "");
AZStd::vector<AZStd::string> tokens;
AZ::StringFunc::Tokenize(syntaxName, tokens, invalidCharacters, false, false);
syntaxName.clear();
AZ::StringFunc::Join(syntaxName, tokens.begin(), tokens.end(), "_");
return syntaxName;
}
}
PythonProxyObject::PythonProxyObject(const AZ::TypeId& typeId)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(typeId);
if (behaviorClass)
{
CreateDefault(behaviorClass);
}
}
PythonProxyObject::PythonProxyObject(const char* typeName)
{
SetByTypeName(typeName);
}
pybind11::object PythonProxyObject::Construct(const AZ::BehaviorClass& behaviorClass, pybind11::args args)
{
// nothing to construct with ...
if (args.size() == 0 || behaviorClass.m_constructors.empty())
{
if (!CreateDefault(&behaviorClass))
{
return pybind11::cast<pybind11::none>(Py_None);
}
return pybind11::cast(this);
}
// find the right constructor
for (AZ::BehaviorMethod* constructor : behaviorClass.m_constructors)
{
const size_t numArgsPlusSelf = args.size() + 1;
AZ_Error("python", constructor, "Missing constructor value in behavior class %s", behaviorClass.m_name.c_str());
if (constructor && constructor->GetNumArguments() == numArgsPlusSelf)
{
bool match = true;
for (size_t index = 0; index < args.size(); ++index)
{
const AZ::BehaviorParameter* behaviorArg = constructor->GetArgument(index + 1);
pybind11::object pythonArg = args[index];
if (!behaviorArg || !CanConvertPythonToBehaviorValue(*behaviorArg, pythonArg))
{
match = false;
break;
}
}
if (match)
{
// prepare wrapped object instance
m_wrappedObject.m_address = behaviorClass.Allocate();
m_wrappedObject.m_typeId = behaviorClass.m_typeId;
PrepareWrappedObject(behaviorClass);
// execute constructor
Call::ClassMethod(constructor, m_wrappedObject, args);
return pybind11::cast(this);
}
}
}
return pybind11::cast<pybind11::none>(Py_None);
}
bool PythonProxyObject::CanConvertPythonToBehaviorValue(const AZ::BehaviorParameter& behaviorArg, pybind11::object pythonArg) const
{
bool canConvert = false;
PythonMarshalTypeRequestBus::EventResult(
canConvert,
behaviorArg.m_typeId,
&PythonMarshalTypeRequestBus::Events::CanConvertPythonToBehaviorValue,
static_cast<PythonMarshalTypeRequests::BehaviorTraits>(behaviorArg.m_traits),
pythonArg);
if (canConvert)
{
return true;
}
// is already a wrapped type?
if (pybind11::isinstance<PythonProxyObject>(pythonArg))
{
auto&& proxyObj = pybind11::cast<PythonProxyObject*>(pythonArg);
if (proxyObj)
{
return behaviorArg.m_azRtti->IsTypeOf(proxyObj->GetWrappedType().value());
}
}
return false;
}
PythonProxyObject::PythonProxyObject(const AZ::BehaviorObject& object)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(object.m_typeId);
if (behaviorClass)
{
m_wrappedObject = behaviorClass->Clone(object);
PrepareWrappedObject(*behaviorClass);
}
}
PythonProxyObject::~PythonProxyObject()
{
ReleaseWrappedObject();
}
const char* PythonProxyObject::GetWrappedTypeName() const
{
return m_wrappedObjectTypeName.c_str();
}
void PythonProxyObject::SetPropertyValue(const char* attributeName, pybind11::object value)
{
if (!m_wrappedObject.IsValid())
{
PyErr_SetString(PyExc_RuntimeError, "The wrapped Proxy Object has not been setup correctly; missing call set_type()?");
AZ_Error("python", false, "The wrapped Proxy Object has not been setup correctly; missing call set_type()?");
return;
}
auto behaviorPropertyIter = m_properties.find(AZ::Crc32(attributeName));
if (behaviorPropertyIter != m_properties.end())
{
AZ::BehaviorProperty* property = behaviorPropertyIter->second;
AZ_Error("python", property->m_setter, "%s is not a writable property in class %s.", attributeName, m_wrappedObjectTypeName.c_str());
if (property->m_setter)
{
EditorPythonBindings::Call::ClassMethod(property->m_setter, m_wrappedObject, pybind11::args(pybind11::make_tuple(value)));
}
}
}
pybind11::object PythonProxyObject::GetPropertyValue(const char* attributeName)
{
if (!m_wrappedObject.IsValid())
{
PyErr_SetString(PyExc_RuntimeError, "The wrapped Proxy Object has not been setup correctly; missing call set_type()?");
AZ_Error("python", false, "The wrapped Proxy Object has not been setup correctly; missing call set_type()?");
return pybind11::cast<pybind11::none>(Py_None);
}
AZ::Crc32 crcAttributeName(attributeName);
// the attribute could refer to a method
auto methodEntry = m_methods.find(crcAttributeName);
if (methodEntry != m_methods.end())
{
AZ::BehaviorMethod* method = methodEntry->second;
return pybind11::cpp_function([this, method](pybind11::args pythonArgs)
{
return EditorPythonBindings::Call::ClassMethod(method, m_wrappedObject, pythonArgs);
});
}
// the attribute could refer to a property
auto behaviorPropertyIter = m_properties.find(crcAttributeName);
if (behaviorPropertyIter != m_properties.end())
{
AZ::BehaviorProperty* property = behaviorPropertyIter->second;
AZ_Error("python", property->m_getter, "%s is not a readable property in class %s.", attributeName, m_wrappedObjectTypeName.c_str());
if (property->m_getter)
{
return EditorPythonBindings::Call::ClassMethod(property->m_getter, m_wrappedObject, pybind11::args());
}
}
return pybind11::cast<pybind11::none>(Py_None);
}
bool PythonProxyObject::SetByTypeName(const char* typeName)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(AZStd::string(typeName));
if (behaviorClass)
{
return CreateDefault(behaviorClass);
}
return false;
}
pybind11::object PythonProxyObject::Invoke(const char* methodName, pybind11::args pythonArgs)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(m_wrappedObject.m_typeId);
if (behaviorClass)
{
auto behaviorMethodIter = behaviorClass->m_methods.find(methodName);
if (behaviorMethodIter != behaviorClass->m_methods.end())
{
AZ::BehaviorMethod* method = behaviorMethodIter->second;
AZ_Error("python", method, "%s is not a method in class %s!", methodName, m_wrappedObjectTypeName.c_str());
if (method && PythonProxyObjectManagement::IsMemberLike(*method, m_wrappedObject.m_typeId))
{
return EditorPythonBindings::Call::ClassMethod(method, m_wrappedObject, pythonArgs);
}
}
}
return pybind11::cast<pybind11::none>(Py_None);
}
AZStd::optional<AZ::TypeId> PythonProxyObject::GetWrappedType() const
{
if (m_wrappedObject.IsValid())
{
return AZStd::make_optional(m_wrappedObject.m_typeId);
}
return AZStd::nullopt;
}
AZStd::optional<AZ::BehaviorObject*> PythonProxyObject::GetBehaviorObject()
{
if (m_wrappedObject.IsValid())
{
return AZStd::make_optional(&m_wrappedObject);
}
return AZStd::nullopt;
}
void PythonProxyObject::PrepareWrappedObject(const AZ::BehaviorClass& behaviorClass)
{
m_ownership = Ownership::Owned;
m_wrappedObjectTypeName = behaviorClass.m_name;
// is this Behavior Class flagged to usage for tool bindings?
if (!Scope::IsBehaviorFlaggedForEditor(behaviorClass.m_attributes))
{
return;
}
PopulateComparisonOperators(behaviorClass);
PopulateMethodsAndProperties(behaviorClass);
for (auto&& baseClassId : behaviorClass.m_baseClasses)
{
const AZ::BehaviorClass* baseClass = AZ::BehaviorContextHelper::GetClass(baseClassId);
if (baseClass)
{
PopulateMethodsAndProperties(*baseClass);
}
}
}
void PythonProxyObject::PopulateComparisonOperators(const AZ::BehaviorClass& behaviorClass)
{
using namespace AZ::Script;
for (auto&& equalMethodCandidatePair : behaviorClass.m_methods)
{
const AZ::AttributeArray& attributes = equalMethodCandidatePair.second->m_attributes;
AZ::Attribute* operatorAttribute = AZ::FindAttribute(Attributes::Operator, attributes);
if (!operatorAttribute)
{
continue;
}
Attributes::OperatorType operatorType;
AZ::AttributeReader scopeAttributeReader(nullptr, operatorAttribute);
if (!scopeAttributeReader.Read<Attributes::OperatorType>(operatorType))
{
continue;
}
AZ::Crc32 namedKey;
if (operatorType == Attributes::OperatorType::Equal)
{
namedKey = AZ::Crc32{ Operator::s_isEqual };
}
else if (operatorType == Attributes::OperatorType::LessThan)
{
namedKey = AZ::Crc32{ Operator::s_lessThan };
}
else if (operatorType == Attributes::OperatorType::LessEqualThan)
{
namedKey = AZ::Crc32{ Operator::s_lessThanOrEqual };
}
else
{
continue;
}
if (m_methods.find(namedKey) == m_methods.end())
{
m_methods[namedKey] = equalMethodCandidatePair.second;
}
}
}
void PythonProxyObject::PopulateMethodsAndProperties(const AZ::BehaviorClass& behaviorClass)
{
AZStd::string baseName;
// cache all the methods for this behavior class
for (const auto& methodEntry : behaviorClass.m_methods)
{
AZ::BehaviorMethod* method = methodEntry.second;
AZ_Error("python", method, "Missing method entry:%s value in behavior class:%s", methodEntry.first.c_str(), m_wrappedObjectTypeName.c_str());
if (method && PythonProxyObjectManagement::IsMemberLike(*method, m_wrappedObject.m_typeId))
{
baseName = methodEntry.first;
Scope::FetchScriptName(method->m_attributes, baseName);
AZ::Crc32 namedKey(baseName);
if (m_methods.find(namedKey) == m_methods.end())
{
m_methods[namedKey] = method;
}
else
{
AZ_TracePrintf("python", "Skipping duplicate method named %s\n", baseName.c_str());
}
}
}
// cache all the properties for this behavior class
for (const auto& behaviorProperty : behaviorClass.m_properties)
{
AZ::BehaviorProperty* property = behaviorProperty.second;
AZ_Error("python", property, "Missing property %s in behavior class:%s", behaviorProperty.first.c_str(), m_wrappedObjectTypeName.c_str());
if (property)
{
baseName = behaviorProperty.first;
Scope::FetchScriptName(property->m_attributes, baseName);
AZ::Crc32 namedKey(baseName);
if (m_properties.find(namedKey) == m_properties.end())
{
m_properties[namedKey] = property;
}
else
{
AZ_TracePrintf("python", "Skipping duplicate property named %s\n", baseName.c_str());
}
}
}
}
pybind11::object PythonProxyObject::GetWrappedObjectRepr()
{
const AZ::Crc32 reprNamedKey { Builtins::s_repr };
// Attempt to call the object's __repr__ implementation first to get the most accurate representation.
AZ::BehaviorMethod* reprMethod = nullptr;
auto methodEntry = m_methods.find(reprNamedKey);
if (methodEntry != m_methods.end())
{
reprMethod = methodEntry->second;
pybind11::object result = Call::ClassMethod(reprMethod, m_wrappedObject, pybind11::args());
if (!result.is_none())
{
return result;
}
else
{
AZ_Warning("python", false, "The %s method in type (%s) did not return a valid value.", Builtins::s_repr, m_wrappedObjectTypeName.c_str());
}
}
// There's no __repr__ implementation in the object, so use a basic representation and cache it.
AZ_Warning("python", false, "The type (%s) does not implement the %s method.", m_wrappedObjectTypeName.c_str(), Builtins::s_repr);
if (m_wrappedObjectCachedRepr.empty())
{
pybind11::module builtinsModule = pybind11::module::import("builtins");
auto idFunc = builtinsModule.attr("id");
pybind11::object resId = idFunc(this);
AZStd::string wrappedObjectId = pybind11::str(resId).operator std::string().c_str();
m_wrappedObjectCachedRepr = AZStd::string::format("<%s via PythonProxyObject at %s>", m_wrappedObjectTypeName.c_str(), wrappedObjectId.c_str());
}
return pybind11::str(m_wrappedObjectCachedRepr.c_str());
}
pybind11::object PythonProxyObject::GetWrappedObjectStr()
{
// Inspect methods with attributes to find the ToString attribute
AZ::BehaviorMethod* strMethod = nullptr;
using namespace AZ::Script;
for (auto&& strMethodCandidatePair : m_methods)
{
const AZ::AttributeArray& attributes = strMethodCandidatePair.second->m_attributes;
AZ::Attribute* operatorAttribute = AZ::FindAttribute(Attributes::Operator, attributes);
if (!operatorAttribute)
{
continue;
}
Attributes::OperatorType operatorType;
AZ::AttributeReader scopeAttributeReader(nullptr, operatorAttribute);
if (!scopeAttributeReader.Read<Attributes::OperatorType>(operatorType))
{
continue;
}
if (operatorType == Attributes::OperatorType::ToString)
{
if (strMethod == nullptr)
{
strMethod = strMethodCandidatePair.second;
}
else
{
AZ_Warning("python", false, "The type (%s) has more than one method with OperatorType::ToString, using the first found.", m_wrappedObjectTypeName.c_str());
break;
}
}
}
if (strMethod != nullptr)
{
pybind11::object result = Call::ClassMethod(strMethod, m_wrappedObject, pybind11::args());
if (!result.is_none())
{
return result;
}
else
{
AZ_Warning("python", false, "The %s method in type (%s) did not return a valid value.", Builtins::s_str, m_wrappedObjectTypeName.c_str());
}
}
// Fallback to __repr__ because there's no __str__ implementation in the object,
// so use a basic representation and cache it.
AZ_TracePrintf("python", "The type (%s) does not implement the %s method or did not return a valid value, trying %s.", m_wrappedObjectTypeName.c_str(), Builtins::s_str, Builtins::s_repr);
return GetWrappedObjectRepr();
}
void PythonProxyObject::ReleaseWrappedObject()
{
if (m_wrappedObject.IsValid() && m_ownership == Ownership::Owned)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(m_wrappedObject.m_typeId);
if (behaviorClass)
{
behaviorClass->Destroy(m_wrappedObject);
m_wrappedObject = {};
m_wrappedObjectTypeName.clear();
m_wrappedObjectCachedRepr.clear();
m_methods.clear();
m_properties.clear();
}
}
}
bool PythonProxyObject::CreateDefault(const AZ::BehaviorClass* behaviorClass)
{
AZ_Error("python", behaviorClass, "Expecting a non-null BehaviorClass");
if (behaviorClass)
{
if (Scope::IsBehaviorFlaggedForEditor(behaviorClass->m_attributes))
{
m_wrappedObject = behaviorClass->Create();
PrepareWrappedObject(*behaviorClass);
return true;
}
AZ_Warning("python", false, "The behavior class (%s) is not flagged for Editor use.", behaviorClass->m_name.c_str());
}
return false;
}
bool PythonProxyObject::DoEqualityEvaluation(pybind11::object pythonOther)
{
constexpr AZ::Crc32 namedEqKey(Operator::s_isEqual);
auto&& equalOperatorMethodEntry = m_methods.find(namedEqKey);
if (equalOperatorMethodEntry != m_methods.end())
{
AZ::BehaviorMethod* method = equalOperatorMethodEntry->second;
pybind11::object result = Call::ClassMethod(method, m_wrappedObject, pybind11::args(pybind11::make_tuple(pythonOther)));
if (result.is_none())
{
return false;
}
return result.cast<bool>();
}
return false;
}
bool PythonProxyObject::DoComparisonEvaluation(pybind11::object pythonOther, Comparison comparison)
{
bool invertLogic = false;
AZ::Crc32 namedKey;
if (comparison == Comparison::LessThan)
{
namedKey = AZ::Crc32{ Operator::s_lessThan };
}
else if (comparison == Comparison::LessThanOrEquals)
{
namedKey = AZ::Crc32{ Operator::s_lessThanOrEqual };
}
else if (comparison == Comparison::GreaterThan)
{
namedKey = AZ::Crc32{ Operator::s_lessThan };
invertLogic = true;
}
else if (comparison == Comparison::GreaterThanOrEquals)
{
namedKey = AZ::Crc32{ Operator::s_lessThan };
invertLogic = true;
}
else
{
return false;
}
auto&& equalOperatorMethodEntry = m_methods.find(namedKey);
if (equalOperatorMethodEntry != m_methods.end())
{
AZ::BehaviorMethod* method = equalOperatorMethodEntry->second;
pybind11::object result = Call::ClassMethod(method, m_wrappedObject, pybind11::args(pybind11::make_tuple(pythonOther)));
if (result.is_none())
{
return false;
}
else if (invertLogic)
{
const bool greaterThanResult = !result.cast<bool>();
// an additional check for "GreaterThanOrEquals" if the result of "LessThan" failed since the invert
// of '3 <= 3' would fail since the 'or equals' would return true and be inverted to false
if (comparison == Comparison::GreaterThanOrEquals && greaterThanResult == false)
{
return DoEqualityEvaluation(pythonOther);
}
return greaterThanResult;
}
return result.cast<bool>();
}
return false;
}
namespace PythonProxyObjectManagement
{
bool IsMemberLike(const AZ::BehaviorMethod& method, const AZ::TypeId& typeId)
{
return method.IsMember() || (method.GetNumArguments() > 0 && method.GetArgument(0)->m_typeId == typeId);
}
bool IsClassConstant(const AZ::BehaviorProperty* property)
{
bool value = false;
AZ::Attribute* classConstantAttribute = AZ::FindAttribute(AZ::Script::Attributes::ClassConstantValue, property->m_attributes);
if (classConstantAttribute)
{
AZ::AttributeReader attributeReader(nullptr, classConstantAttribute);
attributeReader.Read<bool>(value);
}
return value;
}
pybind11::object CreatePythonProxyObject(const AZ::TypeId& typeId, void* data)
{
PythonProxyObject* instance = nullptr;
if (!data)
{
instance = aznew PythonProxyObject(typeId);
}
else
{
instance = aznew PythonProxyObject(AZ::BehaviorObject(data, typeId));
}
if (!instance->GetWrappedType())
{
delete instance;
PyErr_SetString(PyExc_TypeError, "Failed to create proxy object by type name.");
return pybind11::cast<pybind11::none>(Py_None);
}
return pybind11::cast(instance);
}
pybind11::object CreatePythonProxyObjectByTypename(const char* classTypename)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(AZStd::string(classTypename));
AZ_Warning("python", behaviorClass, "Missing Behavior Class for typename:%s", classTypename);
if (!behaviorClass)
{
return pybind11::cast<pybind11::none>(Py_None);
}
return CreatePythonProxyObject(behaviorClass->m_typeId, nullptr);
}
pybind11::object ConstructPythonProxyObjectByTypename(const char* classTypename, pybind11::args args)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(AZStd::string(classTypename));
AZ_Warning("python", behaviorClass, "Missing Behavior Class for typename:%s", classTypename);
if (!behaviorClass)
{
return pybind11::cast<pybind11::none>(Py_None);
}
PythonProxyObject* instance = aznew PythonProxyObject();
pybind11::object pythonInstance = instance->Construct(*behaviorClass, args);
if (pythonInstance.is_none())
{
delete instance;
PyErr_SetString(PyExc_TypeError, "Failed to construct proxy object with provided args.");
return pybind11::cast<pybind11::none>(Py_None);
}
return pybind11::cast(instance);
}
void ExportStaticBehaviorClassElements(pybind11::module parentModule, pybind11::module defaultModule)
{
AZ::BehaviorContext* behaviorContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
AZ_Error("python", behaviorContext, "Behavior context not available");
if (!behaviorContext)
{
return;
}
// this will make the base package modules for namespace "azlmbr.*" and "azlmbr.default" for behavior that does not specify a module name
Module::PackageMapType modulePackageMap;
for (const auto& classEntry : behaviorContext->m_classes)
{
AZ::BehaviorClass* behaviorClass = classEntry.second;
// is this Behavior Class flagged to usage for Editor.exe bindings?
if (!Scope::IsBehaviorFlaggedForEditor(behaviorClass->m_attributes))
{
continue; // skip this class
}
// find the target module of the behavior's static methods
auto moduleName = Module::GetName(behaviorClass->m_attributes);
pybind11::module subModule = Module::DeterminePackageModule(modulePackageMap, moduleName ? *moduleName : "", parentModule, defaultModule, false);
// early detection of instance based elements like constructors or properties
bool hasMemberMethods = behaviorClass->m_constructors.empty() == false;
bool hasMemberProperties = behaviorClass->m_properties.empty() == false;
// does this class define methods that may be reflected in a Python module?
if (!behaviorClass->m_methods.empty())
{
// add the non-member methods as Python 'free' function
for (const auto& methodEntry : behaviorClass->m_methods)
{
const AZStd::string& methodName = methodEntry.first;
AZ::BehaviorMethod* behaviorMethod = methodEntry.second;
if (!PythonProxyObjectManagement::IsMemberLike(*behaviorMethod, behaviorClass->m_typeId))
{
// the name of the static method will be "azlmbr.<sub_module>.<Behavior Class>_<Behavior Method>"
AZStd::string globalMethodName = AZStd::string::format("%s_%s", behaviorClass->m_name.c_str(), methodName.c_str());
if (behaviorMethod->HasResult())
{
subModule.def(globalMethodName.c_str(), [behaviorMethod](pybind11::args args)
{
return Call::StaticMethod(behaviorMethod, args);
});
}
else
{
subModule.def(globalMethodName.c_str(), [behaviorMethod](pybind11::args args)
{
Call::StaticMethod(behaviorMethod, args);
});
}
AZStd::string subModuleName = pybind11::cast<AZStd::string>(subModule.attr("__name__"));
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogClassMethod, subModuleName, globalMethodName, behaviorClass, behaviorMethod);
}
else
{
// any member method means the class should be exported to Python
hasMemberMethods = true;
}
}
}
// expose all the constant class properties for Python to use
for (const auto& propertyEntry : behaviorClass->m_properties)
{
const AZStd::string& propertyEntryName = propertyEntry.first;
AZ::BehaviorProperty* behaviorProperty = propertyEntry.second;
if (IsClassConstant(behaviorProperty))
{
// the name of the property will be "azlmbr.<Module>.<Behavior Class>_<Behavior Property>"
AZStd::string constantPropertyName =
AZStd::string::format("%s_%s", behaviorClass->m_name.c_str(), propertyEntryName.c_str());
pybind11::object constantValue = Call::StaticMethod(behaviorProperty->m_getter, {});
pybind11::setattr(subModule, constantPropertyName.c_str(), constantValue);
AZStd::string subModuleName = pybind11::cast<AZStd::string>(subModule.attr("__name__"));
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogGlobalProperty, subModuleName, constantPropertyName, behaviorProperty);
}
}
// if the Behavior Class has any properties, methods, or constructors then export it
const bool exportBehaviorClass = (hasMemberMethods || hasMemberProperties);
// register all Behavior Class types with a Python function to construct an instance
if (exportBehaviorClass)
{
const char* behaviorClassName = behaviorClass->m_name.c_str();
subModule.attr(behaviorClassName) = pybind11::cpp_function([behaviorClassName](pybind11::args pythonArgs)
{
return ConstructPythonProxyObjectByTypename(behaviorClassName, pythonArgs);
});
AZStd::string subModuleName = pybind11::cast<AZStd::string>(subModule.attr("__name__"));
// register an alternative class name that passes the Python syntax
auto syntaxName = Naming::GetPythonSyntax(*behaviorClass);
if (syntaxName)
{
const char* properSyntax = syntaxName.value().c_str();
subModule.attr(properSyntax) = pybind11::cpp_function([behaviorClassName](pybind11::args pythonArgs)
{
return ConstructPythonProxyObjectByTypename(behaviorClassName, pythonArgs);
});
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogClassWithName, subModuleName, behaviorClass, properSyntax);
}
else
{
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogClass, subModuleName, behaviorClass);
}
}
}
}
pybind11::list ListBehaviorAttributes(const PythonProxyObject& pythonProxyObject)
{
pybind11::list items;
AZStd::string baseName;
auto typeId = pythonProxyObject.GetWrappedType();
if (!typeId)
{
return items;
}
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(typeId.value());
if (!behaviorClass)
{
return items;
}
if (!Scope::IsBehaviorFlaggedForEditor(behaviorClass->m_attributes))
{
return items;
}
for (const auto& methodEntry : behaviorClass->m_methods)
{
AZ::BehaviorMethod* method = methodEntry.second;
if (method && PythonProxyObjectManagement::IsMemberLike(*method, typeId.value()))
{
baseName = methodEntry.first;
Scope::FetchScriptName(method->m_attributes, baseName);
items.append(pybind11::str(baseName.c_str()));
}
}
for (const auto& behaviorProperty : behaviorClass->m_properties)
{
AZ::BehaviorProperty* property = behaviorProperty.second;
if (property)
{
baseName = behaviorProperty.first;
Scope::FetchScriptName(property->m_attributes, baseName);
items.append(pybind11::str(baseName.c_str()));
}
}
return items;
}
pybind11::list ListBehaviorClasses(bool onlyIncludeScopedForAutomation)
{
pybind11::list items;
AZ::BehaviorContext* behaviorContext(nullptr);
AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
if (!behaviorContext)
{
AZ_Error("python", false, "A behavior context is required!");
return items;
}
for (auto&& classEntry : behaviorContext->m_classes)
{
auto&& behaviorClass = classEntry.second;
if (onlyIncludeScopedForAutomation )
{
if (Scope::IsBehaviorFlaggedForEditor(behaviorClass->m_attributes))
{
items.append(pybind11::str(classEntry.first.c_str()));
}
}
else
{
items.append(pybind11::str(classEntry.first.c_str()));
}
}
return items;
}
void CreateSubmodule(pybind11::module parentModule, pybind11::module defaultModule)
{
ExportStaticBehaviorClassElements(parentModule, defaultModule);
auto objectModule = parentModule.def_submodule("object");
objectModule.def("create", &CreatePythonProxyObjectByTypename);
objectModule.def("construct", &ConstructPythonProxyObjectByTypename);
objectModule.def("dir", &ListBehaviorAttributes);
objectModule.def("list_classes", &ListBehaviorClasses, pybind11::arg("onlyIncludeScopedForAutomation") = true);
pybind11::class_<PythonProxyObject>(objectModule, "PythonProxyObject", pybind11::dynamic_attr())
.def(pybind11::init<>())
.def(pybind11::init<const char*>())
.def_property_readonly("typename", &PythonProxyObject::GetWrappedTypeName)
.def("set_type", &PythonProxyObject::SetByTypeName)
.def("set_property", &PythonProxyObject::SetPropertyValue)
.def("get_property", &PythonProxyObject::GetPropertyValue)
.def("invoke", &PythonProxyObject::Invoke)
.def(Operator::s_isEqual, [](PythonProxyObject& self, pybind11::object rhs)
{
return self.DoEqualityEvaluation(rhs);
})
.def(Operator::s_notEqual, [](PythonProxyObject& self, pybind11::object rhs)
{
return self.DoEqualityEvaluation(rhs) == false;
})
.def(Operator::s_greaterThan, [](PythonProxyObject& self, pybind11::object rhs)
{
return self.DoComparisonEvaluation(rhs, PythonProxyObject::Comparison::GreaterThan);
})
.def(Operator::s_greaterThanOrEqual, [](PythonProxyObject& self, pybind11::object rhs)
{
return self.DoComparisonEvaluation(rhs, PythonProxyObject::Comparison::GreaterThanOrEquals);
})
.def(Operator::s_lessThan, [](PythonProxyObject& self, pybind11::object rhs)
{
return self.DoComparisonEvaluation(rhs, PythonProxyObject::Comparison::LessThan);
})
.def(Operator::s_lessThanOrEqual, [](PythonProxyObject& self, pybind11::object rhs)
{
return self.DoComparisonEvaluation(rhs, PythonProxyObject::Comparison::LessThanOrEquals);
})
.def("__setattr__", &PythonProxyObject::SetPropertyValue)
.def("__getattr__", &PythonProxyObject::GetPropertyValue)
.def(Builtins::s_repr, [](PythonProxyObject& self)
{
return self.GetWrappedObjectRepr();
})
.def(Builtins::s_str, [](PythonProxyObject& self)
{
return self.GetWrappedObjectStr();
})
;
}
}
}
@@ -0,0 +1,143 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/Memory/Memory.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/optional.h>
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
namespace EditorPythonBindings
{
//! Wraps an instance of a Behavior Class that is flagged for 'Editor'
class PythonProxyObject final
{
public:
AZ_TYPE_INFO(PythonProxyObject, "{448A4480-CCA8-4F14-9F17-41B0491F9FD1}");
AZ_CLASS_ALLOCATOR(PythonProxyObject, AZ::SystemAllocator, 0);
PythonProxyObject() = default;
explicit PythonProxyObject(const AZ::TypeId& typeId);
explicit PythonProxyObject(const char* typeName);
explicit PythonProxyObject(const AZ::BehaviorObject& object);
~PythonProxyObject();
//! Gets the AZ RTTI type of the BehaviorObject
AZStd::optional<AZ::TypeId> GetWrappedType() const;
//! Returns the wrapped behavior object pointer if it is valid
AZStd::optional<AZ::BehaviorObject*> GetBehaviorObject();
//! Gets the name of the type of the wrapped BehaviorObject
const char* GetWrappedTypeName() const;
//! Assigns a value to a property (by name) a value; the types must match
void SetPropertyValue(const char* propertyName, pybind11::object value);
//! Gets the value or callable held by a property of a wrapped BehaviorObject
pybind11::object GetPropertyValue(const char* attributeName);
//! Creates a default constructed instance a 'typeName'
bool SetByTypeName(const char* typeName);
//! Invokes a method by name on a wrapped BehaviorObject
pybind11::object Invoke(const char* methodName, pybind11::args pythonArgs);
//! Constructs a BehaviorClass using Python arguments
pybind11::object Construct(const AZ::BehaviorClass& behaviorClass, pybind11::args args);
//! Performs an equality operation to compare this object with another object
bool DoEqualityEvaluation(pybind11::object pythonOther);
//! Perform a comparison of a Python operator
enum class Comparison
{
LessThan,
LessThanOrEquals,
GreaterThan,
GreaterThanOrEquals
};
bool DoComparisonEvaluation(pybind11::object pythonOther, Comparison comparison);
//! Gets the wrapped object's __repr__
pybind11::object GetWrappedObjectRepr();
//! Gets the wrapped object's __str__
pybind11::object GetWrappedObjectStr();
protected:
void PrepareWrappedObject(const AZ::BehaviorClass& behaviorClass);
void ReleaseWrappedObject();
bool CreateDefault(const AZ::BehaviorClass* behaviorClass);
void PopulateMethodsAndProperties(const AZ::BehaviorClass& behaviorClass);
void PopulateComparisonOperators(const AZ::BehaviorClass& behaviorClass);
bool CanConvertPythonToBehaviorValue(const AZ::BehaviorParameter& behaviorArg, pybind11::object pythonArg) const;
private:
enum class Ownership
{
None,
Owned,
Released
};
AZ::BehaviorObject m_wrappedObject;
AZStd::string m_wrappedObjectTypeName;
AZStd::string m_wrappedObjectCachedRepr;
Ownership m_ownership = Ownership::None;
AZStd::unordered_map<AZ::Crc32, AZ::BehaviorMethod*> m_methods;
AZStd::unordered_map<AZ::Crc32, AZ::BehaviorProperty*> m_properties;
};
namespace PythonProxyObjectManagement
{
//! Creates the 'azlmbr.object' module so that Python script developers can manage proxy objects
void CreateSubmodule(pybind11::module parentModule, pybind11::module defaultModule);
//! Creates a Python object storing a BehaviorObject backed by a BehaviorClass
pybind11::object CreatePythonProxyObject(const AZ::TypeId& typeId, void* data);
//! Checks if function can be reflected as a class member method
bool IsMemberLike(const AZ::BehaviorMethod& method, const AZ::TypeId& typeId);
}
}
namespace pybind11
{
namespace detail
{
//! Type caster specialization PythonProxyObject to convert between Python <-> AZ Reflection
template <>
struct type_caster<EditorPythonBindings::PythonProxyObject>
: public type_caster_base<EditorPythonBindings::PythonProxyObject>
{
public:
// Conversion (Python -> C++)
bool load(handle src, bool convert)
{
return type_caster_base<EditorPythonBindings::PythonProxyObject>::load(src, convert);
}
// Conversion (C++ -> Python)
static handle cast(const EditorPythonBindings::PythonProxyObject* src, return_value_policy policy, handle parent)
{
return type_caster_base<EditorPythonBindings::PythonProxyObject>::cast(src, policy, parent);
}
};
}
}
@@ -0,0 +1,387 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <PythonReflectionComponent.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <Source/PythonCommon.h>
#include <Source/PythonUtility.h>
#include <Source/PythonTypeCasters.h>
#include <Source/PythonProxyBus.h>
#include <Source/PythonProxyObject.h>
#include <Source/PythonSymbolsBus.h>
#include <pybind11/embed.h>
#include <AzCore/PlatformDef.h>
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/Serialization/EditContextConstants.inl>
#include <AzCore/PlatformDef.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/IO/SystemFile.h>
#include <AzFramework/IO/LocalFileIO.h>
namespace EditorPythonBindings
{
namespace Internal
{
static constexpr const char* s_azlmbr = "azlmbr";
static constexpr const char* s_default = "default";
static constexpr const char* s_globals = "globals";
// a structure for pybind11 to bind to hold constants, properties, and enums from the Behavior Context
struct StaticPropertyHolder final
{
AZ_CLASS_ALLOCATOR(StaticPropertyHolder, AZ::SystemAllocator, 0);
StaticPropertyHolder() = default;
~StaticPropertyHolder() = default;
bool AddToScope(pybind11::module scope)
{
m_behaviorContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(m_behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
AZ_Error("python", m_behaviorContext, "Behavior context not available");
if (m_behaviorContext == nullptr)
{
return false;
}
m_fullName = PyModule_GetName(scope.ptr());
pybind11::setattr(scope, "__getattr__", pybind11::cpp_function([this](const char* attribute)
{
return this->GetPropertyValue(attribute);
}));
pybind11::setattr(scope, "__setattr__", pybind11::cpp_function([this](const char* attribute, pybind11::object value)
{
return this->SetPropertyValue(attribute, value);
}));
return true;
}
void AddProperty(AZStd::string_view name, AZ::BehaviorProperty* behaviorProperty)
{
AZStd::string baseName(name);
Scope::FetchScriptName(behaviorProperty->m_attributes, baseName);
AZ::Crc32 namedKey(baseName);
if (m_properties.find(namedKey) == m_properties.end())
{
m_properties[namedKey] = behaviorProperty;
}
else
{
AZ_Warning("python", false, "Skipping duplicate property named %s\n", baseName.c_str());
}
}
protected:
void SetPropertyValue(const char* attributeName, pybind11::object value)
{
auto behaviorPropertyIter = m_properties.find(AZ::Crc32(attributeName));
if (behaviorPropertyIter != m_properties.end())
{
AZ::BehaviorProperty* property = behaviorPropertyIter->second;
AZ_Error("python", property->m_setter, "%s is not a writable property in %s.", attributeName, m_fullName.c_str());
if (property->m_setter)
{
EditorPythonBindings::Call::StaticMethod(property->m_setter, pybind11::args(pybind11::make_tuple(value)));
}
}
}
pybind11::object GetPropertyValue(const char* attributeName)
{
AZ::Crc32 crcAttributeName(attributeName);
auto behaviorPropertyIter = m_properties.find(crcAttributeName);
if (behaviorPropertyIter != m_properties.end())
{
AZ::BehaviorProperty* property = behaviorPropertyIter->second;
AZ_Error("python", property->m_getter, "%s is not a readable property in %s.", attributeName, m_fullName.c_str());
if (property->m_getter)
{
return EditorPythonBindings::Call::StaticMethod(property->m_getter, pybind11::args());
}
}
return pybind11::cast<pybind11::none>(Py_None);
}
AZ::BehaviorContext* m_behaviorContext = nullptr;
AZStd::unordered_map<AZ::Crc32, AZ::BehaviorProperty*> m_properties;
AZStd::string m_fullName;
};
using StaticPropertyHolderPointer = AZStd::unique_ptr<StaticPropertyHolder>;
using StaticPropertyHolderMapEntry = AZStd::pair<pybind11::module, StaticPropertyHolderPointer>;
struct StaticPropertyHolderMap final
: public AZStd::unordered_map<AZStd::string, StaticPropertyHolderMapEntry>
{
Module::PackageMapType m_packageMap;
void AddToScope()
{
for (auto&& element : *this)
{
StaticPropertyHolderMapEntry& entry = element.second;
entry.second->AddToScope(entry.first);
}
}
void AddProperty(pybind11::module scope, const AZStd::string& propertyName, AZ::BehaviorProperty* behaviorProperty)
{
AZStd::string scopeName = PyModule_GetName(scope.ptr());
auto&& iter = find(scopeName);
if (iter == end())
{
StaticPropertyHolder* holder = aznew StaticPropertyHolder();
insert(AZStd::make_pair(scopeName, StaticPropertyHolderMapEntry{ scope, holder }));
holder->AddProperty(propertyName, behaviorProperty);
}
else
{
StaticPropertyHolderMapEntry& entry = iter->second;
entry.second->AddProperty(propertyName, behaviorProperty);
}
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogGlobalProperty, scopeName, propertyName, behaviorProperty);
}
pybind11::module DetermineScope(pybind11::module scope, const AZStd::string& fullName)
{
return Module::DeterminePackageModule(m_packageMap, fullName, scope, scope, false);
}
};
AZStd::string PyResolvePath(AZStd::string_view path)
{
char pyPath[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath(path.data(), pyPath, AZ_MAX_PATH_LEN);
return { pyPath };
}
void RegisterAliasIfExists(pybind11::module pathsModule, AZStd::string_view alias, AZStd::string_view attribute)
{
const char* aliasPath = AZ::IO::FileIOBase::GetInstance()->GetAlias(alias.data());
if (aliasPath)
{
pathsModule.attr(attribute.data()) = aliasPath;
}
else
{
pathsModule.attr(attribute.data()) = "";
}
}
void RegisterPaths(pybind11::module parentModule)
{
pybind11::module pathsModule = parentModule.def_submodule("paths");
pathsModule.def("resolve_path", [](const char* path)
{
return PyResolvePath(path);
});
pathsModule.def("ensure_alias", [](const char* alias, const char* path)
{
const char* aliasPath = AZ::IO::FileIOBase::GetInstance()->GetAlias(alias);
if (aliasPath == nullptr)
{
AZ::IO::FileIOBase::GetInstance()->SetAlias(alias, path);
}
});
RegisterAliasIfExists(pathsModule, "@devroot@", "devroot");
RegisterAliasIfExists(pathsModule, "@engroot@", "engroot");
RegisterAliasIfExists(pathsModule, "@assets@", "assets");
RegisterAliasIfExists(pathsModule, "@devassets@", "devassets");
RegisterAliasIfExists(pathsModule, "@log@", "log");
RegisterAliasIfExists(pathsModule, "@root@", "root");
const char* executableFolder = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(executableFolder, &AZ::ComponentApplicationBus::Events::GetExecutableFolder);
if (executableFolder)
{
pathsModule.attr("executableFolder") = executableFolder;
}
}
}
//////////////////////////////////////////////////////////////////////////
// PythonReflectionComponent
void PythonReflectionComponent::Reflect(AZ::ReflectContext* context)
{
if (auto&& serialize = azrtti_cast<AZ::SerializeContext*>(context))
{
serialize->Class<PythonReflectionComponent, AZ::Component>()
->Version(1)
->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>{AZ_CRC_CE("AssetBuilder")})
;
}
}
void PythonReflectionComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(PythonReflectionService);
}
void PythonReflectionComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(PythonReflectionService);
}
void PythonReflectionComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(PythonEmbeddedService);
}
void PythonReflectionComponent::Activate()
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusConnect();
}
void PythonReflectionComponent::Deactivate()
{
OnPreFinalize();
}
void PythonReflectionComponent::ExportGlobalsFromBehaviorContext(pybind11::module parentModule)
{
AZ::BehaviorContext* behaviorContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
AZ_Error("Editor", behaviorContext, "Behavior context not available");
if (!behaviorContext)
{
return;
}
// when a global method does not have a Module attribute put into the 'azlmbr.globals' module
auto globalsModule = parentModule.def_submodule(Internal::s_globals);
Module::PackageMapType modulePackageMap;
// add global methods flagged for Automation as Python global functions
for (const auto& methodEntry : behaviorContext->m_methods)
{
const AZStd::string& methodName = methodEntry.first;
AZ::BehaviorMethod* behaviorMethod = methodEntry.second;
if (Scope::IsBehaviorFlaggedForEditor(behaviorMethod->m_attributes))
{
pybind11::module targetModule;
auto moduleNameResult = Module::GetName(behaviorMethod->m_attributes);
if(moduleNameResult)
{
targetModule = Module::DeterminePackageModule(modulePackageMap, *moduleNameResult, parentModule, globalsModule, false);
}
else
{
targetModule = globalsModule;
}
if (behaviorMethod->HasResult())
{
targetModule.def(methodName.c_str(), [behaviorMethod](pybind11::args args)
{
return Call::StaticMethod(behaviorMethod, args);
});
}
else
{
targetModule.def(methodName.c_str(), [behaviorMethod](pybind11::args args)
{
Call::StaticMethod(behaviorMethod, args);
});
}
// log global method symbol
AZStd::string subModuleName = pybind11::cast<AZStd::string>(targetModule.attr("__name__"));
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogGlobalMethod, subModuleName, methodName, behaviorMethod);
}
}
// add global properties flagged for Automation as Python static class properties
m_staticPropertyHolderMap = AZStd::make_shared<Internal::StaticPropertyHolderMap>();
struct GlobalPropertyHolder {};
pybind11::class_<GlobalPropertyHolder> staticPropertyHolder(globalsModule, "property");
for (const auto& propertyEntry : behaviorContext->m_properties)
{
const AZStd::string& propertyName = propertyEntry.first;
AZ::BehaviorProperty* behaviorProperty = propertyEntry.second;
if (Scope::IsBehaviorFlaggedForEditor(behaviorProperty->m_attributes))
{
auto propertyScopeName = Module::GetName(behaviorProperty->m_attributes);
if (propertyScopeName)
{
pybind11::module scope = m_staticPropertyHolderMap->DetermineScope(parentModule, *propertyScopeName);
m_staticPropertyHolderMap->AddProperty(scope, propertyName, behaviorProperty);
}
// log global property symbol
AZStd::string subModuleName = pybind11::cast<AZStd::string>(globalsModule.attr("__name__"));
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::LogGlobalProperty, subModuleName, propertyName, behaviorProperty);
if (behaviorProperty->m_getter && behaviorProperty->m_setter)
{
staticPropertyHolder.def_property_static(
propertyName.c_str(),
[behaviorProperty](pybind11::object) { return Call::StaticMethod(behaviorProperty->m_getter, {}); },
[behaviorProperty](pybind11::object, pybind11::args args) { return Call::StaticMethod(behaviorProperty->m_setter, args); }
);
}
else if (behaviorProperty->m_getter)
{
staticPropertyHolder.def_property_static(
propertyName.c_str(),
[behaviorProperty](pybind11::object) { return Call::StaticMethod(behaviorProperty->m_getter, {}); },
pybind11::cpp_function()
);
}
else if (behaviorProperty->m_setter)
{
AZ_Warning("python", false, "Global property %s only has a m_setter; write only properties not supported", propertyName.c_str());
}
else
{
AZ_Error("python", false, "Global property %s has neither a m_getter or m_setter", propertyName.c_str());
}
}
}
m_staticPropertyHolderMap->AddToScope();
}
void PythonReflectionComponent::OnPreFinalize()
{
m_staticPropertyHolderMap.reset();
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusDisconnect();
}
void PythonReflectionComponent::OnImportModule(PyObject* module)
{
pybind11::module parentModule = pybind11::cast<pybind11::module>(module);
std::string pythonModuleName = pybind11::cast<std::string>(parentModule.attr("__name__"));
if (AzFramework::StringFunc::Equal(pythonModuleName.c_str(), Internal::s_azlmbr))
{
// declare the default module to capture behavior that did not define a "Module" attribute
pybind11::module defaultModule = parentModule.def_submodule(Internal::s_default);
ExportGlobalsFromBehaviorContext(parentModule);
PythonProxyObjectManagement::CreateSubmodule(parentModule, defaultModule);
PythonProxyBusManagement::CreateSubmodule(parentModule);
Internal::RegisterPaths(parentModule);
PythonSymbolEventBus::Broadcast(&PythonSymbolEventBus::Events::Finalize);
}
}
}
@@ -0,0 +1,58 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <EditorPythonBindings/EditorPythonBindingsSymbols.h>
#include <AzCore/Component/Component.h>
#include <EditorPythonBindings/EditorPythonBindingsBus.h>
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
namespace EditorPythonBindings
{
namespace Internal
{
struct StaticPropertyHolderMap;
}
//! Inspects the Behavior Context for methods to expose as Python bindings
class PythonReflectionComponent
: public AZ::Component
, private EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler
{
public:
AZ_COMPONENT(PythonReflectionComponent, PythonReflectionComponentTypeId, AZ::Component);
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
protected:
////////////////////////////////////////////////////////////////////////
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override;
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// EditorPythonBindings::EditorPythonBindingsNotificationBus interface implementation
void OnPreFinalize() override;
void OnImportModule(PyObject* module) override;
////////////////////////////////////////////////////////////////////////
private:
void ExportGlobalsFromBehaviorContext(pybind11::module parentModule);
AZStd::shared_ptr<Internal::StaticPropertyHolderMap> m_staticPropertyHolderMap;
};
}
@@ -0,0 +1,47 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
namespace EditorPythonBindings
{
//! An interface to track exported Python symbols
class PythonSymbolEvents
: public AZ::EBusTraits
{
public:
//! logs a behavior class type
virtual void LogClass(AZStd::string_view moduleName, AZ::BehaviorClass* behaviorClass) = 0;
//! logs a behavior class type with an override to its name
virtual void LogClassWithName(AZStd::string_view moduleName, AZ::BehaviorClass* behaviorClass, AZStd::string_view className) = 0;
//! logs a static class method with a specified global method name
virtual void LogClassMethod(AZStd::string_view moduleName, AZStd::string_view globalMethodName, AZ::BehaviorClass* behaviorClass, AZ::BehaviorMethod* behaviorMethod) = 0;
//! logs a behavior bus with a specified bus name
virtual void LogBus(AZStd::string_view moduleName, AZStd::string_view busName, AZ::BehaviorEBus* behaviorEBus) = 0;
//! logs a global method from the behavior context registry with a specified method name
virtual void LogGlobalMethod(AZStd::string_view moduleName, AZStd::string_view methodName, AZ::BehaviorMethod* behaviorMethod) = 0;
//! logs a global property, enum, or constant from the behavior context registry with a specified property name
virtual void LogGlobalProperty(AZStd::string_view moduleName, AZStd::string_view propertyName, AZ::BehaviorProperty* behaviorProperty) = 0;
//! signals the end of the logging of symbols
virtual void Finalize() = 0;
};
using PythonSymbolEventBus = AZ::EBus<PythonSymbolEvents>;
} // namespace EditorPythonBindings
@@ -0,0 +1,716 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <PythonSystemComponent.h>
#include <EditorPythonBindings/EditorPythonBindingsBus.h>
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include <pybind11/eval.h>
#include <osdefs.h> // for DELIM
#include <AzCore/Component/EntityId.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Module/DynamicModuleHandle.h>
#include <AzCore/Module/Module.h>
#include <AzCore/Module/ModuleManagerBus.h>
#include <AzCore/PlatformDef.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/std/string/conversions.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzFramework/Asset/AssetSystemComponent.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/CommandLine/CommandRegistrationBus.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
namespace Platform
{
// Implemented in each different platform's implentation files, as it differs per platform.
bool InsertPythonBinaryLibraryPaths(AZStd::unordered_set<AZStd::string>& paths, const char* pythonPackage, const char* engineRoot);
AZStd::string GetPythonHomePath(const char* pythonPackage, const char* engineRoot);
}
// this is called the first time a Python script contains "import azlmbr"
PYBIND11_EMBEDDED_MODULE(azlmbr, m)
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindings::EditorPythonBindingsNotificationBus::Events::OnImportModule, m.ptr());
}
namespace RedirectOutput
{
using RedirectOutputFunc = AZStd::function<void(const char*)>;
struct RedirectOutput
{
PyObject_HEAD
RedirectOutputFunc write;
};
PyObject* RedirectWrite(PyObject* self, PyObject* args)
{
std::size_t written(0);
RedirectOutput* selfimpl = reinterpret_cast<RedirectOutput*>(self);
if (selfimpl->write)
{
char* data;
if (!PyArg_ParseTuple(args, "s", &data))
{
return PyLong_FromSize_t(0);
}
selfimpl->write(data);
written = strlen(data);
}
return PyLong_FromSize_t(written);
}
PyObject* RedirectFlush([[maybe_unused]] PyObject* self, [[maybe_unused]] PyObject* args)
{
// no-op
return Py_BuildValue("");
}
PyMethodDef RedirectMethods[] =
{
{"write", RedirectWrite, METH_VARARGS, "sys.stdout.write"},
{"flush", RedirectFlush, METH_VARARGS, "sys.stdout.flush"},
{"write", RedirectWrite, METH_VARARGS, "sys.stderr.write"},
{"flush", RedirectFlush, METH_VARARGS, "sys.stderr.flush"},
{0, 0, 0, 0} // sentinel
};
PyTypeObject RedirectOutputType =
{
PyVarObject_HEAD_INIT(0, 0)
"azlmbr_redirect.RedirectOutputType", // tp_name
sizeof(RedirectOutput), /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"azlmbr_redirect objects", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
RedirectMethods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
0 /* tp_new */
};
PyModuleDef RedirectOutputModule = { PyModuleDef_HEAD_INIT, "azlmbr_redirect", 0, -1, 0, };
// Internal state
PyObject* g_redirect_stdout = nullptr;
PyObject* g_redirect_stdout_saved = nullptr;
PyObject* g_redirect_stderr = nullptr;
PyObject* g_redirect_stderr_saved = nullptr;
PyMODINIT_FUNC PyInit_RedirectOutput(void)
{
g_redirect_stdout = nullptr;
g_redirect_stdout_saved = nullptr;
g_redirect_stderr = nullptr;
g_redirect_stderr_saved = nullptr;
RedirectOutputType.tp_new = PyType_GenericNew;
if (PyType_Ready(&RedirectOutputType) < 0)
{
return 0;
}
PyObject* m = PyModule_Create(&RedirectOutputModule);
if (m)
{
Py_INCREF(&RedirectOutputType);
PyModule_AddObject(m, "Redirect", reinterpret_cast<PyObject*>(&RedirectOutputType));
}
return m;
}
void SetRedirection(const char* funcname, PyObject*& saved, PyObject*& current, RedirectOutputFunc func)
{
if (PyType_Ready(&RedirectOutputType) < 0)
{
AZ_Warning("python", false, "RedirectOutputType not ready!");
return;
}
if (!current)
{
saved = PySys_GetObject(funcname); // borrowed
current = RedirectOutputType.tp_new(&RedirectOutputType, 0, 0);
}
RedirectOutput* redirectOutput = reinterpret_cast<RedirectOutput*>(current);
redirectOutput->write = func;
PySys_SetObject(funcname, current);
}
void ResetRedirection(const char* funcname, PyObject*& saved, PyObject*& current)
{
if (current)
{
PySys_SetObject(funcname, saved);
}
Py_XDECREF(current);
current = nullptr;
}
PyObject* s_RedirectModule = nullptr;
void Intialize(PyObject* module)
{
using namespace AzToolsFramework;
s_RedirectModule = module;
SetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout, [](const char* msg)
{
EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnTraceMessage, msg);
});
SetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr, [](const char* msg)
{
EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnErrorMessage, msg);
});
PySys_WriteStdout("RedirectOutput installed");
}
void Shutdown()
{
ResetRedirection("stdout", g_redirect_stdout_saved, g_redirect_stdout);
ResetRedirection("stderr", g_redirect_stderr_saved, g_redirect_stderr);
Py_XDECREF(s_RedirectModule);
s_RedirectModule = nullptr;
}
} // namespace RedirectOutput
namespace EditorPythonBindings
{
void PythonSystemComponent::Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
{
serialize->Class<PythonSystemComponent, AZ::Component>()
->Version(1)
->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>{AZ_CRC_CE("AssetBuilder")})
;
if (AZ::EditContext* ec = serialize->GetEditContext())
{
ec->Class<PythonSystemComponent>("PythonSystemComponent", "The Python interpreter")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
;
}
}
}
void PythonSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(PythonEmbeddedService);
}
void PythonSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(PythonEmbeddedService);
}
void PythonSystemComponent::Activate()
{
AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Register(this);
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusConnect();
}
void PythonSystemComponent::Deactivate()
{
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Unregister(this);
StopPython(true);
}
bool PythonSystemComponent::StartPython([[maybe_unused]] bool silenceWarnings)
{
struct ReleaseInitalizeWaiterScope final
{
using ReleaseFunction = AZStd::function<void(void)>;
ReleaseInitalizeWaiterScope(ReleaseFunction releaseFunction)
{
m_releaseFunction = AZStd::move(releaseFunction);
}
~ReleaseInitalizeWaiterScope()
{
m_releaseFunction();
}
ReleaseFunction m_releaseFunction;
};
ReleaseInitalizeWaiterScope scope([this]()
{
m_initalizeWaiter.release(m_initalizeWaiterCount);
m_initalizeWaiterCount = 0;
});
if (Py_IsInitialized())
{
AZ_Warning("python", silenceWarnings, "Python is already active!");
return false;
}
PythonPathStack pythonPathStack;
DiscoverPythonPaths(pythonPathStack);
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPreInitialize);
if (StartPythonInterpreter(pythonPathStack))
{
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPostInitialize);
// initialize internal base module and bootstrap scripts
ExecuteByString("import azlmbr", false);
ExecuteBootstrapScripts(pythonPathStack);
return true;
}
return false;
}
bool PythonSystemComponent::StopPython([[maybe_unused]] bool silenceWarnings)
{
if (!Py_IsInitialized())
{
AZ_Warning("python", silenceWarnings, "Python is not active!");
return false;
}
bool result = false;
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPreFinalize);
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
result = StopPythonInterpreter();
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPostFinalize);
return result;
}
void PythonSystemComponent::WaitForInitialization()
{
m_initalizeWaiterCount++;
m_initalizeWaiter.acquire();
}
void PythonSystemComponent::ExecuteWithLock(AZStd::function<void()> executionCallback)
{
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_release release;
pybind11::gil_scoped_acquire acquire;
executionCallback();
}
void PythonSystemComponent::DiscoverPythonPaths(PythonPathStack& pythonPathStack)
{
// the order of the Python paths is the order the Python bootstrap scripts will execute
AZStd::string gameFolder;
auto settingsRegistry = AZ::SettingsRegistry::Get();
settingsRegistry->Get(gameFolder, AZ::SettingsRegistryInterface::FixedValueString::format("%s/%s",
AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey, AzFramework::AssetSystem::ProjectName));
if (gameFolder.empty())
{
return;
}
auto resolveScriptPath = [&pythonPathStack](AZStd::string_view path)
{
AZStd::string editorScriptsPath;
AzFramework::StringFunc::Path::Join(path.data(), "Editor/Scripts", editorScriptsPath);
if (AZ::IO::SystemFile::Exists(editorScriptsPath.c_str()))
{
pythonPathStack.emplace_back(editorScriptsPath);
}
};
// The discovery order will be:
// - engine
// - gems
// - project
// - user(dev)
// engine
const char* engineRoot = nullptr;
AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot);
resolveScriptPath(engineRoot);
// gems
auto moduleCallback = [this, &pythonPathStack, resolveScriptPath, engineRoot](const AZ::ModuleData& moduleData) -> bool
{
if (moduleData.GetDynamicModuleHandle())
{
const AZ::OSString& modulePath = moduleData.GetDynamicModuleHandle()->GetFilename();
AZStd::string fileName;
AzFramework::StringFunc::Path::GetFileName(modulePath.c_str(), fileName);
AZStd::vector<AZStd::string> tokens;
AzFramework::StringFunc::Tokenize(fileName.c_str(), tokens, '.');
if (tokens.size() > 2 && tokens[0] == "Gem")
{
resolveScriptPath(AZStd::string::format("%s/Gems/%s", engineRoot, tokens[1].c_str()));
}
}
return true;
};
AZ::ModuleManagerRequestBus::Broadcast(&AZ::ModuleManagerRequestBus::Events::EnumerateModules, moduleCallback);
// project
const char* appRoot = nullptr;
AzFramework::ApplicationRequests::Bus::BroadcastResult(appRoot, &AzFramework::ApplicationRequests::GetAppRoot);
resolveScriptPath(AZStd::string::format("%s/%s", appRoot, gameFolder.c_str()));
// user
AZStd::string assetsType;
AZ::SettingsRegistryMergeUtils::PlatformGet(*settingsRegistry, assetsType,
AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey, AzFramework::AssetSystem::Assets);
if (!assetsType.empty())
{
// the pattern to user path is <appRoot>/Cache/<gameFolder>/<assetsType>/user e.g. c:/myroot/dev/Cache/MyProject/pc/user
AZStd::string userRelativePath = AZStd::string::format("Cache/%s/%s/user", gameFolder.c_str(), assetsType.c_str());
AZStd::string userCachePath;
AzFramework::StringFunc::Path::Join(appRoot, userRelativePath.c_str(), userCachePath);
resolveScriptPath(userCachePath);
}
}
void PythonSystemComponent::ExecuteBootstrapScripts(const PythonPathStack& pythonPathStack)
{
for(const auto& path : pythonPathStack)
{
AZStd::string bootstrapPath;
AzFramework::StringFunc::Path::Join(path.c_str(), "bootstrap.py", bootstrapPath);
if (AZ::IO::SystemFile::Exists(bootstrapPath.c_str()))
{
ExecuteByFilename(bootstrapPath);
}
}
}
bool PythonSystemComponent::StartPythonInterpreter(const PythonPathStack& pythonPathStack)
{
AZStd::unordered_set<AZStd::string> pyPackageSites(pythonPathStack.begin(), pythonPathStack.end());
const char* engineRoot = nullptr;
AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot);
// set PYTHON_HOME
AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, engineRoot);
if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str()))
{
AZ_Warning("python", false, "Python home path must exist! path:%s", pyBasePath.c_str());
return false;
}
AZStd::wstring pyHomePath;
AZStd::to_wstring(pyHomePath, pyBasePath);
Py_SetPythonHome(pyHomePath.c_str());
// display basic Python information
AZ_TracePrintf("python", "Py_GetVersion=%s \n", Py_GetVersion());
AZ_TracePrintf("python", "Py_GetPath=%ls \n", Py_GetPath());
AZ_TracePrintf("python", "Py_GetExecPrefix=%ls \n", Py_GetExecPrefix());
AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath());
PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput);
try
{
// ignore system location for sites site-packages
Py_IsolatedFlag = 1; // -I - Also sets Py_NoUserSiteDirectory. If removed PyNoUserSiteDirectory should be set.
Py_IgnoreEnvironmentFlag = 1; // -E
const bool initializeSignalHandlers = true;
pybind11::initialize_interpreter(initializeSignalHandlers);
// Add custom site packages after initializing the interpreter above. Calling Py_SetPath before initialization
// alters the behavior of the initializer to not compute default search paths. See https://docs.python.org/3/c-api/init.html#c.Py_SetPath
if (pyPackageSites.size())
{
ExtendSysPath(pyPackageSites);
}
RedirectOutput::Intialize(PyImport_ImportModule("azlmbr_redirect"));
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
// print Python version using AZ logging
const int verRet = PyRun_SimpleStringFlags("import sys \nprint (sys.version) \n", nullptr);
AZ_Error("python", verRet == 0, "Error trying to fetch the version number in Python!");
return verRet == 0 && !PyErr_Occurred();
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("python", false, "Py_Initialize() failed with %s!", e.what());
return false;
}
}
bool PythonSystemComponent::ExtendSysPath(const AZStd::unordered_set<AZStd::string>& extendPaths)
{
AZStd::string oldPath{ Py_EncodeLocale(Py_GetPath(), nullptr) };
AZStd::vector<AZStd::string> pathParts;
AZ::StringFunc::Tokenize(oldPath, pathParts, DELIM, false, false);
AZStd::unordered_set<AZStd::string> oldPathSet{ pathParts.begin(), pathParts.end() };
bool appended{ false };
AZStd::string pathAppend{ "import sys\n" };
for (const auto& thisStr : extendPaths)
{
if (oldPathSet.find(thisStr) == oldPathSet.end())
{
pathAppend.append(AZStd::string::format("sys.path.append('%s')\n", thisStr.c_str()));
appended = true;
}
}
if (appended)
{
ExecuteByString(pathAppend.c_str(), true);
return true;
}
return false;
}
bool PythonSystemComponent::StopPythonInterpreter()
{
if (Py_IsInitialized())
{
RedirectOutput::Shutdown();
pybind11::finalize_interpreter();
}
else
{
AZ_Warning("python", false, "Did not finalize since Py_IsInitialized() was false.");
}
return !PyErr_Occurred();
}
void PythonSystemComponent::ExecuteByString(AZStd::string_view script, bool printResult)
{
if (!Py_IsInitialized())
{
AZ_Error("python", false, "Can not ExecuteByString() since the embeded Python VM is not ready.");
return;
}
if (!script.empty())
{
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
// Acquire scope for __main__ for executing our script
pybind11::object scope = pybind11::module::import("__main__").attr("__dict__");
bool shouldPrintValue = false;
if (printResult)
{
// Attempt to compile our code to determine if it's an expression
// i.e. a Python code object with only an rvalue
// If it is, it can be evaled to produce a PyObject
// If it's not, we can't evaluate it into a result and should fall back to exec
shouldPrintValue = true;
using namespace pybind11::literals;
// codeop.compile_command is a thin wrapper around the Python compile builtin
// We attempt to compile using symbol="eval" to see if the string is valid for eval
// This is similar to what the Python REPL does internally
pybind11::object codeop = pybind11::module::import("codeop");
pybind11::object compileCommand = codeop.attr("compile_command");
try
{
compileCommand(script.data(), "symbol"_a="eval");
}
catch (const pybind11::error_already_set&)
{
shouldPrintValue = false;
}
}
try
{
if (shouldPrintValue)
{
// We're an expression, run and print the result
pybind11::object result = pybind11::eval(script.data(), scope);
pybind11::print(result);
}
else
{
// Just exec the code block
pybind11::exec(script.data(), scope);
}
}
catch (pybind11::error_already_set& pythonError)
{
// Release the exception stack and let Python print it to stderr
pythonError.restore();
PyErr_Print();
}
}
}
void PythonSystemComponent::ExecuteByFilename(AZStd::string_view filename)
{
AZStd::vector<AZStd::string_view> args;
ExecuteByFilenameWithArgs(filename, args);
}
void PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
{
const Result evalResult = EvaluateFile(filename, args);
if (evalResult == Result::Okay)
{
// all good, the test script will need to exit the application now
return;
}
else
{
// something when wrong with executing the test script
AZ::Debug::Trace::Terminate(1);
}
}
void PythonSystemComponent::ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
{
EvaluateFile(filename, args);
}
PythonSystemComponent::Result PythonSystemComponent::EvaluateFile(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
{
if (!Py_IsInitialized())
{
AZ_Error("python", false, "Can not evaluate file since the embedded Python VM is not ready.");
return Result::Error_IsNotInitialized;
}
if (filename.empty())
{
AZ_Error("python", false, "Invalid empty filename detected.");
return Result::Error_InvalidFilename;
}
// support the alias version of a script such as @devroot@/Editor/Scripts/select_story_anim_objects.py
AZStd::string theFilename(filename);
{
char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(theFilename.c_str(), resolvedPath, AZ_MAX_PATH_LEN);
theFilename = resolvedPath;
}
if (!AZ::IO::FileIOBase::GetInstance()->Exists(theFilename.c_str()))
{
AZ_Error("python", false, "Missing Python file named (%s)", theFilename.c_str());
return Result::Error_MissingFile;
}
FILE* file = _Py_fopen(theFilename.data(), "rb");
if (!file)
{
AZ_Error("python", false, "Missing Python file named (%s)", theFilename.c_str());
return Result::Error_FileOpenValidation;
}
Result pythonScriptResult = Result::Okay;
try
{
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
// Create standard "argc" / "argv" command-line parameters to pass in to the Python script via sys.argv.
// argc = number of parameters. This will always be at least 1, since the first parameter is the script name.
// argv = the list of parameters, in wchar format.
// Our expectation is that the args passed into this function does *not* already contain the script name.
int argc = aznumeric_cast<int>(args.size()) + 1;
// Note: This allocates from PyMem to ensure that Python has access to the memory.
wchar_t** argv = static_cast<wchar_t**>(PyMem_Malloc(argc * sizeof(wchar_t*)));
// Python 3.x is expecting wchar* strings for the command-line args.
argv[0] = Py_DecodeLocale(theFilename.c_str(), nullptr);
for (int arg = 0; arg < args.size(); arg++)
{
argv[arg + 1] = Py_DecodeLocale(args[arg].data(), nullptr);
}
// Tell Python the command-line args.
// Note that this has a side effect of adding the script's path to the set of directories checked for "import" commands.
const int updatePath = 1;
PySys_SetArgvEx(argc, argv, updatePath);
PyCompilerFlags flags;
flags.cf_flags = 0;
const int bAutoCloseFile = true;
const int returnCode = PyRun_SimpleFileExFlags(file, theFilename.c_str(), bAutoCloseFile, &flags);
if (returnCode != 0)
{
AZStd::string message = AZStd::string::format("Detected script failure in Python script(%s); return code %d!", theFilename.c_str(), returnCode);
AZ_Warning("python", false, message.c_str());
using namespace AzToolsFramework;
EditorPythonConsoleNotificationBus::Broadcast(&EditorPythonConsoleNotificationBus::Events::OnExceptionMessage, message.c_str());
pythonScriptResult = Result::Error_PythonException;
}
// Free any memory allocated for the command-line args.
for (int arg = 0; arg < argc; arg++)
{
PyMem_RawFree(argv[arg]);
}
PyMem_Free(argv);
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("python", false, "Detected an internal exception %s while running script (%s)!", e.what(), theFilename.c_str());
return Result::Error_InternalException;
}
return pythonScriptResult;
}
} // namespace EditorPythonBindings
@@ -0,0 +1,92 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <EditorPythonBindings/EditorPythonBindingsSymbols.h>
#include <AzCore/Component/Component.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
#include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
#include <AzCore/std/parallel/semaphore.h>
namespace EditorPythonBindings
{
/**
* Manages the Python interpreter inside this Gem (Editor only)
* - redirects the Python standard output and error streams to AZ_TracePrintf and AZ_Warning, respectively
*/
class PythonSystemComponent
: public AZ::Component
, protected AzToolsFramework::EditorPythonEventsInterface
, protected AzToolsFramework::EditorPythonRunnerRequestBus::Handler
{
public:
AZ_COMPONENT(PythonSystemComponent, PythonSystemComponentTypeId, AZ::Component);
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
protected:
////////////////////////////////////////////////////////////////////////
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override;
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorPythonEventsInterface
bool StartPython(bool silenceWarnings = false) override;
bool StopPython(bool silenceWarnings = false) override;
void WaitForInitialization() override;
void ExecuteWithLock(AZStd::function<void()> executionCallback) override;
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorPythonRunnerRequestBus::Handler interface implementation
void ExecuteByString(AZStd::string_view script, bool printResult) override;
void ExecuteByFilename(AZStd::string_view filename) override;
void ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args) override;
void ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args) override;
////////////////////////////////////////////////////////////////////////
private:
// handle multiple Python initializers and threads
AZStd::atomic_int m_initalizeWaiterCount {0};
AZStd::semaphore m_initalizeWaiter;
AZStd::recursive_mutex m_lock;
enum class Result
{
Okay,
Error_IsNotInitialized,
Error_InvalidFilename,
Error_MissingFile,
Error_FileOpenValidation,
Error_InternalException,
Error_PythonException,
};
Result EvaluateFile(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args);
// bootstrap logic and data
using PythonPathStack = AZStd::vector<AZStd::string>;
void DiscoverPythonPaths(PythonPathStack& pythonPathStack);
void ExecuteBootstrapScripts(const PythonPathStack& pythonPathStack);
bool ExtendSysPath(const AZStd::unordered_set<AZStd::string>& extendPaths);
// starts the Python interpreter
bool StartPythonInterpreter(const PythonPathStack& pythonPathStack);
bool StopPythonInterpreter();
};
}
@@ -0,0 +1,72 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <AzCore/std/string/string.h>
namespace pybind11
{
namespace detail
{
//! Converts AZStd::string to/from Python String
template <>
struct type_caster<AZStd::string>
{
public:
PYBIND11_TYPE_CASTER(AZStd::string, _("AZStd::string"));
bool load(handle pythonSource, bool)
{
Py_ssize_t size = 0;
const char* pythonString = PyUnicode_AsUTF8AndSize(pythonSource.ptr(), &size);
if (PyErr_Occurred() || !pythonString)
{
return false;
}
value.assign(pythonString, size);
return true;
}
static handle cast(const AZStd::string& src, return_value_policy, handle)
{
return pybind11::str(src.c_str()).release();
}
};
//! Converts AZStd::string_view to/from Python String
template <>
struct type_caster<AZStd::string_view>
{
public:
PYBIND11_TYPE_CASTER(AZStd::string_view, _("AZStd::string_view"));
bool load(handle pythonSource, bool)
{
Py_ssize_t size = 0;
const char* pythonString = PyUnicode_AsUTF8AndSize(pythonSource.ptr(), &size);
if (PyErr_Occurred() || !pythonString)
{
return false;
}
value = { pythonString, static_cast<size_t>(size) };
return true;
}
static handle cast(const AZStd::string_view& src, return_value_policy, handle)
{
return pybind11::str(src.data(), src.size()).release();
}
};
}
}
@@ -0,0 +1,724 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <Source/PythonUtility.h>
#include <Source/PythonProxyObject.h>
#include <Source/PythonTypeCasters.h>
#include <Source/PythonMarshalComponent.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/RTTI/TypeInfo.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <EditorPythonBindings/CustomTypeBindingBus.h>
#include <pybind11/embed.h>
namespace EditorPythonBindings
{
namespace Module
{
pybind11::module DeterminePackageModule(PackageMapType& modulePackageMap, AZStd::string_view moduleName, pybind11::module parentModule, pybind11::module fallbackModule, [[maybe_unused]] bool alertUsingFallback)
{
if (moduleName.empty() || !moduleName[0])
{
AZ_Warning("python", !alertUsingFallback, "Could not determine missing or empty module; using fallback module");
return fallbackModule;
}
else if (parentModule.is_none())
{
AZ_Warning("python", !alertUsingFallback, "Could not determine using None parent module; using fallback module");
return fallbackModule;
}
AZStd::string parentModuleName(PyModule_GetName(parentModule.ptr()));
modulePackageMap[parentModuleName] = parentModule;
pybind11::module currentModule = parentModule;
AZStd::string fullModuleName(parentModuleName);
fullModuleName.append(".");
fullModuleName.append(moduleName);
AZStd::vector<AZStd::string> moduleParts;
AzFramework::StringFunc::Tokenize(fullModuleName.c_str(), moduleParts, ".", false, false);
for (int modulePartsIndex = 0; modulePartsIndex < moduleParts.size(); ++modulePartsIndex)
{
AZStd::string currentModulePath;
AzFramework::StringFunc::Join(currentModulePath, moduleParts.begin(), moduleParts.begin() + modulePartsIndex + 1, ".");
auto itPackageEntry = modulePackageMap.find(currentModulePath.c_str());
if (itPackageEntry != modulePackageMap.end())
{
currentModule = itPackageEntry->second;
}
else
{
PyObject* newModule = PyImport_AddModule(currentModulePath.c_str());
if (!newModule)
{
AZ_Warning("python", false, "Could not add module named %s; using fallback module", currentModulePath.c_str());
return fallbackModule;
}
else
{
auto newSubModule = pybind11::reinterpret_borrow<pybind11::module>(newModule);
modulePackageMap[currentModulePath] = newSubModule;
const char* subModuleName = moduleParts[modulePartsIndex].c_str();
currentModule.attr(subModuleName) = newSubModule;
currentModule = newSubModule;
}
}
}
return currentModule;
}
}
namespace Internal
{
void LogSerializeTypeInfo(const AZ::TypeId& typeId)
{
AZStd::string info = AZStd::string::format("Serialize class info for typeId %s (", typeId.ToString<AZStd::string>().c_str());
AZ::SerializeContext* serializeContext{ nullptr };
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
if (serializeContext)
{
auto&& classInfo = serializeContext->FindClassData(typeId);
if (classInfo)
{
info = AZStd::string::format("name:%s version:%d isContainer:%s",
classInfo->m_name, classInfo->m_version, classInfo->m_container ? "true" : "false");
}
auto&& genericClassInfo = serializeContext->FindGenericClassInfo(typeId);
if (genericClassInfo)
{
info += " generic:true";
info += AZStd::string::format(" specialized typeId: %s",
genericClassInfo->GetSpecializedTypeId().ToString<AZStd::string>().c_str());
info += AZStd::string::format(" generic typeId: %s",
genericClassInfo->GetGenericTypeId().ToString<AZStd::string>().c_str());
size_t numTemplatedArguments = genericClassInfo->GetNumTemplatedArguments();
info += AZStd::string::format(" template arguments %zu", genericClassInfo->GetNumTemplatedArguments());
for (size_t index = 0; index < numTemplatedArguments; ++index)
{
info += AZStd::string::format(" [%zu] template type: %s",
index,
genericClassInfo->GetTemplatedTypeId(index).ToString<AZStd::string>().c_str());
}
}
}
info += ")";
AZ_Warning("python", false, "Serialize generic class info %s", info.c_str());
}
AZStd::optional<AZ::TypeId> IsEnumClass(const AZ::BehaviorParameter& behaviorParameter)
{
if (behaviorParameter.m_azRtti)
{
// If the underlying type of the supplied type is different, then T is an enum
const AZ::TypeId& underlyingTypeId = AZ::Internal::GetUnderlyingTypeId(*behaviorParameter.m_azRtti);
if (underlyingTypeId != behaviorParameter.m_typeId)
{
return AZStd::make_optional(underlyingTypeId);
}
}
return AZStd::nullopt;
}
template <typename T>
bool ConvertPythonFromEnumClass(const AZ::TypeId& underlyingTypeId, AZ::BehaviorValueParameter& behaviorValue, AZ::s64& outboundPythonValue)
{
if (underlyingTypeId == AZ::AzTypeInfo<T>::Uuid())
{
outboundPythonValue = aznumeric_cast<AZ::s64>(*behaviorValue.GetAsUnsafe<T>());
return true;
}
return false;
}
AZStd::optional<pybind11::object> ConvertFromEnumClass(AZ::BehaviorValueParameter& behaviorValue)
{
if (!behaviorValue.m_azRtti)
{
return AZStd::nullopt;
}
const AZ::TypeId& underlyingTypeId = AZ::Internal::GetUnderlyingTypeId(*behaviorValue.m_azRtti);
if (underlyingTypeId != behaviorValue.m_typeId)
{
AZ::s64 outboundPythonValue = 0;
bool converted =
ConvertPythonFromEnumClass<AZ::u8>(underlyingTypeId, behaviorValue, outboundPythonValue) ||
ConvertPythonFromEnumClass<AZ::u16>(underlyingTypeId, behaviorValue, outboundPythonValue) ||
ConvertPythonFromEnumClass<AZ::u32>(underlyingTypeId, behaviorValue, outboundPythonValue) ||
ConvertPythonFromEnumClass<AZ::u64>(underlyingTypeId, behaviorValue, outboundPythonValue) ||
ConvertPythonFromEnumClass<AZ::s8>(underlyingTypeId, behaviorValue, outboundPythonValue) ||
ConvertPythonFromEnumClass<AZ::s16>(underlyingTypeId, behaviorValue, outboundPythonValue) ||
ConvertPythonFromEnumClass<AZ::s32>(underlyingTypeId, behaviorValue, outboundPythonValue) ||
ConvertPythonFromEnumClass<AZ::s64>(underlyingTypeId, behaviorValue, outboundPythonValue);
AZ_Error("python", converted, "Enumeration backed by a non-numeric integer type.");
return converted ? AZStd::make_optional(pybind11::cast<AZ::s64>(outboundPythonValue)) : AZStd::nullopt;
}
return AZStd::nullopt;
}
template <typename T>
bool ConvertBehaviorParameterEnum(pybind11::object obj, const AZ::TypeId& underlyingTypeId, AZ::BehaviorValueParameter& parameter)
{
if (underlyingTypeId == AZ::AzTypeInfo<T>::Uuid())
{
void* value = parameter.m_tempData.allocate(sizeof(T), AZStd::alignment_of<T>::value, 0);
*reinterpret_cast<T*>(value) = pybind11::cast<T>(obj);
if (parameter.m_traits & AZ::BehaviorParameter::TR_POINTER)
{
*reinterpret_cast<void**>(parameter.m_value) = reinterpret_cast<T*>(&value);
}
else
{
parameter.m_value = value;
}
return true;
}
return false;
}
bool ConvertEnumClassFromPython(pybind11::object obj, const AZ::BehaviorParameter& behaviorArgument, AZ::BehaviorValueParameter& parameter)
{
if (behaviorArgument.m_azRtti)
{
// If the underlying type of the supplied type is different, then T is an enum
const AZ::TypeId underlyingTypeId = AZ::Internal::GetUnderlyingTypeId(*behaviorArgument.m_azRtti);
if (underlyingTypeId != behaviorArgument.m_typeId)
{
parameter.m_name = behaviorArgument.m_name;
parameter.m_azRtti = behaviorArgument.m_azRtti;
parameter.m_traits = behaviorArgument.m_traits;
parameter.m_typeId = behaviorArgument.m_typeId;
bool handled =
ConvertBehaviorParameterEnum<AZ::u8>(obj, underlyingTypeId, parameter) ||
ConvertBehaviorParameterEnum<AZ::u16>(obj, underlyingTypeId, parameter) ||
ConvertBehaviorParameterEnum<AZ::u32>(obj, underlyingTypeId, parameter) ||
ConvertBehaviorParameterEnum<AZ::u64>(obj, underlyingTypeId, parameter) ||
ConvertBehaviorParameterEnum<AZ::s8>(obj, underlyingTypeId, parameter) ||
ConvertBehaviorParameterEnum<AZ::s16>(obj, underlyingTypeId, parameter) ||
ConvertBehaviorParameterEnum<AZ::s32>(obj, underlyingTypeId, parameter) ||
ConvertBehaviorParameterEnum<AZ::s64>(obj, underlyingTypeId, parameter) ;
AZ_Error("python", handled, "Enumeration backed by a non-numeric integer type.");
return handled;
}
}
return false;
}
// type checks
bool IsPrimitiveType(const AZ::TypeId& typeId)
{
return (typeId == AZ::AzTypeInfo<bool>::Uuid() ||
typeId == AZ::AzTypeInfo<char>::Uuid() ||
typeId == AZ::AzTypeInfo<float>::Uuid() ||
typeId == AZ::AzTypeInfo<double>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::s8>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::u8>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::s16>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::u16>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::s32>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::u32>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::s64>::Uuid() ||
typeId == AZ::AzTypeInfo<AZ::u64>::Uuid() );
}
bool IsPointerType(const AZ::u32 traits)
{
return (((traits & AZ::BehaviorParameter::TR_POINTER) == AZ::BehaviorParameter::TR_POINTER) ||
((traits & AZ::BehaviorParameter::TR_REFERENCE) == AZ::BehaviorParameter::TR_REFERENCE));
}
// allocation patterns
void StoreVariableCustomTypeDeleter(
CustomTypeBindingNotifications::ValueHandle handle,
AZ::TypeId typeId,
Convert::StackVariableAllocator& stackVariableAllocator)
{
auto deallocateValue = [typeId = std::move(typeId), handle]() mutable
{
CustomTypeBindingNotificationBus::Event(
typeId,
&CustomTypeBindingNotificationBus::Events::CleanUpValue,
handle);
};
stackVariableAllocator.StoreVariableDeleter(deallocateValue);
}
bool AllocateBehaviorObjectByClass(const AZ::BehaviorClass* behaviorClass, AZ::BehaviorObject& behaviorObject)
{
if (behaviorClass)
{
if (behaviorClass->m_defaultConstructor)
{
AZ::BehaviorObject newBehaviorObject = behaviorClass->Create();
behaviorObject.m_typeId = newBehaviorObject.m_typeId;
behaviorObject.m_address = newBehaviorObject.m_address;
return true;
}
else
{
AZ_Warning("python", behaviorClass->m_defaultConstructor, "Missing default constructor for AZ::BehaviorClass for typeId:%s", behaviorClass->m_name.c_str());
}
}
return false;
}
bool AllocateBehaviorValueParameter(const AZ::BehaviorMethod* behaviorMethod, AZ::BehaviorValueParameter& result, Convert::StackVariableAllocator& stackVariableAllocator)
{
if (const AZ::BehaviorParameter* resultType = behaviorMethod->GetResult())
{
result.Set(*resultType);
if (auto underlyingTypeId = Internal::IsEnumClass(result); underlyingTypeId)
{
result.m_typeId = underlyingTypeId.value();
}
if (resultType->m_traits & AZ::BehaviorParameter::TR_POINTER)
{
result.m_value = result.m_tempData.allocate(sizeof(intptr_t), alignof(intptr_t));
return true;
}
if (resultType->m_traits & AZ::BehaviorParameter::TR_REFERENCE)
{
return true;
}
if (IsPrimitiveType(resultType->m_typeId))
{
result.m_value = result.m_tempData.allocate(sizeof(intptr_t), alignof(intptr_t));
return true;
}
AZ::BehaviorContext* behaviorContext(nullptr);
AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
if (!behaviorContext)
{
AZ_Assert(false, "A behavior context is required!");
return false;
}
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(behaviorContext, resultType->m_typeId);
if (behaviorClass)
{
AZ::BehaviorObject behaviorObject;
if (AllocateBehaviorObjectByClass(behaviorClass, behaviorObject))
{
result.m_value = behaviorObject.m_address;
result.m_typeId = resultType->m_typeId;
return true;
}
}
else
{
CustomTypeBindingNotifications::AllocationHandle allocationHandleResult;
CustomTypeBindingNotificationBus::EventResult(
allocationHandleResult,
result.m_typeId,
&CustomTypeBindingNotificationBus::Events::AllocateDefault);
if (allocationHandleResult)
{
CustomTypeBindingNotifications::ValueHandle handle = allocationHandleResult.value().first;
const AZ::BehaviorObject& behaviorObject = allocationHandleResult.value().second;
StoreVariableCustomTypeDeleter(handle, behaviorObject.m_typeId, stackVariableAllocator);
result.m_value = behaviorObject.m_address;
result.m_typeId = behaviorObject.m_typeId;
return true;
}
// So far no allocation scheme has been found for this typeId, but the SerializeContext might have more information
// so this code tries to pull out more type information about the typeId so that the user can get more human readable
// information than a UUID
LogSerializeTypeInfo(resultType->m_typeId);
AZ_Error("python", behaviorClass, "A behavior class is missing for %s!",
resultType->m_typeId.ToString<AZStd::string>().c_str());
}
}
return false;
}
void DeallocateBehaviorValueParameter(AZ::BehaviorValueParameter& valueParameter)
{
if (IsPointerType(valueParameter.m_traits) || IsPrimitiveType(valueParameter.m_typeId))
{
// no constructor was used
return;
}
AZ::BehaviorContext* behaviorContext(nullptr);
AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext);
if (!behaviorContext)
{
AZ_Assert(false, "A behavior context is required!");
}
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(behaviorContext, valueParameter.m_typeId);
if (behaviorClass)
{
AZ::BehaviorObject behaviorObject;
behaviorObject.m_address = valueParameter.m_value;
behaviorObject.m_typeId = valueParameter.m_typeId;
behaviorClass->Destroy(behaviorObject);
}
}
}
namespace Convert
{
// StackVariableAllocator
StackVariableAllocator::~StackVariableAllocator()
{
for (auto& cleanUp : m_cleanUpItems)
{
cleanUp();
}
}
void StackVariableAllocator::StoreVariableDeleter(VariableDeleter&& deleter)
{
m_cleanUpItems.emplace_back(deleter);
}
// from Python to BehaviorValueParameter
bool PythonProxyObjectToBehaviorValueParameter(const AZ::BehaviorParameter& behaviorArgument, pybind11::object pyObj, AZ::BehaviorValueParameter& parameter)
{
auto behaviorObject = pybind11::cast<EditorPythonBindings::PythonProxyObject*>(pyObj)->GetBehaviorObject();
if (behaviorObject)
{
const AZ::BehaviorClass* behaviorClass = AZ::BehaviorContextHelper::GetClass(behaviorObject.value()->m_typeId);
if (!behaviorClass)
{
AZ_Warning("python", false, "Missing BehaviorClass for typeId %s", behaviorObject.value()->m_typeId.ToString<AZStd::string>().c_str());
return false;
}
if (behaviorClass->m_azRtti)
{
// is exact type or can be down casted?
if (!behaviorClass->m_azRtti->IsTypeOf(behaviorArgument.m_typeId))
{
return false;
}
}
else if (behaviorObject.value()->m_typeId != behaviorArgument.m_typeId)
{
// type mismatch detected
return false;
}
if ((behaviorArgument.m_traits & AZ::BehaviorParameter::TR_POINTER) == AZ::BehaviorParameter::TR_POINTER)
{
parameter.m_value = &behaviorObject.value()->m_address;
}
else
{
parameter.m_value = behaviorObject.value()->m_address;
}
parameter.m_typeId = behaviorClass->m_typeId;
parameter.m_azRtti = behaviorClass->m_azRtti;
parameter.m_traits = behaviorArgument.m_traits;
parameter.m_name = behaviorArgument.m_name;
return true;
}
return false;
}
bool CustomPythonToBehavior(
const AZ::BehaviorParameter& behaviorArgument,
pybind11::object pyObj,
AZ::BehaviorValueParameter& outBehavior,
StackVariableAllocator& stackVariableAllocator)
{
AZStd::optional<CustomTypeBindingNotifications::ValueHandle> handle;
CustomTypeBindingNotificationBus::EventResult(
handle,
behaviorArgument.m_typeId,
&CustomTypeBindingNotificationBus::Events::PythonToBehavior,
pyObj.ptr(),
static_cast<AZ::BehaviorParameter::Traits>(behaviorArgument.m_traits),
outBehavior);
if (handle)
{
Internal::StoreVariableCustomTypeDeleter(handle.value(), behaviorArgument.m_typeId, stackVariableAllocator);
outBehavior.m_typeId = behaviorArgument.m_typeId;
outBehavior.m_traits = behaviorArgument.m_traits;
outBehavior.m_name = behaviorArgument.m_name;
outBehavior.m_azRtti = behaviorArgument.m_azRtti;
return true;
}
return false;
}
bool PythonToBehaviorValueParameter(const AZ::BehaviorParameter& behaviorArgument, pybind11::object pyObj, AZ::BehaviorValueParameter& parameter, Convert::StackVariableAllocator& stackVariableAllocator)
{
AZStd::optional<PythonMarshalTypeRequests::BehaviorValueResult> result;
PythonMarshalTypeRequests::BehaviorTraits traits = static_cast<PythonMarshalTypeRequests::BehaviorTraits>(behaviorArgument.m_traits);
PythonMarshalTypeRequestBus::EventResult(result, behaviorArgument.m_typeId, &PythonMarshalTypeRequestBus::Events::PythonToBehaviorValueParameter, traits, pyObj, parameter);
if (result && result.value().first)
{
auto deleter = AZStd::move(result.value().second);
if (deleter)
{
stackVariableAllocator.StoreVariableDeleter(AZStd::move(deleter));
}
parameter.m_typeId = behaviorArgument.m_typeId;
parameter.m_traits = behaviorArgument.m_traits;
parameter.m_name = behaviorArgument.m_name;
parameter.m_azRtti = behaviorArgument.m_azRtti;
return true;
}
else if (auto underlyingTypeId = Internal::IsEnumClass(behaviorArgument); underlyingTypeId)
{
AZ::BehaviorParameter tempArg;
tempArg.m_azRtti = behaviorArgument.m_azRtti;
tempArg.m_traits = behaviorArgument.m_traits;
tempArg.m_name = behaviorArgument.m_name;
tempArg.m_typeId = underlyingTypeId.value();
if (PythonToBehaviorValueParameter(tempArg, pyObj, parameter, stackVariableAllocator))
{
parameter.m_typeId = behaviorArgument.m_typeId;
return true;
}
}
else if (pybind11::isinstance<EditorPythonBindings::PythonProxyObject>(pyObj))
{
return PythonProxyObjectToBehaviorValueParameter(behaviorArgument, pyObj, parameter);
}
else if (CustomPythonToBehavior(behaviorArgument, pyObj, parameter, stackVariableAllocator))
{
return true;
}
return false;
}
// from BehaviorValueParameter to Python
AZStd::optional<pybind11::object> CustomBehaviorToPython(AZ::BehaviorValueParameter& behaviorValue, Convert::StackVariableAllocator& stackVariableAllocator)
{
AZStd::optional<CustomTypeBindingNotifications::ValueHandle> handle;
PyObject* outPyObj = nullptr;
CustomTypeBindingNotificationBus::EventResult(
handle,
behaviorValue.m_typeId,
&CustomTypeBindingNotificationBus::Events::BehaviorToPython,
behaviorValue,
outPyObj);
if (outPyObj != nullptr && handle)
{
Internal::StoreVariableCustomTypeDeleter(handle.value(), behaviorValue.m_typeId, stackVariableAllocator);
return { pybind11::reinterpret_borrow<pybind11::object>(outPyObj) };
}
return AZStd::nullopt;
}
pybind11::object BehaviorValueParameterToPython(AZ::BehaviorValueParameter& behaviorValue, Convert::StackVariableAllocator& stackVariableAllocator)
{
auto pyValue = Internal::ConvertFromEnumClass(behaviorValue);
if (pyValue.has_value())
{
return pyValue.value();
}
AZStd::optional<PythonMarshalTypeRequests::PythonValueResult> result;
PythonMarshalTypeRequestBus::EventResult(result, behaviorValue.m_typeId, &PythonMarshalTypeRequestBus::Events::BehaviorValueParameterToPython, behaviorValue);
if (result.has_value())
{
auto deleter = AZStd::move(result.value().second);
if (deleter)
{
stackVariableAllocator.StoreVariableDeleter(AZStd::move(deleter));
}
return result.value().first;
}
else if (auto customResult = CustomBehaviorToPython(behaviorValue, stackVariableAllocator); customResult)
{
return customResult.value();
}
else if (behaviorValue.m_typeId != AZ::Uuid::CreateNull() && behaviorValue.GetValueAddress())
{
return PythonProxyObjectManagement::CreatePythonProxyObject(behaviorValue.m_typeId, behaviorValue.GetValueAddress());
}
AZ_Warning("python", false, "Cannot convert type %s",
behaviorValue.m_name ? behaviorValue.m_name : behaviorValue.m_typeId.ToString<AZStd::string>().c_str());
return pybind11::cast<pybind11::none>(Py_None);
}
AZStd::string GetPythonTypeName(pybind11::object pyObj)
{
if (pybind11::isinstance<PythonProxyObject>(pyObj))
{
return pybind11::cast<PythonProxyObject*>(pyObj)->GetWrappedTypeName();
}
return pybind11::cast<AZStd::string>(pybind11::str(pyObj.get_type()));
}
}
namespace Call
{
constexpr size_t MaxBehaviorMethodArguments = 32;
using BehaviorMethodArgumentArray = AZStd::array<AZ::BehaviorValueParameter, MaxBehaviorMethodArguments>;
pybind11::object InvokeBehaviorMethodWithResult(AZ::BehaviorMethod* behaviorMethod, pybind11::args pythonInputArgs, AZ::BehaviorObject self, AZ::BehaviorValueParameter& result)
{
if (behaviorMethod->GetNumArguments() > MaxBehaviorMethodArguments || pythonInputArgs.size() > MaxBehaviorMethodArguments)
{
AZ_Error("python", false, "Too many arguments for class method; set:%zu max:%zu", behaviorMethod->GetMinNumberOfArguments(), MaxBehaviorMethodArguments);
return pybind11::cast<pybind11::none>(Py_None);
}
Convert::StackVariableAllocator stackVariableAllocator;
BehaviorMethodArgumentArray parameters;
int parameterCount = 0;
if (self.IsValid())
{
// record the "this" pointer's metadata like its RTTI so that it can be
// down casted to a parent class type if needed to invoke a parent method
AZ::BehaviorValueParameter theThisPointer;
if (const AZ::BehaviorParameter* thisInfo = behaviorMethod->GetArgument(0))
{
// avoiding the "Special handling for the generic object holder." since it assumes
// the BehaviorObject.m_value is a pointer; the reference version is already dereferenced
if ((thisInfo->m_traits & AZ::BehaviorParameter::TR_POINTER) == AZ::BehaviorParameter::TR_POINTER)
{
theThisPointer.m_value = &self.m_address;
}
else
{
theThisPointer.m_value = self.m_address;
}
theThisPointer.Set(*thisInfo);
parameters[0].Set(theThisPointer);
++parameterCount;
}
else
{
AZ_Warning("python", false, "Missing self info index 0 in class method %s", behaviorMethod->m_name.c_str());
return pybind11::cast<pybind11::none>(Py_None);
}
}
// prepare the parameters for the BehaviorMethod
for (auto pythonArg : pythonInputArgs)
{
if (parameterCount < behaviorMethod->GetNumArguments())
{
auto currentPythonArg = pybind11::cast<pybind11::object>(pythonArg);
const AZ::BehaviorParameter* behaviorArgument = behaviorMethod->GetArgument(parameterCount);
if (!behaviorArgument)
{
AZ_Warning("python", false, "Missing argument at index %d in class method %s", parameterCount, behaviorMethod->m_name.c_str());
return pybind11::cast<pybind11::none>(Py_None);
}
if (!Convert::PythonToBehaviorValueParameter(*behaviorArgument, currentPythonArg, parameters[parameterCount], stackVariableAllocator))
{
AZ_Warning("python", false, "BehaviorMethod %s: Parameter at [%d] index expects (%s:%s) for method but got type (%s)",
behaviorMethod->m_name.c_str(), parameterCount,
behaviorArgument->m_name, behaviorArgument->m_typeId.ToString<AZStd::string>().c_str(),
Convert::GetPythonTypeName(currentPythonArg).c_str());
return pybind11::cast<pybind11::none>(Py_None);
}
++parameterCount;
}
}
// did the Python script send the right amount of arguments?
const auto totalPythonArgs = pythonInputArgs.size() + (self.IsValid() ? 1 : 0); // +1 for the 'this' coming in from a marshaled Python/BehaviorObject
if (totalPythonArgs < behaviorMethod->GetMinNumberOfArguments())
{
AZ_Warning("python", false, "Method %s requires at least %zu parameters got %zu", behaviorMethod->m_name.c_str(), behaviorMethod->GetMinNumberOfArguments(), totalPythonArgs);
return pybind11::cast<pybind11::none>(Py_None);
}
else if (totalPythonArgs > behaviorMethod->GetNumArguments())
{
AZ_Warning("python", false, "Method %s requires %zu parameters but it got more (%zu) - excess parameters will not be used.", behaviorMethod->m_name.c_str(), behaviorMethod->GetMinNumberOfArguments(), totalPythonArgs);
}
if (behaviorMethod->HasResult())
{
if (Internal::AllocateBehaviorValueParameter(behaviorMethod, result, stackVariableAllocator))
{
if (behaviorMethod->Call(parameters.begin(), static_cast<unsigned int>(totalPythonArgs), &result))
{
result.m_azRtti = behaviorMethod->GetResult()->m_azRtti;
result.m_typeId = behaviorMethod->GetResult()->m_typeId;
result.m_traits = behaviorMethod->GetResult()->m_traits;
return Convert::BehaviorValueParameterToPython(result, stackVariableAllocator);
}
else
{
AZ_Warning("python", false, "Failed to call class method %s", behaviorMethod->m_name.c_str());
}
}
else
{
AZ_Warning("python", false, "Failed to allocate return value for method %s", behaviorMethod->m_name.c_str());
}
}
else if (!behaviorMethod->Call(parameters.begin(), static_cast<unsigned int>(totalPythonArgs)))
{
AZ_Warning("python", false, "Failed to invoke class method %s", behaviorMethod->m_name.c_str());
}
return pybind11::cast<pybind11::none>(Py_None);
}
pybind11::object InvokeBehaviorMethod(AZ::BehaviorMethod* behaviorMethod, pybind11::args pythonInputArgs, AZ::BehaviorObject self)
{
AZ::BehaviorValueParameter result;
result.m_value = nullptr;
pybind11::object pythonOutput = InvokeBehaviorMethodWithResult(behaviorMethod, pythonInputArgs, self, result);
if (result.m_value)
{
Internal::DeallocateBehaviorValueParameter(result);
}
return pythonOutput;
}
pybind11::object StaticMethod(AZ::BehaviorMethod* behaviorMethod, pybind11::args pythonInputArgs)
{
return InvokeBehaviorMethod(behaviorMethod, pythonInputArgs, {});
}
pybind11::object ClassMethod(AZ::BehaviorMethod* behaviorMethod, AZ::BehaviorObject self, pybind11::args pythonInputArgs)
{
if (behaviorMethod->GetNumArguments() == 0)
{
AZ_Error("python", false, "A member level function should require at least one argument");
}
else if (!self.IsValid())
{
AZ_Error("python", false, "Method %s requires at valid self object to invoke", behaviorMethod->m_name.c_str());
}
else
{
return InvokeBehaviorMethod(behaviorMethod, pythonInputArgs, self);
}
return pybind11::cast<pybind11::none>(Py_None);
}
}
}
@@ -0,0 +1,135 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <Source/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/std/string/string_view.h>
#include <AzCore/std/optional.h>
namespace AZ
{
struct BehaviorParameter;
struct BehaviorValueParameter;
class BehaviorMethod;
}
namespace EditorPythonBindings
{
namespace Scope
{
inline bool IsBehaviorFlaggedForEditor(const AZ::AttributeArray& attributes)
{
// defaults to Launcher
AZ::Script::Attributes::ScopeFlags scopeType = AZ::Script::Attributes::ScopeFlags::Launcher;
AZ::Attribute* scopeAttribute = AZ::FindAttribute(AZ::Script::Attributes::Scope, attributes);
if (scopeAttribute)
{
AZ::AttributeReader scopeAttributeReader(nullptr, scopeAttribute);
scopeAttributeReader.Read<AZ::Script::Attributes::ScopeFlags>(scopeType);
}
return (scopeType == AZ::Script::Attributes::ScopeFlags::Automation || scopeType == AZ::Script::Attributes::ScopeFlags::Common);
}
inline void FetchScriptName(const AZ::AttributeArray& attributes, AZStd::string& baseName)
{
AZ::Attribute* scriptNameAttribute = AZ::FindAttribute(AZ::Script::Attributes::Alias, attributes);
if (scriptNameAttribute)
{
AZ::AttributeReader scopeAttributeReader(nullptr, scriptNameAttribute);
scopeAttributeReader.Read<AZStd::string>(baseName);
}
}
}
namespace Module
{
using PackageMapType = AZStd::unordered_map<AZStd::string, pybind11::module>;
//! Finds or creates a sub-module to add a base parent module; create all the sub-modules as well
//! @param modulePackageMap keeps track of the known modules
//! @param moduleName can be a dot separated string such as "mygen.mypackage.mymodule"
//! @param parentModule the module to add new sub-modules
//! @param fallbackModule the module to add new sub-modules
//! @param alertUsingFallback issue a warning if using the fallback module
//! @return the new submodule
pybind11::module DeterminePackageModule(PackageMapType& modulePackageMap, AZStd::string_view moduleName, pybind11::module parentModule, pybind11::module fallbackModule, bool alertUsingFallback);
inline AZStd::optional<AZStd::string_view> GetName(const AZ::AttributeArray& attributes)
{
AZ::Attribute* moduleAttribute = AZ::FindAttribute(AZ::Script::Attributes::Module, attributes);
if (moduleAttribute)
{
const char* moduleName = nullptr;
AZ::AttributeReader scopeAttributeReader(nullptr, moduleAttribute);
scopeAttributeReader.Read<const char*>(moduleName);
if (moduleName)
{
return { moduleName };
}
}
return {};
}
}
namespace Convert
{
// allocation pattern for BehaviorValueParameters being stored in the stack and needs to be cleaned at the end of a block
using VariableDeleter = AZStd::function<void()>;
struct StackVariableAllocator final
: public AZStd::static_buffer_allocator<256, 16>
{
public:
~StackVariableAllocator();
void StoreVariableDeleter(VariableDeleter&& deleter);
private:
AZStd::vector<VariableDeleter> m_cleanUpItems;
};
//! Converts a behavior value parameter to a Python object
//! @param behaviorValue is a parameter that came from a result or some prepared behavior value
//! @param stackVariableAllocator manages the allocated parameter while in scope
//! @return a valid Python object or None if no conversion was possible
pybind11::object BehaviorValueParameterToPython(AZ::BehaviorValueParameter& behaviorValue, Convert::StackVariableAllocator& stackVariableAllocator);
//! Converts Python object to a behavior value parameter using an existing behaviorArgument from a Behavior Method
//! @param behaviorArgument the stored argument slot from a Behavior Method to match with the pyObj to covert in the parameter
//! @param parameter is the output of the conversion from Python to a Behavior value
//! @param stackVariableAllocator manages the allocated parameter while in scope
//! @return true if the conversion happened
bool PythonToBehaviorValueParameter(const AZ::BehaviorParameter& behaviorArgument, pybind11::object pyObj, AZ::BehaviorValueParameter& parameter, Convert::StackVariableAllocator& stackVariableAllocator);
//! Converts Python object to a PythonProxyObject, if possible
//! @param behaviorArgument A stored PythonProxyObject in Python, returns FALSE if the Python object does not point to a PythonProxyObject
//! @param parameter is the output of the conversion from Python to a Behavior value
//! @return true if the conversion happened
bool PythonProxyObjectToBehaviorValueParameter(const AZ::BehaviorParameter& behaviorArgument, pybind11::object pyObj, AZ::BehaviorValueParameter& parameter);
//! Gets a readable type name for the Python object; this will unwrap a PythonProxyObject to find its underlying type name
//! @param pyObj any valid Python object value
//! @return text form of the Python object value type
AZStd::string GetPythonTypeName(pybind11::object pyObj);
}
namespace Call
{
//! Calls a BehaviorMethod with a tuple of arguments for non-member functions
pybind11::object StaticMethod(AZ::BehaviorMethod* behaviorMethod, pybind11::args args);
//! Calls a BehaviorMethod with a tuple of arguments for member class level functions
pybind11::object ClassMethod(AZ::BehaviorMethod* behaviorMethod, AZ::BehaviorObject self, pybind11::args args);
}
}