Browse Source

Merge remote-tracking branch 'origin/master' into WIP_kill_extruder_manager

Lipu Fei 6 years ago
parent
commit
18cb21186c

+ 47 - 4
Jenkinsfile

@@ -52,10 +52,53 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
 
                 // Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
                 stage('Unit Test') {
-                    try {
-                        make('test')
-                    } catch(e) {
-                        currentBuild.result = "UNSTABLE"
+                    if (isUnix()) {
+                        // For Linux to show everything
+                        def branch = env.BRANCH_NAME
+                        if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
+                            branch = "master"
+                        }
+                        def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
+
+                        try {
+                            sh """
+                                cd ..
+                                export PYTHONPATH=.:"${uranium_dir}"
+                                ${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
+                            """
+                        } catch(e) {
+                            currentBuild.result = "UNSTABLE"
+                        }
+                    }
+                    else {
+                        // For Windows
+                        try {
+                            // This also does code style checks.
+                            bat 'ctest -V'
+                        } catch(e) {
+                            currentBuild.result = "UNSTABLE"
+                        }
+                    }
+                }
+
+                stage('Code Style') {
+                    if (isUnix()) {
+                        // For Linux to show everything
+                        def branch = env.BRANCH_NAME
+                        if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
+                            branch = "master"
+                        }
+                        def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
+
+                        try {
+                            sh """
+                                cd ..
+                                export PYTHONPATH=.:"${uranium_dir}"
+                                ${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
+                            """
+                        } catch(e) {
+                            currentBuild.result = "UNSTABLE"
+                        }
                     }
                 }
             }

+ 1 - 1
cmake/CuraTests.cmake

@@ -34,7 +34,7 @@ function(cura_add_test)
     if (NOT ${test_exists})
         add_test(
             NAME ${_NAME}
-            COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
+            COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
         )
         set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
         set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")

+ 117 - 0
cura/API/Account.py

@@ -0,0 +1,117 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+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
+
+if TYPE_CHECKING:
+    from cura.CuraApplication import CuraApplication
+
+i18n_catalog = i18nCatalog("cura")
+
+
+##  The account API provides a version-proof bridge to use Ultimaker Accounts
+#
+#   Usage:
+#       ``from cura.API import CuraAPI
+#       api = CuraAPI()
+#       api.account.login()
+#       api.account.logout()
+#       api.account.userProfile # Who is logged in``
+#
+class Account(QObject):
+    # Signal emitted when user logged in or out.
+    loginStateChanged = pyqtSignal(bool)
+
+    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
+
+        self._callback_port = 32118
+        self._oauth_root = "https://account.ultimaker.com"
+        self._cloud_api_root = "https://api.ultimaker.com"
+
+        self._oauth_settings = OAuth2Settings(
+            OAUTH_SERVER_URL= self._oauth_root,
+            CALLBACK_PORT=self._callback_port,
+            CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
+            CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
+            CLIENT_SCOPES="user.read drive.backups.read drive.backups.write",
+            AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
+            AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
+            AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
+        )
+
+        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()
+
+    @pyqtProperty(bool, notify=loginStateChanged)
+    def isLoggedIn(self) -> bool:
+        return self._logged_in
+
+    def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
+        if error_message:
+            if self._error_message:
+                self._error_message.hide()
+            self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
+            self._error_message.show()
+
+        if self._logged_in != logged_in:
+            self._logged_in = logged_in
+            self.loginStateChanged.emit(logged_in)
+
+    @pyqtSlot()
+    def login(self) -> None:
+        if self._logged_in:
+            # Nothing to do, user already logged in.
+            return
+        self._authorization_service.startAuthorizationFlow()
+
+    @pyqtProperty(str, notify=loginStateChanged)
+    def userName(self):
+        user_profile = self._authorization_service.getUserProfile()
+        if not user_profile:
+            return None
+        return user_profile.username
+
+    @pyqtProperty(str, notify = loginStateChanged)
+    def profileImageUrl(self):
+        user_profile = self._authorization_service.getUserProfile()
+        if not user_profile:
+            return None
+        return user_profile.profile_image_url
+
+    @pyqtProperty(str, notify=loginStateChanged)
+    def accessToken(self) -> Optional[str]:
+        return self._authorization_service.getAccessToken()
+
+    #   Get the profile of the logged in user
+    #   @returns None if no user is logged in, a dict containing user_id, username and profile_image_url
+    @pyqtProperty("QVariantMap", notify = loginStateChanged)
+    def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
+        user_profile = self._authorization_service.getUserProfile()
+        if not user_profile:
+            return None
+        return user_profile.__dict__
+
+    @pyqtSlot()
+    def logout(self) -> None:
+        if not self._logged_in:
+            return  # Nothing to do, user isn't logged in.
+
+        self._authorization_service.deleteAuthData()

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

+ 48 - 6
cura/API/__init__.py

@@ -1,8 +1,17 @@
 # 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
 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.
 #
@@ -10,14 +19,47 @@ from cura.API.Interface import Interface
 #   this API provides a version-safe interface with proper deprecation warnings
 #   etc. Usage of any other methods than the ones provided in this API can cause
 #   plug-ins to be unstable.
-
-class CuraAPI:
+class CuraAPI(QObject):
 
     # For now we use the same API version to be consistent.
     VERSION = PluginRegistry.APIVersion
+    __instance = None  # type: "CuraAPI"
+    _application = None  # type: CuraApplication
+
+    #   This is done to ensure that the first time an instance is created, it's forced that the application is set.
+    #   The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
+    #   Since the API is intended to be used by plugins, the cura application should have already created this.
+    def __new__(cls, application: Optional["CuraApplication"] = None):
+        if cls.__instance is None:
+            if application is None:
+                raise Exception("Upon first time creation, the application must be set.")
+            cls.__instance = super(CuraAPI, cls).__new__(cls)
+            cls._application = application
+        return cls.__instance
+
+    def __init__(self, application: Optional["CuraApplication"] = None) -> None:
+        super().__init__(parent = CuraAPI._application)
+
+        # Accounts API
+        self._account = Account(self._application)
+
+        # 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 self._account
 
-    # Backups API
-    backups = Backups()
+    @property
+    def backups(self) -> "Backups":
+        return self._backups
 
-    # Interface API
-    interface = Interface()
+    @property
+    def interface(self) -> "Interface":
+        return self._interface

+ 12 - 11
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,24 +29,25 @@ 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]]
 
     ##  Create a back-up from the current user config folder.
     def makeFromCurrent(self) -> None:
-        cura_release = CuraApplication.getInstance().getVersion()
+        cura_release = self._application.getVersion()
         version_data_dir = Resources.getDataStoragePath()
 
         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)
@@ -58,7 +59,7 @@ class Backup:
         if archive is None:
             return
         files = archive.namelist()
-        
+
         # Count the metadata items. We do this in a rather naive way at the moment.
         machine_count = len([s for s in files if "machine_instances/" in s]) - 1
         material_count = len([s for s in files if "materials/" in s]) - 1
@@ -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.

+ 0 - 2
cura/BuildVolume.py

@@ -479,8 +479,6 @@ class BuildVolume(SceneNode):
             maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
         )
 
-        self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
-
         self.updateNodeBoundaryCheck()
 
     def getBoundingBox(self) -> AxisAlignedBox:

Some files were not shown because too many files changed in this diff