Merge pull request #4441 from Ultimaker/resolve_dependencies_oauth

Resolve circular imports for CuraAPI
This commit is contained in:
Jaime van Kessel 2018-10-01 15:15:14 +02:00 committed by GitHub
commit feaa10094e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 108 additions and 50 deletions

View File

@ -1,15 +1,18 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # 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 PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Message import Message from UM.Message import Message
from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings 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") i18n_catalog = i18nCatalog("cura")
@ -26,8 +29,9 @@ class Account(QObject):
# Signal emitted when user logged in or out. # Signal emitted when user logged in or out.
loginStateChanged = pyqtSignal(bool) loginStateChanged = pyqtSignal(bool)
def __init__(self, parent = None) -> None: def __init__(self, application: "CuraApplication", parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = application
self._error_message = None # type: Optional[Message] self._error_message = None # type: Optional[Message]
self._logged_in = False self._logged_in = False
@ -47,7 +51,11 @@ class Account(QObject):
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root) 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.onAuthStateChanged.connect(self._onLoginStateChanged)
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged) self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
self._authorization_service.loadAuthDataFromPreferences() self._authorization_service.loadAuthDataFromPreferences()

View File

@ -1,9 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # 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 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 ## The back-ups API provides a version-proof bridge between Cura's
# BackupManager and plug-ins that hook into it. # BackupManager and plug-ins that hook into it.
@ -13,9 +16,10 @@ from cura.Backups.BackupsManager import BackupsManager
# api = CuraAPI() # api = CuraAPI()
# api.backups.createBackup() # api.backups.createBackup()
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` # api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
class Backups: 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. ## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict # \return Tuple containing a ZIP file with the back-up data and a dict

View File

@ -1,7 +1,11 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # 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 ## The Interface.Settings API provides a version-proof bridge between Cura's
# (currently) sidebar UI and plug-ins that hook into it. # (currently) sidebar UI and plug-ins that hook into it.
@ -19,8 +23,9 @@ from cura.CuraApplication import CuraApplication
# api.interface.settings.addContextMenuItem(data)`` # api.interface.settings.addContextMenuItem(data)``
class Settings: 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. ## Add items to the sidebar context menu.
# \param menu_item dict containing the menu item to add. # \param menu_item dict containing the menu item to add.

View File

@ -1,9 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.API.Interface.Settings import Settings 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 ## The Interface class serves as a common root for the specific API
# methods for each interface element. # methods for each interface element.
# #
@ -20,5 +26,6 @@ class Interface:
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion VERSION = PluginRegistry.APIVersion
def __init__(self, application: "CuraApplication") -> None:
# API methods specific to the settings portion of the UI # API methods specific to the settings portion of the UI
settings = Settings() self.settings = Settings(application)

View File

@ -1,5 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtProperty from PyQt5.QtCore import QObject, pyqtProperty
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
@ -7,6 +9,9 @@ from cura.API.Backups import Backups
from cura.API.Interface import Interface from cura.API.Interface import Interface
from cura.API.Account import Account 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. ## 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. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion VERSION = PluginRegistry.APIVersion
def __init__(self, application: "CuraApplication") -> None:
super().__init__(parent = application)
self._application = application
# Accounts API
self._account = Account(self._application)
# Backups API # Backups API
backups = Backups() self._backups = Backups(self._application)
# Interface API # Interface API
interface = Interface() self._interface = Interface(self._application)
_account = Account() def initialize(self) -> None:
self._account.initialize()
@pyqtProperty(QObject, constant = True) @pyqtProperty(QObject, constant = True)
def account(self) -> Account: def account(self) -> "Account":
return CuraAPI._account return self._account
@property
def backups(self) -> "Backups":
return self._backups
@property
def interface(self) -> "Interface":
return self._interface

View File

@ -4,18 +4,18 @@
import io import io
import os import os
import re import re
import shutil import shutil
from typing import Dict, Optional
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
from typing import Dict, Optional, TYPE_CHECKING
from UM import i18nCatalog from UM import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Platform import Platform from UM.Platform import Platform
from UM.Resources import Resources 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. ## The back-up class holds all data about a back-up.
@ -29,7 +29,8 @@ class Backup:
# Re-use translation catalog. # Re-use translation catalog.
catalog = i18nCatalog("cura") 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.zip_file = zip_file # type: Optional[bytes]
self.meta_data = meta_data # type: Optional[Dict[str, str]] 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) Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
# Ensure all current settings are saved. # 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. # 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. # When restoring a backup on Linux, we move it back.
if Platform.isLinux(): 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)) 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)) 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) 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.")) "Tried to restore a Cura backup without having proper data or meta data."))
return False return False
current_version = CuraApplication.getInstance().getVersion() current_version = self._application.getVersion()
version_to_restore = self.meta_data.get("cura_release", "master") version_to_restore = self.meta_data.get("cura_release", "master")
if current_version != version_to_restore: if current_version != version_to_restore:
# Cannot restore version older or newer than current because settings might have changed. # 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. # Under Linux, preferences are stored elsewhere, so we copy the file to there.
if Platform.isLinux(): 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)) 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)) 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) Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)

View File

@ -1,11 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # 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 UM.Logger import Logger
from cura.Backups.Backup import Backup 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 ## 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. # Back-ups themselves are represented in a different class.
class BackupsManager: class BackupsManager:
def __init__(self): def __init__(self, application: "CuraApplication") -> None:
self._application = CuraApplication.getInstance() self._application = application
## Get a back-up of the current configuration. ## Get a back-up of the current configuration.
# \return A tuple containing a ZipFile (the actual back-up) and a dict # \return A tuple containing a ZipFile (the actual back-up) and a dict
# containing some metadata (like version). # containing some metadata (like version).
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
self._disableAutoSave() self._disableAutoSave()
backup = Backup() backup = Backup(self._application)
backup.makeFromCurrent() backup.makeFromCurrent()
self._enableAutoSave() self._enableAutoSave()
# We don't return a Backup here because we want plugins only to interact with our API and not full objects. # 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() 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() restored = backup.restore()
if restored: if restored:
# At this point, Cura will need to restart for the changes to take effect. # At this point, Cura will need to restart for the changes to take effect.

View File

@ -44,6 +44,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
from cura.API import CuraAPI
from cura.Arranging.Arrange import Arrange from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
@ -204,7 +205,7 @@ class CuraApplication(QtApplication):
self._quality_profile_drop_down_menu_model = None self._quality_profile_drop_down_menu_model = None
self._custom_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._physics = None
self._volume = None self._volume = None
@ -713,6 +714,9 @@ class CuraApplication(QtApplication):
default_visibility_profile = self._setting_visibility_presets_model.getItem(0) default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) 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 # Detect in which mode to run and execute that mode
if self._is_headless: if self._is_headless:
self.runWithoutGUI() self.runWithoutGUI()
@ -900,10 +904,7 @@ class CuraApplication(QtApplication):
self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self) self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
return self._custom_quality_profile_drop_down_menu_model return self._custom_quality_profile_drop_down_menu_model
def getCuraAPI(self, *args, **kwargs): def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
if self._cura_API is None:
from cura.API import CuraAPI
self._cura_API = CuraAPI()
return self._cura_API return self._cura_API
## Registers objects for the QML engine to use. ## Registers objects for the QML engine to use.

View File

@ -29,7 +29,7 @@ class AuthorizationService:
# Emit signal when authentication failed. # Emit signal when authentication failed.
onAuthenticationError = Signal() 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._settings = settings
self._auth_helpers = AuthorizationHelpers(settings) self._auth_helpers = AuthorizationHelpers(settings)
self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL) self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
@ -38,6 +38,9 @@ class AuthorizationService:
self._preferences = preferences self._preferences = preferences
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True) 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: if self._preferences:
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")

View File

@ -31,15 +31,17 @@ MALFORMED_AUTH_RESPONSE = AuthenticationResponse()
def test_cleanAuthService() -> None: def test_cleanAuthService() -> None:
# Ensure that when setting up an AuthorizationService, no data is set. # 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.getUserProfile() is None
assert authorization_service.getAccessToken() is None assert authorization_service.getAccessToken() is None
def test_failedLogin() -> None: def test_failedLogin() -> None:
authorization_service = AuthorizationService(Preferences(), OAUTH_SETTINGS) authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences())
authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthenticationError.emit = MagicMock()
authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock()
authorization_service.initialize()
# Let the service think there was a failed response # Let the service think there was a failed response
authorization_service._onAuthStateChanged(FAILED_AUTH_RESPONSE) authorization_service._onAuthStateChanged(FAILED_AUTH_RESPONSE)
@ -58,7 +60,8 @@ def test_failedLogin() -> None:
@patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile()) @patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile())
def test_storeAuthData(get_user_profile) -> None: def test_storeAuthData(get_user_profile) -> None:
preferences = Preferences() preferences = Preferences()
authorization_service = AuthorizationService(preferences, OAUTH_SETTINGS) authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
authorization_service.initialize()
# Write stuff to the preferences. # Write stuff to the preferences.
authorization_service._storeAuthData(SUCCESFULL_AUTH_RESPONSE) 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 != {} assert preference_value is not None and preference_value != {}
# Create a second auth service, so we can load the data. # 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() second_auth_service.loadAuthDataFromPreferences()
assert second_auth_service.getAccessToken() == SUCCESFULL_AUTH_RESPONSE.access_token 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") @patch.object(webbrowser, "open_new")
def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None: def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None:
preferences = Preferences() preferences = Preferences()
authorization_service = AuthorizationService(preferences, OAUTH_SETTINGS) authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
authorization_service.startAuthorizationFlow() authorization_service.startAuthorizationFlow()
assert webbrowser_open.call_count == 1 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: def test_loginAndLogout() -> None:
preferences = Preferences() preferences = Preferences()
authorization_service = AuthorizationService(preferences, OAUTH_SETTINGS) authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
authorization_service.onAuthenticationError.emit = MagicMock() authorization_service.onAuthenticationError.emit = MagicMock()
authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.onAuthStateChanged.emit = MagicMock()
authorization_service.initialize()
# Let the service think there was a succesfull response # Let the service think there was a succesfull response
with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()): with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()):
@ -121,7 +126,8 @@ def test_loginAndLogout() -> None:
def test_wrongServerResponses() -> 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()): with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()):
authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE) authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE)
assert authorization_service.getUserProfile() is None assert authorization_service.getUserProfile() is None