Browse Source

Merge pull request #4441 from Ultimaker/resolve_dependencies_oauth

Resolve circular imports for CuraAPI
Jaime van Kessel 6 years ago
parent
commit
feaa10094e

+ 13 - 5
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()

+ 7 - 3
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

+ 9 - 4
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()
+        return self.application.getSidebarCustomMenuItems()

+ 9 - 2
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)

+ 28 - 7
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
+
+        # Accounts API
+        self._account = Account(self._application)
 
-    # Interface API
-    interface = Interface()
+        # Backups API
+        self._backups = Backups(self._application)
 
-    _account = Account()
+        # 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

+ 10 - 9
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)

+ 8 - 6
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.

+ 6 - 5
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.

+ 4 - 1
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, "{}")
 

+ 14 - 8
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
+    assert authorization_service.getUserProfile() is None