diff --git a/cura/API/Account.py b/cura/API/Account.py index 93738a78e9..bc1ce8c2b9 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -1,15 +1,18 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict +from typing import Optional, Dict, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty +from UM.i18n import i18nCatalog from UM.Message import Message + from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.Models import OAuth2Settings -from UM.Application import Application -from UM.i18n import i18nCatalog +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + i18n_catalog = i18nCatalog("cura") @@ -26,8 +29,9 @@ class Account(QObject): # Signal emitted when user logged in or out. loginStateChanged = pyqtSignal(bool) - def __init__(self, parent = None) -> None: + def __init__(self, application: "CuraApplication", parent = None) -> None: super().__init__(parent) + self._application = application self._error_message = None # type: Optional[Message] self._logged_in = False @@ -47,7 +51,11 @@ class Account(QObject): AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root) ) - self._authorization_service = AuthorizationService(Application.getInstance().getPreferences(), self._oauth_settings) + self._authorization_service = AuthorizationService(self._oauth_settings) + + def initialize(self) -> None: + self._authorization_service.initialize(self._application.getPreferences()) + self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged) self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged) self._authorization_service.loadAuthDataFromPreferences() diff --git a/cura/API/Backups.py b/cura/API/Backups.py index f31933c844..8e5cd7b83a 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -1,9 +1,12 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Tuple, Optional +from typing import Tuple, Optional, TYPE_CHECKING from cura.Backups.BackupsManager import BackupsManager +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + ## The back-ups API provides a version-proof bridge between Cura's # BackupManager and plug-ins that hook into it. @@ -13,9 +16,10 @@ from cura.Backups.BackupsManager import BackupsManager # api = CuraAPI() # api.backups.createBackup() # api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` - class Backups: - manager = BackupsManager() # Re-used instance of the backups manager. + + def __init__(self, application: "CuraApplication") -> None: + self.manager = BackupsManager(application) ## Create a new back-up using the BackupsManager. # \return Tuple containing a ZIP file with the back-up data and a dict diff --git a/cura/API/Interface/Settings.py b/cura/API/Interface/Settings.py index 2889db7022..371c40c14c 100644 --- a/cura/API/Interface/Settings.py +++ b/cura/API/Interface/Settings.py @@ -1,7 +1,11 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from cura.CuraApplication import CuraApplication +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + ## The Interface.Settings API provides a version-proof bridge between Cura's # (currently) sidebar UI and plug-ins that hook into it. @@ -19,8 +23,9 @@ from cura.CuraApplication import CuraApplication # api.interface.settings.addContextMenuItem(data)`` class Settings: - # Re-used instance of Cura: - application = CuraApplication.getInstance() # type: CuraApplication + + def __init__(self, application: "CuraApplication") -> None: + self.application = application ## Add items to the sidebar context menu. # \param menu_item dict containing the menu item to add. @@ -30,4 +35,4 @@ class Settings: ## Get all custom items currently added to the sidebar context menu. # \return List containing all custom context menu items. def getContextMenuItems(self) -> list: - return self.application.getSidebarCustomMenuItems() \ No newline at end of file + return self.application.getSidebarCustomMenuItems() diff --git a/cura/API/Interface/__init__.py b/cura/API/Interface/__init__.py index b38118949b..742254a1a4 100644 --- a/cura/API/Interface/__init__.py +++ b/cura/API/Interface/__init__.py @@ -1,9 +1,15 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import TYPE_CHECKING + from UM.PluginRegistry import PluginRegistry from cura.API.Interface.Settings import Settings +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + + ## The Interface class serves as a common root for the specific API # methods for each interface element. # @@ -20,5 +26,6 @@ class Interface: # For now we use the same API version to be consistent. VERSION = PluginRegistry.APIVersion - # API methods specific to the settings portion of the UI - settings = Settings() + def __init__(self, application: "CuraApplication") -> None: + # API methods specific to the settings portion of the UI + self.settings = Settings(application) diff --git a/cura/API/__init__.py b/cura/API/__init__.py index 54f5c1f8b0..e9aba86a41 100644 --- a/cura/API/__init__.py +++ b/cura/API/__init__.py @@ -1,5 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional, TYPE_CHECKING + from PyQt5.QtCore import QObject, pyqtProperty from UM.PluginRegistry import PluginRegistry @@ -7,6 +9,9 @@ from cura.API.Backups import Backups from cura.API.Interface import Interface from cura.API.Account import Account +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + ## The official Cura API that plug-ins can use to interact with Cura. # @@ -19,14 +24,30 @@ class CuraAPI(QObject): # For now we use the same API version to be consistent. VERSION = PluginRegistry.APIVersion - # Backups API - backups = Backups() + def __init__(self, application: "CuraApplication") -> None: + super().__init__(parent = application) + self._application = application - # Interface API - interface = Interface() + # Accounts API + self._account = Account(self._application) - _account = Account() + # Backups API + self._backups = Backups(self._application) + + # Interface API + self._interface = Interface(self._application) + + def initialize(self) -> None: + self._account.initialize() @pyqtProperty(QObject, constant = True) - def account(self) -> Account: - return CuraAPI._account + def account(self) -> "Account": + return self._account + + @property + def backups(self) -> "Backups": + return self._backups + + @property + def interface(self) -> "Interface": + return self._interface \ No newline at end of file diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index cc47df770e..82157a163a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -4,18 +4,18 @@ import io import os import re - import shutil - -from typing import Dict, Optional from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile +from typing import Dict, Optional, TYPE_CHECKING from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform from UM.Resources import Resources -from cura.CuraApplication import CuraApplication + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication ## The back-up class holds all data about a back-up. @@ -29,7 +29,8 @@ class Backup: # Re-use translation catalog. catalog = i18nCatalog("cura") - def __init__(self, zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None: + def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None: + self._application = application self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[Dict[str, str]] @@ -41,12 +42,12 @@ class Backup: Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) # Ensure all current settings are saved. - CuraApplication.getInstance().saveSettings() + self._application.saveSettings() # We copy the preferences file to the user data directory in Linux as it's in a different location there. # When restoring a backup on Linux, we move it back. if Platform.isLinux(): - preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file_name = self._application.getApplicationName() preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) @@ -112,7 +113,7 @@ class Backup: "Tried to restore a Cura backup without having proper data or meta data.")) return False - current_version = CuraApplication.getInstance().getVersion() + current_version = self._application.getVersion() version_to_restore = self.meta_data.get("cura_release", "master") if current_version != version_to_restore: # Cannot restore version older or newer than current because settings might have changed. @@ -128,7 +129,7 @@ class Backup: # Under Linux, preferences are stored elsewhere, so we copy the file to there. if Platform.isLinux(): - preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file_name = self._application.getApplicationName() preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 67e2a222f1..a0d3881209 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -1,11 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Dict, Optional, Tuple +from typing import Dict, Optional, Tuple, TYPE_CHECKING from UM.Logger import Logger from cura.Backups.Backup import Backup -from cura.CuraApplication import CuraApplication + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication ## The BackupsManager is responsible for managing the creating and restoring of @@ -13,15 +15,15 @@ from cura.CuraApplication import CuraApplication # # Back-ups themselves are represented in a different class. class BackupsManager: - def __init__(self): - self._application = CuraApplication.getInstance() + def __init__(self, application: "CuraApplication") -> None: + self._application = application ## Get a back-up of the current configuration. # \return A tuple containing a ZipFile (the actual back-up) and a dict # containing some metadata (like version). def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: self._disableAutoSave() - backup = Backup() + backup = Backup(self._application) backup.makeFromCurrent() self._enableAutoSave() # We don't return a Backup here because we want plugins only to interact with our API and not full objects. @@ -39,7 +41,7 @@ class BackupsManager: self._disableAutoSave() - backup = Backup(zip_file = zip_file, meta_data = meta_data) + backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data) restored = backup.restore() if restored: # At this point, Cura will need to restart for the changes to take effect. diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6fb79403cc..e6fa00869a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -44,6 +44,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation +from cura.API import CuraAPI from cura.Arranging.Arrange import Arrange from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob @@ -204,7 +205,7 @@ class CuraApplication(QtApplication): self._quality_profile_drop_down_menu_model = None self._custom_quality_profile_drop_down_menu_model = None - self._cura_API = None + self._cura_API = CuraAPI(self) self._physics = None self._volume = None @@ -713,6 +714,9 @@ class CuraApplication(QtApplication): default_visibility_profile = self._setting_visibility_presets_model.getItem(0) self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) + # Initialize Cura API + self._cura_API.initialize() + # Detect in which mode to run and execute that mode if self._is_headless: self.runWithoutGUI() @@ -900,10 +904,7 @@ class CuraApplication(QtApplication): self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self) return self._custom_quality_profile_drop_down_menu_model - def getCuraAPI(self, *args, **kwargs): - if self._cura_API is None: - from cura.API import CuraAPI - self._cura_API = CuraAPI() + def getCuraAPI(self, *args, **kwargs) -> "CuraAPI": return self._cura_API ## Registers objects for the QML engine to use. diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 16f525625e..65b31f1ed7 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -29,7 +29,7 @@ class AuthorizationService: # Emit signal when authentication failed. onAuthenticationError = Signal() - def __init__(self, preferences: Optional["Preferences"], settings: "OAuth2Settings") -> None: + def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None: self._settings = settings self._auth_helpers = AuthorizationHelpers(settings) self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL) @@ -38,6 +38,9 @@ class AuthorizationService: self._preferences = preferences self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True) + def initialize(self, preferences: Optional["Preferences"] = None) -> None: + if preferences is not None: + self._preferences = preferences if self._preferences: self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") diff --git a/tests/TestOAuth2.py b/tests/TestOAuth2.py index 78585804f5..608d529e9f 100644 --- a/tests/TestOAuth2.py +++ b/tests/TestOAuth2.py @@ -31,15 +31,17 @@ MALFORMED_AUTH_RESPONSE = AuthenticationResponse() def test_cleanAuthService() -> None: # Ensure that when setting up an AuthorizationService, no data is set. - authorization_service = AuthorizationService(Preferences(), OAUTH_SETTINGS) + authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) + authorization_service.initialize() assert authorization_service.getUserProfile() is None assert authorization_service.getAccessToken() is None def test_failedLogin() -> None: - authorization_service = AuthorizationService(Preferences(), OAUTH_SETTINGS) + authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock() + authorization_service.initialize() # Let the service think there was a failed response authorization_service._onAuthStateChanged(FAILED_AUTH_RESPONSE) @@ -58,7 +60,8 @@ def test_failedLogin() -> None: @patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()) def test_storeAuthData(get_user_profile) -> None: preferences = Preferences() - authorization_service = AuthorizationService(preferences, OAUTH_SETTINGS) + authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) + authorization_service.initialize() # Write stuff to the preferences. authorization_service._storeAuthData(SUCCESFULL_AUTH_RESPONSE) @@ -67,7 +70,8 @@ def test_storeAuthData(get_user_profile) -> None: assert preference_value is not None and preference_value != {} # Create a second auth service, so we can load the data. - second_auth_service = AuthorizationService(preferences, OAUTH_SETTINGS) + second_auth_service = AuthorizationService(OAUTH_SETTINGS, preferences) + second_auth_service.initialize() second_auth_service.loadAuthDataFromPreferences() assert second_auth_service.getAccessToken() == SUCCESFULL_AUTH_RESPONSE.access_token @@ -77,7 +81,7 @@ def test_storeAuthData(get_user_profile) -> None: @patch.object(webbrowser, "open_new") def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None: preferences = Preferences() - authorization_service = AuthorizationService(preferences, OAUTH_SETTINGS) + authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.startAuthorizationFlow() assert webbrowser_open.call_count == 1 @@ -92,9 +96,10 @@ def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) - def test_loginAndLogout() -> None: preferences = Preferences() - authorization_service = AuthorizationService(preferences, OAUTH_SETTINGS) + authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock() + authorization_service.initialize() # Let the service think there was a succesfull response with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()): @@ -121,7 +126,8 @@ def test_loginAndLogout() -> None: def test_wrongServerResponses() -> None: - authorization_service = AuthorizationService(Preferences(), OAUTH_SETTINGS) + authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences()) + authorization_service.initialize() with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()): authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE) - assert authorization_service.getUserProfile() is None \ No newline at end of file + assert authorization_service.getUserProfile() is None