Integrating latest 47acbe8
This commit is contained in:
@@ -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
|
||||
|
||||
+27
@@ -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)
|
||||
+15
@@ -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
|
||||
)
|
||||
+15
@@ -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
|
||||
)
|
||||
+15
@@ -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
|
||||
)
|
||||
+15
@@ -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
|
||||
)
|
||||
|
||||
+57
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user