diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 5c51bbe1e0..3e76f1e3e8 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -9,6 +9,7 @@ import tempfile from typing import Union, Callable, Dict from zipfile import ZipFile from pathlib import Path +from speedcopy import copyfile from appdirs import user_data_dir from pype.version import __version__ @@ -85,6 +86,11 @@ class BootstrapRepos: def install_live_repos(self, repo_dir: Path = None) -> Union[Path, None]: """Copy zip created from Pype repositories to user data dir. + This detect Pype version either in local "live" Pype repository + or in user provided path. Then it will zip in in temporary directory + and finally it will move it to destination which is user data + directory. Existing files will be replaced. + Args: repo_dir (Path, optional): Path to Pype repository. @@ -92,6 +98,9 @@ class BootstrapRepos: Path: path of installed repository file. """ + # if repo dir is not set, we detect local "live" Pype repository + # version and use it as a source. Otherwise repo_dir is user + # entered location. if not repo_dir: version = self.get_local_version() repo_dir = self.live_repo_dir @@ -99,10 +108,10 @@ class BootstrapRepos: version = self.get_version(repo_dir) # create destination directory - try: - os.makedirs(self.data_dir) - except OSError: - self._log.error("directory already exists") + if not self.data_dir.exists(): + self.data_dir.mkdir(parents=True) + + # create zip inside temporary directory. with tempfile.TemporaryDirectory() as temp_dir: temp_zip = \ Path(temp_dir) / f"pype-repositories-v{version}.zip" @@ -214,7 +223,8 @@ class BootstrapRepos: os.environ["PYTHONPATH"] = os.pathsep.join(paths) - def find_pype(self) -> Union[Dict[str, Path], None]: + def find_pype( + self, pype_path: Path = None) -> Union[Dict[str, Path], None]: """Get ordered dict of detected Pype version. Resolution order for Pype is following: @@ -223,6 +233,9 @@ class BootstrapRepos: 2) We try to find ``pypePath`` in registry setting 3) We use user data directory + Args: + pype_path (Path, optional): Try to find Pype on the given path. + Returns: dict of Path: Dictionary of detected Pype version. Key is version, value is path to zip file. @@ -244,6 +257,10 @@ class BootstrapRepos: # nothing found in registry, we'll use data dir pass + # if we have pyp_path specified, search only there. + if pype_path: + dir_to_search = pype_path + # pype installation dir doesn't exists if not dir_to_search.exists(): return None @@ -275,7 +292,7 @@ class BootstrapRepos: None: if not. """ - os.environ["AVALON_MONGO"] = mongo_url + os.environ["PYPE_MONGO"] = mongo_url env = load_environments() if not env.get("PYPE_PATH"): return None @@ -299,19 +316,59 @@ class BootstrapRepos: """ pype_path = None + # try to get pype path from mongo. if location.startswith("mongodb"): pype_path = self._get_pype_from_mongo(location) if not pype_path: self._log.error("cannot find PYPE_PATH in settings.") return None + # if not successful, consider location to be fs path. if not pype_path: pype_path = Path(location) + # test if this path does exist. if not pype_path.exists(): self._log.error(f"{pype_path} doesn't exists.") return None + # find pype zip files in location. In that location, there can be + # either "live" Pype repository, or multiple zip files. + versions = self.find_pype(pype_path) + if versions: + latest_version = ( + list(versions.keys())[-1], list(versions.values())[-1]) + self._log.info(f"found Pype zips in [ {pype_path} ].") + self._log.info(f"latest version found is [ {latest_version[0]} ]") + + destination = self.data_dir / latest_version[1].name + + # test if destination file already exist, if so lets delete it. + # we consider path on location as authoritative place. + if destination.exists(): + try: + destination.unlink() + except OSError: + self._log.error( + f"cannot remove already existing {destination}", + exc_info=True) + return None + + # create destination parent directories even if they don't exist. + if not destination.parent.exists(): + destination.parent.mkdir(parents=True) + + try: + copyfile(latest_version[1].as_posix(), destination.as_posix()) + except OSError: + self._log.error( + "cannot copy detected version to user data directory", + exc_info=True) + return None + return destination + + # if we got here, it means that location is "live" Pype repository. + # we'll create zip from it and move it to user data dir. repo_file = self.install_live_repos(pype_path) if not repo_file.exists(): self._log.error(f"installing zip {repo_file} failed.") diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index fa79eba871..762e1c2fa1 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -7,7 +7,7 @@ from Qt import QtCore, QtGui, QtWidgets from Qt.QtGui import QValidator from .install_thread import InstallThread -from .tools import validate_path_string +from .tools import validate_path_string, validate_mongo_connection class InstallDialog(QtWidgets.QDialog): @@ -152,8 +152,27 @@ class InstallDialog(QtWidgets.QDialog): def get_mongo_url(self): return self._mongo_url + def set_valid(self): + self._mongo_input.setStyleSheet( + """ + background-color: rgb(19, 19, 19); + color: rgb(64, 230, 132); + padding: 0.5em; + border: 1px solid rgb(32, 64, 32); + """ + ) + + def set_invalid(self): + self._mongo_input.setStyleSheet( + """ + background-color: rgb(32, 19, 19); + color: rgb(255, 69, 0); + padding: 0.5em; + border: 1px solid rgb(32, 64, 32); + """ + ) + self._mongo = MongoWidget(self) - self._mongo.hide() # Bottom button bar # -------------------------------------------------------------------- @@ -321,10 +340,19 @@ class InstallDialog(QtWidgets.QDialog): border: 1px solid rgb(32, 64, 32); """ ) + if not self._path or not self._path.startswith("mongodb"): + valid, reason = validate_mongo_connection( + self._mongo.get_mongo_url() + ) + if not valid: + self._mongo.set_invalid() + self._update_console(f"!!! {reason}", True) + return + else: + self._mongo.set_valid() self._disable_buttons() self._install_thread = InstallThread(self) - self._install_thread.ask_for_mongo.connect(self._show_mongo) self._install_thread.message.connect(self._update_console) self._install_thread.progress.connect(self._update_progress) self._install_thread.finished.connect(self._enable_buttons) @@ -338,9 +366,6 @@ class InstallDialog(QtWidgets.QDialog): def _on_exit_clicked(self): self.close() - def _show_mongo(self): - self._update_console("mongo showed") - def _path_changed(self, path: str) -> str: """Set path.""" self._path = path diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 5ef976a8a4..8b2641e33a 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -2,7 +2,8 @@ """Working thread for installer.""" import os from Qt.QtCore import QThread, Signal -from igniter.tools import load_environments + +from speedcopy import copyfile from .bootstrap_repos import BootstrapRepos from .tools import validate_mongo_connection @@ -33,7 +34,15 @@ class InstallThread(QThread): QThread.__init__(self, parent) def run(self): + """Thread entry point. + + Using :class:`BootstrapRepos` to either install Pype as zip files + or copy them from location specified by user or retrieved from + database. + + """ self.message.emit("Installing Pype ...", False) + # find local version of Pype bs = BootstrapRepos(progress_callback=self.set_progress) local_version = bs.get_local_version() @@ -42,7 +51,24 @@ class InstallThread(QThread): # zip content of `repos`, copy it to user data dir and append # version to it. if not self._path: - self.ask_for_mongo.emit(True) + # user did not entered url + if not self._mongo: + # it not set in environment + if not os.getenv("PYPE_MONGO"): + # try to get it from settings registry + try: + self._mongo = bs.registry.get_secure_item("pypeMongo") + except ValueError: + self.message.emit( + "!!! We need MongoDB URL to proceed.", True) + return + else: + self._mongo = os.getenv("PYPE_MONGO") + else: + bs.registry.set_secure_item("pypeMongo", self._mongo) + + os.environ["PYPE_MONGO"] = self._mongo + self.message.emit( f"We will use local Pype version {local_version}", False) repo_file = bs.install_live_repos() @@ -52,13 +78,15 @@ class InstallThread(QThread): return self.message.emit(f"installed as {repo_file}", False) else: + # if we have mongo connection string, validate it, set it to + # user settings and get PYPE_PATH from there. if self._mongo: if not validate_mongo_connection(self._mongo): self.message.emit( f"!!! invalid mongo url {self._mongo}", True) return - bs.registry.set_secure_item("avalonMongo", self._mongo) - os.environ["AVALON_MONGO"] = self._mongo + bs.registry.set_secure_item("pypeMongo", self._mongo) + os.environ["PYPE_MONGO"] = self._mongo repo_file = bs.process_entered_location(self._path) @@ -72,5 +100,5 @@ class InstallThread(QThread): def set_mongo(self, mongo: str) -> None: self._mongo = mongo - def set_progress(self, progress: int): + def set_progress(self, progress: int) -> None: self.progress.emit(progress) diff --git a/igniter/tools.py b/igniter/tools.py index 6f5907861b..ba67ad4f8c 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -61,13 +61,15 @@ def validate_path_string(path: str) -> (bool, str): the reason why it failed. """ + if not path: + return True, "Empty string" parsed = urlparse(path) if parsed.scheme == "mongodb": return validate_mongo_connection(path) # test for uuid try: uuid.UUID(path) - except ValueError: + except (ValueError, TypeError): # not uuid if not os.path.exists(path): return False, "Path doesn't exist or invalid token" diff --git a/pype.py b/pype.py index 435c7b6467..b28e54ff16 100644 --- a/pype.py +++ b/pype.py @@ -86,16 +86,21 @@ def boot(): use_version = m.group('version') break - if not os.getenv("AVALON_MONGO"): + if not os.getenv("PYPE_MONGO"): try: - avalon_mongo = bootstrap.registry.get_secure_item("avalonMongo") + pype_mongo = bootstrap.registry.get_secure_item("pypeMongo") except ValueError: print("*** No DB connection string specified.") + print("--- launching setup UI ...") import igniter igniter.run() - set_environments() + return else: - os.environ["AVALON_MONGO"] = avalon_mongo + os.environ["PYPE_MONGO"] = pype_mongo + + # FIXME (antirotor): we need to set those in different way + if not os.getenv("AVALON_MONGO"): + os.environ["AVALON_MONGO"] = os.environ["PYPE_MONGO"] if getattr(sys, 'frozen', False): if not pype_versions: @@ -118,6 +123,13 @@ def boot(): else: # run through repos and add them to sys.path and PYTHONPATH pype_root = os.path.dirname(os.path.realpath(__file__)) + local_version = bootstrap.get_local_version() + if use_version and use_version != local_version: + if use_version in pype_versions.keys(): + # use specified + bootstrap.add_paths_from_archive(pype_versions[use_version]) + use_version = pype_versions[use_version] + os.environ["PYPE_ROOT"] = pype_root repos = os.listdir(os.path.join(pype_root, "repos")) repos = [os.path.join(pype_root, "repos", repo) for repo in repos] diff --git a/pype/lib/user_settings.py b/pype/lib/user_settings.py index d1cb12e8f1..216ebf0d77 100644 --- a/pype/lib/user_settings.py +++ b/pype/lib/user_settings.py @@ -321,7 +321,8 @@ class JSONSettingRegistry(ASettingRegistry): "registry": {} } - self._registry_file.parent.mkdir(parents=True) + if not self._registry_file.parent.exists(): + self._registry_file.parent.mkdir(parents=True) if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: json.dump(header, cfg, indent=4) diff --git a/pype/pype_commands.py b/pype/pype_commands.py index 2ae8c00bbb..e3c3b42779 100644 --- a/pype/pype_commands.py +++ b/pype/pype_commands.py @@ -1,5 +1,12 @@ # -*- coding: utf-8 -*- """Implementation of Pype commands.""" +import os +import sys +import subprocess +from pathlib import Path + +from pype.lib import execute +from pype.lib import PypeLogger as Logger class PypeCommands: @@ -7,9 +14,43 @@ class PypeCommands: Most of its methods are called by :mod:`cli` module. """ + @staticmethod + def launch_tray(debug): + if debug: + execute([ + sys.executable, + "-m", + "pype.tools.tray" + ]) + return + + detached_process = 0x00000008 # noqa: N806 + + args = [sys.executable, "-m", "pype.tools.tray"] + if sys.platform.startswith('linux'): + subprocess.Popen( + args, + universal_newlines=True, + bufsize=1, + env=os.environ, + stdout=None, + stderr=None, + preexec_fn=os.setpgrp + ) + + if sys.platform == 'win32': + args = ["pythonw", "-m", "pype.tools.tray"] + subprocess.Popen( + args, + universal_newlines=True, + bufsize=1, + cwd=None, + env=os.environ, + stdout=open(Logger.get_file_path(), 'w+'), + stderr=subprocess.STDOUT, + creationflags=detached_process + ) - def launch_tray(self, debug): - pass def launch_eventservercli(self, args): pass diff --git a/tests/igniter/test_bootstrap_repos.py b/tests/igniter/test_bootstrap_repos.py index a97aa21538..7f343b1023 100644 --- a/tests/igniter/test_bootstrap_repos.py +++ b/tests/igniter/test_bootstrap_repos.py @@ -118,3 +118,9 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): assert list(result.values())[-1] == Path( r_path / test_versions_2[0] ), "not a latest version of Pype 2" + + result = fix_bootstrap.find_pype(e_path) + assert result is not None, "no Pype version found" + assert list(result.values())[-1] == Path( + e_path / test_versions_1[1] + ), "not a latest version of Pype 1"