From a4e1abf6dff41319aa446d8461b3571eda4ade1d Mon Sep 17 00:00:00 2001 From: Vincent Liu <5900509+onecent1101@users.noreply.github.com> Date: Fri, 23 Apr 2021 11:34:37 -0700 Subject: [PATCH] [LYN-2470] Integrate resource mapping tool with engine python environment (#17) 1. Add support to use engine python runtime environment. Ideally we should use project executable directory which contains required qt binaries after build. But currently Qt plugins are not put under proper subdirectory which causes huge overhead of loading plugin binaries (see details in this jira https://jira.agscollab.com/browse/LYN-2669 ). The temporary solution is to use AWSCore.Editor target to create a subdirectory for our use case. 2. **Minor Fix** After migrating to engine python environment, see boto3 warning while using debug mode, seems like a existing issue for a long time https://github.com/boto/boto3/issues/454. Register a after call event to close connection 3. **Minor Fix** After migrating to engine python environment, see Qt multithread warning while using debug mode, change direct setter function call to qt signal --- Gems/AWSCore/Code/CMakeLists.txt | 1 + .../Code/Tools/ResourceMappingTool/README.md | 64 ++++++++++++----- .../controller/import_resources_controller.py | 24 +++---- .../controller/view_edit_controller.py | 33 +++++---- .../manager/controller_manager.py | 3 +- .../model/notification_label_text.py | 2 +- .../model/view_size_constants.py | 6 +- .../resource_mapping_tool.py | 46 +++++++++--- .../test_import_resources_controller.py | 25 +++---- .../controller/test_view_edit_controller.py | 26 +++---- .../unit/manager/test_controller_manager.py | 4 +- .../tests/unit/manager/test_view_manager.py | 4 +- .../unit/utils/test_environment_utils.py | 44 ++++++++++++ .../tests/unit/utils/test_file_utils.py | 35 +++++++--- .../ResourceMappingTool/utils/aws_utils.py | 19 +++-- .../utils/environment_utils.py | 70 +++++++++++++++++++ .../ResourceMappingTool/utils/file_utils.py | 22 ++++-- .../view/common_view_components.py | 5 +- .../view/import_resources_page.py | 12 ++-- .../view/view_edit_page.py | 24 +++---- 20 files changed, 343 insertions(+), 126 deletions(-) create mode 100644 Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_environment_utils.py create mode 100644 Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/environment_utils.py diff --git a/Gems/AWSCore/Code/CMakeLists.txt b/Gems/AWSCore/Code/CMakeLists.txt index 6ec0f72180..cfb646710e 100644 --- a/Gems/AWSCore/Code/CMakeLists.txt +++ b/Gems/AWSCore/Code/CMakeLists.txt @@ -70,6 +70,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( NAME AWSCore.Editor MODULE NAMESPACE Gem + OUTPUT_SUBDIRECTORY AWSCoreEditorPlugins FILES_CMAKE awscore_editor_shared_files.cmake INCLUDE_DIRECTORIES diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/README.md b/Gems/AWSCore/Code/Tools/ResourceMappingTool/README.md index 578592f77c..d90f78465b 100644 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/README.md +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/README.md @@ -1,7 +1,50 @@ # Welcome to the AWS Core Resource Mapping Tool project! -This project is set up like a standard Python project. The initialization +## Setup aws config and credential +Resource mapping tool is using boto3 to interact with aws services: + * Follow boto3 + [Configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html) to setup default aws region. + * Follow boto3 + [Credentials](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) to setup default profile or credential keys. + +Or follow **AWS CLI** configuration which can be reused by boto3 lib: + * Follow + [Quick configuration with aws configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) + +**In Progress** - Override default aws profile in resource mapping tool + +## Python Environment Setup Options +### 1. Engine python environment +In order to use engine python environment, it requires to link Qt binaries for this tool. +Follow cmake instructions to configure your project, for example: + +``` +$ cmake -B -S . -G "Visual Studio 16 2019" -DLY_3RDPARTY_PATH= -DLY_PROJECTS= +``` + +Build project with **AWSCore.Editor** target to generate required Qt binaries. +(Or use **Editor** target) + +``` +$ cmake --build --target AWSCore.Editor --config -j +``` + +Launch resource mapping tool under engine root folder: + +#### Windows +##### release mode +``` +$ python\python.cmd Gems\AWSCore\Code\Tools\ResourceMappingTool\resource_mapping_tool.py --binaries_path \bin\profile\AWSCoreEditorPlugins +``` +##### debug mode +``` +$ python\python.cmd debug Gems\AWSCore\Code\Tools\ResourceMappingTool\resource_mapping_tool.py --binaries_path \bin\debug\AWSCoreEditorPlugins +``` + + +### 2. Python virtual environment +This project is set up like a standard Python project. The initialization process also creates a virtualenv within this project, stored under the `.env` directory. To create the virtualenv it assumes that there is a `python3` (or `python` for Windows) executable in your path with access to the `venv` @@ -32,28 +75,15 @@ Once the virtualenv is activated, you can install the required dependencies. $ pip install -r requirements.txt ``` -## Setup aws config and credential -Resource mapping tool is using boto3 to interact with aws services: - * Follow boto3 - [Configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html) to setup default aws region. - * Follow boto3 - [Credentials](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) to setup default profile or credential keys. - -Or follow **AWS CLI** configuration which can be reused by boto3 lib: - * Follow - [Quick configuration with aws configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) - -**In Progress** - Override default aws profile in resource mapping tool - -## Launch Options -### 1. Launch Resource Mapping Tool from python directly +#### 2.1 Launch Options +##### 2.1.1 Launch Resource Mapping Tool from python directly At this point you can launch tool like other standard python project. ``` $ python resource_mapping_tool.py ``` -### 2. Launch Resource Mapping Tool from batch script/Editor +##### 2.1.2 Launch Resource Mapping Tool from batch script/Editor Update `resource_mapping_tool.cmd` with your virtualenv full path. * **VIRTUALENV_PATH**: Fill this variable with your virtualenv full path. diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/import_resources_controller.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/import_resources_controller.py index 1425772bec..33160b1960 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/import_resources_controller.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/import_resources_controller.py @@ -28,13 +28,12 @@ logger = logging.getLogger(__name__) class ImportResourcesController(QObject): - add_import_resources = Signal(list) + add_import_resources_sender: Signal = Signal(list) + set_notification_frame_text_sender: Signal = Signal(str) """ ImportResourcesController is the place to bind ImportResource view with its corresponding behavior - - TODO: add error handling once it is ready """ def __init__(self) -> None: @@ -44,6 +43,8 @@ class ImportResourcesController(QObject): self._view_manager: ViewManager = ViewManager.get_instance() # Initialize view and model related references self._import_resources_page: ImportResourcesPage = self._view_manager.get_import_resources_page() + self.set_notification_frame_text_sender.connect( + self._import_resources_page.notification_frame.set_frame_text_receiver) self._tree_view: ResourceTreeView = self._import_resources_page.tree_view self._proxy_model: ResourceProxyModel = self._tree_view.resource_proxy_model @@ -60,11 +61,10 @@ class ImportResourcesController(QObject): self._proxy_model.deduplicate_selected_import_resources(self._tree_view.selectedIndexes()) if unique_resources: logger.debug(f"Importing selected resources: {unique_resources} ...") - self.add_import_resources.emit(unique_resources) + self.add_import_resources_sender.emit(unique_resources) self._back_to_view_edit_page() else: - self._import_resources_page.set_notification_frame_text( - error_messages.IMPORT_RESOURCES_PAGE_NO_RESOURCES_SELECTED_ERROR_MESSAGE) + self.set_notification_frame_text_sender.emit(error_messages.IMPORT_RESOURCES_PAGE_NO_RESOURCES_SELECTED_ERROR_MESSAGE) def _start_search_resources_async(self) -> None: configuration: Configuration = self._configuration_manager.configuration @@ -77,8 +77,7 @@ class ImportResourcesController(QObject): async_worker = FunctionWorker(self._request_cfn_resources_callback, configuration.region) async_worker.signals.result.connect(self._load_cfn_resources_callback) else: - self._import_resources_page.set_notification_frame_text( - error_messages.IMPORT_RESOURCES_PAGE_SEARCH_VERSION_ERROR_MESSAGE) + self.set_notification_frame_text_sender.emit(error_messages.IMPORT_RESOURCES_PAGE_SEARCH_VERSION_ERROR_MESSAGE) return self._tree_view.reset_view() @@ -100,7 +99,7 @@ class ImportResourcesController(QObject): resources[stack_name] = resource_type_and_names return resources except RuntimeError as e: - self._import_resources_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) def _request_typed_resources_callback(self, region: str) -> List[str]: resource_type_index: int = self._import_resources_page.typed_resources_combobox.currentIndex() @@ -115,12 +114,11 @@ class ImportResourcesController(QObject): elif resource_type_index == constants.AWS_RESOURCE_S3_BUCKET_INDEX: resources = aws_utils.list_s3_buckets(region) else: - self._import_resources_page.set_notification_frame_text( - error_messages.IMPORT_RESOURCES_PAGE_RESOURCE_TYPE_ERROR_MESSAGE) + self.set_notification_frame_text_sender.emit(error_messages.IMPORT_RESOURCES_PAGE_RESOURCE_TYPE_ERROR_MESSAGE) return resources except RuntimeError as e: - self._import_resources_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) def _load_cfn_resources_callback(self, resources: Dict[str, List[BasicResourceAttributes]]) -> None: if not resources: @@ -179,7 +177,7 @@ class ImportResourcesController(QObject): def reset_page(self): """Reset import resources page to its default state""" self._tree_view.reset_view() - self._import_resources_page.hide_notification_frame() + self._import_resources_page.notification_frame.setVisible(False) self._import_resources_page.set_current_main_view_index(ImportResourcesPageConstants.TREE_VIEW_PAGE_INDEX) self._import_resources_page.typed_resources_combobox.setCurrentIndex(-1) self._import_resources_page.search_version = None diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/view_edit_controller.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/view_edit_controller.py index be7bde3b6d..aa0a74b090 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/view_edit_controller.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/view_edit_controller.py @@ -10,7 +10,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ import logging -from PySide2.QtCore import (QCoreApplication, QModelIndex, QObject, Slot) +from PySide2.QtCore import (QCoreApplication, QModelIndex, QObject, Signal, Slot) from PySide2.QtWidgets import QFileDialog from typing import (Dict, List) @@ -32,6 +32,9 @@ logger = logging.getLogger(__name__) class ViewEditController(QObject): + set_notification_frame_text_sender: Signal = Signal(str) + set_notification_page_frame_text_sender: Signal = Signal(str) + """ ViewEditController is the place to bind ViewEdit view with its corresponding behavior @@ -45,6 +48,10 @@ class ViewEditController(QObject): self._view_manager: ViewManager = ViewManager.get_instance() # Initialize view and model related references self._view_edit_page: ViewEditPage = self._view_manager.get_view_edit_page() + self.set_notification_frame_text_sender.connect( + self._view_edit_page.notification_frame.set_frame_text_receiver) + self.set_notification_page_frame_text_sender.connect( + self._view_edit_page.notification_page_frame.set_frame_text_receiver) self._table_view: ResourceTableView = self._view_edit_page.table_view self._proxy_model: ResourceProxyModel = self._table_view.resource_proxy_model @@ -78,7 +85,7 @@ class ViewEditController(QObject): return True except IOError as e: logger.exception(e) - self._view_edit_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) return False def _convert_and_load_into_model(self, config_file_name: str) -> None: @@ -92,7 +99,7 @@ class ViewEditController(QObject): resources = json_utils.convert_json_dict_to_resources(self._config_file_json_source) except (IOError, ValueError, KeyError) as e: logger.exception(e) - self._view_edit_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) self._view_edit_page.set_table_view_page_interactions_enabled(False) # load resources into model @@ -109,7 +116,7 @@ class ViewEditController(QObject): new_config_file_path, configuration.account_id, configuration.region) except IOError as e: logger.exception(e) - self._view_edit_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) return self._rescan_config_directory() @@ -145,7 +152,7 @@ class ViewEditController(QObject): self._start_search_config_files_async(new_config_directory) except RuntimeError as e: logger.exception(e) - self._view_edit_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) def _rescan_config_directory(self) -> None: configuration: Configuration = self._configuration_manager.configuration @@ -155,14 +162,14 @@ class ViewEditController(QObject): configuration.config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX) except FileNotFoundError as e: logger.exception(e) - self._view_edit_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) return self._configuration_manager.configuration.config_files = config_files self._view_edit_page.set_config_files(config_files) def _reset_page(self) -> None: - self._view_edit_page.hide_notification_frame() + self._view_edit_page.notification_frame.setVisible(False) self._view_edit_page.set_table_view_page_interactions_enabled(True) def _save_changes(self) -> None: @@ -178,14 +185,14 @@ class ViewEditController(QObject): self._proxy_model.override_all_resources_status( ResourceMappingAttributesStatus(ResourceMappingAttributesStatus.SUCCESS_STATUS_VALUE, [ResourceMappingAttributesStatus.SUCCESS_STATUS_VALUE])) - self._view_edit_page.set_notification_frame_text( + self.set_notification_frame_text_sender.emit( notification_label_text.VIEW_EDIT_PAGE_SAVING_SUCCEED_MESSAGE.format(config_file)) def _search_complete_callback(self) -> None: self._reset_page() def _start_search_config_files_async(self, config_directory: str) -> None: - self._view_edit_page.set_notification_page_text(notification_label_text.NOTIFICATION_LOADING_MESSAGE) + self.set_notification_page_frame_text_sender.emit(notification_label_text.NOTIFICATION_LOADING_MESSAGE) self._view_edit_page.set_current_main_view_index(ViewEditPageConstants.NOTIFICATION_PAGE_INDEX) self._config_file_json_source.clear() self._table_view.reset_view() @@ -202,7 +209,7 @@ class ViewEditController(QObject): config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX) except FileNotFoundError as e: logger.exception(e) - self._view_edit_page.set_notification_frame_text(str(e)) + self.set_notification_frame_text_sender.emit(str(e)) def _select_config_file(self) -> None: if self._view_edit_page.config_file_combobox.currentIndex() == -1: @@ -219,7 +226,7 @@ class ViewEditController(QObject): self._proxy_model.emit_source_model_layout_changed() else: self._view_edit_page.set_table_view_page_interactions_enabled(False) - self._view_edit_page.set_notification_frame_text( + self.set_notification_frame_text_sender.emit( error_messages.VIEW_EDIT_PAGE_READ_FROM_JSON_FAILED_WITH_UNEXPECTED_FILE_ERROR_MESSAGE.format(config_file_name)) self._view_edit_page.set_current_main_view_index(ViewEditPageConstants.TABLE_VIEW_PAGE_INDEX) @@ -262,13 +269,13 @@ class ViewEditController(QObject): invalid_details)) invalid_proxy_rows: List[int] = self._proxy_model.map_from_source_rows(list(invalid_sources.keys())) - self._view_edit_page.set_notification_frame_text( + self.set_notification_frame_text_sender.emit( error_messages.VIEW_EDIT_PAGE_SAVING_FAILED_WITH_INVALID_ROW_ERROR_MESSAGE.format(invalid_proxy_rows)) return False return True @Slot(list) - def add_import_resources(self, resources: List[BasicResourceAttributes]) -> None: + def add_import_resources_receiver(self, resources: List[BasicResourceAttributes]) -> None: resource: BasicResourceAttributes for resource in resources: resource_builder: ResourceMappingAttributesBuilder = ResourceMappingAttributesBuilder() \ diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py index 5d0a00e74e..48c45d0350 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py @@ -51,4 +51,5 @@ class ControllerManager(object): logger.info("Setting up ViewEdit and ImportResource controllers ...") self._view_edit_controller.setup() self._import_resources_controller.setup() - self._import_resources_controller.add_import_resources.connect(self._view_edit_controller.add_import_resources) + self._import_resources_controller.add_import_resources_sender.connect( + self._view_edit_controller.add_import_resources_receiver) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py index d8f1d1821c..1819e4c4ce 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py @@ -32,7 +32,7 @@ VIEW_EDIT_PAGE_SAVING_SUCCEED_MESSAGE: str = "Config file {} is saved successful IMPORT_RESOURCES_PAGE_BACK_TEXT: str = "Back" IMPORT_RESOURCES_PAGE_AWS_SEARCH_TYPE_TEXT: str = "AWS Resource Type" -IMPORT_RESOURCES_PAGE_SEARCH_TEXT: str = " Search" +IMPORT_RESOURCES_PAGE_SEARCH_TEXT: str = "Search" IMPORT_RESOURCES_PAGE_IMPORT_TEXT: str = "Import" IMPORT_RESOURCES_PAGE_SEARCH_PLACEHOLDER_TEXT: str = "Search for resources by Type or Name/ID" diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py index dea4cea773..c01d81b75c 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py @@ -25,10 +25,10 @@ VIEW_EDIT_PAGE_FOOTER_AREA_HEIGHT: int = 50 VIEW_EDIT_PAGE_MARGIN_TOPBOTTOM: int = 10 # header area -CONFIG_FILE_LABEL_WIDTH: int = 70 +CONFIG_FILE_LABEL_WIDTH: int = 65 CONFIG_FILE_COMBOBOX_WIDTH: int = 250 -CONFIG_LOCATION_LABEL_WIDTH: int = 110 -CONFIG_LOCATION_TEXT_WIDTH: int = 190 +CONFIG_LOCATION_LABEL_WIDTH: int = 100 +CONFIG_LOCATION_TEXT_WIDTH: int = 180 HEADER_AREA_SEPARATOR_WIDTH: int = 5 # center area diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py index 8febe76111..b067105ce3 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py @@ -9,32 +9,56 @@ remove or modify any license notices. This file is distributed on an "AS IS" BAS WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ +from argparse import (ArgumentParser, Namespace) import logging import sys -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QApplication - -from manager.configuration_manager import ConfigurationManager -from manager.controller_manager import ControllerManager -from manager.thread_manager import ThreadManager -from manager.view_manager import ViewManager -from style import azqtcomponents_resources +from utils import environment_utils from utils import file_utils +# arguments setup +argument_parser: ArgumentParser = ArgumentParser() +argument_parser.add_argument('--binaries_path', help='Path to QT Binaries necessary for PySide.') +argument_parser.add_argument('--debug', action='store_true', help='Execute on debug mode.') +arguments: Namespace = argument_parser.parse_args() + # logging setup -logging.basicConfig(filename="resource_mapping_tool.log", filemode='w', level=logging.INFO, +logging_level: int = logging.INFO +if arguments.debug: + logging_level = logging.DEBUG +logging_path: str = file_utils.join_path(file_utils.get_parent_directory_path(__file__), + 'resource_mapping_tool.log') +logging.basicConfig(filename=logging_path, filemode='w', level=logging_level, format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', datefmt='%H:%M:%S') logging.getLogger('boto3').setLevel(logging.CRITICAL) logging.getLogger('botocore').setLevel(logging.CRITICAL) logging.getLogger('s3transfer').setLevel(logging.CRITICAL) logging.getLogger('urllib3').setLevel(logging.CRITICAL) - logger = logging.getLogger(__name__) + if __name__ == "__main__": + if arguments.binaries_path and not environment_utils.is_qt_linked(): + logger.info("Setting up Qt environment ...") + environment_utils.setup_qt_environment(arguments.binaries_path) + + try: + logger.info("Importing tool required modules ...") + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QApplication + from manager.configuration_manager import ConfigurationManager + from manager.controller_manager import ControllerManager + from manager.thread_manager import ThreadManager + from manager.view_manager import ViewManager + from style import azqtcomponents_resources + except ImportError as e: + logger.error(f"Failed to import module [{e.name}] {e}") + environment_utils.cleanup_qt_environment() + exit(-1) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) app: QApplication = QApplication(sys.argv) + app.aboutToQuit.connect(environment_utils.cleanup_qt_environment) try: style_sheet_path: str = file_utils.join_path(file_utils.get_parent_directory_path(__file__), @@ -62,5 +86,5 @@ if __name__ == "__main__": controller_manager.setup() view_manager.show() - + sys.exit(app.exec_()) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_import_resources_controller.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_import_resources_controller.py index f2eb51ee60..eb3815e788 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_import_resources_controller.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_import_resources_controller.py @@ -62,7 +62,8 @@ class TestImportResourcesController(TestCase): self._mocked_proxy_model: MagicMock = self._mocked_tree_view.resource_proxy_model self._test_import_resources_controller: ImportResourcesController = ImportResourcesController() - self._test_import_resources_controller.add_import_resources = MagicMock() + self._test_import_resources_controller.add_import_resources_sender = MagicMock() + self._test_import_resources_controller.set_notification_frame_text_sender = MagicMock() self._test_import_resources_controller.setup() def test_reset_page_resetting_page_with_expected_state(self) -> None: @@ -116,7 +117,7 @@ class TestImportResourcesController(TestCase): self._mocked_import_resources_page.typed_resources_search_button.clicked.connect.call_args[0] mocked_call_args[0]() # triggering search_button connected function - self._mocked_import_resources_page.set_notification_frame_text.assert_called_once() + self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_tree_view.reset_view.assert_not_called() self._mocked_import_resources_page.set_current_main_view_index.assert_not_called() @@ -170,7 +171,7 @@ class TestImportResourcesController(TestCase): mock_aws_utils.list_cloudformation_stacks.assert_called_once_with( TestImportResourcesController._expected_region) mock_aws_utils.list_cloudformation_stack_resources.assert_not_called() - self._mocked_import_resources_page.set_notification_frame_text.assert_called_once() + self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_proxy_model.load_resource.assert_not_called() self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once() self._mocked_import_resources_page.set_current_main_view_index.assert_called_with( @@ -195,7 +196,7 @@ class TestImportResourcesController(TestCase): TestImportResourcesController._expected_region) mock_aws_utils.list_cloudformation_stack_resources.assert_called_once_with( TestImportResourcesController._expected_cfn_stack_name, TestImportResourcesController._expected_region) - self._mocked_import_resources_page.set_notification_frame_text.assert_called_once() + self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_proxy_model.load_resource.assert_not_called() self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once() self._mocked_import_resources_page.set_current_main_view_index.assert_called_with( @@ -258,8 +259,8 @@ class TestImportResourcesController(TestCase): self._mocked_import_resources_page.cfn_stacks_import_button.clicked.connect.call_args[0] mocked_call_args[0]() # triggering cfn_stacks_import_button connected function - self._test_import_resources_controller.add_import_resources.emit.assert_not_called() - self._mocked_import_resources_page.set_notification_frame_text.assert_called_once() + self._test_import_resources_controller.add_import_resources_sender.emit.assert_not_called() + self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once() def test_page_cfn_stacks_import_button_emit_signal_with_expected_resources_and_switch_to_expected_page(self) -> None: self._mocked_proxy_model.deduplicate_selected_import_resources.return_value = \ @@ -268,7 +269,7 @@ class TestImportResourcesController(TestCase): self._mocked_import_resources_page.cfn_stacks_import_button.clicked.connect.call_args[0] mocked_call_args[0]() # triggering cfn_stacks_import_button connected function - self._test_import_resources_controller.add_import_resources.emit.assert_called_once_with( + self._test_import_resources_controller.add_import_resources_sender.emit.assert_called_once_with( [TestImportResourcesController._expected_lambda_resource]) self._mocked_view_manager.switch_to_view_edit_page.assert_called_once() self._mocked_tree_view.reset_view.assert_called_once() @@ -329,7 +330,7 @@ class TestImportResourcesController(TestCase): mock_aws_utils.list_lambda_functions.assert_called_once_with( TestImportResourcesController._expected_region) - self._mocked_import_resources_page.set_notification_frame_text.assert_called_once() + self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_proxy_model.load_resource.assert_not_called() self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once() self._mocked_import_resources_page.set_current_main_view_index.assert_called_with( @@ -348,7 +349,7 @@ class TestImportResourcesController(TestCase): mocked_async_call_args: call = mock_thread_manager.get_instance.return_value.start.call_args[0] mocked_async_call_args[0].run() # triggering async function - self._mocked_import_resources_page.set_notification_frame_text.assert_called_once() + self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_proxy_model.load_resource.assert_not_called() self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once() self._mocked_import_resources_page.set_current_main_view_index.assert_called_with( @@ -383,8 +384,8 @@ class TestImportResourcesController(TestCase): self._mocked_import_resources_page.typed_resources_import_button.clicked.connect.call_args[0] mocked_call_args[0]() # triggering typed_resources_import_button connected function - self._test_import_resources_controller.add_import_resources.emit.assert_not_called() - self._mocked_import_resources_page.set_notification_frame_text.assert_called_once() + self._test_import_resources_controller.add_import_resources_sender.emit.assert_not_called() + self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once() def test_page_typed_resources_import_button_emit_signal_with_expected_resources_and_switch_to_expected_page(self) -> None: self._mocked_proxy_model.deduplicate_selected_import_resources.return_value = \ @@ -393,7 +394,7 @@ class TestImportResourcesController(TestCase): self._mocked_import_resources_page.typed_resources_import_button.clicked.connect.call_args[0] mocked_call_args[0]() # triggering typed_resources_import_button connected function - self._test_import_resources_controller.add_import_resources.emit.assert_called_once_with( + self._test_import_resources_controller.add_import_resources_sender.emit.assert_called_once_with( [TestImportResourcesController._expected_lambda_resource]) self._mocked_view_manager.switch_to_view_edit_page.assert_called_once() self._mocked_tree_view.reset_view.assert_called_once() diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_view_edit_controller.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_view_edit_controller.py index 5442ef87a7..86bb9bd227 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_view_edit_controller.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/controller/test_view_edit_controller.py @@ -62,10 +62,12 @@ class TestViewEditController(TestCase): self._mocked_proxy_model: MagicMock = self._mocked_table_view.resource_proxy_model self._test_view_edit_controller: ViewEditController = ViewEditController() + self._test_view_edit_controller.set_notification_frame_text_sender = MagicMock() + self._test_view_edit_controller.set_notification_page_frame_text_sender = MagicMock() self._test_view_edit_controller.setup() def test_add_import_resources_expected_resource_gets_loaded_into_model(self) -> None: - self._test_view_edit_controller.add_import_resources([TestViewEditController._expected_resource]) + self._test_view_edit_controller.add_import_resources_receiver([TestViewEditController._expected_resource]) self._mocked_proxy_model.add_resource.assert_called_once() mocked_call_args: call = self._mocked_proxy_model.add_resource.call_args[0] # mock call args index is 0 @@ -128,7 +130,7 @@ class TestViewEditController(TestCase): self._mocked_proxy_model.emit_source_model_layout_changed.assert_not_called() self._mocked_proxy_model.load_resource.assert_not_called() self._mocked_view_edit_page.set_table_view_page_interactions_enabled.assert_called_with(False) - self._mocked_view_edit_page.set_notification_frame_text.assert_called_once() + self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_view_edit_page.set_current_main_view_index.assert_called_with( ViewEditPageConstants.TABLE_VIEW_PAGE_INDEX) @@ -189,7 +191,7 @@ class TestViewEditController(TestCase): mock_json_utils.validate_json_dict_according_to_json_schema.assert_called_once_with({}) mock_json_utils.convert_json_dict_to_resources.assert_not_called() self._mocked_proxy_model.load_resource.assert_not_called() - self._mocked_view_edit_page.set_notification_frame_text.assert_called_once() + self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_view_edit_page.set_table_view_page_interactions_enabled.assert_called_with(False) @patch("controller.view_edit_controller.file_utils") @@ -239,7 +241,7 @@ class TestViewEditController(TestCase): self._mocked_view_edit_page.config_file_combobox.currentText.assert_called_once() self._mocked_table_view.reset_view.assert_called_once() self._mocked_proxy_model.override_resource_status.assert_called_once() - self._mocked_view_edit_page.set_notification_frame_text.assert_called_once() + self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_proxy_model.emit_source_model_layout_changed.assert_has_calls([call(), call()]) self._mocked_view_edit_page.set_current_main_view_index.assert_called_with( ViewEditPageConstants.TABLE_VIEW_PAGE_INDEX) @@ -275,7 +277,7 @@ class TestViewEditController(TestCase): mocked_call_args[0]() # triggering config_location_button connected function mock_file_dialog.getExistingDirectory.assert_called_once() - self._mocked_view_edit_page.set_notification_page_text.assert_called_with( + self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_with( notification_label_text.NOTIFICATION_LOADING_MESSAGE) self._mocked_view_edit_page.set_current_main_view_index.assert_called_with( ViewEditPageConstants.NOTIFICATION_PAGE_INDEX) @@ -303,7 +305,7 @@ class TestViewEditController(TestCase): expected_new_config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX) assert self._mocked_configuration_manager.configuration.config_files == [] self._mocked_view_edit_page.set_config_files.assert_called_with([]) - self._mocked_view_edit_page.set_notification_page_text.assert_called_once_with( + self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_once_with( notification_label_text.NOTIFICATION_LOADING_MESSAGE) @patch("controller.view_edit_controller.ThreadManager") @@ -325,7 +327,7 @@ class TestViewEditController(TestCase): expected_new_config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX) assert self._mocked_configuration_manager.configuration.config_files == [] self._mocked_view_edit_page.set_config_files.assert_called_with([]) - self._mocked_view_edit_page.set_notification_page_text.assert_called_once_with( + self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_once_with( notification_label_text.NOTIFICATION_LOADING_MESSAGE) @patch("controller.view_edit_controller.ThreadManager") @@ -348,7 +350,7 @@ class TestViewEditController(TestCase): expected_new_config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX) assert self._mocked_configuration_manager.configuration.config_files == expected_new_config_files self._mocked_view_edit_page.set_config_files.assert_called_with(expected_new_config_files) - self._mocked_view_edit_page.set_notification_page_text.assert_called_once_with( + self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_once_with( notification_label_text.NOTIFICATION_LOADING_MESSAGE) def test_page_add_row_button_expected_resource_gets_loaded_into_model(self) -> None: @@ -395,7 +397,7 @@ class TestViewEditController(TestCase): mocked_call_args[0]() # triggering save_changes_button connected function self._mocked_proxy_model.override_resource_status.assert_called_once() - self._mocked_view_edit_page.set_notification_frame_text.assert_called_once() + self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_proxy_model.override_all_resources_status.assert_not_called() @patch("controller.view_edit_controller.json_utils") @@ -451,7 +453,7 @@ class TestViewEditController(TestCase): mock_json_utils.convert_resources_to_json_dict.assert_called_once() mock_json_utils.write_into_json_file.assert_called_once_with( TestViewEditController._expected_config_file_full_path, expected_json_dict) - self._mocked_view_edit_page.set_notification_frame_text.assert_called_once() + self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once() self._mocked_proxy_model.override_all_resources_status.assert_not_called() def test_page_search_filter_input_invoke_proxy_model_with_expected_filter_text(self) -> None: @@ -498,7 +500,7 @@ class TestViewEditController(TestCase): mock_file_utils.join_path.assert_called_once() mock_json_utils.create_empty_resource_mapping_file.assert_called_once() mock_file_utils.find_files_with_suffix_under_directory.assert_not_called() - self._mocked_view_edit_page.set_notification_frame_text.assert_called_once() + self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once() @patch("controller.view_edit_controller.file_utils") def test_page_rescan_button_post_notification_when_find_files_throw_exception( @@ -509,4 +511,4 @@ class TestViewEditController(TestCase): mocked_call_args[0]() # triggering rescan_button connected function mock_file_utils.find_files_with_suffix_under_directory.assert_called_once() - self._mocked_view_edit_page.set_notification_frame_text.assert_called_once() + self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once() diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py index 3a9b368bfd..93e49415ca 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py @@ -55,5 +55,5 @@ class TestControllerManager(TestCase): TestControllerManager._expected_controller_manager.setup() mocked_view_edit_controller.setup.assert_called_once() mocked_import_resources_controller.setup.assert_called_once() - mocked_import_resources_controller.add_import_resources.connect.assert_called_once_with( - mocked_view_edit_controller.add_import_resources) + mocked_import_resources_controller.add_import_resources_sender.connect.assert_called_once_with( + mocked_view_edit_controller.add_import_resources_receiver) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py index 12e9a9e6be..eb8304182f 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py @@ -18,7 +18,7 @@ from manager.view_manager import (ViewManager, ViewManagerConstants) class TestViewManager(TestCase): """ - ThreadManager unit test cases + ViewManager unit test cases """ _mock_import_resources_page: MagicMock _mock_view_edit_page: MagicMock @@ -36,6 +36,8 @@ class TestViewManager(TestCase): main_window_patcher: patch = patch("manager.view_manager.QMainWindow") cls._mock_main_window = main_window_patcher.start() + window_icon_patcher: patch = patch("manager.view_manager.QPixmap") + window_icon_patcher.start() stacked_pages_patcher: patch = patch("manager.view_manager.QStackedWidget") cls._mock_stacked_pages = stacked_pages_patcher.start() diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_environment_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_environment_utils.py new file mode 100644 index 0000000000..7f7892bf00 --- /dev/null +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_environment_utils.py @@ -0,0 +1,44 @@ +""" +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. +""" + +from typing import List +from unittest import TestCase +from unittest.mock import (ANY, call, MagicMock, patch) + +from model import constants +from model.basic_resource_attributes import (BasicResourceAttributes, BasicResourceAttributesBuilder) +from utils import environment_utils + + +class TestEnvironmentUtils(TestCase): + """ + environment utils unit test cases + """ + def setUp(self) -> None: + os_environ_patcher: patch = patch("os.environ") + self.addCleanup(os_environ_patcher.stop) + self._mock_os_environ: MagicMock = os_environ_patcher.start() + + os_pathsep_patcher: patch = patch("os.pathsep") + self.addCleanup(os_pathsep_patcher.stop) + self._mock_os_pathsep: MagicMock = os_pathsep_patcher.start() + + def test_setup_qt_environment_global_flag_is_set(self) -> None: + environment_utils.setup_qt_environment("dummy") + self._mock_os_environ.copy.assert_called_once() + self._mock_os_pathsep.join.assert_called_once() + assert environment_utils.is_qt_linked() is True + + def test_cleanup_qt_environment_global_flag_is_set(self) -> None: + environment_utils.setup_qt_environment("dummy") + assert environment_utils.is_qt_linked() is True + environment_utils.cleanup_qt_environment() + assert environment_utils.is_qt_linked() is False diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py index 70c45f3622..f7361150ea 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py @@ -30,10 +30,6 @@ class TestFileUtils(TestCase): self.addCleanup(path_patcher.stop) self._mock_path: MagicMock = path_patcher.start() - windows_path_patcher: patch = patch("pathlib.WindowsPath") - self.addCleanup(windows_path_patcher.stop) - self._mock_windows_path: MagicMock = windows_path_patcher.start() - def test_check_path_exists_returns_true(self) -> None: mocked_path: MagicMock = self._mock_path.return_value mocked_path.exists.return_value = True @@ -105,12 +101,35 @@ class TestFileUtils(TestCase): assert not actual_files def test_join_path_return_expected_result(self) -> None: - mocked_windows_path: MagicMock = self._mock_windows_path.return_value + mocked_path: MagicMock = self._mock_path.return_value expected_join_path_name: str = f"{TestFileUtils._expected_path_name}{TestFileUtils._expected_file_name}" - mocked_windows_path.joinpath.return_value = expected_join_path_name + mocked_path.joinpath.return_value = expected_join_path_name actual_join_path_name: str = file_utils.join_path(TestFileUtils._expected_path_name, TestFileUtils._expected_file_name) - self._mock_windows_path.assert_called_once_with(TestFileUtils._expected_path_name) - mocked_windows_path.joinpath.assert_called_once_with(TestFileUtils._expected_file_name) + self._mock_path.assert_called_once_with(TestFileUtils._expected_path_name) + mocked_path.joinpath.assert_called_once_with(TestFileUtils._expected_file_name) assert actual_join_path_name == expected_join_path_name + + def test_normalize_file_path_return_empty_when_input_is_empty(self) -> None: + actual_normalized_path: str = file_utils.normalize_file_path("") + assert actual_normalized_path == "" + + def test_normalize_file_path_return_expected_result(self) -> None: + mocked_path: MagicMock = self._mock_path.return_value + expected_resolve_path: str = TestFileUtils._expected_path_name + mocked_path.resolve.return_value = expected_resolve_path + + actual_resolve_path: str = file_utils.normalize_file_path("dummy") + self._mock_path.assert_called_once() + mocked_path.resolve.assert_called_once() + assert actual_resolve_path == expected_resolve_path + + def test_normalize_file_path_return_empty_when_exception_raised(self) -> None: + mocked_path: MagicMock = self._mock_path.return_value + mocked_path.resolve.side_effect = RuntimeError() + + actual_resolve_path: str = file_utils.normalize_file_path("dummy") + self._mock_path.assert_called_once() + mocked_path.resolve.assert_called_once() + assert actual_resolve_path == "" diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/aws_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/aws_utils.py index b05ac08575..329d3ff44e 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/aws_utils.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/aws_utils.py @@ -44,15 +44,26 @@ class AWSConstants(object): S3_SERVICE_NAME: str = "s3" +def _close_client_connection(client: BaseClient) -> None: + session: boto3.session.Session = client._endpoint.http_session + managers: List[object] = [session._manager, *session._proxy_managers.values()] + for manager in managers: + manager.clear() + + def _initialize_boto3_aws_client(service: str, region: str = "") -> BaseClient: if region: - return boto3.client(service, region_name=region) + boto3_client: BaseClient = boto3.client(service, region_name=region) else: - return boto3.client(service) + boto3_client: BaseClient = boto3.client(service) + boto3_client.meta.events.register( + f"after-call.{service}.*", lambda **kwargs: _close_client_connection(boto3_client) + ) + return boto3_client def get_default_account_id() -> str: - sts_client: BaseClient = boto3.client(AWSConstants.STS_SERVICE_NAME) + sts_client: BaseClient = _initialize_boto3_aws_client(AWSConstants.STS_SERVICE_NAME) try: return sts_client.get_caller_identity()["Account"] except ClientError as error: @@ -65,7 +76,7 @@ def get_default_region() -> str: if region: return region - sts_client: BaseClient = boto3.client(AWSConstants.STS_SERVICE_NAME) + sts_client: BaseClient = _initialize_boto3_aws_client(AWSConstants.STS_SERVICE_NAME) region = sts_client.meta.region_name if region: return region diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/environment_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/environment_utils.py new file mode 100644 index 0000000000..040d95829f --- /dev/null +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/environment_utils.py @@ -0,0 +1,70 @@ +""" +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. +""" + +import logging +import os +from typing import Dict + +from utils import file_utils + +""" +Environment Utils provide functions to setup python environment libs for resource mapping tool +""" +logger = logging.getLogger(__name__) + +qt_binaries_linked: bool = False +old_os_env: Dict[str, str] = os.environ.copy() + + +def setup_qt_environment(bin_path: str) -> None: + """ + Setup Qt binaries for o3de python runtime environment + :param bin_path: The path of Qt binaries + """ + if is_qt_linked(): + logger.info("Qt binaries have already been linked, skip Qt setup") + return + global old_os_env + old_os_env = os.environ.copy() + binaries_path: str = file_utils.normalize_file_path(bin_path) + os.environ["QT_PLUGIN_PATH"] = binaries_path + + path = os.environ['PATH'] + + new_path = os.pathsep.join([binaries_path, path]) + os.environ['PATH'] = new_path + + global qt_binaries_linked + qt_binaries_linked = True + + +def is_qt_linked() -> bool: + """ + Check whether Qt binaries have been linked in o3de python runtime environment + :return: True if Qt binaries have been linked; False if not + """ + return qt_binaries_linked + + +def cleanup_qt_environment() -> None: + """ + Clean up the linked Qt binaries in o3de python runtime environment + """ + if not is_qt_linked(): + logger.info("Qt binaries have not been linked, skip Qt uninstall") + return + global old_os_env + if old_os_env.get("QT_PLUGIN_PATH"): + old_os_env.pop("QT_PLUGIN_PATH") + os.environ = old_os_env + + global qt_binaries_linked + qt_binaries_linked = False diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py index a320f2ac1b..365d25834e 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py @@ -20,8 +20,8 @@ path, check file existence, etc logger = logging.getLogger(__name__) -def check_path_exists(full_path: str) -> bool: - return pathlib.Path(full_path).exists() +def check_path_exists(file_path: str) -> bool: + return pathlib.Path(file_path).exists() def get_current_directory_path() -> str: @@ -42,8 +42,16 @@ def find_files_with_suffix_under_directory(dir_path: str, suffix: str) -> List[s if matched_path.is_file(): results.append(str(matched_path.name)) return results - - -def join_path(dir_path: str, file_name: str) -> str: - # TODO: expand usage to support Mac and Linux - return str(pathlib.WindowsPath(dir_path).joinpath(file_name)) + + +def normalize_file_path(file_path: str) -> str: + if file_path: + try: + return str(pathlib.Path(file_path).resolve(True)) + except (FileNotFoundError, RuntimeError): + logger.warning(f"Failed to normalize file path {file_path}, return empty string instead") + return "" + + +def join_path(this_path: str, other_path: str) -> str: + return str(pathlib.Path(this_path).joinpath(other_path)) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py index f5ac5bf436..8aad0a785c 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py @@ -9,6 +9,7 @@ remove or modify any license notices. This file is distributed on an "AS IS" BAS WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ +from PySide2.QtCore import Slot from PySide2.QtGui import (QIcon, QPixmap) from PySide2.QtWidgets import (QFrame, QHBoxLayout, QLabel, QLayout, QLineEdit, QPushButton, QSizePolicy, QWidget) @@ -58,5 +59,7 @@ class NotificationFrame(QFrame): self.setVisible(False) - def set_text(self, text: str) -> None: + @Slot(str) + def set_frame_text_receiver(self, text: str) -> None: self._title_label.setText(text) + self.setVisible(True) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/import_resources_page.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/import_resources_page.py index 004eeb1f6b..15af8e6aad 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/import_resources_page.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/import_resources_page.py @@ -136,7 +136,6 @@ class ImportResourcesPage(QWidget): self._back_button.setObjectName("Secondary") self._back_button.setText(f" {notification_label_text.IMPORT_RESOURCES_PAGE_BACK_TEXT}") self._back_button.setIcon(QIcon(":/Breadcrumb/img/UI20/Breadcrumb/arrow_left-default.svg")) - self._back_button.setFlat(True) self._back_button.setMinimumSize(view_size_constants.BACK_BUTTON_WIDTH, view_size_constants.INTERACTION_COMPONENT_HEIGHT) header_area_layout.addWidget(self._back_button) @@ -325,6 +324,10 @@ class ImportResourcesPage(QWidget): def search_version(self) -> str: return self._search_version + @property + def notification_frame(self) -> NotificationFrame: + return self._notification_frame + @search_version.setter def search_version(self, new_search_version: str) -> None: self._search_version = new_search_version @@ -343,10 +346,3 @@ class ImportResourcesPage(QWidget): def set_current_main_view_index(self, index: int) -> None: """Switch main view page based on given index""" self._stacked_pages.setCurrentIndex(index) - - def hide_notification_frame(self) -> None: - self._notification_frame.setVisible(False) - - def set_notification_frame_text(self, text: str) -> None: - self._notification_frame.set_text(text) - self._notification_frame.setVisible(True) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/view_edit_page.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/view_edit_page.py index cb082b428b..8e43b1a348 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/view_edit_page.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/view_edit_page.py @@ -410,6 +410,14 @@ class ViewEditPage(QWidget): def rescan_button(self) -> QPushButton: return self._rescan_button + @property + def notification_frame(self) -> NotificationFrame: + return self._notification_frame + + @property + def notification_page_frame(self) -> NotificationFrame: + return self._notification_page_frame + def set_current_main_view_index(self, index: int) -> None: """Switch main view page based on given index""" if index == ViewEditPageConstants.NOTIFICATION_PAGE_INDEX: @@ -436,11 +444,13 @@ class ViewEditPage(QWidget): self._config_file_combobox.setCurrentIndex(-1) if config_files: - self._notification_page_frame.set_text(notification_label_text.VIEW_EDIT_PAGE_SELECT_CONFIG_FILE_MESSAGE) + self._notification_page_frame.set_frame_text_receiver( + notification_label_text.VIEW_EDIT_PAGE_SELECT_CONFIG_FILE_MESSAGE) self._create_new_button.setVisible(False) self._rescan_button.setVisible(False) else: - self._notification_page_frame.set_text(notification_label_text.VIEW_EDIT_PAGE_NO_CONFIG_FILE_FOUND_MESSAGE) + self._notification_page_frame.set_frame_text_receiver( + notification_label_text.VIEW_EDIT_PAGE_NO_CONFIG_FILE_FOUND_MESSAGE) self._create_new_button.setVisible(True) self._rescan_button.setVisible(True) @@ -455,16 +465,6 @@ class ViewEditPage(QWidget): self._config_location_text.setText(elided_text) self._config_location_text.setToolTip(config_location) - def set_notification_page_text(self, text: str) -> None: - self._notification_page_frame.set_text(text) - - def hide_notification_frame(self) -> None: - self._notification_frame.setVisible(False) - - def set_notification_frame_text(self, text: str) -> None: - self._notification_frame.set_text(text) - self._notification_frame.setVisible(True) - def set_table_view_page_interactions_enabled(self, enabled: bool) -> None: self._table_view_page.setEnabled(enabled) self._save_changes_button.setEnabled(enabled)