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.
# 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()

View File

@ -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

View File

@ -1,8 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
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.

View File

@ -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
def __init__(self, application: "CuraApplication") -> None:
# 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.
# 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
def __init__(self, application: "CuraApplication") -> None:
super().__init__(parent = application)
self._application = application
# Accounts API
self._account = Account(self._application)
# Backups API
backups = Backups()
self._backups = Backups(self._application)
# Interface API
interface = Interface()
self._interface = Interface(self._application)
_account = Account()
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

View File

@ -4,17 +4,17 @@
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
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
@ -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)

View File

@ -1,10 +1,12 @@
# 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
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
@ -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.

View File

@ -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.

View File

@ -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, "{}")

View File

@ -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