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,93 @@
#
# 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.
#
if(NOT PAL_TRAIT_BUILD_HOST_TOOLS)
return()
endif()
# This will set python_package_name to whatever the package 'Python' is associated with
ly_get_package_association(Python python_package_name)
if (NOT python_package_name)
set(python_package_name "python-no-package-assocation-found")
message(WARNING "Python was not found in the package assocation list. Did someone call ly_associate_package(xxxxxxx Python) ?")
endif()
ly_add_target(
NAME EditorPythonBindings.Static STATIC
NAMESPACE Gem
FILES_CMAKE
editorpythonbindings_common_files.cmake
Source/Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake
PLATFORM_INCLUDE_FILES
Source/Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake
Source/Platform/Common/${PAL_TRAIT_COMPILER_ID}/editorpythonbindings_static_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake
COMPILE_DEFINITIONS
PRIVATE
PY_PACKAGE="${python_package_name}"
INCLUDE_DIRECTORIES
PRIVATE
.
Source
PUBLIC
Include
BUILD_DEPENDENCIES
PRIVATE
AZ::AzCore
PUBLIC
3rdParty::Python
3rdParty::pybind11
AZ::AzToolsFramework
)
ly_add_target(
NAME EditorPythonBindings.Editor MODULE
NAMESPACE Gem
OUTPUT_NAME Gem.EditorPythonBindings.Editor.b658359393884c4381c2fe2952b1472a.v0.1.0
FILES_CMAKE
editorpythonbindings_editor_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
Source
PUBLIC
Include
BUILD_DEPENDENCIES
PRIVATE
Gem::EditorPythonBindings.Static
)
################################################################################
# Tests
################################################################################
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_add_target(
NAME EditorPythonBindings.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Gem
FILES_CMAKE
editorpythonbindings_tests_files.cmake
PLATFORM_INCLUDE_FILES
Source/Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake
Source/Platform/Common/${PAL_TRAIT_COMPILER_ID}/editorpythonbindings_tests_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
Tests
Include
BUILD_DEPENDENCIES
PRIVATE
AZ::AzTest
Gem::EditorPythonBindings.Static
)
ly_add_googletest(
NAME Gem::EditorPythonBindings.Tests
COMPONENT TestTools
)
endif()
@@ -0,0 +1,69 @@
/*
* 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>
#include <AzCore/Interface/Interface.h>
#include <AzCore/RTTI/BehaviorContext.h>
// forward declare the C typedef of a PyObject*
struct _object;
using PyObject = _object;
namespace EditorPythonBindings
{
//! A team can define custom generic types to be created for a TypeId
//! The handler will need to allocate, deallocate, and convert behavior data to Python values
//! NOTE: if the TypeId is registered with the Behavior Context then that will be used instead of this custom binding
class CustomTypeBindingNotifications
: 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;
//////////////////////////////////////////////////////////////////////////
//! This handle is used to identify the allocations an external module used to prepare the value
//! After the EPB gem is done with the conversion, this handle will be sent back via the CleanUpValue() notification
//! to indicate that the module should clean up the allocations for that value conversion
using ValueHandle = std::intptr_t;
//! Allocate a default value for the supplied type
using AllocationHandle = AZStd::optional<AZStd::pair<ValueHandle, AZ::BehaviorObject>>;
virtual AllocationHandle AllocateDefault() = 0;
//! This method converts an incoming Python value into a behavior value; it should fill out the outValue fields
virtual AZStd::optional<ValueHandle> PythonToBehavior(
PyObject* pyObj,
AZ::BehaviorParameter::Traits traits,
AZ::BehaviorValueParameter& outValue) = 0;
//! This method convert an incoming behavior value into a Python value; it should fill out the outPyObj pointer
virtual AZStd::optional<ValueHandle> BehaviorToPython(
const AZ::BehaviorValueParameter& behaviorValue,
PyObject*& outPyObj) = 0;
//! This method is used to determine that the behavior value can be processed using the Python object type as input
//! NOTE: it should not actually do the conversion only detect IF it can be done with the supplied Python type
virtual bool CanConvertPythonToBehavior(
AZ::BehaviorParameter::Traits traits,
PyObject* pyObj) const = 0;
//! This is used to deallocate the value used by the PythonToBehavior() or BehaviorToPython() methods
//! The notification module is responsible for mapping the handle to the value's allocation(s)
virtual void CleanUpValue(ValueHandle handle) = 0;
};
using CustomTypeBindingNotificationBus = AZ::EBus<CustomTypeBindingNotifications>;
}
@@ -0,0 +1,53 @@
/*
* 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>
// forward declare the C typedef of a PyObject*
struct _object;
using PyObject = _object;
namespace EditorPythonBindings
{
/**
* Python notifications during interpreter operations
*/
class EditorPythonBindingsNotifications
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
//! Sent when the Python VM is about to start
virtual void OnPreInitialize() {}
//! Sent when the Python VM has started
virtual void OnPostInitialize() {}
//! Sent when the Python VM is about to shutdown
virtual void OnPreFinalize() {}
//! Sent when the Python VM has shutdown
virtual void OnPostFinalize() {}
//! Sent when any module is being installed from Python script code (normally from an import statement in a script)
virtual void OnImportModule(PyObject* module) { AZ_UNUSED(module); }
};
using EditorPythonBindingsNotificationBus = AZ::EBus<EditorPythonBindingsNotifications>;
} // namespace EditorPythonBindings
@@ -0,0 +1,27 @@
/*
* 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/Math/Crc.h>
namespace EditorPythonBindings
{
// components
constexpr const char PythonMarshalComponentTypeId[] = "{C733E1AD-9FDD-484E-A8D9-3EAB944B7841}";
constexpr const char PythonReflectionComponentTypeId[] = "{CBF32BE1-292C-4988-9E64-25127A8525A7}";
constexpr const char PythonSystemComponentTypeId[] = "{97F88B0F-CF68-4623-9541-549E59EE5F0C}";
// services
constexpr AZ::Crc32 PythonMarshalingService = AZ_CRC_CE("PythonMarshalingService");
constexpr AZ::Crc32 PythonReflectionService = AZ_CRC_CE("PythonReflectionService");
constexpr AZ::Crc32 PythonEmbeddedService = AZ_CRC_CE("PythonEmbeddedService");
}
@@ -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);
}
}
@@ -0,0 +1,534 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <EditorPythonBindings/CustomTypeBindingBus.h>
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonProxyObject.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Math/MathUtils.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace AZ
{
template <typename T>
struct CustomType
{
CustomType() = default;
CustomType(T value)
{
m_value = value;
}
T m_value = {};
};
struct MyCustomData
{
AZ::s32 s32Field = -32;
AZ::u32 u32Field = 32;
AZ::s16 s16Field = -16;
AZ::u16 u16Field = 16;
bool Compare(const MyCustomData& other) const
{
return other.s32Field == s32Field
&& other.u32Field == u32Field
&& other.s16Field == s16Field
&& other.u16Field == u16Field;
}
};
AZ_TYPE_INFO_SPECIALIZE(CustomType<int>, "{78BFA28F-7FF3-4DC6-B9E9-2DF158E6496B}");
AZ_TYPE_INFO_SPECIALIZE(CustomType<float>, "{4B71C5C7-6947-4510-88A6-87F9F975F9CB}");
AZ_TYPE_INFO_SPECIALIZE(CustomType<AZStd::string>, "{61ED57E0-50B2-4AD7-997A-FD343A964C49}");
AZ_TYPE_INFO_SPECIALIZE(CustomType<MyCustomData>, "{839E35B3-14EF-4776-A5A1-C7B914374A66}");
}
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// test class/struts
struct CustomTypeHandlerTester final
{
AZ_TYPE_INFO(CustomTypeHandlerTester, "{C59220A9-1479-434C-BBBD-4262090507FA}");
AZ::CustomType<int> CreateCustomTypeInt(int value)
{
return AZ::CustomType<int>(value);
}
int ReturnCustomTypeInt(const AZ::CustomType<int>& value) const
{
return value.m_value;
}
AZ::CustomType<float> CreateCustomTypeFloat(float value)
{
return AZ::CustomType<float>(value);
}
float ReturnCustomTypeFloat(const AZ::CustomType<float>& value) const
{
return value.m_value;
}
bool CompareCustomTypeFloatValues(const AZ::CustomType<float>& lhs, const AZ::CustomType<float>& rhs) const
{
return AZ::IsClose(lhs.m_value, rhs.m_value, std::numeric_limits<float>::epsilon());
}
AZ::CustomType<AZStd::string> CreateCustomTypeString(const AZStd::string& value)
{
return AZ::CustomType<AZStd::string>(value);
}
AZ::CustomType<AZStd::string> CombineCustomTypeString(const AZ::CustomType<AZStd::string>& lhs, const AZ::CustomType<AZStd::string>& rhs)
{
return AZ::CustomType<AZStd::string>(lhs.m_value + rhs.m_value);
}
AZStd::string ReturnCustomTypeString(const AZ::CustomType<AZStd::string>& value)
{
return value.m_value;
}
AZ::CustomType<AZ::MyCustomData> CreateCustomData(
AZ::s32 s32Value,
AZ::u32 u32Value,
AZ::s16 s16Value,
AZ::u16 u16Value)
{
auto value = AZ::CustomType<AZ::MyCustomData>();
value.m_value.s32Field = s32Value;
value.m_value.u32Field = u32Value;
value.m_value.s16Field = s16Value;
value.m_value.u16Field = u16Value;
return value;
}
AZ::CustomType<AZ::MyCustomData> CombineCustomData(const AZ::CustomType<AZ::MyCustomData>& lhs, const AZ::CustomType<AZ::MyCustomData>& rhs)
{
AZ::CustomType<AZ::MyCustomData> combined;
combined.m_value.s32Field = lhs.m_value.s32Field + rhs.m_value.s32Field;
combined.m_value.u32Field = lhs.m_value.u32Field + rhs.m_value.u32Field;
combined.m_value.s16Field = lhs.m_value.s16Field + rhs.m_value.s16Field;
combined.m_value.u16Field = lhs.m_value.u16Field + rhs.m_value.u16Field;
return combined;
}
void Reflect(AZ::ReflectContext* context)
{
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<CustomTypeHandlerTester>("CustomTypeHandlerTester")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Method("CreateCustomTypeInt", &CustomTypeHandlerTester::CreateCustomTypeInt)
->Method("ReturnCustomTypeInt", &CustomTypeHandlerTester::ReturnCustomTypeInt)
->Method("CreateCustomTypeFloat", &CustomTypeHandlerTester::CreateCustomTypeFloat)
->Method("ReturnCustomTypeFloat", &CustomTypeHandlerTester::ReturnCustomTypeFloat)
->Method("CompareCustomTypeFloatValues", &CustomTypeHandlerTester::CompareCustomTypeFloatValues)
->Method("CreateCustomTypeString", &CustomTypeHandlerTester::CreateCustomTypeString)
->Method("CombineCustomTypeString", &CustomTypeHandlerTester::CombineCustomTypeString)
->Method("ReturnCustomTypeString", &CustomTypeHandlerTester::ReturnCustomTypeString)
->Method("CreateCustomData", &CustomTypeHandlerTester::CreateCustomData)
->Method("CombineCustomData", &CustomTypeHandlerTester::CombineCustomData)
;
}
}
};
struct CustomTypeBindingNotificationBusHandler final
: public EditorPythonBindings::CustomTypeBindingNotificationBus::MultiHandler
{
using Handle = EditorPythonBindings::CustomTypeBindingNotifications::ValueHandle;
constexpr static Handle NoAllocation { ~0LL };
AZStd::unordered_map<void*, AZ::TypeId> m_allocationMap;
CustomTypeBindingNotificationBusHandler()
{
BusConnect(azrtti_typeid<AZ::CustomType<int>>());
BusConnect(azrtti_typeid<AZ::CustomType<float>>());
BusConnect(azrtti_typeid<AZ::CustomType<AZStd::string>>());
BusConnect(azrtti_typeid<AZ::CustomType<AZ::MyCustomData>>());
}
~CustomTypeBindingNotificationBusHandler()
{
BusDisconnect();
}
using AllocationHandle = EditorPythonBindings::CustomTypeBindingNotifications::AllocationHandle;
AllocationHandle AllocateDefault() override
{
AZ::BehaviorObject behaviorObject;
const AZ::TypeId& typeId = *EditorPythonBindings::CustomTypeBindingNotificationBus::GetCurrentBusId();
if (typeId == azrtti_typeid<AZ::CustomType<int>>())
{
behaviorObject.m_address = azmalloc(sizeof(AZ::CustomType<int>));
behaviorObject.m_typeId = typeId;
m_allocationMap[behaviorObject.m_address] = behaviorObject.m_typeId;
return { {reinterpret_cast<Handle>(behaviorObject.m_address), AZStd::move(behaviorObject)} };
}
else if (typeId == azrtti_typeid<AZ::CustomType<float>>())
{
behaviorObject.m_address = azmalloc(sizeof(AZ::CustomType<float>));
behaviorObject.m_typeId = typeId;
m_allocationMap[behaviorObject.m_address] = behaviorObject.m_typeId;
return { {reinterpret_cast<Handle>(behaviorObject.m_address), AZStd::move(behaviorObject)} };
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZStd::string>>())
{
behaviorObject.m_address = new AZ::CustomType<AZStd::string>();
behaviorObject.m_typeId = typeId;
m_allocationMap[behaviorObject.m_address] = behaviorObject.m_typeId;
return { {reinterpret_cast<Handle>(behaviorObject.m_address), AZStd::move(behaviorObject)} };
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZ::MyCustomData>>())
{
behaviorObject.m_address = azmalloc(sizeof(AZ::CustomType<AZ::MyCustomData>));
new (behaviorObject.m_address) AZ::CustomType<AZ::MyCustomData>();
behaviorObject.m_typeId = typeId;
m_allocationMap[behaviorObject.m_address] = behaviorObject.m_typeId;
return { {reinterpret_cast<Handle>(behaviorObject.m_address), AZStd::move(behaviorObject)} };
}
return AZStd::nullopt;
}
AZStd::optional<ValueHandle> PythonToBehavior(
PyObject* pyObj,
[[maybe_unused]] AZ::BehaviorParameter::Traits traits,
AZ::BehaviorValueParameter& outValue) override
{
const AZ::TypeId& typeId = *EditorPythonBindings::CustomTypeBindingNotificationBus::GetCurrentBusId();
if (typeId == azrtti_typeid<AZ::CustomType<int>>())
{
outValue.ConvertTo<AZ::CustomType<int>>();
outValue.StoreInTempData<AZ::CustomType<int>>({ aznumeric_cast<int>(PyLong_AsLong(pyObj)) });
return { NoAllocation };
}
else if (typeId == azrtti_typeid<AZ::CustomType<float>>())
{
float floatValue = aznumeric_cast<float>(PyFloat_AsDouble(pyObj));
outValue.ConvertTo<AZ::CustomType<float>>();
outValue.StoreInTempData<AZ::CustomType<float>>({ floatValue });
return { NoAllocation };
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZStd::string>>())
{
if (PyUnicode_Check(pyObj))
{
Py_ssize_t pySize = 0;
const char* pyData = PyUnicode_AsUTF8AndSize(pyObj, &pySize);
if (pyData)
{
auto data = new AZ::CustomType<AZStd::string>();
data->m_value.assign(pyData, pyData + pySize);
outValue.ConvertTo<AZ::CustomType<AZStd::string>>();
outValue.m_value = data;
m_allocationMap[outValue.m_value] = typeId;
return { reinterpret_cast<Handle>(outValue.m_value) };
}
return { NoAllocation };
}
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZ::MyCustomData>>())
{
if (PyTuple_Check(pyObj) && PyTuple_Size(pyObj) == 4)
{
void* data = azmalloc(sizeof(AZ::CustomType<AZ::MyCustomData>));
new (data) AZ::CustomType<AZ::MyCustomData>();
m_allocationMap[data] = typeId;
AZ::CustomType<AZ::MyCustomData>* myData = reinterpret_cast<AZ::CustomType<AZ::MyCustomData>*>(data);
myData->m_value.s32Field = aznumeric_cast<AZ::s32>(PyLong_AsLong(PyTuple_GetItem(pyObj, 0)));
myData->m_value.u32Field = aznumeric_cast<AZ::u32>(PyLong_AsLong(PyTuple_GetItem(pyObj, 1)));
myData->m_value.s16Field = aznumeric_cast<AZ::s16>(PyLong_AsLong(PyTuple_GetItem(pyObj, 2)));
myData->m_value.u16Field = aznumeric_cast<AZ::u16>(PyLong_AsLong(PyTuple_GetItem(pyObj, 3)));
outValue.ConvertTo<AZ::CustomType<AZ::MyCustomData>>();
outValue.m_value = data;
return { reinterpret_cast<Handle>(data) };
}
}
return AZStd::nullopt;
}
AZStd::optional<ValueHandle> BehaviorToPython(
const AZ::BehaviorValueParameter& behaviorValue,
PyObject*& outPyObj) override
{
const AZ::TypeId& typeId = *EditorPythonBindings::CustomTypeBindingNotificationBus::GetCurrentBusId();
if (typeId == azrtti_typeid<AZ::CustomType<int>>())
{
AZ::CustomType<int>* value = behaviorValue.GetAsUnsafe<AZ::CustomType<int>>();
outPyObj = PyLong_FromLong(value->m_value);
return { NoAllocation };
}
else if (typeId == azrtti_typeid<AZ::CustomType<float>>())
{
AZ::CustomType<float>* value = behaviorValue.GetAsUnsafe<AZ::CustomType<float>>();
outPyObj = PyFloat_FromDouble(value->m_value);
return { NoAllocation };
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZStd::string>>())
{
AZ::CustomType<AZStd::string>* value = behaviorValue.GetAsUnsafe<AZ::CustomType<AZStd::string>>();
outPyObj = PyUnicode_FromString(value->m_value.c_str());
return { NoAllocation };
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZ::MyCustomData>>())
{
AZ::CustomType<AZ::MyCustomData>* value = behaviorValue.GetAsUnsafe<AZ::CustomType<AZ::MyCustomData>>();
outPyObj = PyTuple_New(4);
PyTuple_SetItem(outPyObj, 0, PyLong_FromLong(value->m_value.s32Field));
PyTuple_SetItem(outPyObj, 1, PyLong_FromLong(value->m_value.u32Field));
PyTuple_SetItem(outPyObj, 2, PyLong_FromLong(value->m_value.s16Field));
PyTuple_SetItem(outPyObj, 3, PyLong_FromLong(value->m_value.u16Field));
return { NoAllocation };
}
return AZStd::nullopt;
}
bool CanConvertPythonToBehavior(
[[maybe_unused]] AZ::BehaviorParameter::Traits traits,
PyObject* pyObj) const override
{
const AZ::TypeId& typeId = *EditorPythonBindings::CustomTypeBindingNotificationBus::GetCurrentBusId();
if (typeId == azrtti_typeid<AZ::CustomType<int>>())
{
return PyLong_Check(pyObj);
}
else if (typeId == azrtti_typeid<AZ::CustomType<float>>())
{
return PyFloat_Check(pyObj);
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZStd::string>>())
{
return PyUnicode_Check(pyObj);
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZ::MyCustomData>>())
{
return PyTuple_Check(pyObj);
}
return false;
}
void CleanUpValue(ValueHandle handle) override
{
auto handleEntry = m_allocationMap.find(reinterpret_cast<void*>(handle));
if (handleEntry != m_allocationMap.end())
{
m_allocationMap.erase(handleEntry);
const AZ::TypeId& typeId = handleEntry->second;
if (typeId == azrtti_typeid<AZ::CustomType<int>>())
{
azfree(reinterpret_cast<void*>(handle));
}
else if (typeId == azrtti_typeid<AZ::CustomType<float>>())
{
azfree(reinterpret_cast<void*>(handle));
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZStd::string>>())
{
delete reinterpret_cast<AZ::CustomType<AZStd::string>*>(handle);
}
else if (typeId == azrtti_typeid<AZ::CustomType<AZ::MyCustomData>>())
{
azfree(reinterpret_cast<void*>(handle));
}
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct CustomTypeHandlerTests
: public PythonTestingFixture
{
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
PythonTestingFixture::TearDown();
}
};
//////////////////////////////////////////////////////////////////////////
// tests
TEST_F(CustomTypeHandlerTests, CustomTypeHandler_ReturnsCustom_Works)
{
CustomTypeBindingNotificationBusHandler customTypeBindingNotificationBusHandler;
CustomTypeHandlerTester customTypeHandlerTester;
customTypeHandlerTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test
tester = azlmbr.test.CustomTypeHandlerTester()
customValue = tester.CreateCustomTypeInt(42)
if (None == customValue):
raise RuntimeError('None == customValue')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
e.Deactivate();
}
TEST_F(CustomTypeHandlerTests, CustomTypeHandler_AcceptsCustom_Works)
{
CustomTypeBindingNotificationBusHandler customTypeBindingNotificationBusHandler;
CustomTypeHandlerTester customTypeHandlerTester;
customTypeHandlerTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test
tester = azlmbr.test.CustomTypeHandlerTester()
customValue = tester.CreateCustomTypeInt(42)
value = tester.ReturnCustomTypeInt(customValue)
if (value != 42):
raise RuntimeError('value != 42')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
e.Deactivate();
}
TEST_F(CustomTypeHandlerTests, CustomTypeHandler_CustomFloatValues_Works)
{
CustomTypeBindingNotificationBusHandler customTypeBindingNotificationBusHandler;
CustomTypeHandlerTester customTypeHandlerTester;
customTypeHandlerTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test
tester = azlmbr.test.CustomTypeHandlerTester()
lhsValue = tester.CreateCustomTypeFloat(42.0)
rhsValue = tester.CreateCustomTypeFloat(tester.ReturnCustomTypeFloat(lhsValue))
if (tester.CompareCustomTypeFloatValues(lhsValue,rhsValue) is False):
raise RuntimeError('tester.CompareCustomTypeFloatValues(lhsValue,rhsValue) is False')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
e.Deactivate();
}
TEST_F(CustomTypeHandlerTests, CustomTypeHandler_CustomStringValues_Works)
{
CustomTypeBindingNotificationBusHandler customTypeBindingNotificationBusHandler;
CustomTypeHandlerTester customTypeHandlerTester;
customTypeHandlerTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test
tester = azlmbr.test.CustomTypeHandlerTester()
babble = tester.CreateCustomTypeString('babble')
fish = tester.CreateCustomTypeString('fish')
babbleFish = tester.CombineCustomTypeString(babble, fish)
if (tester.ReturnCustomTypeString(babbleFish) != 'babblefish'):
raise RuntimeError("tester.ReturnCustomTypeString(babbleFish) != 'babblefish'")
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
e.Deactivate();
}
TEST_F(CustomTypeHandlerTests, CustomTypeHandler_CustomDataValue_Works)
{
CustomTypeBindingNotificationBusHandler customTypeBindingNotificationBusHandler;
CustomTypeHandlerTester customTypeHandlerTester;
customTypeHandlerTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test
tester = azlmbr.test.CustomTypeHandlerTester()
lhs = tester.CreateCustomData(-1, 1, -2, 2)
rhs = tester.CreateCustomData(0, 0, 1, 1)
outTuple = tester.CombineCustomData(lhs, rhs)
if (outTuple[0] != -1 or outTuple[1] != 1 or outTuple[2] != -1 or outTuple[3] != 3):
raise RuntimeError("outTuple[0] != -1 or outTuple[1] != 1 or outTuple[2] != -2 or outTuple[3] != 2")
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
e.Deactivate();
}
}
@@ -0,0 +1,469 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <EditorPythonBindings/EditorPythonBindingsBus.h>
#include <AzCore/Debug/TraceMessageBus.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
namespace UnitTest
{
struct EditorPythonBindingsNotificationBusSink final
: public EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler
{
EditorPythonBindingsNotificationBusSink()
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusConnect();
}
~EditorPythonBindingsNotificationBusSink()
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusDisconnect();
}
//////////////////////////////////////////////////////////////////////////
// handles EditorPythonBindingsNotificationBus
int m_OnPreInitializeCount = 0;
int m_OnPostInitializeCount = 0;
int m_OnPreFinalizeCount = 0;
int m_OnPostFinalizeCount = 0;
void OnPreInitialize() override { m_OnPreInitializeCount++; }
void OnPostInitialize() override { m_OnPostInitializeCount++; }
void OnPreFinalize() override { m_OnPreFinalizeCount++; }
void OnPostFinalize() override { m_OnPostFinalizeCount++; }
};
class EditorPythonBindingsTest
: public PythonTestingFixture
{
public:
PythonTraceMessageSink m_testSink;
EditorPythonBindingsNotificationBusSink m_notificationSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
m_app.RegisterComponentDescriptor(EditorPythonBindings::PythonSystemComponent::CreateDescriptor());
}
void TearDown() override
{
// clearing up memory
m_notificationSink = EditorPythonBindingsNotificationBusSink();
m_testSink = PythonTraceMessageSink();
// shutdown time!
PythonTestingFixture::TearDown();
}
};
TEST_F(EditorPythonBindingsTest, FireUpPythonVM)
{
enum class LogTypes
{
Skip = 0,
General,
RedirectOutputInstalled
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "RedirectOutput installed"))
{
return static_cast<int>(LogTypes::RedirectOutputInstalled);
}
return static_cast<int>(LogTypes::General);
}
return static_cast<int>(LogTypes::Skip);
};
AZ::Entity e;
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.Init();
e.Activate();
SimulateEditorBecomingInitialized();
e.Deactivate();
EXPECT_GT(m_testSink.m_evaluationMap[(int)LogTypes::General], 0);
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::RedirectOutputInstalled], 1);
EXPECT_EQ(m_notificationSink.m_OnPreInitializeCount, 1);
EXPECT_EQ(m_notificationSink.m_OnPostFinalizeCount, 1);
EXPECT_EQ(m_notificationSink.m_OnPreFinalizeCount, 1);
EXPECT_EQ(m_notificationSink.m_OnPostFinalizeCount, 1);
}
TEST_F(EditorPythonBindingsTest, RunScriptTextBuffer)
{
enum class LogTypes
{
Skip = 0,
ScriptWorked
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "EditorPythonBindingsTest_RunScriptTextBuffer"))
{
return static_cast<int>(LogTypes::ScriptWorked);
}
}
return static_cast<int>(LogTypes::Skip);
};
AZ::Entity e;
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.Init();
e.Activate();
SimulateEditorBecomingInitialized();
const char* script =
R"(
import sys
print ('EditorPythonBindingsTest_RunScriptTextBuffer')
)";
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString, script, false);
e.Deactivate();
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::ScriptWorked], 1);
}
TEST_F(EditorPythonBindingsTest, RunScriptTextBufferAndPrint)
{
AZ::Entity e;
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.Init();
e.Activate();
SimulateEditorBecomingInitialized();
AZStd::string capturedOutput;
m_testSink.m_evaluateMessage = [&capturedOutput](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
capturedOutput.append(message);
}
return 0;
};
// Expressions should log their result
// Any other statement shouldn't log anything
capturedOutput.clear();
const char* script = "5+5";
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString, script, true);
EXPECT_EQ(capturedOutput, "10\n");
capturedOutput.clear();
script =
R"(
import sys
sys.version
)";
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString, script, true);
EXPECT_EQ(capturedOutput, "");
capturedOutput.clear();
script = "variable = 'test'";
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString, script, true);
EXPECT_EQ(capturedOutput, "");
capturedOutput.clear();
script = "variable";
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString, script, true);
EXPECT_EQ(capturedOutput, "test\n");
}
TEST_F(EditorPythonBindingsTest, RunScriptFile)
{
enum class LogTypes
{
Skip = 0,
RanFromFile
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
AZStd::string_view m(message);
if (AzFramework::StringFunc::Equal(message, "EditorPythonBindingsTest_RunScriptFile"))
{
return static_cast<int>(LogTypes::RanFromFile);
}
}
return static_cast<int>(LogTypes::Skip);
};
AZStd::string filename;
AzFramework::StringFunc::Path::ConstructFull(m_engineRoot, "Gems/EditorPythonBindings/Code/Tests", "EditorPythonBindingsTest", "py", filename);
AZ::Entity e;
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.Init();
e.Activate();
SimulateEditorBecomingInitialized();
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilename, filename.c_str());
e.Deactivate();
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::RanFromFile], 1);
}
TEST_F(EditorPythonBindingsTest, RunScriptFileWithArgs)
{
enum class LogTypes
{
Skip = 0,
RanFromFile,
NumArgsCorrect,
ScriptNameCorrect,
Arg1Correct,
Arg2Correct,
Arg3Correct
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
AZStd::string_view m(message);
if (AzFramework::StringFunc::Equal(message, "EditorPythonBindingsTestWithArgs_RunScriptFile"))
{
return static_cast<int>(LogTypes::RanFromFile);
}
else if (AzFramework::StringFunc::Equal(message, "num args: 4"))
{
return static_cast<int>(LogTypes::NumArgsCorrect);
}
else if (AzFramework::StringFunc::Equal(message, "script name: EditorPythonBindingsTestWithArgs.py"))
{
return static_cast<int>(LogTypes::ScriptNameCorrect);
}
else if (AzFramework::StringFunc::Equal(message, "arg 1: arg1"))
{
return static_cast<int>(LogTypes::Arg1Correct);
}
else if (AzFramework::StringFunc::Equal(message, "arg 2: 2"))
{
return static_cast<int>(LogTypes::Arg2Correct);
}
else if (AzFramework::StringFunc::Equal(message, "arg 3: arg3"))
{
return static_cast<int>(LogTypes::Arg3Correct);
}
}
return static_cast<int>(LogTypes::Skip);
};
AZStd::string filename;
AzFramework::StringFunc::Path::ConstructFull(m_engineRoot, "Gems/EditorPythonBindings/Code/Tests", "EditorPythonBindingsTestWithArgs", "py", filename);
AZ::Entity e;
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.Init();
e.Activate();
SimulateEditorBecomingInitialized();
AZStd::vector<AZStd::string_view> args;
args.push_back("arg1");
args.push_back("2");
args.push_back("arg3");
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs, filename.c_str(), args);
e.Deactivate();
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::RanFromFile], 1);
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::NumArgsCorrect], 1);
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::ScriptNameCorrect], 1);
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::Arg1Correct], 1);
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::Arg2Correct], 1);
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::Arg3Correct], 1);
}
//
// Tests that makes sure that basic Python libraries can be loaded
//
class EditorPythonBindingsLibraryTest
: public PythonTestingFixture
{
public:
PythonTraceMessageSink m_testSink;
EditorPythonBindingsNotificationBusSink m_notificationSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
m_app.RegisterComponentDescriptor(EditorPythonBindings::PythonSystemComponent::CreateDescriptor());
}
void TearDown() override
{
// clearing up memory
m_notificationSink = EditorPythonBindingsNotificationBusSink();
m_testSink = PythonTraceMessageSink();
// shutdown time!
PythonTestingFixture::TearDown();
}
void DoLibraryTest(const char* libName)
{
bool executedLine = false;
m_testSink.m_evaluateMessage = [&executedLine](const char* window, const char* message)
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "python_vm_loaded_lib"))
{
executedLine = true;
}
}
return false;
};
AZ::Entity e;
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.Init();
e.Activate();
try
{
SimulateEditorBecomingInitialized();
const bool printResult = false;
AZStd::string script(AZStd::string::format("import %s\nprint ('python_vm_loaded_lib')", libName));
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString,
script.c_str(),
printResult);
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
e.Deactivate();
EXPECT_TRUE(executedLine);
}
};
// This test makes sure that some of the expected built-in libraries
// Are present in the version of python we are using (the ones most problematic for building)
TEST_F(EditorPythonBindingsTest, VerifyExpectedLibrariesPresent)
{
enum class LogTypes
{
Skip = 0,
ScriptWorked
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "EditorPythonBindingsTest_VerifyExpectedLibrariesPresent"))
{
return static_cast<int>(LogTypes::ScriptWorked);
}
}
return static_cast<int>(LogTypes::Skip);
};
AZ::Entity e;
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.Init();
e.Activate();
SimulateEditorBecomingInitialized();
const char* script =
R"(
import sys
import sqlite3
import ssl
print ('EditorPythonBindingsTest_VerifyExpectedLibrariesPresent')
)";
AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString, script, true);
e.Deactivate();
EXPECT_EQ(m_testSink.m_evaluationMap[(int)LogTypes::ScriptWorked], 1);
}
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_sys_Works)
{
DoLibraryTest("sys");
}
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_ctypes_Works)
{
DoLibraryTest("ctypes");
}
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_bz2_Works)
{
DoLibraryTest("bz2");
}
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_lzma_Works)
{
DoLibraryTest("lzma");
}
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_socket_Works)
{
DoLibraryTest("socket");
}
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_sqlite3_Works)
{
DoLibraryTest("sqlite3");
}
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_ssl_Works)
{
DoLibraryTest("ssl");
}
// This library lives in Editor/Scripts. We're testing that our sys.path extension code in ExtendSysPath works as expected
TEST_F(EditorPythonBindingsLibraryTest, PythonVMLoads_SysPathExtendedToGemScripts_EditorPythonBindingsValidaitonFound)
{
DoLibraryTest("editor_script_validation");
}
}
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
@@ -0,0 +1,17 @@
"""
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.
"""
#
# testing Python code
#
import sys
print ('EditorPythonBindingsTest_RunScriptFile')
@@ -0,0 +1,29 @@
"""
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.
"""
#
# testing Python code
#
import sys
import os
import os.path
print('EditorPythonBindingsTestWithArgs_RunScriptFile')
print('num args: {}'.format(len(sys.argv)))
# Intentionally print script name separately from the other args.
# The path that it prints will be non-deterministic based on where the code
# has been synced to, so we strip it off, enabling us to just validate the script name
# and the other args made it through successfully.
print('script name: {}'.format(os.path.basename(sys.argv[0])))
for arg in range(1, len(sys.argv)):
print('arg {}: {}'.format(arg, sys.argv[arg]))
@@ -0,0 +1,792 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Memory/PoolAllocator.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/Asset/SimpleAsset.h>
namespace UnitTest
{
class FooMockSimpleAsset
{
public:
AZ_TYPE_INFO(FooMockSimpleAsset, "{0298F78A-77EF-47CE-9912-B0BC80060016}");
static const char* GetFileFilter()
{
return "foo";
}
};
}
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// test class/struts
struct MockBinding final
{
AZ_TYPE_INFO(MockBinding, "{0B22887C-6377-4573-8FE5-418947640D3F}");
AZ::Data::AssetId m_mockAssetId;
MockBinding() = default;
MockBinding(const AZ::Data::AssetId& value)
{
m_mockAssetId = value;
}
const AZ::Data::AssetId& GetAssetId() const
{
return m_mockAssetId;
}
static void Reflect(AZ::ReflectContext* reflection)
{
auto&& behaviorContext = azrtti_cast<AZ::BehaviorContext*>(reflection);
if (behaviorContext)
{
behaviorContext->Class<MockBinding>("MockBinding")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "mock")
->Constructor()
->Constructor<const AZ::Data::AssetId&>()
->Method("GetAssetId", &MockBinding::GetAssetId)
;
}
}
};
class MockAsset
: public AzFramework::SimpleAssetReferenceBase
{
public:
AZ_RTTI(MockAsset, "{C783597C-568F-4B94-911C-506CBD161E10}", AzFramework::SimpleAssetReferenceBase);
MockAsset()
{
SetAssetPath("a/fake/path.foo");
}
static void Reflect(AZ::ReflectContext* reflection)
{
auto&& serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<MockAsset, AzFramework::SimpleAssetReferenceBase>();
AzFramework::SimpleAssetReference<FooMockSimpleAsset>::Register(*serializeContext);
}
auto&& behaviorContext = azrtti_cast<AZ::BehaviorContext*>(reflection);
if (behaviorContext)
{
behaviorContext->Class<MockAsset>("MockAsset")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
;
}
}
AZ::Data::AssetType GetAssetType() const override
{
// Use an arbitrary ID for the asset type.
return AZ::Data::AssetType("{7FD86523-3903-4037-BCD1-542027BFC553}");
}
virtual const char* GetFileFilter() const
{
return nullptr;
}
};
struct MockAssetData
: public AZ::Data::AssetData
{
void SetUseCount(AZ::s32 value)
{
m_useCount = value;
}
void SetAssetId(AZ::Data::AssetId value)
{
m_assetId = value;
}
};
struct MyTestAssetData
: public AZ::Data::AssetData
{
AZ_RTTI(MyTestAssetData, "{B78C6629-95F4-4211-AE7F-4DE58C0D3C33}", AZ::Data::AssetData);
AZ::u64 m_number = 0;
void SetUseCount(AZ::s32 value)
{
m_useCount = value;
}
};
class ClassWithAssets
{
public:
AZ_RTTI(ClassWithAssets, "{06E4DC78-DD42-44A8-83A1-5B333B557DE9}");
virtual ~ClassWithAssets() = default;
static void Reflect(AZ::ReflectContext* reflection)
{
auto&& serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<ClassWithAssets>()
->Field("assetId", &ClassWithAssets::m_assetId)
->Field("assetData", &ClassWithAssets::m_assetData)
->Field("mockAsset", &ClassWithAssets::m_mockAsset)
->Field("simpleAssetReference", &ClassWithAssets::m_simpleAssetReference)
;
}
auto&& behaviorContext = azrtti_cast<AZ::BehaviorContext*>(reflection);
if (behaviorContext)
{
behaviorContext->Class<ClassWithAssets>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Property("assetId", BehaviorValueProperty(&ClassWithAssets::m_assetId))
->Property("assetData", BehaviorValueProperty(&ClassWithAssets::m_assetData))
->Property("mockAsset", BehaviorValueProperty(&ClassWithAssets::m_mockAsset))
->Property("simpleAssetReference", BehaviorValueProperty(&ClassWithAssets::m_simpleAssetReference))
->Method("createFooMockSimpleAsset",&ClassWithAssets::CreateFooMockSimpleAsset)
->Method("printFooMockSimpleAsset", &ClassWithAssets::PrintFooMockSimpleAsset)
;
}
}
AzFramework::SimpleAssetReference<FooMockSimpleAsset> CreateFooMockSimpleAsset(AZStd::string_view assetPath)
{
AZ_TracePrintf("python", "SimpleAssetReference creating asset for path %.*s", static_cast<int>(assetPath.size()), assetPath.data());
AzFramework::SimpleAssetReference<FooMockSimpleAsset> fooMockSimpleAsset;
fooMockSimpleAsset.SetAssetPath(assetPath.data());
return fooMockSimpleAsset;
}
void PrintFooMockSimpleAsset([[maybe_unused]] AzFramework::SimpleAssetReference<FooMockSimpleAsset>& fooMockSimpleAsset)
{
AZ_TracePrintf("python", "SimpleAssetReference asset path is (%s) \n", fooMockSimpleAsset.GetAssetPath().c_str());
}
AZ::Data::AssetId m_assetId = AZ::Data::AssetId(AZ::Uuid::Create(), 512);
MockAsset m_mockAsset;
AZ::Data::Asset<AZ::Data::AssetData> m_assetData;
AzFramework::SimpleAssetReference<FooMockSimpleAsset> m_simpleAssetReference;
};
namespace Internal
{
MockAsset s_mockAsset;
MockAssetData s_mockAssetData;
AZ::Data::Asset<AZ::Data::AssetData> s_asset;
AZ::Data::AssetId s_assetId;
}
struct PythonReflectionAssetTypes
{
AZ_TYPE_INFO(PythonReflectionAssetTypes, "{04C929EE-67FA-4BDB-BC56-3680D61C9DEC}");
AZ::Data::AssetId m_assetId;
AZ::Data::Asset<AZ::Data::AssetData> m_assetData;
AZ::Data::Asset<MyTestAssetData> m_myTestAssetDataAsset;
MyTestAssetData m_testAssetData;
ClassWithAssets m_mockDescriptor;
AZStd::unique_ptr<MyTestAssetData> m_myTestAssetData;
PythonReflectionAssetTypes()
{
m_testAssetData.m_number = 2;
m_myTestAssetDataAsset = AZ::Data::Asset<MyTestAssetData>(
static_cast<AZ::Data::AssetData*>(&m_testAssetData),
AZ::Data::AssetLoadBehavior::NoLoad);
m_assetId.m_guid = AZ::Uuid::CreateRandom();
m_assetId.m_subId = 1234;
}
~PythonReflectionAssetTypes()
{
// manually releasing the m_testAssetData
m_testAssetData.SetUseCount(2);
m_testAssetData.AcquireWeak();
m_myTestAssetDataAsset = {};
}
static void PrintAssetData([[maybe_unused]] const AZ::Data::Asset<AZ::Data::AssetData>& assetData)
{
AZ_TracePrintf("python", "Asset Data ID = %s\n",
assetData.GetId().ToString<AZStd::string>().c_str());
}
static void PrintSimpleAssetReference([[maybe_unused]] const AzFramework::SimpleAssetReferenceBase& simpleAssetRef)
{
AZ_TracePrintf("python", "SimpleAssetReference of asset type = %s\n",
simpleAssetRef.GetAssetType().ToString<AZStd::string>().c_str());
}
static AZ::Data::Asset<AZ::Data::AssetData> GenerateAsset()
{
Internal::s_assetId = AZ::Data::AssetId(AZ::Uuid::Create(), 42);
Internal::s_mockAssetData.SetAssetId(Internal::s_assetId);
Internal::s_asset = AZ::Data::Asset<AZ::Data::AssetData>(
static_cast<AZ::Data::AssetData*>(&Internal::s_mockAssetData),
AZ::Data::AssetLoadBehavior::NoLoad);
return Internal::s_asset;
}
static AZ::Data::AssetId CreateAssetId(AZStd::string_view assetUuid)
{
return AZ::Data::AssetId::CreateString(assetUuid);
}
static bool CompareAssetIds(const AZ::Data::AssetId& lhs, const AZ::Data::AssetId& rhs)
{
return lhs == rhs;
}
static bool CompareAssetData(const AZ::Data::Asset<AZ::Data::AssetData>& lhs, const AZ::Data::Asset<AZ::Data::AssetData>& rhs)
{
const bool sameId = lhs.GetId() == rhs.GetId();
const bool sameType = lhs.GetType() == rhs.GetType();
const bool sameHint = lhs.GetHint() == rhs.GetHint();
return sameId && sameType && sameHint;
}
static bool CompareMockAssets(const MockAsset& lhs, const MockAsset& rhs)
{
return lhs.GetAssetPath() == rhs.GetAssetPath();
}
AZ::Data::Asset<MyTestAssetData> CreateMyTestAssetData()
{
m_myTestAssetData = AZStd::make_unique<MyTestAssetData>();
m_myTestAssetData->m_number = 42;
return AZ::Data::Asset<MyTestAssetData>(
static_cast<AZ::Data::AssetData*>(m_myTestAssetData.get()),
AZ::Data::AssetLoadBehavior::NoLoad);
}
void ReadMyTestAssetData(const AZ::Data::Asset<MyTestAssetData>& data)
{
if (data.Get())
{
AZ_TracePrintf("python", "AssetData: MyTestAssetData read in data \n");
}
}
AZ::Data::Asset<AZ::Data::AssetData> CreateAssetHandle(const AZ::Data::AssetId& assetId)
{
return AZ::Data::Asset<AZ::Data::AssetData>(assetId, m_mockDescriptor.m_mockAsset.GetAssetType(), "test");
}
void Reflect(AZ::ReflectContext* context)
{
ClassWithAssets::Reflect(context);
MockAsset::Reflect(context);
auto&& serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<PythonReflectionAssetTypes>()
->Field("assetId", &PythonReflectionAssetTypes::m_assetId)
->Field("assetData", &PythonReflectionAssetTypes::m_assetData)
->Field("myTestAssetData", &PythonReflectionAssetTypes::m_myTestAssetData)
->Field("mockDescriptor", &PythonReflectionAssetTypes::m_mockDescriptor)
;
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<PythonReflectionAssetTypes>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
// class methods
->Method("compare_asset_ids", &PythonReflectionAssetTypes::CompareAssetIds)
->Method("compare_asset_data", &PythonReflectionAssetTypes::CompareAssetData)
->Method("compare_mock_assets", &PythonReflectionAssetTypes::CompareMockAssets)
->Method("create_asset_id", &PythonReflectionAssetTypes::CreateAssetId)
->Method("print_asset_data", &PythonReflectionAssetTypes::PrintAssetData)
->Method("print_simple_asset_reference", &PythonReflectionAssetTypes::PrintSimpleAssetReference)
->Method("generate_asset", &PythonReflectionAssetTypes::GenerateAsset)
// instance methods
->Method("create_asset_handle", &PythonReflectionAssetTypes::CreateAssetHandle)
->Method("create_my_test_asset_data", &PythonReflectionAssetTypes::CreateMyTestAssetData)
->Method("read_my_test_asset_data", &PythonReflectionAssetTypes::ReadMyTestAssetData)
// instance properties
->Property("assetId", BehaviorValueProperty(&PythonReflectionAssetTypes::m_assetId))
->Property("assetData", BehaviorValueProperty(&PythonReflectionAssetTypes::m_assetData))
->Property("mockDescriptor", BehaviorValueProperty(&PythonReflectionAssetTypes::m_mockDescriptor))
->Property("myTestAssetDataAsset", BehaviorValueProperty(&PythonReflectionAssetTypes::m_myTestAssetDataAsset))
;
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonAssetTypesTests
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
};
TEST_F(PythonAssetTypesTests, AssetOnDemand)
{
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetSerializeContext());
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetBehaviorContext());
PythonReflectionAssetTypes pythonReflectionAssetTypes;
pythonReflectionAssetTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionAssetTypes.Reflect(m_app.GetBehaviorContext());
// make sure expected class names exist in the Behavior Context
auto&& behaviorClasses = m_app.GetBehaviorContext()->m_classes;
EXPECT_TRUE(behaviorClasses.find("Asset<AssetData>") != behaviorClasses.end());
EXPECT_TRUE(behaviorClasses.find("Asset<MyTestAssetData>") != behaviorClasses.end());
EXPECT_TRUE(behaviorClasses.find("SimpleAssetReferenceBase") != behaviorClasses.end());
EXPECT_TRUE(behaviorClasses.find("SimpleAssetReference<AssetType><FooMockSimpleAsset >") != behaviorClasses.end());
}
TEST_F(PythonAssetTypesTests, AssetIdValues)
{
enum class LogTypes
{
Skip = 0,
AssetId,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "AssetId"))
{
return static_cast<int>(LogTypes::AssetId);
}
}
return static_cast<int>(LogTypes::Skip);
};
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetSerializeContext());
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetBehaviorContext());
PythonReflectionAssetTypes pythonReflectionAssetTypes;
pythonReflectionAssetTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionAssetTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr
import azlmbr.asset
import azlmbr.test
compare_asset_ids = azlmbr.test.PythonReflectionAssetTypes_compare_asset_ids
create_asset_id = azlmbr.test.PythonReflectionAssetTypes_create_asset_id
assetIdOne = create_asset_id('{1F5252DC-467A-4E2E-8168-EE1551C92F74}:0')
assetIdTwo = create_asset_id('{1F5252DC-467A-4E2E-8168-EE1551C92F74}:1')
assetIdThree = azlmbr.asset.AssetId_CreateString('{BA5EBA11-DEAD-AB1E-FACE-01234567890A}:0')
if(assetIdTwo.to_string() == '{1F5252DC-467A-4E2E-8168-EE1551C92F74}:1'):
print ('AssetId: compare_asset_ids assetIdTwo')
if(assetIdThree.to_string() == '{BA5EBA11-DEAD-AB1E-FACE-01234567890A}:0'):
print ('AssetId: compare_asset_ids assetIdThree')
if (compare_asset_ids(assetIdOne, assetIdOne)):
print ('AssetId: compare_asset_ids AFF')
if (compare_asset_ids(assetIdOne, assetIdTwo) is False):
print ('AssetId: compare_asset_ids NEG')
tester = azlmbr.test.PythonReflectionAssetTypes()
tester.assetId = assetIdOne
if (compare_asset_ids(tester.assetId, assetIdOne)):
print ('AssetId: compare_asset_ids tester')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on to run script buffer with %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(5, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::AssetId)]);
}
TEST_F(PythonAssetTypesTests, AssetDataTypes)
{
enum class LogTypes
{
Skip = 0,
AssetData
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "AssetData"))
{
return static_cast<int>(LogTypes::AssetData);
}
}
return static_cast<int>(LogTypes::Skip);
};
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetSerializeContext());
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetBehaviorContext());
PythonReflectionAssetTypes pythonReflectionAssetTypes;
pythonReflectionAssetTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionAssetTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr
import azlmbr.asset
import azlmbr.test
compare_asset_data = azlmbr.test.PythonReflectionAssetTypes_compare_asset_data
print_asset_data = azlmbr.test.PythonReflectionAssetTypes_print_asset_data
generate_asset = azlmbr.test.PythonReflectionAssetTypes_generate_asset
create_asset_id = azlmbr.test.PythonReflectionAssetTypes_create_asset_id
tester = azlmbr.test.PythonReflectionAssetTypes()
# AZ::Data::Asset<> testing
assetIdOne = create_asset_id('{1F5252DC-467A-4E2E-8168-EE1551C92F74}:0')
dataAsset = tester.create_asset_handle(assetIdOne)
print_asset_data(tester.assetData)
print_asset_data(dataAsset)
tester.assetData = dataAsset
mockAsset0 = generate_asset()
mockAsset1 = generate_asset()
if (compare_asset_data(mockAsset1, mockAsset1)):
print ('AssetData: compare_asset_data tester')
# Compare testing
if (compare_asset_data(tester.assetData, dataAsset)):
print ('AssetData: compare_asset_data tester.assetData')
# handling generic Asset<MyTestAssetData>
tester.read_my_test_asset_data(tester.myTestAssetDataAsset)
testAssetData = tester.create_my_test_asset_data()
tester.read_my_test_asset_data(testAssetData)
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on to run script buffer with %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(4, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::AssetData)]);
}
TEST_F(PythonAssetTypesTests, MockAssetTypes)
{
enum class LogTypes
{
Skip = 0,
MockAsset
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "MockAsset"))
{
return static_cast<int>(LogTypes::MockAsset);
}
}
return static_cast<int>(LogTypes::Skip);
};
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetSerializeContext());
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetBehaviorContext());
PythonReflectionAssetTypes pythonReflectionAssetTypes;
pythonReflectionAssetTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionAssetTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr
import azlmbr.asset
import azlmbr.test
compare_mock_assets = azlmbr.test.PythonReflectionAssetTypes_compare_mock_assets
tester0 = azlmbr.test.PythonReflectionAssetTypes()
tester1 = azlmbr.test.PythonReflectionAssetTypes()
if (compare_mock_assets(tester0.mockDescriptor.mockAsset, tester1.mockDescriptor.mockAsset)):
print('MockAsset: mock assets match')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on to run script buffer with %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::MockAsset)]);
}
TEST_F(PythonAssetTypesTests, SimpleAssetReferenceTypes)
{
enum class LogTypes
{
Skip = 0,
SimpleAssetReference
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "SimpleAssetReference"))
{
return static_cast<int>(LogTypes::SimpleAssetReference);
}
}
return static_cast<int>(LogTypes::Skip);
};
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetSerializeContext());
AzFramework::SimpleAssetReferenceBase::Reflect(m_app.GetBehaviorContext());
PythonReflectionAssetTypes pythonReflectionAssetTypes;
pythonReflectionAssetTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionAssetTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr
import azlmbr.asset
import azlmbr.test
create_asset_id = azlmbr.test.PythonReflectionAssetTypes_create_asset_id
print_simple_asset_reference = azlmbr.test.PythonReflectionAssetTypes_print_simple_asset_reference
assetIdOne = create_asset_id('{1F5252DC-467A-4E2E-8168-EE1551C92F74}:0')
assetIdTwo = create_asset_id('{1F5252DC-467A-4E2E-8168-EE1551C92F74}:1')
tester = azlmbr.test.PythonReflectionAssetTypes()
# SimpleAssetReferenceBase basic testing
tester.testAssetId = assetIdOne
accessAssetPath = tester.mockDescriptor.mockAsset.assetPath
print_simple_asset_reference(tester.mockDescriptor.simpleAssetReference)
# SimpleAssetReference<> testing
fakeAssetPath = 'a/fake/asset_file.foo'
mocker = tester.mockDescriptor
simpleAssetReference = mocker.simpleAssetReference
mocker.printFooMockSimpleAsset(simpleAssetReference)
outAssetRef = mocker.createFooMockSimpleAsset(fakeAssetPath)
if(simpleAssetReference.assetPath == fakeAssetPath):
print('SimpleAssetReference: path access matches {}'.format(fakeAssetPath))
# using FooMockSimpleAsset inside a SimpleAssetReference<> template
newFakeAssetPath = 'another/fake/asset_file.foo'
simpleRef = azlmbr.object.construct('SimpleAssetReference<AssetType><FooMockSimpleAsset >')
simpleRef.set_asset_path(newFakeAssetPath)
if(simpleRef.assetPath == newFakeAssetPath):
print('SimpleAssetReference: simpleRef {}'.format(newFakeAssetPath))
if(simpleRef.assetPath is not simpleAssetReference.assetPath):
print('SimpleAssetReference: simpleRef does not match simpleAssetReference')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on to run script buffer with %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(5, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::SimpleAssetReference)]);
}
TEST_F(PythonAssetTypesTests, MockBindingAssetIds)
{
enum class LogTypes
{
Skip = 0,
MockBinding
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "MockBinding"))
{
return static_cast<int>(LogTypes::MockBinding);
}
}
return static_cast<int>(LogTypes::Skip);
};
MockBinding::Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr
import azlmbr.mock
import azlmbr.asset
assetIdStringValue = '{13DACEEC-69B9-4CE4-9F43-50675D73FD8C}:0'
testId = azlmbr.asset.AssetId_CreateString(assetIdStringValue)
if (testId is not None):
print('MockBinding: created mock asset ID')
if (testId.to_string() == assetIdStringValue):
print('MockBinding: created mock asset ID')
testMock = azlmbr.mock.MockBinding(testId)
if (testMock is not None):
print('MockBinding: mock binding created with asset ID')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on to run script buffer with %s", e.what());
}
e.Deactivate();
EXPECT_EQ(3, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::MockBinding)]);
}
TEST_F(PythonAssetTypesTests, AssetIdsEqualOperators)
{
enum class LogTypes
{
Skip = 0,
EqualOperators
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "EqualOperators"))
{
return static_cast<int>(LogTypes::EqualOperators);
}
}
return static_cast<int>(LogTypes::Skip);
};
MockBinding::Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr
import azlmbr.asset
assetIdStringValue0 = '{13DACEEC-69B9-4CE4-9F43-50675D73FD8C}:0'
assetIdStringValue1 = '{13DACEEC-69B9-4CE4-9F43-50675D73FD8C}:1'
testId0 = azlmbr.asset.AssetId_CreateString(assetIdStringValue0)
if (testId0 is not None):
print('EqualOperators: created testId0')
testId1 = azlmbr.asset.AssetId_CreateString(assetIdStringValue1)
if (testId1 is not None):
print('EqualOperators: created testId1')
if (testId1 == azlmbr.asset.AssetId_CreateString(assetIdStringValue1)):
print('EqualOperators: testId1 == testId1')
if (testId0 != testId1):
print('EqualOperators: testId0 != testId1')
if ((testId0 == assetIdStringValue0) is not True):
print('EqualOperators: testId0 != assetIdStringValue0')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on to run script buffer with %s", e.what());
}
e.Deactivate();
EXPECT_EQ(5, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::EqualOperators)]);
}
}
@@ -0,0 +1,259 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
namespace UnitTest
{
struct PythonReflectUnorderedSet
{
AZ_TYPE_INFO(PythonReflectUnorderedSet, "{A596466F-2F29-4479-A721-0E50FA704962}");
AZStd::unordered_set<AZ::u8> m_u8Set {1,2};
AZStd::unordered_set<AZ::u16> m_u16Set {4,8};
AZStd::unordered_set<AZ::u32> m_u32Set {16,32};
AZStd::unordered_set<AZ::u64> m_u64Set {64,128};
AZStd::unordered_set<AZ::s8> m_s8Set {-1,-2};
AZStd::unordered_set<AZ::s16> m_s16Set {-4,-8};
AZStd::unordered_set<AZ::s32> m_s32Set {-16,-32};
AZStd::unordered_set<AZ::s64> m_s64Set {-64,-128};
AZStd::unordered_set<double> m_floatSet {1.0f, 2.0f};
AZStd::unordered_set<float> m_doubleSet {0.1, 0.2};
AZStd::unordered_set<AZStd::string> m_stringSet {"one", "two"};
void Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->RegisterGenericType<decltype(m_u8Set)>();
serializeContext->RegisterGenericType<decltype(m_u16Set)>();
serializeContext->RegisterGenericType<decltype(m_u32Set)>();
serializeContext->RegisterGenericType<decltype(m_u64Set)>();
serializeContext->RegisterGenericType<decltype(m_s8Set)>();
serializeContext->RegisterGenericType<decltype(m_s16Set)>();
serializeContext->RegisterGenericType<decltype(m_s32Set)>();
serializeContext->RegisterGenericType<decltype(m_s64Set)>();
serializeContext->RegisterGenericType<decltype(m_floatSet)>();
serializeContext->RegisterGenericType<decltype(m_doubleSet)>();
serializeContext->RegisterGenericType<decltype(m_stringSet)>();
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<PythonReflectUnorderedSet>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test.set")
->Property("u8Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_u8Set) )
->Property("u16Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_u16Set))
->Property("u32Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_u32Set))
->Property("u64Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_u64Set))
->Property("s8Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_s8Set))
->Property("s16Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_s16Set))
->Property("s32Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_s32Set))
->Property("s64Set", BehaviorValueProperty(&PythonReflectUnorderedSet::m_s64Set))
->Property("floatSet", BehaviorValueProperty(&PythonReflectUnorderedSet::m_floatSet))
->Property("doubleSet", BehaviorValueProperty(&PythonReflectUnorderedSet::m_doubleSet))
->Property("stringSet", BehaviorValueProperty(&PythonReflectUnorderedSet::m_stringSet))
;
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonAssociativeTest
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
};
TEST_F(PythonAssociativeTest, SimpleUnorderedSet_Assignment)
{
enum class LogTypes
{
Skip = 0,
Update,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AZ::StringFunc::Equal(window, "python"))
{
if (AZ::StringFunc::StartsWith(message, "Update"))
{
return aznumeric_cast<int>(LogTypes::Update);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
PythonReflectUnorderedSet pythonReflectUnorderedSet;
pythonReflectUnorderedSet.Reflect(m_app.GetSerializeContext());
pythonReflectUnorderedSet.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.set
tester = azlmbr.test.set.PythonReflectUnorderedSet()
def updateNumberDataSet(memberSet, dataSet):
memberSet = dataSet
for value in memberSet:
if (value in dataSet):
print ('Update_worked_{}'.format(memberSet))
updateNumberDataSet(tester.u8Set, {2, 1})
updateNumberDataSet(tester.u16Set, {8, 4})
updateNumberDataSet(tester.u32Set, {32, 16})
updateNumberDataSet(tester.u64Set, {128, 64})
updateNumberDataSet(tester.s8Set, {-2, -1})
updateNumberDataSet(tester.s16Set, {-8, -4})
updateNumberDataSet(tester.s32Set, {-32, -16})
updateNumberDataSet(tester.s64Set, {-128, -64})
from azlmbr.math import Math_IsClose
def updateFloatDataSet(memberFloatSet, dataSet):
memberFloatSet = dataSet
for dataItem in dataSet:
for memberItem in memberFloatSet:
if (Math_IsClose(dataItem, memberItem)):
print ('Update_float_worked_{}'.format(memberFloatSet))
updateFloatDataSet(tester.floatSet, {4.0, 8.0})
updateFloatDataSet(tester.doubleSet, {0.4, 0.8})
stringDataSet = {'three','four'}
tester.stringSet = stringDataSet
for dataItem in stringDataSet:
for memberItem in tester.stringSet:
if (dataItem == memberItem):
print ('Update_string_worked')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed with Python exception of %s", e.what());
}
e.Deactivate();
EXPECT_EQ(22, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::Update)]);
}
TEST_F(PythonAssociativeTest, SimpleUnorderedSet_Creation)
{
enum class LogTypes
{
Skip = 0,
Create,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AZ::StringFunc::Equal(window, "python"))
{
if (AZ::StringFunc::StartsWith(message, "Create"))
{
return aznumeric_cast<int>(LogTypes::Create);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
PythonReflectUnorderedSet pythonReflectUnorderedSet;
pythonReflectUnorderedSet.Reflect(m_app.GetSerializeContext());
pythonReflectUnorderedSet.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.set
tester = azlmbr.test.set.PythonReflectUnorderedSet()
if (tester.u8Set == {1, 2}):
print ('Create_Works_u8Set')
if (tester.u16Set == {4, 8}):
print ('Create_Works_u16Set')
if (tester.u32Set == {16, 32}):
print ('Create_Works_u32Set')
if (tester.u64Set == {64, 128}):
print ('Create_Works_u64Set')
if (tester.s8Set == {-1, -2}):
print ('Create_Works_s8Set')
if (tester.s16Set == {-4, -8}):
print ('Create_Works_s16Set')
if (tester.s32Set == {-16, -32}):
print ('Create_Works_s32Set')
if (tester.s64Set == {-64, -128}):
print ('Create_Works_s64Set')
from azlmbr.math import Math_IsClose
for value in tester.floatSet:
if (Math_IsClose(value, 1.0) or Math_IsClose(value, 2.0)):
print ('Create_Works_floatSet')
for value in tester.doubleSet:
if (Math_IsClose(value, 0.1) or Math_IsClose(value, 0.2)):
print ('Create_Works_doubleSet')
for value in tester.stringSet:
if ((value == 'one') or (value == 'two')):
print ('Create_Works_stringSet')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed with Python exception of %s", e.what());
}
e.Deactivate();
EXPECT_EQ(14, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::Create)]);
}
}
@@ -0,0 +1,426 @@
/*
* 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/PythonSystemComponent.h>
#include <Source/PythonCommon.h>
#include <Source/PythonTypeCasters.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include <EditorPythonBindings/EditorPythonBindingsBus.h>
#include <AzFramework/StringFunc/StringFunc.h>
// an example converter for an "AZ type"
namespace TestTypes
{
void RegisterAzEntityId(pybind11::module m)
{
auto classEntityId = pybind11::class_<AZ::EntityId>(m, AZ::AzTypeInfo<AZ::EntityId>::Name());
classEntityId.def(pybind11::init<AZ::u64>());
classEntityId.def("isValid", &AZ::EntityId::IsValid);
classEntityId.def("setInvalid", &AZ::EntityId::SetInvalid);
classEntityId.def_property_readonly("id", [](const AZ::EntityId& e) { return static_cast<AZ::u64>(e); });
classEntityId.def("__repr__", &AZ::EntityId::ToString);
}
}
// this is called the first time a Python script "import azlmbrtest"
PYBIND11_EMBEDDED_MODULE(azlmbrtest, m)
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindings::EditorPythonBindingsNotificationBus::Events::OnImportModule, m.ptr());
TestTypes::RegisterAzEntityId(m);
}
namespace UnitTest
{
struct MyPythonBindings final
: public EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler
{
int m_onImportModuleCount = 0;
MyPythonBindings()
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusConnect();
}
~MyPythonBindings()
{
EditorPythonBindings::EditorPythonBindingsNotificationBus::Handler::BusDisconnect();
}
static long DoAdd(int lhs, int rhs)
{
return lhs + rhs;
}
static void AZPrintf([[maybe_unused]] const AZStd::string& message)
{
AZ_TracePrintf("python", "%s", message.c_str());
}
void ImportTestSubModule(pybind11::module module)
{
pybind11::module subModule = module.def_submodule("tester", "A submodule for 'test'");
subModule.def("add", &DoAdd);
subModule.def("print", &AZPrintf);
}
void OnImportModule(PyObject* module) override
{
pybind11::module m = pybind11::cast<pybind11::module>(module);
std::string szName = pybind11::cast<std::string>(m.attr("__name__"));
if (szName == "azlmbrtest")
{
m_onImportModuleCount++;
ImportTestSubModule(m);
}
}
};
class PythonBindingLibTest
: public PythonTestingFixture
{
protected:
void SetUp() override
{
PythonTestingFixture::SetUp();
RegisterComponentDescriptors();
}
void TearDown() override
{
PythonTestingFixture::TearDown();
}
};
TEST_F(PythonBindingLibTest, ImportBaseModule)
{
AZ::Entity entity;
entity.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
entity.Init();
entity.Activate();
SimulateEditorBecomingInitialized();
{
MyPythonBindings pythonBindings;
pybind11::module::import("azlmbrtest");
EXPECT_EQ(pythonBindings.m_onImportModuleCount, 1);
}
entity.Deactivate();
}
TEST_F(PythonBindingLibTest, ImportBaseModuleTwice)
{
const char* script =
R"(
import azlmbrtest
import azlmbrtest
)";
AZ::Entity entity;
entity.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
entity.Init();
entity.Activate();
SimulateEditorBecomingInitialized();
// Python keeps track of the module import count so that multiple attempts should result into a single import count
{
MyPythonBindings pythonBindings;
EXPECT_EQ(PyRun_SimpleString(script), 0);
EXPECT_EQ(pythonBindings.m_onImportModuleCount, 1);
}
entity.Deactivate();
}
TEST_F(PythonBindingLibTest, ExecuteSimpleBinding)
{
enum class LogTypes
{
Skip = 0,
TesterAdd,
TesterPrinted
};
PythonTraceMessageSink testSink;
testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
AZStd::string_view w(window);
if (w == "python")
{
AZStd::string_view m(message);
if (m == "tester add equals 42")
{
return (int)LogTypes::TesterAdd;
}
if (m == "tester says yo")
{
return (int)LogTypes::TesterPrinted;
}
}
return (int)LogTypes::Skip;
};
const char* script =
R"(
import azlmbrtest
value = azlmbrtest.tester.add(40, 2)
print ('tester add equals ' + str(value))
value = azlmbrtest.tester.print('tester says yo')
)";
AZ::Entity entity;
entity.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
entity.Init();
entity.Activate();
SimulateEditorBecomingInitialized();
{
MyPythonBindings pythonBindings;
EXPECT_EQ(PyRun_SimpleString(script), 0);
EXPECT_EQ(pythonBindings.m_onImportModuleCount, 1);
EXPECT_EQ(testSink.m_evaluationMap[(int)LogTypes::TesterAdd], 1);
EXPECT_EQ(testSink.m_evaluationMap[(int)LogTypes::TesterPrinted], 1);
}
entity.Deactivate();
}
TEST_F(PythonBindingLibTest, ConvertAZTypes)
{
enum class LogTypes
{
Skip = 0,
TypeConverted,
IdIsValid,
IdHasRepr,
IdNowInvalid
};
PythonTraceMessageSink testSink;
testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
AZStd::string_view m(message);
AZStd::string_view w(window);
if (w == "python")
{
if (m == "entityId equals 10")
{
return (int)LogTypes::TypeConverted;
}
else if (m == "entityId is valid True")
{
return (int)LogTypes::IdIsValid;
}
else if (m == "entityId is repr [10]")
{
return (int)LogTypes::IdHasRepr;
}
else if (m == "entityId invalid is 4294967295")
{
return (int)LogTypes::IdNowInvalid;
}
}
return (int)LogTypes::Skip;
};
const char* script =
R"(
import azlmbrtest
entityId = azlmbrtest.EntityId(10)
print ('entityId equals ' + str(entityId.id))
print ('entityId is valid ' + str(entityId.isValid()))
print ('entityId is repr ' + str(entityId))
entityId.setInvalid()
print ('entityId invalid is ' + str(entityId.id))
)";
AZ::Entity entity;
entity.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
entity.Init();
entity.Activate();
SimulateEditorBecomingInitialized();
EXPECT_EQ(PyRun_SimpleString(script), 0);
EXPECT_EQ(testSink.m_evaluationMap[(int)LogTypes::TypeConverted], 1);
EXPECT_EQ(testSink.m_evaluationMap[(int)LogTypes::IdIsValid], 1);
EXPECT_EQ(testSink.m_evaluationMap[(int)LogTypes::IdHasRepr], 1);
EXPECT_EQ(testSink.m_evaluationMap[(int)LogTypes::IdNowInvalid], 1);
entity.Deactivate();
}
TEST_F(PythonBindingLibTest, ImportProjectModules)
{
enum class LogTypes
{
Skip = 0,
ImportModule,
TestCallHit,
TestTypeDoCall1
};
PythonTraceMessageSink testSink;
testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "ImportModule"))
{
return static_cast<int>(LogTypes::ImportModule);
}
else if (AzFramework::StringFunc::Equal(message, "test_call_hit"))
{
return static_cast<int>(LogTypes::TestCallHit);
}
else if (AzFramework::StringFunc::Equal(message, "TestType.do_call.1"))
{
return static_cast<int>(LogTypes::TestTypeDoCall1);
}
}
return static_cast<int>(LogTypes::Skip);
};
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import sys, os
import azlmbr.paths
sys.path.append(os.path.join(azlmbr.paths.devroot,'Gems','EditorPythonBindings','Code','Tests'))
from test_package import import_test as itest
print('ImportModule')
itest.test_call()
testInst = itest.TestType()
testInst.do_call(1)
)");
}
catch ([[maybe_unused]] const std::exception& exception)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", exception.what());
}
e.Deactivate();
EXPECT_EQ(1, testSink.m_evaluationMap[static_cast<int>(LogTypes::ImportModule)]);
EXPECT_EQ(1, testSink.m_evaluationMap[static_cast<int>(LogTypes::TestCallHit)]);
EXPECT_EQ(1, testSink.m_evaluationMap[static_cast<int>(LogTypes::TestTypeDoCall1)]);
}
TEST_F(PythonBindingLibTest, PyDocHelp_AzlmbrGlobals_Works)
{
enum class LogTypes
{
Skip = 0,
Worked
};
PythonTraceMessageSink testSink;
testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "Worked"))
{
return aznumeric_cast<int>(LogTypes::Worked);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import pydoc
import azlmbr.globals
pydoc.help(azlmbr.globals)
print('Worked')
)");
}
catch ([[maybe_unused]] const std::exception& exception)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", exception.what());
}
e.Deactivate();
EXPECT_EQ(1, testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::Worked)]);
}
TEST_F(PythonBindingLibTest, ImportAzLmbrTwice)
{
enum class LogTypes
{
Skip = 0,
ImportAzLmbrTwice,
SawEntityId
};
PythonTraceMessageSink testSink;
testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "ImportAzLmbrTwice"))
{
return aznumeric_cast<int>(LogTypes::ImportAzLmbrTwice);
}
else if (AzFramework::StringFunc::StartsWith(message, "entity_id 101"))
{
return aznumeric_cast<int>(LogTypes::SawEntityId);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import sys, os
import azlmbr.paths
sys.path.append(os.path.join(azlmbr.paths.devroot,'Gems','EditorPythonBindings','Code','Tests'))
sys.path.append(os.path.join(azlmbr.paths.devroot,'Gems','EditorPythonBindings','Code','Tests','test_package'))
from test_package import import_many
import_many.test_many_entity_id()
print('ImportAzLmbrTwice')
)");
}
catch ([[maybe_unused]] const std::exception& exception)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", exception.what());
}
e.Deactivate();
EXPECT_EQ(1, testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::ImportAzLmbrTwice)]);
EXPECT_EQ(1, testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::SawEntityId)]);
}
}
@@ -0,0 +1,481 @@
/*
* 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/PythonCommon.h>
#include <AzCore/PlatformDef.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
#include <Source/PythonProxyObject.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace CustomTest
{
template <typename T>
struct MyTemplate
{
MyTemplate() = default;
MyTemplate(T value) : m_value(value) {}
T m_value = {};
};
}
namespace AZ
{
template<typename T>
struct OnDemandReflection<CustomTest::MyTemplate<T>>
{
using MyTemplateType = CustomTest::MyTemplate<T>;
static void Reflect(ReflectContext* context)
{
if (BehaviorContext* behaviorContext = azrtti_cast<BehaviorContext*>(context))
{
behaviorContext->Class<MyTemplateType>()
->Attribute(Script::Attributes::Scope, Script::Attributes::ScopeFlags::Automation)
->Attribute(Script::Attributes::Module, "test.template")
->Property("Value",
[] (MyTemplateType* that) -> T { return that->m_value; },
[] (MyTemplateType* that, const T& value) { that->m_value = value; })
;
}
}
};
AZ_TYPE_INFO_TEMPLATE(CustomTest::MyTemplate, "{82B9D060-F077-4FAA-9EF4-EF4C3A2A6332}", AZ_TYPE_INFO_CLASS);
}
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// test class/struts
struct CustomTypeHolder
{
AZ_TYPE_INFO(MyTemplate, "{46543B40-D8AF-4498-BCD0-2FF2A040B42C}");
CustomTest::MyTemplate<float> m_testFloat;
CustomTest::MyTemplate<AZStd::string> m_testString;
CustomTest::MyTemplate<int> m_testInt;
CustomTypeHolder()
: m_testFloat(42.0f)
, m_testString("42")
, m_testInt(42)
{
}
void Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->RegisterGenericType<CustomTest::MyTemplate<float>>();
serializeContext->RegisterGenericType<CustomTest::MyTemplate<AZStd::string>>();
serializeContext->RegisterGenericType<CustomTest::MyTemplate<int>>();
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<CustomTypeHolder>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Method("set_float", [](CustomTypeHolder& self, float value) { self.m_testFloat.m_value = value; })
->Property("test_float", BehaviorValueProperty(&CustomTypeHolder::m_testFloat))
->Property("test_string", BehaviorValueProperty(&CustomTypeHolder::m_testString))
->Property("test_int", BehaviorValueProperty(&CustomTypeHolder::m_testInt))
;
}
}
};
struct Descriptor final
{
AZ_TYPE_INFO(Descriptor, "{0DFEE628-EFE2-4B9B-BAF2-40ED2965E663}");
void Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->RegisterGenericType<Descriptor>();
serializeContext->RegisterGenericType<AZStd::vector<Descriptor>>();
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<Descriptor>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Property("s32", BehaviorValueProperty(&Descriptor::m_s32))
->Property("u32", BehaviorValueProperty(&Descriptor::m_u32))
->Property("scalar", BehaviorValueProperty(&Descriptor::m_scalar))
->Property("bool_value", BehaviorValueProperty(&Descriptor::m_bool))
->Property("string_value", BehaviorValueProperty(&Descriptor::m_stringValue))
->Method("return_dummy_descriptor", []() { static Descriptor dummy; return dummy; }, nullptr, "")
->Method("return_dummy_vector_descriptor", []() { static AZStd::vector<Descriptor> dummy; return dummy; }, nullptr, "")
;
}
}
Descriptor() = default;
~Descriptor() = default;
AZ::s32 m_s32 = -1234;
AZ::u32 m_u32 = 0xDEADBEEF;
float m_scalar = -456.0f;
bool m_bool = true;
AZStd::string m_stringValue;
};
struct PythonReflectionAnyContainer
{
AZ_TYPE_INFO(PythonReflectionAnyContainer, "{D7D45479-9A46-469E-BE75-F305EBE8F848}");
AZStd::any m_anyList; // will store a container like vector
PythonReflectionAnyContainer()
{
AZStd::vector<AZ::s64> numbers{ 1,2,3,5,8,13 };
m_anyList = AZStd::make_any<AZStd::vector<AZ::s64>>(numbers);
}
void MutateAnyContainer(const AZStd::any& value)
{
m_anyList = value;
if(m_anyList.is<AZStd::vector<Descriptor>>())
{
const AZStd::vector<Descriptor>* ptr = AZStd::any_cast<AZStd::vector<Descriptor>>(&m_anyList);
if (!ptr->empty())
{
AZ_Printf("python", "ReplaceAnyList_AZStd::vector<Descriptor>", ptr->size());
}
}
}
const AZStd::any& AccessAnyContainer() const
{
if (m_anyList.is<AZStd::vector<Descriptor>>())
{
const AZStd::vector<Descriptor>* ptr = AZStd::any_cast<AZStd::vector<Descriptor>>(&m_anyList);
if (!ptr->empty())
{
AZ_Printf("python", "AccessAnyList_AZStd::vector<Descriptor>", ptr->size());
}
}
return m_anyList;
}
void Reflect(AZ::ReflectContext* context)
{
using namespace EditorPythonBindings;
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->RegisterGenericType<AZStd::vector<AZStd::any>>();
serializeContext->RegisterGenericType<AZStd::vector<AZ::s64>>();
serializeContext->RegisterGenericType<AZStd::vector<double>>();
serializeContext->RegisterGenericType<AZStd::vector<bool>>();
serializeContext->RegisterGenericType<AZStd::vector<AZStd::string>>();
serializeContext->RegisterGenericType<AZStd::vector<PythonProxyObject>>();
;
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<PythonReflectionAnyContainer>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Method("mutate_any_container", &PythonReflectionAnyContainer::MutateAnyContainer, nullptr, "")
->Method("access_any_container", &PythonReflectionAnyContainer::AccessAnyContainer, nullptr, "")
->Method("return_dummy_vector_integer", []() { static AZStd::vector<AZ::s64> dummy; return dummy; }, nullptr, "")
->Method("return_dummy_vector_double", []() { static AZStd::vector<double> dummy; return dummy; }, nullptr, "")
->Method("return_dummy_vector_bool", []() { static AZStd::vector<bool> dummy; return dummy; }, nullptr, "")
->Method("return_dummy_vector_string", []() { static AZStd::vector<AZStd::string> dummy; return dummy; }, nullptr, "")
->Method("return_dummy_vector_proxy", []() { static AZStd::vector<PythonProxyObject> dummy; return dummy; }, nullptr, "")
;
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonReflectAnyContainerTests
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
};
TEST_F(PythonReflectAnyContainerTests, AccessReplaceVectorTypes)
{
enum class LogTypes
{
Skip = 0,
AccessAnyList,
ReplaceAnyList,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "AccessAnyList"))
{
return aznumeric_cast<int>(LogTypes::AccessAnyList);
}
else if (AzFramework::StringFunc::StartsWith(message, "ReplaceAnyList"))
{
return aznumeric_cast<int>(LogTypes::ReplaceAnyList);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
PythonReflectionAnyContainer pythonReflectionAnyContainer;
pythonReflectionAnyContainer.Reflect(m_app.GetBehaviorContext());
pythonReflectionAnyContainer.Reflect(m_app.GetSerializeContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test as test
testObject = test.PythonReflectionAnyContainer()
target = [1,2,3,5,8,13]
values = testObject.access_any_container()
if (len(values) > 0):
print ('AccessAnyList_for_values')
if (values == target):
print ('AccessAnyList_matching_ends')
target.reverse()
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if (values == target):
print ('ReplaceAnyList_replaced_as_reversed')
target = [True,False,True,True]
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if( type(values[0]) is bool):
print ('AccessAnyList_matching_bools')
target.reverse()
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if (values == target):
print ('ReplaceAnyList_replaced_bools')
target = [-1.0,1.0,-10.0,10.0]
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if (values == target):
print ('AccessAnyList_matching_floats')
target.reverse()
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if (values == target):
print ('ReplaceAnyList_replaced_floats')
target = ['one','2','three','0x4']
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if (values == target):
print ('AccessAnyList_matching_strings')
target.reverse()
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if (values == target):
print ('ReplaceAnyList_strings')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed with %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(5, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::AccessAnyList)]);
EXPECT_EQ(4, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::ReplaceAnyList)]);
}
TEST_F(PythonReflectAnyContainerTests, AccessReplaceComplexTypes)
{
enum class LogTypes
{
Skip = 0,
AccessAnyList,
ReplaceAnyList,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "AccessAnyList"))
{
return aznumeric_cast<int>(LogTypes::AccessAnyList);
}
else if (AzFramework::StringFunc::StartsWith(message, "ReplaceAnyList"))
{
return aznumeric_cast<int>(LogTypes::ReplaceAnyList);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
PythonReflectionAnyContainer pythonReflectionAnyContainer;
pythonReflectionAnyContainer.Reflect(m_app.GetBehaviorContext());
pythonReflectionAnyContainer.Reflect(m_app.GetSerializeContext());
Descriptor descriptor;
descriptor.Reflect(m_app.GetBehaviorContext());
descriptor.Reflect(m_app.GetSerializeContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test as test
import azlmbr.object
testObject = test.PythonReflectionAnyContainer()
def create_descriptor(s32, u32, scalar, bool_value, string_value):
descriptor = test.Descriptor()
descriptor.s32 = s32
descriptor.u32 = u32
descriptor.scalar = scalar
descriptor.bool_value = bool_value
descriptor.string_value = string_value
return descriptor
def equals_descriptor(lhs, rhs):
return (lhs.s32 == rhs.s32 and
lhs.u32 == rhs.u32 and
lhs.scalar == rhs.scalar and
lhs.bool_value == rhs.bool_value and
lhs.string_value == rhs.string_value)
target = []
target.append(create_descriptor(-1, 2, 3.0, True, 'one'))
target.append(create_descriptor(-2, 3, 4.0, False, '0X2'))
target.append(create_descriptor(-3, 4, 5.0, True, 'T H R E E'))
testObject.mutate_any_container(target)
values = testObject.access_any_container()
if( isinstance(values[0], azlmbr.object.PythonProxyObject) and values[0].typename == 'Descriptor'):
print ('AccessAnyList_matches_descriptor_type')
target.reverse()
testObject.mutate_any_container(target)
values = testObject.access_any_container()
for x in range(0, len(values)):
if ( equals_descriptor(values[x], target[x]) ):
print ('ReplaceAnyList_replaced_descriptors')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed with %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(3, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::AccessAnyList)]);
EXPECT_EQ(5, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::ReplaceAnyList)]);
}
TEST_F(PythonReflectAnyContainerTests, CustomTypeTemplates)
{
enum class LogTypes
{
Skip = 0,
Float,
String,
Integer
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "Float"))
{
return aznumeric_cast<int>(LogTypes::Float);
}
else if (AzFramework::StringFunc::StartsWith(message, "String"))
{
return aznumeric_cast<int>(LogTypes::String);
}
else if (AzFramework::StringFunc::StartsWith(message, "Integer"))
{
return aznumeric_cast<int>(LogTypes::Integer);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
CustomTypeHolder customTypeHolder;
customTypeHolder.Reflect(m_app.GetBehaviorContext());
customTypeHolder.Reflect(m_app.GetSerializeContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test
import azlmbr.test.template
templateFloat = azlmbr.test.template.CustomTest_MyTemplate_float(40.0 + 2.0)
print('Float - created template with float')
templateString = azlmbr.test.template.CustomTest_MyTemplate_string('forty-two')
print('String - created template with string')
templateInt = azlmbr.test.template.CustomTest_MyTemplate_int(40 + 2)
print('Integer - created template with int')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed with %s", e.what());
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::Float)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::String)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::Integer)]);
}
}
@@ -0,0 +1,340 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace UnitTest
{
struct PythonReflectionDictionaryTypes final
{
AZ_TYPE_INFO(PythonReflectionDictionaryTypes, "{478AD363-467D-4285-BE40-4D1CB1A09A19}");
template <typename K, typename V>
struct MapOf
{
using MapType = AZStd::unordered_map<K,V>;
MapType m_map;
explicit MapOf(const std::initializer_list<AZStd::pair<K, V>> map)
{
m_map = map;
}
const MapType& ReturnMap() const
{
return m_map;
}
void AcceptMap(const MapType& other)
{
m_map = other;
}
void RegisterGenericType(AZ::SerializeContext& serializeContext)
{
serializeContext.RegisterGenericType<AZStd::unordered_map<K,V>>();
}
};
MapOf<AZ::u8, AZ::u32> m_indexOfu8tou32 { {1, 4}, {2, 5}, {3, 6}, {4, 7} };
MapOf<AZ::u16, float> m_indexOfu16toFloat { {1, 0.4f}, {2, 0.5f}, {3, 0.6f}, {4, 0.7f} };
MapOf<AZStd::string, AZ::s32> m_indexOfStringTos32 { {"1", -4}, {"2", 5}, {"3", -6}, {"4", 7} };
MapOf<AZStd::string, AZStd::string> m_indexOfStringToString { {"hello", "foo"}, {"world", "bar"}, {"bye", "baz"}, {"sky", "qux"} };
MapOf<AZStd::string, AZ::Vector3> m_indexOfStringToVec3{ {"up", AZ::Vector3{ 0, 1.0, 0 }}, {"down", AZ::Vector3{0, -1.0, 0}},
{"left", AZ::Vector3{1.0, 0, 0}}, {"right", AZ::Vector3{-1, 0, 0}} };
void Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
m_indexOfu8tou32.RegisterGenericType(*serializeContext);
m_indexOfu16toFloat.RegisterGenericType(*serializeContext);
m_indexOfStringTos32.RegisterGenericType(*serializeContext);
m_indexOfStringToString.RegisterGenericType(*serializeContext);
m_indexOfStringToVec3.RegisterGenericType(*serializeContext);
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<PythonReflectionDictionaryTypes>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test.dictionary")
->Method("return_dict_of_u8u32", [](PythonReflectionDictionaryTypes* self) { return self->m_indexOfu8tou32.ReturnMap(); }, nullptr, "")
->Method("accept_dict_of_u8u32", [](PythonReflectionDictionaryTypes* self, const MapOf<AZ::u8, AZ::u32>::MapType& map) { self->m_indexOfu8tou32.AcceptMap(map); }, nullptr, "")
->Method("return_dict_of_u16toFloat", [](PythonReflectionDictionaryTypes* self) { return self->m_indexOfu16toFloat.ReturnMap(); }, nullptr, "")
->Method("accept_dict_of_u16toFloat", [](PythonReflectionDictionaryTypes* self, const MapOf<AZ::u16, float>::MapType& map) { self->m_indexOfu16toFloat.AcceptMap(map); }, nullptr, "")
->Method("return_dict_of_stringTos32", [](PythonReflectionDictionaryTypes* self) { return self->m_indexOfStringTos32.ReturnMap(); }, nullptr, "")
->Method("accept_dict_of_stringTos32", [](PythonReflectionDictionaryTypes* self, const MapOf<AZStd::string, AZ::s32>::MapType& map) { self->m_indexOfStringTos32.AcceptMap(map); }, nullptr, "")
->Method("return_dict_of_stringToString", [](PythonReflectionDictionaryTypes* self) { return self->m_indexOfStringToString.ReturnMap(); }, nullptr, "")
->Method("accept_dict_of_stringToString", [](PythonReflectionDictionaryTypes* self, const MapOf<AZStd::string, AZStd::string>::MapType& map) { self->m_indexOfStringToString.AcceptMap(map); }, nullptr, "")
->Method("return_dict_of_stringToVec3", [](PythonReflectionDictionaryTypes* self) { return self->m_indexOfStringToVec3.ReturnMap(); }, nullptr, "")
->Method("accept_dict_of_stringToVec3", [](PythonReflectionDictionaryTypes* self, const MapOf<AZStd::string, AZ::Vector3>::MapType& map) { self->m_indexOfStringToVec3.AcceptMap(map); }, nullptr, "")
;
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonReflectionDictionaryTests
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
};
TEST_F(PythonReflectionDictionaryTests, InstallingPythonDictionaries)
{
AZ::Entity e;
Activate(e);
EXPECT_EQ(AZ::Entity::State::Active, e.GetState());
SimulateEditorBecomingInitialized();
e.Deactivate();
}
TEST_F(PythonReflectionDictionaryTests, MapSimpleTypes)
{
enum class LogTypes
{
Skip = 0,
ContainerTypes_Input,
ContainerTypes_Output,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "ContainerTypes_Input"))
{
return static_cast<int>(LogTypes::ContainerTypes_Input);
}
else if (AzFramework::StringFunc::StartsWith(message, "ContainerTypes_Output"))
{
return static_cast<int>(LogTypes::ContainerTypes_Output);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonReflectionDictionaryTypes pythonReflectionDictionaryTypes;
pythonReflectionDictionaryTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionDictionaryTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.dictionary
import azlmbr.object
test = azlmbr.object.create('PythonReflectionDictionaryTypes')
result = test.return_dict_of_u8u32()
if (len(result.items()) == 4):
print ('ContainerTypes_Output_u8u32')
test.accept_dict_of_u8u32({4: 1, 3: 2})
result = test.return_dict_of_u8u32()
if (len(result.items()) == 2):
print ('ContainerTypes_Input_u8u32')
result = test.return_dict_of_u16toFloat()
if (len(result.items()) == 4):
print ('ContainerTypes_Output_u16toFloat')
test.accept_dict_of_u16toFloat({4: 0.1, 3: 0.2})
result = test.return_dict_of_u16toFloat()
if (len(result.items()) == 2):
print ('ContainerTypes_Input_u16toFloat')
result = test.return_dict_of_stringTos32()
if (len(result.items()) == 4):
print ('ContainerTypes_Output_stringTos32')
test.accept_dict_of_stringTos32({'4': -1, '3': 2})
result = test.return_dict_of_stringTos32()
if (len(result.items()) == 2):
print ('ContainerTypes_Input_stringTos32')
result = test.return_dict_of_stringToString()
if (len(result.items()) == 4):
print ('ContainerTypes_Output_stringToString')
test.accept_dict_of_stringToString({'one': '1', 'two': '2'})
result = test.return_dict_of_stringToString()
if (len(result.items()) == 2):
print ('ContainerTypes_Input_stringToString')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed with Python exception of %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(4, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::ContainerTypes_Input)]);
EXPECT_EQ(4, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::ContainerTypes_Output)]);
}
TEST_F(PythonReflectionDictionaryTests, MapTypes_Mismatch_Detected)
{
enum class LogTypes
{
Skip = 0,
Detection,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
constexpr AZStd::string_view warningTypeMismatch =
"Could not convert to pair element type value2 for the pair<>; failed to marshal Python input <class 'int'>";
constexpr AZStd::string_view warningSizeMismatch =
"Python Dict size:2 does not match the size of the unordered_map:0";
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, warningTypeMismatch))
{
return aznumeric_cast<int>(LogTypes::Detection);
}
else if (AzFramework::StringFunc::StartsWith(message, warningSizeMismatch))
{
return aznumeric_cast<int>(LogTypes::Detection);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
PythonReflectionDictionaryTypes pythonReflectionDictionaryTypes;
pythonReflectionDictionaryTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionDictionaryTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.dictionary
import azlmbr.object
test = azlmbr.object.create('PythonReflectionDictionaryTypes')
mismatchMap = {'one': 1, 'two': 2}
test.accept_dict_of_stringToString(mismatchMap)
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed with Python exception of %s", e.what());
}
e.Deactivate();
EXPECT_EQ(3, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::Detection)]);
}
TEST_F(PythonReflectionDictionaryTests, MapComplexTypes)
{
enum class LogTypes
{
Skip = 0,
ContainerTypes_Input,
ContainerTypes_Output,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "ContainerTypes_Input"))
{
return static_cast<int>(LogTypes::ContainerTypes_Input);
}
else if (AzFramework::StringFunc::StartsWith(message, "ContainerTypes_Output"))
{
return static_cast<int>(LogTypes::ContainerTypes_Output);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonReflectionDictionaryTypes pythonReflectionDictionaryTypes;
pythonReflectionDictionaryTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionDictionaryTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.dictionary
import azlmbr.object
test = azlmbr.object.create('PythonReflectionDictionaryTypes')
result = test.return_dict_of_stringToVec3()
if (len(result.items()) == 4):
print ('ContainerTypes_Output_stringToVec3')
vec3dict = {}
vec3dict['120'] = azlmbr.math.Vector3(1.0, -2.0, 0.0)
vec3dict['456'] = azlmbr.math.Vector3(0.4, 0.5, 0.6)
test.accept_dict_of_stringToVec3(vec3dict)
result = test.return_dict_of_stringToVec3()
if (len(result.items()) == 2):
if (result['120'].x > 0 and result['120'].y < 0 and result['120'].z == 0):
print ('ContainerTypes_Input_stringToVec3')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed with Python exception of %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::ContainerTypes_Input)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::ContainerTypes_Output)]);
}
}
@@ -0,0 +1,677 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonProxyObject.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace UnitTest
{
void AcceptTwoStrings(AZStd::string stringValue1, AZStd::string stringValue2)
{
AZ_TracePrintf("python", stringValue1.empty() ? "stringValue1_is_empty" : "stringValue1_has_data");
AZ_TracePrintf("python", stringValue2.empty() ? "stringValue2_is_empty" : "stringValue2_has_data");
}
//////////////////////////////////////////////////////////////////////////
// test class/struts
struct PythonGlobalsTester
{
AZ_TYPE_INFO(PythonGlobalsTester, "{00EC83FE-2E9D-42D0-8A59-2940669C7BCA}");
enum GlobalEnums : AZ::u16
{
GE_NONE,
GE_LUMBER = 101,
GE_YARD
};
enum class MyTypes
{
One = 1,
Two = 2,
};
static AZ::s32 s_staticValue;
static AZ::u32 s_pingCount;
static GlobalEnums s_result1;
static GlobalEnums s_result2;
static constexpr AZ::u8 s_one = 1;
static AZ::Uuid s_myTypeId;
static AZStd::string s_myString;
static AZ::s32 GetValue()
{
return s_staticValue;
}
static void SetValue(AZ::s32 value)
{
s_staticValue = value;
}
static AZ::u32 Ping()
{
++s_pingCount;
return s_pingCount;
}
static void Reset()
{
s_pingCount = 0;
s_staticValue = 0;
s_result1 = GlobalEnums::GE_NONE;
s_result2 = GlobalEnums::GE_NONE;
s_myTypeId = AZ::TypeId::CreateString("{DEADBEE5-F983-4153-848A-EE9F99502811}");
s_myString = AZStd::string("my string");
}
void Reflect(AZ::ReflectContext* context)
{
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
// Methods
behaviorContext->Method("ping", &PythonGlobalsTester::Ping)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test.pinger");
behaviorContext->Method("reset", &PythonGlobalsTester::Reset)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
behaviorContext->Method("accept_two_strings", AcceptTwoStrings)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
// Property
behaviorContext->Property("constantNumber", []() { return PythonGlobalsTester::GetValue(); }, nullptr)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
behaviorContext->Property("coolProperty", &PythonGlobalsTester::GetValue, &PythonGlobalsTester::SetValue)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
behaviorContext->Property("pingCount", BehaviorValueGetter(&s_pingCount), BehaviorValueSetter(&s_pingCount))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
// Enums
behaviorContext->EnumProperty<GlobalEnums::GE_LUMBER>("GE_LUMBER")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
behaviorContext->EnumProperty<GlobalEnums::GE_YARD>("GE_YARD")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
// azlmbr.my.enum.One
behaviorContext->EnumProperty<aznumeric_cast<int>(MyTypes::One)>("One")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "my.enum")
;
// azlmbr.my.enum.Two
behaviorContext->EnumProperty<aznumeric_cast<int>(MyTypes::Two)>("Two")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "my.enum")
;
behaviorContext->Property("result1", []() { return s_result1; }, [](GlobalEnums value) { s_result1 = value; })
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
behaviorContext->Property("result2", []() { return s_result2; }, [](GlobalEnums value) { s_result2 = value; })
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
// Constants
behaviorContext->ConstantProperty("ONE", []() { return s_one; })
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
// azlmbr.constant.MY_TYPE
behaviorContext->ConstantProperty("MY_TYPE", []() { return s_myTypeId; })
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "constant")
;
// azlmbr.constant.MY_STRING
behaviorContext->ConstantProperty("MY_STRING", []() { return s_myString; })
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "constant")
;
}
}
};
AZ::s32 PythonGlobalsTester::s_staticValue = 0;
AZ::u32 PythonGlobalsTester::s_pingCount = 0;
PythonGlobalsTester::GlobalEnums PythonGlobalsTester::s_result1 = PythonGlobalsTester::GlobalEnums::GE_NONE;
PythonGlobalsTester::GlobalEnums PythonGlobalsTester::s_result2 = PythonGlobalsTester::GlobalEnums::GE_NONE;
AZ::Uuid PythonGlobalsTester::s_myTypeId;
AZStd::string PythonGlobalsTester::s_myString;
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonGlobalsTests
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
void Deactivate(AZ::Entity& entity)
{
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (editorPythonEventsInterface)
{
editorPythonEventsInterface->StopPython();
}
entity.Deactivate();
}
};
//////////////////////////////////////////////////////////////////////////
// tests
TEST_F(PythonGlobalsTests, GlobalMethodTest)
{
PythonGlobalsTester pythonGlobalsTester;
pythonGlobalsTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
# testing global methods
import azlmbr.globals
import azlmbr.test.pinger
azlmbr.globals.reset()
for i in range(830):
azlmbr.test.pinger.ping()
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(830, PythonGlobalsTester::s_pingCount);
}
TEST_F(PythonGlobalsTests, GlobalPropertyTest)
{
enum class LogTypes
{
Skip = 0,
GlobalPropertyTest_NotNone,
GlobalPropertyTest_Is40,
GlobalPropertyTest_Is42,
GlobalPropertyTest_PingWorked,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "GlobalPropertyTest_NotNone"))
{
return static_cast<int>(LogTypes::GlobalPropertyTest_NotNone);
}
else if (AzFramework::StringFunc::Equal(message, "GlobalPropertyTest_Is40"))
{
return static_cast<int>(LogTypes::GlobalPropertyTest_Is40);
}
else if (AzFramework::StringFunc::Equal(message, "GlobalPropertyTest_Is42"))
{
return static_cast<int>(LogTypes::GlobalPropertyTest_Is42);
}
else if (AzFramework::StringFunc::Equal(message, "GlobalPropertyTest_PingWorked"))
{
return static_cast<int>(LogTypes::GlobalPropertyTest_PingWorked);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonGlobalsTester pythonGlobalsTester;
pythonGlobalsTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.globals
import azlmbr.test.pinger
# testing global properties
if (azlmbr.globals.property.constantNumber == 0):
print ('GlobalPropertyTest_NotNone')
azlmbr.globals.property.coolProperty = 40
if (azlmbr.globals.property.coolProperty == 40):
print ('GlobalPropertyTest_Is40')
azlmbr.globals.property.coolProperty = azlmbr.globals.property.coolProperty + 2
if (azlmbr.globals.property.constantNumber == 42):
print ('GlobalPropertyTest_Is42')
azlmbr.globals.property.pingCount = 0
for i in range(830):
azlmbr.test.pinger.ping()
if (azlmbr.globals.property.pingCount == 830):
print ('GlobalPropertyTest_PingWorked')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalPropertyTest_NotNone)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalPropertyTest_Is40)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalPropertyTest_Is42)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalPropertyTest_PingWorked)]);
}
TEST_F(PythonGlobalsTests, GlobalEnumTest)
{
enum class LogTypes
{
Skip = 0,
GlobalEnumTest_Lumber,
GlobalEnumTest_Yard
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "GlobalEnumTest_Lumber"))
{
return static_cast<int>(LogTypes::GlobalEnumTest_Lumber);
}
else if (AzFramework::StringFunc::Equal(message, "GlobalEnumTest_Yard"))
{
return static_cast<int>(LogTypes::GlobalEnumTest_Yard);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonGlobalsTester pythonGlobalsTester;
pythonGlobalsTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.globals
azlmbr.globals.reset()
# testing global enum constant values
if (azlmbr.globals.property.GE_LUMBER == 101):
print ('GlobalEnumTest_Lumber')
if (azlmbr.globals.property.GE_YARD == 102):
print ('GlobalEnumTest_Yard')
azlmbr.globals.property.result1 = azlmbr.globals.property.GE_LUMBER
azlmbr.globals.property.result2 = azlmbr.globals.property.GE_YARD
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalEnumTest_Lumber)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalEnumTest_Yard)]);
EXPECT_EQ(PythonGlobalsTester::GlobalEnums::GE_LUMBER, PythonGlobalsTester::s_result1);
EXPECT_EQ(PythonGlobalsTester::GlobalEnums::GE_YARD, PythonGlobalsTester::s_result2);
}
TEST_F(PythonGlobalsTests, GlobalConstantTest)
{
enum class LogTypes
{
Skip = 0,
GlobalConstantTest_Fetch,
GlobalConstantTest_Adds
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "GlobalConstantTest_Fetch"))
{
return static_cast<int>(LogTypes::GlobalConstantTest_Fetch);
}
else if (AzFramework::StringFunc::Equal(message, "GlobalConstantTest_Adds"))
{
return static_cast<int>(LogTypes::GlobalConstantTest_Adds);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonGlobalsTester pythonGlobalsTester;
pythonGlobalsTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.globals
azlmbr.globals.reset()
# testing global enum constant values
if (azlmbr.globals.property.ONE == 1):
print ('GlobalConstantTest_Fetch')
a = azlmbr.globals.property.ONE
b = azlmbr.globals.property.ONE
if ((a + b) == 2):
print ('GlobalConstantTest_Adds')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalConstantTest_Fetch)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::GlobalConstantTest_Adds)]);
}
TEST_F(PythonGlobalsTests, TryAcceptTwoStrings)
{
PythonGlobalsTester pythonGlobalsTester;
pythonGlobalsTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
stringValue1_has_data,
stringValue2_is_empty
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "stringValue1_has_data"))
{
return aznumeric_cast<int>(LogTypes::stringValue1_has_data);
}
else if (AzFramework::StringFunc::Equal(message, "stringValue2_is_empty"))
{
return aznumeric_cast<int>(LogTypes::stringValue2_is_empty);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
try
{
pybind11::exec(R"(
import azlmbr.globals
azlmbr.globals.accept_two_strings("Test 01", "")
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::stringValue1_has_data)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::stringValue2_is_empty)]);
}
TEST_F(PythonGlobalsTests, GlobalListAllClasses)
{
PythonGlobalsTester pythonGlobalsTester;
pythonGlobalsTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
ClassesFound
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "ClassListFound"))
{
return aznumeric_cast<int>(LogTypes::ClassesFound);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
try
{
pybind11::exec(R"(
import azlmbr.object
classList = azlmbr.object.list_classes()
if (len(classList) > 0):
print ('ClassListFound')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::ClassesFound)]);
}
TEST_F(PythonGlobalsTests, GlobalModuleDefinedTypeId)
{
PythonGlobalsTester pythonGlobalsTester;
pythonGlobalsTester.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
TypeIsValid,
StringTypeIsValid,
EnumIsValid,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "TypeIsValid"))
{
return aznumeric_cast<int>(LogTypes::TypeIsValid);
}
else if (AzFramework::StringFunc::Equal(message, "StringTypeIsValid"))
{
return aznumeric_cast<int>(LogTypes::StringTypeIsValid);
}
else if (AzFramework::StringFunc::Equal(message, "EnumIsValid"))
{
return aznumeric_cast<int>(LogTypes::EnumIsValid);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
try
{
pybind11::exec(R"(
import azlmbr.constant
import azlmbr.my.enum
import azlmbr.globals
azlmbr.globals.reset()
type = azlmbr.constant.MY_TYPE
if (type.ToString().startswith('{DEADBEE5-')):
print ('TypeIsValid')
if (azlmbr.constant.MY_STRING == 'my string'):
print ('StringTypeIsValid')
if (azlmbr.my.enum.One == 1):
print ('EnumIsValid')
if (azlmbr.my.enum.Two == 2):
print ('EnumIsValid')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::TypeIsValid)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::StringTypeIsValid)]);
EXPECT_EQ(2, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::EnumIsValid)]);
}
TEST_F(PythonGlobalsTests, CompareEqualityOperators)
{
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
IsGreaterThan,
IsGreaterEqualTo,
IsLessThan,
IsLessEqualTo,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "IsGreaterThan"))
{
return aznumeric_cast<int>(LogTypes::IsGreaterThan);
}
else if (AzFramework::StringFunc::StartsWith(message, "IsGreaterEqualTo"))
{
return aznumeric_cast<int>(LogTypes::IsGreaterEqualTo);
}
else if (AzFramework::StringFunc::StartsWith(message, "IsLessThan"))
{
return aznumeric_cast<int>(LogTypes::IsLessThan);
}
else if (AzFramework::StringFunc::StartsWith(message, "IsLessEqualTo"))
{
return aznumeric_cast<int>(LogTypes::IsLessEqualTo);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
try
{
pybind11::exec(R"(
import azlmbr.math
import azlmbr.globals
pointA = azlmbr.math.Vector2(40.0)
pointB = azlmbr.math.Vector2(2.0)
if (pointB < pointA):
print ('IsLessThan')
if (pointB <= pointA):
print ('IsLessEqualTo')
if (pointB <= pointB):
print ('IsLessEqualTo')
if (pointA > pointB):
print ('IsGreaterThan')
if (pointA >= pointB):
print ('IsGreaterEqualTo')
if (pointA >= pointA):
print ('IsGreaterEqualTo')
if (pointB >= pointA):
print ('IsGreaterEqualTo')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
Deactivate(e);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::IsGreaterThan)]);
EXPECT_EQ(2, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::IsGreaterEqualTo)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::IsLessThan)]);
EXPECT_EQ(2, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::IsLessEqualTo)]);
}
}
@@ -0,0 +1,214 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
#include <Source/PythonLogSymbolsComponent.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// test classes/structs
class PythonLogSymbolsTestComponent :
public EditorPythonBindings::PythonLogSymbolsComponent
{
public:
AZ_COMPONENT(PythonLogSymbolsTestComponent, "{D5802A34-1B57-470B-8C30-FFC273C9F4ED}", EditorPythonBindings::PythonLogSymbolsComponent);
AZStd::string_view FetchPythonTypeAndTraitsWrapper(const AZ::TypeId& typeId, AZ::u32 traits)
{
return FetchPythonTypeAndTraits(typeId, traits);
}
AZStd::string_view FetchPythonTypeWrapper(const AZ::BehaviorParameter& param)
{
return FetchPythonType(param);
}
};
class SimpleClass
{
public:
AZ_TYPE_INFO(SimpleClass, "{DFA153D8-F168-44F9-8DEF-55CDBBAA5AA2}")
};
class CustomClass
{
public:
AZ_TYPE_INFO(CustomClass, "{361A9A18-40E6-4D16-920A-0F38F55D63BF}")
void NoOp() const
{}
};
struct TestTypesReflectionContainer
{
AZ_TYPE_INFO(TestTypesReflectionContainer, "{5DE28B62-F9A1-4307-9684-6C95B9EE3225}")
void Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->RegisterGenericType<AZStd::vector<int>>();
serializeContext->RegisterGenericType<AZStd::vector<SimpleClass>>();
serializeContext->RegisterGenericType<AZStd::vector<CustomClass>>();
serializeContext->RegisterGenericType<AZStd::map<int, int>>();
serializeContext->RegisterGenericType<AZStd::map<int, SimpleClass>>();
serializeContext->RegisterGenericType<AZStd::map<int, CustomClass>>();
serializeContext->RegisterGenericType<AZ::Outcome<int, int>>();
serializeContext->RegisterGenericType<AZ::Outcome<int, SimpleClass>>();
serializeContext->RegisterGenericType<AZ::Outcome<int, CustomClass>>();
serializeContext->Class<CustomClass>()
->Version(1)
;
// SimpleClass registration ommited for testing cases where type cannot be determined.
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonLogSymbolsComponentTest
: public PythonTestingFixture
{
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
// Registering test types
TestTypesReflectionContainer typesContainer;
typesContainer.Reflect(m_app.GetSerializeContext());
typesContainer.Reflect(m_app.GetBehaviorContext());
}
void TearDown() override
{
// clearing up memory
PythonTestingFixture::TearDown();
}
};
//////////////////////////////////////////////////////////////////////////
// tests
TEST_F(PythonLogSymbolsComponentTest, FetchSupportedTypesByTypeAndTraits_PythonTypeReturned)
{
PythonLogSymbolsTestComponent pythonLogSymbolsComponent;
AZStd::vector<AZStd::tuple<AZ::TypeId, AZ::u32, AZStd::string>> typesToTest =
{
// Simple types
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::string_view>::Uuid(), AZ::BehaviorParameter::TR_NONE, "str"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::string>::Uuid(), AZ::BehaviorParameter::TR_NONE, "str"),
AZStd::make_tuple(AZ::AzTypeInfo<char>::Uuid(), AZ::BehaviorParameter::TR_POINTER | AZ::BehaviorParameter::TR_CONST, "str"),
AZStd::make_tuple(AZ::AzTypeInfo<float>::Uuid(), AZ::BehaviorParameter::TR_NONE, "float"),
AZStd::make_tuple(AZ::AzTypeInfo<double>::Uuid(), AZ::BehaviorParameter::TR_NONE, "float"),
AZStd::make_tuple(AZ::AzTypeInfo<bool>::Uuid(), AZ::BehaviorParameter::TR_NONE, "bool"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::s8>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::u8>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::s16>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::u16>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::s32>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::u32>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::s64>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::u64>::Uuid(), AZ::BehaviorParameter::TR_NONE, "int"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::vector<AZ::u8>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "bytes"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::any>::Uuid(), AZ::BehaviorParameter::TR_NONE, "object"),
AZStd::make_tuple(AZ::AzTypeInfo<void>::Uuid(), AZ::BehaviorParameter::TR_NONE, "None"),
// Container types
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::vector<SimpleClass>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "list"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::vector<int>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "List[int]"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::vector<CustomClass>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "List[CustomClass]"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::map<int, SimpleClass>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "dict"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::map<int, int>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "Dict[int, int]"),
AZStd::make_tuple(AZ::AzTypeInfo<AZStd::map<int, CustomClass>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "Dict[int, CustomClass]"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::Outcome<int, SimpleClass>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "Outcome"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::Outcome<int, int>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "Outcome[int, int]"),
AZStd::make_tuple(AZ::AzTypeInfo<AZ::Outcome<int, CustomClass>>::Uuid(), AZ::BehaviorParameter::TR_NONE, "Outcome[int, CustomClass]"),
// Fallback to name
AZStd::make_tuple(AZ::AzTypeInfo<SimpleClass>::Uuid(), AZ::BehaviorParameter::TR_NONE, ""),
AZStd::make_tuple(AZ::AzTypeInfo<CustomClass>::Uuid(), AZ::BehaviorParameter::TR_NONE, "CustomClass")
};
auto stringViewHelper = [](const AZStd::string_view& s)
{
return AZStd::string::format(AZ_STRING_FORMAT, AZ_STRING_ARG(s));
};
auto uuidHelper = [](const AZ::Uuid& uuid)
{
char buffer[AZ::Uuid::MaxStringBuffer];
uuid.ToString(buffer, AZ::Uuid::MaxStringBuffer, true, true);
return AZStd::string(buffer);
};
for (auto& typeInfo : typesToTest)
{
AZStd::string_view result = pythonLogSymbolsComponent.FetchPythonTypeAndTraitsWrapper(AZStd::get<0>(typeInfo), AZStd::get<1>(typeInfo));
EXPECT_EQ(result, AZStd::get<2>(typeInfo))
<< "Expected '" << stringViewHelper(AZStd::get<2>(typeInfo)).c_str()
<< "' when converting type with id " << uuidHelper(AZStd::get<0>(typeInfo)).c_str()
<< " but got '" << stringViewHelper(result).c_str() << "'.";
}
}
TEST_F(PythonLogSymbolsComponentTest, FetchByParam_ReturnPythonType)
{
PythonLogSymbolsTestComponent pythonLogSymbolsComponent;
AZ::BehaviorParameter intParam;
intParam.m_name = "foo";
intParam.m_typeId = AZ::AzTypeInfo<AZ::s8>::Uuid(); // Uuid for a supported type
intParam.m_traits = AZ::BehaviorParameter::TR_NONE;
AZStd::string_view result = pythonLogSymbolsComponent.FetchPythonTypeWrapper(intParam);
EXPECT_EQ(result, "int");
}
TEST_F(PythonLogSymbolsComponentTest, FetchVoidByParam_ReturnNone)
{
PythonLogSymbolsTestComponent m_pythonLogSymbolsComponent;
AZ::BehaviorParameter voidParam;
voidParam.m_name = "void";
voidParam.m_typeId = AZ::Uuid("{9B3E8886-B749-418E-A696-6D7E9EB4D691}"); // A random Uuid
voidParam.m_traits = AZ::BehaviorParameter::TR_NONE;
AZStd::string_view result = m_pythonLogSymbolsComponent.FetchPythonTypeWrapper(voidParam);
EXPECT_EQ(result, "None");
}
}
@@ -0,0 +1,436 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
#include <Source/PythonProxyObject.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include "PythonPairTests.h"
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// test class/structs
struct PythonReflectionPairTypes
{
AZ_TYPE_INFO(PythonReflectionPairTypes, "{037C067F-7A03-47BE-A30E-124D8157EDA2}");
template <typename K, typename V>
struct PairOf
{
using PairType = AZStd::pair<K, V>;
PairType m_pair;
explicit PairOf(const PairType& pair)
{
m_pair = pair;
}
explicit PairOf(const K& k, const V& v)
{
m_pair = PairType(k, v);
}
const PairType& ReturnPair() const
{
return m_pair;
}
void AcceptPair(const PairType& other)
{
m_pair = other;
}
void RegisterGenericType(AZ::SerializeContext& serializeContext)
{
serializeContext.RegisterGenericType<PairType>();
}
};
PairOf<bool, bool> m_pairOfBoolToBool { false, true };
PairOf<AZ::u8, AZ::u32> m_pairOfu8tou32 {1, 4};
PairOf<AZ::u16, float> m_pairOfu16toFloat {1, 0.4f};
PairOf<AZStd::string, AZ::s32> m_pairOfStringTos32 {"1", -4};
PairOf<AZStd::string, AZStd::string> m_pairOfStringToString {"one", "foo"};
PairOf<AZStd::string, MyCustomType> m_pairOfStringToCustomType{ "foo", MyCustomType() };
void Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
m_pairOfBoolToBool.RegisterGenericType(*serializeContext);
m_pairOfu8tou32.RegisterGenericType(*serializeContext);
m_pairOfu16toFloat.RegisterGenericType(*serializeContext);
m_pairOfStringTos32.RegisterGenericType(*serializeContext);
m_pairOfStringToString.RegisterGenericType(*serializeContext);
m_pairOfStringToCustomType.RegisterGenericType(*serializeContext);
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<PythonReflectionPairTypes>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test.pair")
->Method("return_pair_of_boolToBool", [](PythonReflectionPairTypes* self) { return self->m_pairOfBoolToBool.ReturnPair(); }, nullptr, "")
->Method("accept_pair_of_boolToBool", [](PythonReflectionPairTypes* self, const PairOf<bool, bool>::PairType& pair) { self->m_pairOfBoolToBool.AcceptPair(pair); }, nullptr, "")
->Method("return_pair_of_u8u32", [](PythonReflectionPairTypes* self) { return self->m_pairOfu8tou32.ReturnPair(); }, nullptr, "")
->Method("accept_pair_of_u8u32", [](PythonReflectionPairTypes* self, const PairOf<AZ::u8, AZ::u32>::PairType& pair) { self->m_pairOfu8tou32.AcceptPair(pair); }, nullptr, "")
->Method("return_pair_of_u16toFloat", [](PythonReflectionPairTypes* self) { return self->m_pairOfu16toFloat.ReturnPair(); }, nullptr, "")
->Method("accept_pair_of_u16toFloat", [](PythonReflectionPairTypes* self, const PairOf<AZ::u16, float>::PairType& pair) { self->m_pairOfu16toFloat.AcceptPair(pair); }, nullptr, "")
->Method("return_pair_of_stringTos32", [](PythonReflectionPairTypes* self) { return self->m_pairOfStringTos32.ReturnPair(); }, nullptr, "")
->Method("accept_pair_of_stringTos32", [](PythonReflectionPairTypes* self, const PairOf<AZStd::string, AZ::s32>::PairType& pair) { self->m_pairOfStringTos32.AcceptPair(pair); }, nullptr, "")
->Method("return_pair_of_stringToString", [](PythonReflectionPairTypes* self) { return self->m_pairOfStringToString.ReturnPair(); }, nullptr, "")
->Method("accept_pair_of_stringToString", [](PythonReflectionPairTypes* self, const PairOf<AZStd::string, AZStd::string>::PairType& pair) { self->m_pairOfStringToString.AcceptPair(pair); }, nullptr, "")
->Method("return_pair_of_stringToCustomType", [](PythonReflectionPairTypes* self) { return self->m_pairOfStringToCustomType.ReturnPair(); }, nullptr, "")
->Method("accept_pair_of_stringToCustomType", [](PythonReflectionPairTypes* self, const PairOf<AZStd::string, MyCustomType>::PairType& pair) { self->m_pairOfStringToCustomType.AcceptPair(pair); }, nullptr, "")
;
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonReflectionPairTests
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
};
TEST_F(PythonReflectionPairTests, SimpleTypes_Constructed)
{
enum class LogTypes
{
Skip = 0,
PairTypeTest_ConstructBoolDefault,
PairTypeTest_ConstructBoolParams,
PairTypeTest_UseConstructed
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "PairTypeTest_ConstructBoolDefault"))
{
return static_cast<int>(LogTypes::PairTypeTest_ConstructBoolDefault);
}
else if (AzFramework::StringFunc::StartsWith(message, "PairTypeTest_ConstructBoolParams"))
{
return static_cast<int>(LogTypes::PairTypeTest_ConstructBoolParams);
}
else if (AzFramework::StringFunc::StartsWith(message, "PairTypeTest_UseConstructed"))
{
return static_cast<int>(LogTypes::PairTypeTest_UseConstructed);
}
}
return static_cast<int>(LogTypes::Skip);
};
MyCustomType::Reflect(m_app.GetSerializeContext());
MyCustomType::Reflect(m_app.GetBehaviorContext());
PythonReflectionPairTypes pythonReflectionPairTypes;
pythonReflectionPairTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionPairTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.pair
import azlmbr.object
import azlmbr.std
test = azlmbr.object.create('PythonReflectionPairTypes')
test_pair = azlmbr.object.create('AZStd::pair<bool, bool>')
if (test_pair):
print ('PairTypeTest_ConstructBoolDefault')
test_pair = azlmbr.object.construct('AZStd::pair<bool, bool>', True, False)
if (test_pair and test_pair.first == True and test_pair.second == False):
print ('PairTypeTest_ConstructBoolParams')
test_pair.first = False
test_pair.second = True
test.accept_pair_of_boolToBool(test_pair)
result = test.return_pair_of_boolToBool()
if (len(result) == 2 and result[0] == False and result[1] == True):
print ('PairTypeTest_UseConstructed')
)");
}
catch ([[maybe_unused]] const std::exception& ex)
{
AZ_Warning("UnitTest", false, "Failed with Python exception of %s", ex.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairTypeTest_ConstructBoolDefault)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairTypeTest_ConstructBoolParams)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairTypeTest_UseConstructed)]);
}
TEST_F(PythonReflectionPairTests, SimpleTypes_ConvertedCorrectly)
{
enum class LogTypes
{
Skip = 0,
PairTypeTest_Input,
PairTypeTest_Output,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "PairTypeTest_Input"))
{
return static_cast<int>(LogTypes::PairTypeTest_Input);
}
else if (AzFramework::StringFunc::StartsWith(message, "PairTypeTest_Output"))
{
return static_cast<int>(LogTypes::PairTypeTest_Output);
}
}
return static_cast<int>(LogTypes::Skip);
};
MyCustomType::Reflect(m_app.GetSerializeContext());
MyCustomType::Reflect(m_app.GetBehaviorContext());
PythonReflectionPairTypes pythonReflectionPairTypes;
pythonReflectionPairTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionPairTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.pair
import azlmbr.object
import azlmbr.std
test = azlmbr.object.create('PythonReflectionPairTypes')
result = test.return_pair_of_u8u32()
if (len(result) == 2):
print ('PairTypeTest_Output_u8u32')
test.accept_pair_of_u8u32([42, 0])
result = test.return_pair_of_u8u32()
if (len(result) == 2 and result[0] == 42 and result[1] == 0):
print ('PairTypeTest_Input_u8u32_list')
test.accept_pair_of_u8u32((1, 2))
result = test.return_pair_of_u8u32()
if (len(result) == 2 and result[0] == 1 and result[1] == 2):
print ('PairTypeTest_Input_u8u32')
result = test.return_pair_of_u16toFloat()
if (len(result) == 2):
print ('PairTypeTest_Output_u16toFloat')
test.accept_pair_of_u16toFloat((4, -0.01))
result = test.return_pair_of_u16toFloat()
if (len(result) == 2 and result[0] == 4 and result[1] < 0):
print ('PairTypeTest_Input_u16toFloat')
result = test.return_pair_of_stringTos32()
if (len(result) == 2):
print ('PairTypeTest_Output_stringTos32')
test.accept_pair_of_stringTos32(('abc', -1))
result = test.return_pair_of_stringTos32()
if (len(result) == 2 and result[0] == 'abc' and result[1] == -1):
print ('PairTypeTest_Input_stringTos32')
result = test.return_pair_of_stringToString()
if (len(result) == 2):
print ('PairTypeTest_Output_stringToString')
test.accept_pair_of_stringToString(('one', 'two'))
result = test.return_pair_of_stringToString()
if (len(result) == 2 and result[0] == 'one' and result[1] == 'two'):
print ('PairTypeTest_Input_stringToString')
)");
}
catch ([[maybe_unused]] const std::exception& ex)
{
AZ_Warning("UnitTest", false, "Failed with Python exception of %s", ex.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(5, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairTypeTest_Input)]);
EXPECT_EQ(4, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairTypeTest_Output)]);
}
TEST_F(PythonReflectionPairTests, CustomTypes_ConvertedCorrectly)
{
enum class LogTypes
{
Skip = 0,
PairCustomTypeTest_Input,
PairCustomTypeTest_Output,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "PairCustomTypeTest_Input"))
{
return static_cast<int>(LogTypes::PairCustomTypeTest_Input);
}
else if (AzFramework::StringFunc::StartsWith(message, "PairCustomTypeTest_Output"))
{
return static_cast<int>(LogTypes::PairCustomTypeTest_Output);
}
}
return static_cast<int>(LogTypes::Skip);
};
MyCustomType::Reflect(m_app.GetSerializeContext());
MyCustomType::Reflect(m_app.GetBehaviorContext());
PythonReflectionPairTypes pythonReflectionPairTypes;
pythonReflectionPairTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionPairTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.pair
import azlmbr.object
import azlmbr.std
test = azlmbr.object.create('PythonReflectionPairTypes')
result = test.return_pair_of_stringToCustomType()
if (len(result) == 2):
print ('PairCustomTypeTest_Output_stringToCustomType')
custom = azlmbr.object.create('MyCustomType')
custom.set_data(42)
test.accept_pair_of_stringToCustomType(('def', custom))
result = test.return_pair_of_stringToCustomType()
if (len(result) == 2):
if (result[0] == 'def' and result[1].get_data() == 42):
print ('PairCustomTypeTest_Input_stringToCustomType_tuple')
)");
}
catch ([[maybe_unused]] const std::exception& ex)
{
AZ_Warning("UnitTest", false, "Failed with Python exception of %s", ex.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairCustomTypeTest_Input)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairCustomTypeTest_Output)]);
}
TEST_F(PythonReflectionPairTests, UnsupportedTypes_ErrorLogged)
{
enum class LogTypes
{
Skip = 0,
PairUnsupportedTypeTest_CannotConvert
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "Cannot convert pair container for"))
{
return static_cast<int>(LogTypes::PairUnsupportedTypeTest_CannotConvert);
}
}
return static_cast<int>(LogTypes::Skip);
};
MyCustomType::Reflect(m_app.GetSerializeContext());
MyCustomType::Reflect(m_app.GetBehaviorContext());
PythonReflectionPairTypes pythonReflectionDictionaryTypes;
pythonReflectionDictionaryTypes.Reflect(m_app.GetSerializeContext());
pythonReflectionDictionaryTypes.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.test.pair
import azlmbr.object
import azlmbr.std
test = azlmbr.object.create('PythonReflectionPairTypes')
test.accept_pair_of_u8u32([42, 0, 1])
test.accept_pair_of_u8u32({42, 0})
)");
}
catch ([[maybe_unused]] const std::exception& ex)
{
AZ_Warning("UnitTest", false, "Failed with Python exception of %s", ex.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(2, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::PairUnsupportedTypeTest_CannotConvert)]);
}
}
@@ -0,0 +1,87 @@
/*
* 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 <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
#include <Source/PythonProxyObject.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzCore/std/hash.h>
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// test class/structs
struct MyCustomType
{
AZ_TYPE_INFO(MyCustomType, "{E4BE9816-E3E0-49EA-99B0-D72403461548}");
public:
AZ::u8 m_data;
void SetData(AZ::u8 v)
{
m_data = v;
}
AZ::u8 GetData() const
{
return m_data;
}
static void Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<MyCustomType>()
->Version(1)
->Field("data", &MyCustomType::m_data)
;
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<MyCustomType>("MyCustomType")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test.pair")
->Method("set_data", &MyCustomType::SetData)
->Method("get_data", &MyCustomType::GetData)
;
}
}
};
}
// AZStd::hash specialization for UnitTest::MyCustomType, required by BehaviorContext for AZStd::pair with custom types.
template<>
struct AZStd::hash<UnitTest::MyCustomType>
{
typedef UnitTest::MyCustomType argument_type;
typedef AZStd::size_t result_type;
constexpr result_type operator()(const argument_type& value) const
{
return AZStd::hash<AZ::u8>()(value.m_data);
}
};
@@ -0,0 +1,929 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonProxyBus.h>
#include <Source/PythonProxyObject.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// test class/struts
class FakeComponentId
{
public:
AZ_TYPE_INFO(FakeComponentId, "{A0A9A069-9C3D-465A-B7AD-0D6CC803990A}");
AZ_CLASS_ALLOCATOR(FakeComponentId, AZ::SystemAllocator, 0);
FakeComponentId() = default;
bool operator==(const FakeComponentId& rhs) const { return m_id == rhs.m_id; }
bool IsValid() const { return m_id != AZ::InvalidComponentId; }
AZStd::string ToString() const { return AZStd::string::format("[%llu]", m_id); }
void Set(AZ::u64 id)
{
m_id = id;
}
AZ::ComponentId m_id = AZ::InvalidComponentId;
static void Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<FakeComponentId>()
->Version(1)
->Field("ComponentId", &FakeComponentId::m_id)
;
serializeContext->RegisterGenericType<AZStd::vector<FakeComponentId>>();
}
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<FakeComponentId>("FakeComponentId")
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::Preview)
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "entity")
->Constructor()
->Method("IsValid", &FakeComponentId::IsValid)
->Method("Equal", &FakeComponentId::operator==)
->Attribute(AZ::Script::Attributes::Operator, AZ::Script::Attributes::OperatorType::Equal)
->Method("ToString", &FakeComponentId::ToString)
->Attribute(AZ::Script::Attributes::Operator, AZ::Script::Attributes::OperatorType::ToString)
->Method("Set", &FakeComponentId::Set)
;
}
}
};
struct PythonTestBroadcastRequests
: AZ::EBusTraits
{
static const bool EnableEventQueue = true;
virtual AZ::u32 GetBits() = 0;
virtual void SetBits(AZ::u32 value) = 0;
virtual void Ping() = 0;
virtual void AcceptProxyList(const AZStd::vector<FakeComponentId>& componentIds) = 0;
};
using PythonTestBroadcastRequestBus = AZ::EBus<PythonTestBroadcastRequests>;
struct PythonTestBroadcastRequestsHandler final
: public PythonTestBroadcastRequestBus::Handler
{
PythonTestBroadcastRequestsHandler()
{
PythonTestBroadcastRequestBus::Handler::BusConnect();
}
virtual ~PythonTestBroadcastRequestsHandler()
{
PythonTestBroadcastRequestBus::Handler::BusDisconnect();
}
AZ::u32 m_bits = 0;
AZ::u32 GetBits() override
{
return m_bits;
}
void SetBits(AZ::u32 value) override
{
m_bits |= value;
}
AZ::u64 m_pingCount = 0;
void Ping() override
{
++m_pingCount;
}
void AcceptProxyList(const AZStd::vector<FakeComponentId>& componentIds) override
{
AZStd::vector<AZ::Component*> components;
for (auto componentId : componentIds)
{
if (componentId.IsValid())
{
AZ_Printf("python", "BasicRequests_AcceptProxyList:%s", componentId.ToString().c_str());
}
else
{
AZ_Warning("python", false, "AcceptProxyList failed - found invalid componentId.");
}
}
}
void Reflect(AZ::ReflectContext* context)
{
FakeComponentId::Reflect(context);
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<PythonTestBroadcastRequestBus>("PythonTestBroadcastRequestBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Event("SetBits", &PythonTestBroadcastRequestBus::Events::SetBits)
->Event("GetBits", &PythonTestBroadcastRequestBus::Events::GetBits)
->Event("Ping", &PythonTestBroadcastRequestBus::Events::Ping)
->Event("AcceptProxyList", &PythonTestBroadcastRequestBus::Events::AcceptProxyList)
;
}
}
};
//
struct PythonTestEventRequests
: AZ::EBusTraits
{
static const bool EnableEventQueue = true;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = AZ::u32;
virtual AZ::s32 Add(AZ::s32 a, AZ::s32 b) = 0;
virtual void Pong() = 0;
};
using PythonTestEventRequestBus = AZ::EBus<PythonTestEventRequests>;
struct PythonTestEventRequestsHandler final
: public PythonTestEventRequestBus::Handler
{
PythonTestEventRequestsHandler()
{
PythonTestEventRequestBus::Handler::BusConnect(101);
}
virtual ~PythonTestEventRequestsHandler()
{
PythonTestEventRequestBus::Handler::BusDisconnect();
}
AZ::s32 Add(AZ::s32 a, AZ::s32 b) override
{
return a + b;
}
AZ::u64 m_pongCount = 0;
void Pong() override
{
++m_pongCount;
}
void Reflect(AZ::ReflectContext* context)
{
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<PythonTestEventRequestBus>("PythonTestEventRequestBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Event("Add", &PythonTestEventRequestBus::Events::Add)
->Event("Pong", &PythonTestEventRequestBus::Events::Pong)
;
}
}
};
// an example of an EBus Notification bus using a single address & BusIdType=NullBusId
struct PythonTestSingleAddressNotifications
: AZ::EBusTraits
{
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
virtual ~PythonTestSingleAddressNotifications() = default;
virtual void OnPing(AZ::u64 count) = 0;
virtual void OnPong(AZ::u64 count) = 0;
virtual void MultipleInputs(AZ::u64 one, AZ::s8 two, AZStd::string_view three) = 0;
virtual AZStd::string OnAddFish(AZStd::string_view value) = 0;
};
using PythonTestSingleAddressNotificationBus = AZ::EBus<PythonTestSingleAddressNotifications>;
struct PythonTestNotificationHandler final
: public PythonTestSingleAddressNotificationBus::Handler
, public AZ::BehaviorEBusHandler
{
AZ_EBUS_BEHAVIOR_BINDER(PythonTestNotificationHandler, "{97052D15-A4E8-461B-B065-91D16E31C4F7}", AZ::SystemAllocator,
OnPing, OnPong, MultipleInputs, OnAddFish);
virtual ~PythonTestNotificationHandler() = default;
void OnPing(AZ::u64 count) override
{
Call(FN_OnPing, count);
}
void OnPong(AZ::u64 count) override
{
Call(FN_OnPong, count);
}
void MultipleInputs(AZ::u64 one, AZ::s8 two, AZStd::string_view three) override
{
Call(FN_MultipleInputs, one, two, three);
}
AZStd::string OnAddFish(AZStd::string_view value) override
{
AZStd::string result;
CallResult(result, FN_OnAddFish, value);
return result;
}
static AZ::u64 s_pongCount;
static AZ::u64 s_pingCount;
static void DoPing()
{
// notify the listeners about Ping
++s_pingCount;
PythonTestSingleAddressNotificationBus::Broadcast(&PythonTestSingleAddressNotificationBus::Events::OnPing, s_pingCount);
}
static void DoPong()
{
// notify the listeners about Pong
++s_pongCount;
PythonTestSingleAddressNotificationBus::Broadcast(&PythonTestSingleAddressNotificationBus::Events::OnPong, s_pongCount);
}
static AZStd::string DoAddFish(AZStd::string value)
{
AZStd::string result;
PythonTestSingleAddressNotificationBus::BroadcastResult(result, &PythonTestSingleAddressNotificationBus::Events::OnAddFish, value);
return result;
}
static void Reset()
{
s_pingCount = 0;
s_pongCount = 0;
}
void Reflect(AZ::ReflectContext* context)
{
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<PythonTestSingleAddressNotificationBus>("PythonTestSingleAddressNotificationBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Handler<PythonTestNotificationHandler>()
->Event("on_ping", &PythonTestSingleAddressNotificationBus::Events::OnPing)
->Event("on_pong", &PythonTestSingleAddressNotificationBus::Events::OnPong)
->Event("MultipleInputs", &PythonTestSingleAddressNotificationBus::Events::MultipleInputs)
->Event("OnAddFish", &PythonTestSingleAddressNotificationBus::Events::OnAddFish)
;
// for testing from Python to send out the events
behaviorContext->Class<PythonTestNotificationHandler>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Method("do_ping", &PythonTestNotificationHandler::DoPing)
->Method("do_pong", &PythonTestNotificationHandler::DoPong)
->Method("do_add_fish", &PythonTestNotificationHandler::DoAddFish)
;
}
}
};
AZ::u64 PythonTestNotificationHandler::s_pongCount = 0;
AZ::u64 PythonTestNotificationHandler::s_pingCount = 0;
// an example of an EBus Notification bus connecting to a bus by id
struct PythonTestByIdNotifications
: public AZ::EBusTraits
{
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
using BusIdType = AZ::s32;
virtual void OnResult(AZ::s64 result) = 0;
};
using PythonTestByIdNotificationBus = AZ::EBus<PythonTestByIdNotifications>;
struct PythonTestByIdNotificationsHandler final
: public PythonTestByIdNotificationBus::Handler
, public AZ::BehaviorEBusHandler
{
AZ_EBUS_BEHAVIOR_BINDER(PythonTestByIdNotificationsHandler, "{5F091D4B-86C4-4D25-B982-2ECAFD8AFF0F}", AZ::SystemAllocator, OnResult);
virtual ~PythonTestByIdNotificationsHandler() = default;
void OnResult(AZ::s64 result) override
{
Call(FN_OnResult, result);
}
void Reflect(AZ::ReflectContext* context)
{
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<PythonTestByIdNotificationBus>("PythonTestByIdNotificationBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Handler<PythonTestByIdNotificationsHandler>()
->Event("OnResult", &PythonTestByIdNotificationBus::Events::OnResult)
;
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixture
struct PythonBusProxyTests
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
};
//////////////////////////////////////////////////////////////////////////
// tests
TEST_F(PythonBusProxyTests, ImportEbus)
{
enum class LogTypes
{
Skip = 0,
BasicRequests_ImportEbus,
BasicRequests_ImportEbusCount,
BasicRequests_AcceptProxyList
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "BasicRequests_ImportEbus"))
{
return static_cast<int>(LogTypes::BasicRequests_ImportEbus);
}
else if (AzFramework::StringFunc::Equal(message, "BasicRequests_ImportEbusCount"))
{
return static_cast<int>(LogTypes::BasicRequests_ImportEbusCount);
}
else if (AzFramework::StringFunc::StartsWith(message, "BasicRequests_AcceptProxyList"))
{
return static_cast<int>(LogTypes::BasicRequests_AcceptProxyList);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonTestBroadcastRequestsHandler pythonTestBroadcastRequestsHandler;
pythonTestBroadcastRequestsHandler.Reflect(m_app.GetBehaviorContext());
pythonTestBroadcastRequestsHandler.Reflect(m_app.GetSerializeContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.bus
import azlmbr.entity
import azlmbr.object
eventType = azlmbr.bus.Event
if (eventType != None):
print ('BasicRequests_ImportEbus')
if len(azlmbr.bus.__dict__) > 0:
print ('BasicRequests_ImportEbusCount')
componentId101 = azlmbr.object.create('FakeComponentId')
componentId101.Set(101)
componentId102 = azlmbr.object.create('FakeComponentId')
componentId102.Set(102)
componentList = [componentId101, componentId102]
azlmbr.bus.PythonTestBroadcastRequestBus(azlmbr.bus.Broadcast, 'AcceptProxyList', componentList)
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::BasicRequests_ImportEbus)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::BasicRequests_ImportEbusCount)]);
EXPECT_EQ(2, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::BasicRequests_AcceptProxyList)]);
}
TEST_F(PythonBusProxyTests, BroadcastRequests)
{
enum class LogTypes
{
Skip = 0,
BroadcastRequests_SetBits,
BroadcastRequests_GetBits
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "BroadcastRequests_SetBits"))
{
return static_cast<int>(LogTypes::BroadcastRequests_SetBits);
}
else if (AzFramework::StringFunc::Equal(message, "BroadcastRequests_GetBits"))
{
return static_cast<int>(LogTypes::BroadcastRequests_GetBits);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonTestBroadcastRequestsHandler pythonTestBroadcastRequestsHandler;
pythonTestBroadcastRequestsHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.bus
bits = azlmbr.bus.PythonTestBroadcastRequestBus(azlmbr.bus.Broadcast, 'GetBits')
if (bits == 0):
print ('BroadcastRequests_GetBits')
azlmbr.bus.PythonTestBroadcastRequestBus(azlmbr.bus.Broadcast, 'SetBits', bits | 3)
bits = azlmbr.bus.PythonTestBroadcastRequestBus(azlmbr.bus.Broadcast, 'GetBits')
if (bits == 3):
print ('BroadcastRequests_SetBits')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::BroadcastRequests_SetBits)]);
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::BroadcastRequests_GetBits)]);
}
TEST_F(PythonBusProxyTests, QueueBroadcastRequests)
{
PythonTestBroadcastRequestsHandler pythonTestBroadcastRequestsHandler;
pythonTestBroadcastRequestsHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.bus
for i in range(2019):
azlmbr.bus.PythonTestBroadcastRequestBus(azlmbr.bus.QueueBroadcast, 'Ping')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
EXPECT_EQ(0, pythonTestBroadcastRequestsHandler.m_pingCount);
PythonTestBroadcastRequestBus::ExecuteQueuedEvents();
EXPECT_EQ(2019, pythonTestBroadcastRequestsHandler.m_pingCount);
e.Deactivate();
}
TEST_F(PythonBusProxyTests, EventRequests)
{
enum class LogTypes
{
Skip = 0,
EventRequests_Add
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "EventRequests_Add"))
{
return static_cast<int>(LogTypes::EventRequests_Add);
}
}
return static_cast<int>(LogTypes::Skip);
};
PythonTestEventRequestsHandler pythonTestEventRequestsHandler;
pythonTestEventRequestsHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.bus
import azlmbr.test
address = 101
answer = azlmbr.test.PythonTestEventRequestBus(azlmbr.bus.Event, 'Add', address, 40, 2)
if (answer == 42):
print ('EventRequests_Add')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::EventRequests_Add)]);
}
TEST_F(PythonBusProxyTests, QueueEventRequests)
{
PythonTestEventRequestsHandler pythonTestEventRequestsHandler;
pythonTestEventRequestsHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
pybind11::exec(R"(
import azlmbr.bus
import azlmbr.test
address = 101
for i in range(address * 2):
azlmbr.test.PythonTestEventRequestBus(azlmbr.bus.QueueEvent, 'Pong', address)
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
EXPECT_EQ(0, pythonTestEventRequestsHandler.m_pongCount);
PythonTestEventRequestBus::ExecuteQueuedEvents();
EXPECT_EQ(202, pythonTestEventRequestsHandler.m_pongCount);
e.Deactivate();
}
TEST_F(PythonBusProxyTests, SingleAddressNotifications)
{
PythonTestNotificationHandler pythonTestNotificationHandler;
pythonTestNotificationHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
Notifications_OnPing,
Notifications_OnPong,
Notifications_Match,
Notifications_Multi,
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "Notifications_OnPing"))
{
return static_cast<int>(LogTypes::Notifications_OnPing);
}
else if (AzFramework::StringFunc::Equal(message, "Notifications_OnPong"))
{
return static_cast<int>(LogTypes::Notifications_OnPong);
}
else if (AzFramework::StringFunc::Equal(message, "Notifications_Match"))
{
return static_cast<int>(LogTypes::Notifications_Match);
}
else if (AzFramework::StringFunc::StartsWith(message, "Notifications_Multi"))
{
return static_cast<int>(LogTypes::Notifications_Multi);
}
}
return static_cast<int>(LogTypes::Skip);
};
UnitTest::PythonTestNotificationHandler::Reset();
try
{
pybind11::exec(R"(
import azlmbr.bus
import azlmbr.test
pingCount = 0
pongCount = 0
def OnPing(parameters):
global pingCount
pingCount = parameters[0]
print ('Notifications_OnPing')
def OnPong(parameters):
global pongCount
pongCount = parameters[0]
print ('Notifications_OnPong')
def OnMultipleInputs(parameters):
if(len(parameters) == 3):
print ('Notifications_Multi1')
if(parameters[0] == 1):
print ('Notifications_Multi2')
if(parameters[1] == 2):
print ('Notifications_Multi3')
if(parameters[2] == '3'):
print ('Notifications_Multi4')
handler = azlmbr.bus.NotificationHandler('PythonTestSingleAddressNotificationBus')
handler.connect(None)
handler.add_callback('OnPing', OnPing)
handler.add_callback('OnPong', OnPong)
handler.add_callback('MultipleInputs', OnMultipleInputs)
azlmbr.test.PythonTestSingleAddressNotificationBus(azlmbr.bus.Broadcast, 'MultipleInputs', 1, 2, '3')
for i in range(40):
azlmbr.test.PythonTestNotificationHandler_do_ping()
for i in range(2):
azlmbr.test.PythonTestNotificationHandler_do_pong()
if (pingCount == 40):
print ('Notifications_Match')
if (pongCount == 2):
print ('Notifications_Match')
if ((pingCount + pongCount) == 42):
print ('Notifications_Match')
handler.disconnect()
def OnMultipleInputsAgain(parameters):
if(len(parameters) == 3):
print ('Notifications_Multi5')
if(parameters[0] == 4):
print ('Notifications_Multi6')
if(parameters[1] == 5):
print ('Notifications_Multi7')
if(parameters[2] == 'six'):
print ('Notifications_Multi8')
handler = azlmbr.test.PythonTestSingleAddressNotificationBusHandler()
handler.connect(None)
handler.add_callback('MultipleInputs', OnMultipleInputsAgain)
azlmbr.test.PythonTestSingleAddressNotificationBus(azlmbr.bus.Broadcast, 'MultipleInputs', 4, 5, 'six')
handler.disconnect()
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(40, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::Notifications_OnPing)]);
EXPECT_EQ(2, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::Notifications_OnPong)]);
EXPECT_EQ(3, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::Notifications_Match)]);
EXPECT_EQ(8, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::Notifications_Multi)]);
}
TEST_F(PythonBusProxyTests, NotificationsAtAddress)
{
PythonTestByIdNotificationsHandler pythonTestByIdNotificationsHandler;
pythonTestByIdNotificationsHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
AtAddress_Match
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "AtAddress_Match"))
{
return static_cast<int>(LogTypes::AtAddress_Match);
}
}
return static_cast<int>(LogTypes::Skip);
};
try
{
pybind11::exec(R"(
import azlmbr.bus
import azlmbr.default
answer = 0
def OnResult(parameters):
global answer
answer = int(parameters[0])
handler = azlmbr.bus.NotificationHandler('PythonTestByIdNotificationBus')
handler.connect(101)
handler.add_callback('OnResult', OnResult)
address = 101
result = 40 + 2
azlmbr.bus.PythonTestByIdNotificationBus(azlmbr.bus.Event, 'OnResult', address, result)
if (answer == 42):
print ('AtAddress_Match')
handler.disconnect()
azlmbr.bus.PythonTestByIdNotificationBus(azlmbr.bus.Event, 'OnResult', address, 2)
if (answer == 42):
print ('AtAddress_Match')
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(2, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::AtAddress_Match)]);
}
TEST_F(PythonBusProxyTests, NotificationsWithNoAddress)
{
PythonTestNotificationHandler pythonTestNotificationHandler;
pythonTestNotificationHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
NoAddressConnect
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::Equal(message, "NoAddressConnect"))
{
return static_cast<int>(LogTypes::NoAddressConnect);
}
}
return static_cast<int>(LogTypes::Skip);
};
try
{
pybind11::exec(R"(
import azlmbr.bus
import azlmbr.test
def on_ping(args):
print('NoAddressConnect')
handler = azlmbr.test.PythonTestSingleAddressNotificationBusHandler()
handler.connect()
handler.add_callback('OnPing', on_ping)
azlmbr.test.PythonTestNotificationHandler_do_ping()
handler.disconnect()
azlmbr.test.PythonTestNotificationHandler_do_ping()
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("UnitTest", false, "Failed on with Python exception: %s", e.what());
FAIL();
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::NoAddressConnect)]);
}
TEST_F(PythonBusProxyTests, NotificationsWithResult)
{
PythonTestNotificationHandler pythonTestNotificationHandler;
pythonTestNotificationHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
enum class LogTypes
{
Skip = 0,
WithResult
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "WithResult"))
{
return aznumeric_cast<int>(LogTypes::WithResult);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
try
{
pybind11::exec(R"(
import azlmbr.bus
import azlmbr.test
def on_add_fish(args):
value = args[0] + 'fish'
return value
handler = azlmbr.test.PythonTestSingleAddressNotificationBusHandler()
handler.connect()
handler.add_callback('OnAddFish', on_add_fish)
babblefish = azlmbr.test.PythonTestNotificationHandler_do_add_fish('babble')
if (babblefish == 'babblefish'):
print('WithResult_babblefish')
handler.disconnect()
)");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed on with Python exception: %s", e.what());
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[static_cast<int>(LogTypes::WithResult)]);
}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,155 @@
/*
* 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/ComponentApplication.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/Application/Application.h>
#include <AzFramework/CommandLine/CommandRegistrationBus.h>
#include <AzQtComponents/Utilities/QtPluginPaths.h>
#include <QCoreApplication>
#include <AzTest/AzTest.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
#include <Source/PythonSystemComponent.h>
#include <Source/PythonReflectionComponent.h>
#include <Source/PythonMarshalComponent.h>
namespace UnitTest
{
struct CommandRegistrationBusSupression
: public AzFramework::CommandRegistrationBus::Handler
{
CommandRegistrationBusSupression()
{
BusConnect();
}
~CommandRegistrationBusSupression()
{
BusDisconnect();
}
bool RegisterCommand(AZStd::string_view, AZStd::string_view, AZ::u32, AzFramework::CommandFunction) override
{
return true;
}
bool UnregisterCommand(AZStd::string_view) override
{
return true;
}
};
struct PythonTestingFixture
: public ::testing::Test
, protected AzFramework::ApplicationRequests::Bus::Handler
{
class FileIOHelper
{
public:
AZ::IO::LocalFileIO m_fileIO;
AZ::IO::FileIOBase* m_prevFileIO;
FileIOHelper()
{
m_prevFileIO = AZ::IO::FileIOBase::GetInstance();
AZ::IO::FileIOBase::SetInstance(&m_fileIO);
}
~FileIOHelper()
{
AZ::IO::FileIOBase::SetInstance(m_prevFileIO);
}
};
void SetUp() override
{
// fetch the Engine Root folder
{
int argc = 0;
char** argv = nullptr;
QCoreApplication qtApp(argc, argv);
azsnprintf(m_engineRoot, sizeof(m_engineRoot), AzQtComponents::FindEngineRootDir(nullptr).toLocal8Bit().data());
}
m_fileIOHelper = AZStd::make_unique<FileIOHelper>();
m_fileIOHelper->m_fileIO.SetAlias("@devroot@", m_engineRoot);
m_fileIOHelper->m_fileIO.SetAlias("@engroot@", m_engineRoot);
AzFramework::Application::Descriptor appDesc;
appDesc.m_enableDrilling = false;
m_app.Create(appDesc);
AzFramework::ApplicationRequests::Bus::Handler::BusConnect();
}
void TearDown() override
{
AzFramework::ApplicationRequests::Bus::Handler::BusDisconnect();
m_commandRegistrationBusSupression.reset();
m_fileIOHelper.reset();
m_app.Destroy();
}
void SimulateEditorBecomingInitialized(bool useCommandRegistrationBusSupression = true)
{
if (useCommandRegistrationBusSupression)
{
m_commandRegistrationBusSupression = AZStd::make_unique<CommandRegistrationBusSupression>();
}
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (editorPythonEventsInterface)
{
editorPythonEventsInterface->StartPython();
}
}
void RegisterComponentDescriptors()
{
m_app.RegisterComponentDescriptor(EditorPythonBindings::PythonSystemComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(EditorPythonBindings::PythonReflectionComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(EditorPythonBindings::PythonMarshalComponent::CreateDescriptor());
}
void Activate(AZ::Entity& e)
{
e.CreateComponent<EditorPythonBindings::PythonSystemComponent>();
e.CreateComponent<EditorPythonBindings::PythonReflectionComponent>();
e.CreateComponent<EditorPythonBindings::PythonMarshalComponent>();
e.Init();
e.Activate();
}
//////////////////////////////////////////////////////////////////////////
// AzFramework::ApplicationRequests::Bus::Handler
// required pure virtual overrides
void NormalizePath(AZStd::string& ) override {}
void NormalizePathKeepCase(AZStd::string& ) override {}
void CalculateBranchTokenForAppRoot(AZStd::string& ) const override {}
// Gets the engine root path for testing
const char* GetEngineRoot() const override { return m_engineRoot; }
// Retrieves the app root path for testing
const char* GetAppRoot() const override { return m_engineRoot; }
AZ::ComponentApplication m_app;
AZStd::unique_ptr<FileIOHelper> m_fileIOHelper;
AZStd::unique_ptr<CommandRegistrationBusSupression> m_commandRegistrationBusSupression;
char m_engineRoot[1024];
};
}
@@ -0,0 +1,224 @@
/*
* 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/PythonCommon.h>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>
#include "PythonTraceMessageSink.h"
#include "PythonTestingUtility.h"
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
namespace UnitTest
{
//////////////////////////////////////////////////////////////////////////
// behavior
struct PythonThreadNotifications
: public AZ::EBusTraits
{
virtual AZ::s64 OnNotification(AZ::s64 value) = 0;
};
using PythonThreadNotificationBus = AZ::EBus<PythonThreadNotifications>;
struct PythonThreadNotificationBusHandler final
: public PythonThreadNotificationBus::Handler
, public AZ::BehaviorEBusHandler
{
AZ_EBUS_BEHAVIOR_BINDER(PythonThreadNotificationBusHandler, "{CADEF35D-D88C-4DE0-B5FC-A88D383C124E}", AZ::SystemAllocator,
OnNotification);
virtual ~PythonThreadNotificationBusHandler() = default;
AZ::s64 OnNotification(AZ::s64 value) override
{
AZ::s64 result = 0;
CallResult(result, FN_OnNotification, value);
return result;
}
void Reflect(AZ::ReflectContext* context)
{
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<PythonThreadNotificationBus>("PythonThreadNotificationBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
->Attribute(AZ::Script::Attributes::Module, "test")
->Handler<PythonThreadNotificationBusHandler>()
->Event("OnNotification", &PythonThreadNotificationBus::Events::OnNotification)
;
}
}
};
//////////////////////////////////////////////////////////////////////////
// fixtures
struct PythonThreadingTest
: public PythonTestingFixture
{
PythonTraceMessageSink m_testSink;
void SetUp() override
{
PythonTestingFixture::SetUp();
PythonTestingFixture::RegisterComponentDescriptors();
}
void TearDown() override
{
// clearing up memory
m_testSink = PythonTraceMessageSink();
PythonTestingFixture::TearDown();
}
};
//////////////////////////////////////////////////////////////////////////
// tests
TEST_F(PythonThreadingTest, PythonInterface_ThreadLogic_Runs)
{
enum class LogTypes
{
Skip = 0,
RanInThread
};
m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
{
if (AzFramework::StringFunc::Equal(window, "python"))
{
if (AzFramework::StringFunc::StartsWith(message, "RanInThread"))
{
return aznumeric_cast<int>(LogTypes::RanInThread);
}
}
return aznumeric_cast<int>(LogTypes::Skip);
};
PythonThreadNotificationBusHandler pythonThreadNotificationBusHandler;
pythonThreadNotificationBusHandler.Reflect(m_app.GetSerializeContext());
pythonThreadNotificationBusHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
// prepare handler on this thread
pybind11::exec(R"(
import azlmbr.test
def on_notification(args):
value = args[0] + 2
print ('RanInThread')
return value
handler = azlmbr.test.PythonThreadNotificationBusHandler()
handler.connect()
handler.add_callback('OnNotification', on_notification)
)");
// start thread; in thread issue notification
auto threadCallback = []()
{
AZ::s64 result = 0;
auto notificationCallback = [&result]()
{
PythonThreadNotificationBus::BroadcastResult(result, &PythonThreadNotificationBus::Events::OnNotification, 40);
};
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (editorPythonEventsInterface)
{
editorPythonEventsInterface->ExecuteWithLock(notificationCallback);
}
EXPECT_EQ(42, result);
};
AZStd::thread theThread(threadCallback);
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(100));
theThread.join();
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed during thread test with %s", e.what());
}
e.Deactivate();
EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::RanInThread)]);
}
TEST_F(PythonThreadingTest, PythonInterface_ThreadLogic_HandlesPythonException)
{
PythonThreadNotificationBusHandler pythonThreadNotificationBusHandler;
pythonThreadNotificationBusHandler.Reflect(m_app.GetSerializeContext());
pythonThreadNotificationBusHandler.Reflect(m_app.GetBehaviorContext());
AZ::Entity e;
Activate(e);
SimulateEditorBecomingInitialized();
try
{
AZ_TEST_START_TRACE_SUPPRESSION;
// prepare handler on this thread, but will throw a Python exception
pybind11::exec(R"(
import azlmbr.test
def on_notification(args):
raise NotImplementedError("boom")
handler = azlmbr.test.PythonThreadNotificationBusHandler()
handler.connect()
handler.add_callback('OnNotification', on_notification)
)");
// start thread; in thread issue notification
auto threadCallback = []()
{
AZ::s64 result = 0;
auto notificationCallback = [&result]()
{
PythonThreadNotificationBus::BroadcastResult(result, &PythonThreadNotificationBus::Events::OnNotification, 40);
};
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (editorPythonEventsInterface)
{
editorPythonEventsInterface->ExecuteWithLock(notificationCallback);
}
EXPECT_EQ(0, result);
};
AZStd::thread theThread(threadCallback);
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(100));
theThread.join();
// the Python script above raises an exception which causes two AZ_Error() message lines:
// "Python callback threw an exception NotImplementedError : boom At : <string>(6) : on_notification"
// "Python callback threw an exception TypeError : 'NoneType' object is not callable At : <string>(7) : on_notification"
AZ_TEST_STOP_TRACE_SUPPRESSION(2);
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Error("UnitTest", false, "Failed during thread test with %s", e.what());
}
e.Deactivate();
}
}
@@ -0,0 +1,89 @@
/*
* 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/Debug/TraceMessagesDrillerBus.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
namespace UnitTest
{
/** Trace message handler to track messages during tests
*/
struct PythonTraceMessageSink final
: public AZ::Debug::TraceMessageDrillerBus::Handler
, public AzToolsFramework::EditorPythonConsoleNotificationBus::Handler
{
PythonTraceMessageSink()
{
AZ::Debug::TraceMessageDrillerBus::Handler::BusConnect();
AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusConnect();
}
~PythonTraceMessageSink()
{
AZ::Debug::TraceMessageDrillerBus::Handler::BusDisconnect();
AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusDisconnect();
}
// returns an index-tag for a message type that will be counted inside of m_evaluationMap
using EvaluateMessageFunc = AZStd::function<int(const char* window, const char* message)>;
EvaluateMessageFunc m_evaluateMessage;
using EvaluationMap = AZStd::unordered_map<int, int>; // tag to count
EvaluationMap m_evaluationMap;
//////////////////////////////////////////////////////////////////////////
// TraceMessageDrillerBus
void OnPrintf(const char* window, const char* message) override
{
OnOutput(window, message);
}
void OnOutput(const char* window, const char* message) override
{
if (m_evaluateMessage)
{
int key = m_evaluateMessage(window, message);
if (key != 0)
{
auto entryIt = m_evaluationMap.find(key);
if (m_evaluationMap.end() == entryIt)
{
m_evaluationMap[key] = 1;
}
else
{
m_evaluationMap[key] = m_evaluationMap[key] + 1;
}
}
}
}
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorPythonConsoleNotificationBus
void OnTraceMessage([[maybe_unused]] AZStd::string_view message) override
{
AZ_TracePrintf("python", "%.*s", static_cast<int>(message.size()), message.data());
}
void OnErrorMessage([[maybe_unused]] AZStd::string_view message) override
{
AZ_Error("python", false, "%.*s", static_cast<int>(message.size()), message.data());
}
void OnExceptionMessage([[maybe_unused]] AZStd::string_view message) override
{
AZ_Error("python", false, "EXCEPTION: %.*s", static_cast<int>(message.size()), message.data());
}
};
}
@@ -0,0 +1,12 @@
"""
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,12 @@
"""
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,22 @@
"""
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.
"""
#
# does some work
#
import sys
import os
import azlmbr
import azlmbrtest
def print_entity_id(entityId):
print ('entity_id {} {}'.format(entityId.id, entityId.isValid()))
@@ -0,0 +1,21 @@
"""
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.
"""
#
# testing mulitple test modules with azlmbr
#
import azlmbr
import azlmbrtest
import do_work
def test_many_entity_id():
do_work.print_entity_id(azlmbrtest.EntityId(101))
@@ -0,0 +1,25 @@
"""
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.
"""
#
# testing Python import package
#
import sys
def test_call():
print ('test_call_hit')
class TestType:
def __init__(self, *args, **kwargs):
return super().__init__(*args, **kwargs)
def do_call(self, value):
print ('TestType.do_call.{}'.format(value))
@@ -0,0 +1,33 @@
#
# 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
Include/EditorPythonBindings/CustomTypeBindingBus.h
Include/EditorPythonBindings/EditorPythonBindingsBus.h
Include/EditorPythonBindings/EditorPythonBindingsSymbols.h
Source/PythonCommon.h
Source/PythonLogSymbolsComponent.cpp
Source/PythonLogSymbolsComponent.h
Source/PythonMarshalComponent.cpp
Source/PythonMarshalComponent.h
Source/PythonProxyBus.cpp
Source/PythonProxyBus.h
Source/PythonProxyObject.cpp
Source/PythonProxyObject.h
Source/PythonReflectionComponent.cpp
Source/PythonReflectionComponent.h
Source/PythonSymbolsBus.h
Source/PythonSystemComponent.cpp
Source/PythonSystemComponent.h
Source/PythonTypeCasters.h
Source/PythonUtility.cpp
Source/PythonUtility.h
)
@@ -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
Include/EditorPythonBindings/EditorPythonBindingsBus.h
)
@@ -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
Source/EditorPythonBindingsModule.cpp
)
@@ -0,0 +1,30 @@
#
# 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
Tests/CustomTypeBindingBusTests.cpp
Tests/EditorPythonBindingsTest.cpp
Tests/PythonAssetTypesTests.cpp
Tests/PythonAssociativeTests.cpp
Tests/PythonBindingLibTests.cpp
Tests/PythonContainerAnyTests.cpp
Tests/PythonDictionaryTests.cpp
Tests/PythonGlobalsTests.cpp
Tests/PythonLogSymbolsComponentTests.cpp
Tests/PythonPairTests.cpp
Tests/PythonPairTests.h
Tests/PythonProxyBusTests.cpp
Tests/PythonProxyObjectTests.cpp
Tests/PythonReflectionComponentTests.cpp
Tests/PythonTestingUtility.h
Tests/PythonThreadingTests.cpp
Tests/PythonTraceMessageSink.h
)