From 10686e06c5a411bc616afa83194b8acccb0e743c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 15 Jan 2021 22:42:16 +0100 Subject: [PATCH] change version comparsion, handling of venv and startup --- igniter/bootstrap_repos.py | 97 ++++-- igniter/splash.txt | 413 +++++++++++++++++++++++ igniter/terminal_splash.py | 43 +++ igniter/tools.py | 145 ++++++-- igniter/user_settings.py | 466 ++++++++++++++++++++++++++ pype/lib/terminal_splash.py | 11 +- pype/lib/user_settings.py | 11 +- setup.py | 3 +- start.py | 59 ++-- tests/igniter/test_bootstrap_repos.py | 35 +- tools/build.ps1 | 2 + tools/build_dependencies.py | 119 +++++++ tools/create_zip.ps1 | 5 +- 13 files changed, 1295 insertions(+), 114 deletions(-) create mode 100644 igniter/splash.txt create mode 100644 igniter/terminal_splash.py create mode 100644 igniter/user_settings.py create mode 100644 tools/build_dependencies.py diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index b485054920..38de3007b4 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -14,8 +14,7 @@ from zipfile import ZipFile, BadZipFile from appdirs import user_data_dir from speedcopy import copyfile -from pype.lib import PypeSettingsRegistry -from pype.version import __version__ +from .user_settings import PypeSettingsRegistry from .tools import load_environments @@ -24,23 +23,23 @@ class PypeVersion: """Class for storing information about Pype version. Attributes: - major (int): [1].2.3-variant-client - minor (int): 1.[2].3-variant-client - subversion (int): 1.2.[3]-variant-client - variant (str): 1.2.3-[variant]-client - client (str): 1.2.3-variant-[client] + major (int): [1].2.3-client-variant + minor (int): 1.[2].3-client-variant + subversion (int): 1.2.[3]-client-variant + client (str): 1.2.3-[client]-variant + variant (str): 1.2.3-client-[variant] path (str): path to Pype """ major = 0 minor = 0 subversion = 0 - variant = "production" + variant = "" client = None path = None _version_regex = re.compile( - r"(?P\d+)\.(?P\d+)\.(?P\d+)(-?((?Pstaging)|(?P.+))(-(?P.+))?)?") # noqa: E501 + r"(?P\d+)\.(?P\d+)\.(?P\d+)(-(?Pstaging)|-(?P.+)(-(?Pstaging)))?") # noqa: E501 @property def version(self): @@ -58,12 +57,12 @@ class PypeVersion: def __init__(self, major: int = None, minor: int = None, subversion: int = None, version: str = None, - variant: str = "production", client: str = None, + variant: str = "", client: str = None, path: Path = None): self.path = path if ( - major is None or minor is None or subversion is None + major is None or minor is None or subversion is None ) and version is None: raise ValueError("Need version specified in some way.") if version: @@ -85,12 +84,13 @@ class PypeVersion: def _compose_version(self): version = "{}.{}.{}".format(self.major, self.minor, self.subversion) - if self.variant == "staging": - version = "{}-{}".format(version, self.variant) if self.client: version = "{}-{}".format(version, self.client) + if self.variant == "staging": + version = "{}-{}".format(version, self.variant) + return version @classmethod @@ -101,10 +101,10 @@ class PypeVersion: "Cannot parse version string: {}".format(version_string)) variant = None - if m.group("variant") == "staging": + if m.group("var1") == "staging" or m.group("var2") == "staging": variant = "staging" - client = m.group("client") or m.group("cli") + client = m.group("client") return (int(m.group("major")), int(m.group("minor")), int(m.group("sub")), variant, client) @@ -125,26 +125,48 @@ class PypeVersion: return hash(self.version) def __lt__(self, other): - if self.major < other.major: + if (self.major, self.minor, self.subversion) < \ + (other.major, other.minor, other.subversion): return True - if self.major <= other.major and self.minor < other.minor: - return True - if self.major <= other.major and self.minor <= other.minor and \ - self.subversion < other.subversion: + # 1.2.3-staging < 1.2.3-client-staging + if self.get_main_version() == other.get_main_version() and \ + not self.client and self.variant and \ + other.client and other.variant: return True - # Directory takes precedence over file - if ( - self.path - and other.path - and other.path.is_dir() - and self.path.is_file() - ): + # 1.2.3 < 1.2.3-staging + if self.get_main_version() == other.get_main_version() and \ + not self.client and self.variant and \ + not other.client and not other.variant: return True - return self.major == other.major and self.minor == other.minor and \ - self.subversion == other.subversion and self.variant == "staging" + # 1.2.3 < 1.2.3-client + if self.get_main_version() == other.get_main_version() and \ + not self.client and not self.variant and \ + other.client and not other.variant: + return True + + # 1.2.3 < 1.2.3-client-staging + if self.get_main_version() == other.get_main_version() and \ + not self.client and not self.variant and other.client: + return True + + # 1.2.3-client-staging < 1.2.3-client + if self.get_main_version() == other.get_main_version() and \ + self.client and self.variant and \ + other.client and not other.variant: + return True + + # prefer path over no path + if self.version == other.version and \ + not self.path and other.path: + return True + + # prefer path with dir over path with file + return self.version == other.version and self.path and \ + other.path and self.path.is_file() and \ + other.path.is_dir() def is_staging(self) -> bool: """Test if current version is staging one.""" @@ -252,7 +274,12 @@ class BootstrapRepos: @staticmethod def get_local_live_version() -> str: """Get version of local Pype.""" - return __version__ + + version = {} + path = Path(os.path.dirname(__file__)).parent / "pype" / "version.py" + with open(path, "r") as fp: + exec(fp.read(), version) + return version["__version__"] @staticmethod def get_version(repo_dir: Path) -> Union[str, None]: @@ -494,6 +521,7 @@ class BootstrapRepos: def find_pype( self, pype_path: Path = None, + staging: bool = False, include_zips: bool = False) -> Union[List[PypeVersion], None]: """Get ordered dict of detected Pype version. @@ -505,6 +533,8 @@ class BootstrapRepos: Args: pype_path (Path, optional): Try to find Pype on the given path. + staging (bool, optional): Filter only staging version, skip them + otherwise. include_zips (bool, optional): If set True it will try to find Pype in zip files in given directory. @@ -549,6 +579,7 @@ class BootstrapRepos: result = PypeVersion.version_in_str(name) if result[0]: + detected_version: PypeVersion detected_version = result[1] if file.is_dir(): @@ -614,7 +645,11 @@ class BootstrapRepos: continue detected_version.path = file - _pype_versions.append(detected_version) + if staging and detected_version.is_staging(): + _pype_versions.append(detected_version) + + if not staging and not detected_version.is_staging(): + _pype_versions.append(detected_version) return sorted(_pype_versions) diff --git a/igniter/splash.txt b/igniter/splash.txt new file mode 100644 index 0000000000..833bcd4b9c --- /dev/null +++ b/igniter/splash.txt @@ -0,0 +1,413 @@ + + + + * + + + + + + + .* + + + + + + * + .* + * + + + + . + * + .* + * + . + + . + * + .* + .* + .* + * + . + . + * + .* + .* + .* + * + . + _. + /** + \ * + \* + * + * + . + __. + ---* + \ \* + \ * + \* + * + . + \___. + /* * + \ \ * + \ \* + \ * + \* + . + |____. + /* * + \|\ * + \ \ * + \ \ * + \ \* + \/. + _/_____. + /* * + / \ * + \ \ * + \ \ * + \ \__* + \/__. + __________. + --*-- ___* + \ \ \/_* + \ \ __* + \ \ \_* + \ \____\* + \/____/. + \____________ . + /* ___ \* + \ \ \/_\ * + \ \ _____* + \ \ \___/* + \ \____\ * + \/____/ . + |___________ . + /* ___ \ * + \|\ \/_\ \ * + \ \ _____/ * + \ \ \___/ * + \ \____\ / * + \/____/ \. + _/__________ . + /* ___ \ * + / \ \/_\ \ * + \ \ _____/ * + \ \ \___/ ---* + \ \____\ / \__* + \/____/ \/__. + ____________ . + --*-- ___ \ * + \ \ \/_\ \ * + \ \ _____/ * + \ \ \___/ ---- * + \ \____\ / \____\* + \/____/ \/____/. + ____________ + /\ ___ \ . + \ \ \/_\ \ * + \ \ _____/ * + \ \ \___/ ---- * + \ \____\ / \____\ . + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ . + \ \ _____/ * + \ \ \___/ ---- * + \ \____\ / \____\ . + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ . + \ \ \___/ ---- * + \ \____\ / \____\ . + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ + \ \ \___/ ---- * + \ \____\ / \____\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ + \ \ \___/ ---- . + \ \____\ / \____\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ _ + \ \ \___/ ---- + \ \____\ / \____\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ + \ \ \___/ ---- + \ \____\ / \____\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ + \ \ \___/ ---- \ + \ \____\ / \____\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ + \ \ \___/ ---- \ + \ \____\ / \____\ \ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ + \ \ \___/ ---- \ + \ \____\ / \____\ __\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ + \ \ \___/ ---- \ + \ \____\ / \____\ \__\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ + \ \ \___/ ---- \ \ + \ \____\ / \____\ \__\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ + \ \ \___/ ---- \ \ + \ \____\ / \____\ \__\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___. + \ \ \___/ ---- \ \\ + \ \____\ / \____\ \__\, + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ . + \ \ \___/ ---- \ \\ + \ \____\ / \____\ \__\\, + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ _. + \ \ \___/ ---- \ \\\ + \ \____\ / \____\ \__\\\ + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ __. + \ \ \___/ ---- \ \\ \ + \ \____\ / \____\ \__\\_/. + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___. + \ \ \___/ ---- \ \\ \\ + \ \____\ / \____\ \__\\__\. + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ . + \ \ \___/ ---- \ \\ \\ + \ \____\ / \____\ \__\\__\\. + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ _. + \ \ \___/ ---- \ \\ \\\ + \ \____\ / \____\ \__\\__\\. + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ __. + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\_. + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ __. + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__. + \/____/ \/____/ + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ * + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ O* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ .oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ ..oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . .oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . p.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . Py.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYp.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPe.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE .oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE c.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE C1.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE ClU.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE CluB.oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE Club .oO* + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE Club . .. + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE Club . .. + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE Club . . + ____________ + /\ ___ \ + \ \ \/_\ \ + \ \ _____/ ___ ___ ___ + \ \ \___/ ---- \ \\ \\ \ + \ \____\ / \____\ \__\\__\\__\ + \/____/ \/____/ . PYPE Club . diff --git a/igniter/terminal_splash.py b/igniter/terminal_splash.py new file mode 100644 index 0000000000..1a7645571e --- /dev/null +++ b/igniter/terminal_splash.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Pype terminal animation.""" +import blessed +from pathlib import Path +from time import sleep + +NO_TERMINAL = False + +try: + term = blessed.Terminal() +except AttributeError: + # this happens when blessed cannot find proper terminal. + # If so, skip printing ascii art animation. + NO_TERMINAL = True + + +def play_animation(): + """Play ASCII art Pype animation.""" + if NO_TERMINAL: + return + print(term.home + term.clear) + frame_size = 7 + splash_file = Path(__file__).parent / "splash.txt" + with splash_file.open("r") as sf: + animation = sf.readlines() + + animation_length = int(len(animation) / frame_size) + current_frame = 0 + for _ in range(animation_length): + frame = "".join( + scanline + for y, scanline in enumerate( + animation[current_frame : current_frame + frame_size] + ) + ) + + with term.location(0, 0): + # term.aquamarine3_bold(frame) + print(f"{term.bold}{term.aquamarine3}{frame}{term.normal}") + + sleep(0.02) + current_frame += frame_size + print(term.move_y(7)) diff --git a/igniter/tools.py b/igniter/tools.py index e7cb99865d..d9a315834a 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -1,13 +1,104 @@ # -*- coding: utf-8 -*- -"""Tools used in **Igniter** GUI.""" +"""Tools used in **Igniter** GUI. + +Functions ``compose_url()`` and ``decompose_url()`` are the same as in +``pype.lib`` and they are here to avoid importing pype module before its +version is decided. + +""" + import os import uuid -from urllib.parse import urlparse +from typing import Dict +from urllib.parse import urlparse, parse_qs from pymongo import MongoClient from pymongo.errors import ServerSelectionTimeoutError, InvalidURI -from pype.lib import decompose_url, compose_url + +def decompose_url(url: str) -> Dict: + """Decompose mongodb url to its separate components. + + Args: + url (str): Mongodb url. + + Returns: + dict: Dictionary of components. + + """ + components = { + "scheme": None, + "host": None, + "port": None, + "username": None, + "password": None, + "auth_db": None + } + + result = urlparse(url) + if result.scheme is None: + _url = "mongodb://{}".format(url) + result = urlparse(_url) + + components["scheme"] = result.scheme + components["host"] = result.hostname + try: + components["port"] = result.port + except ValueError: + raise RuntimeError("invalid port specified") + components["username"] = result.username + components["password"] = result.password + + try: + components["auth_db"] = parse_qs(result.query)['authSource'][0] + except KeyError: + # no auth db provided, mongo will use the one we are connecting to + pass + + return components + + +def compose_url(scheme: str = None, + host: str = None, + username: str = None, + password: str = None, + port: int = None, + auth_db: str = None) -> str: + """Compose mongodb url from its individual components. + + Args: + scheme (str, optional): + host (str, optional): + username (str, optional): + password (str, optional): + port (str, optional): + auth_db (str, optional): + + Returns: + str: mongodb url + + """ + + url = "{scheme}://" + + if username and password: + url += "{username}:{password}@" + + url += "{host}" + if port: + url += ":{port}" + + if auth_db: + url += "?authSource={auth_db}" + + return url.format(**{ + "scheme": scheme, + "host": host, + "username": username, + "password": password, + "port": port, + "auth_db": auth_db + }) def validate_mongo_connection(cnx: str) -> (bool, str): @@ -21,30 +112,29 @@ def validate_mongo_connection(cnx: str) -> (bool, str): """ parsed = urlparse(cnx) - if parsed.scheme in ["mongodb", "mongodb+srv"]: - # we have mongo connection string. Let's try if we can connect. - components = decompose_url(cnx) - mongo_args = { - "host": compose_url(**components), - "serverSelectionTimeoutMS": 1000 - } - port = components.get("port") - if port is not None: - mongo_args["port"] = int(port) - - try: - client = MongoClient(**mongo_args) - client.server_info() - except ServerSelectionTimeoutError as e: - return False, f"Cannot connect to server {cnx} - {e}" - except ValueError: - return False, f"Invalid port specified {parsed.port}" - except InvalidURI as e: - return False, str(e) - else: - return True, "Connection is successful" - else: + if parsed.scheme not in ["mongodb", "mongodb+srv"]: return False, "Not mongodb schema" + # we have mongo connection string. Let's try if we can connect. + components = decompose_url(cnx) + mongo_args = { + "host": compose_url(**components), + "serverSelectionTimeoutMS": 1000 + } + port = components.get("port") + if port is not None: + mongo_args["port"] = int(port) + + try: + client = MongoClient(**mongo_args) + client.server_info() + except ServerSelectionTimeoutError as e: + return False, f"Cannot connect to server {cnx} - {e}" + except ValueError: + return False, f"Invalid port specified {parsed.port}" + except InvalidURI as e: + return False, str(e) + else: + return True, "Connection is successful" def validate_path_string(path: str) -> (bool, str): @@ -109,5 +199,4 @@ def load_environments(sections: list = None) -> dict: continue merged_env = acre.append(merged_env, parsed_env) - env = acre.compute(merged_env, cleanup=True) - return env + return acre.compute(merged_env, cleanup=True) diff --git a/igniter/user_settings.py b/igniter/user_settings.py new file mode 100644 index 0000000000..00ce68cb0b --- /dev/null +++ b/igniter/user_settings.py @@ -0,0 +1,466 @@ +# -*- coding: utf-8 -*- +"""Package to deal with saving and retrieving user specific settings.""" +import os +from datetime import datetime +from abc import ABCMeta, abstractmethod +import json + +# disable lru cache in Python 2 +try: + from functools import lru_cache +except ImportError: + def lru_cache(maxsize): + def max_size(func): + def wrapper(*args, **kwargs): + value = func(*args, **kwargs) + return value + return wrapper + return max_size + +# ConfigParser was renamed in python3 to configparser +try: + import configparser +except ImportError: + import ConfigParser as configparser + +import platform + +import appdirs +import six + + +@six.add_metaclass(ABCMeta) +class ASettingRegistry(): + """Abstract class defining structure of **SettingRegistry** class. + + It is implementing methods to store secure items into keyring, otherwise + mechanism for storing common items must be implemented in abstract + methods. + + Attributes: + _name (str): Registry names. + + """ + + def __init__(self, name): + # type: (str) -> ASettingRegistry + super(ASettingRegistry, self).__init__() + + if six.PY3: + import keyring + # hack for cx_freeze and Windows keyring backend + if platform.system() == "Windows": + from keyring.backends import Windows + keyring.set_keyring(Windows.WinVaultKeyring()) + + self._name = name + self._items = {} + + def set_item(self, name, value): + # type: (str, str) -> None + """Set item to settings registry. + + Args: + name (str): Name of the item. + value (str): Value of the item. + + """ + self._set_item(name, value) + + @abstractmethod + def _set_item(self, name, value): + # type: (str, str) -> None + # Implement it + pass + + def __setitem__(self, name, value): + self._items[name] = value + self._set_item(name, value) + + def get_item(self, name): + # type: (str) -> str + """Get item from settings registry. + + Args: + name (str): Name of the item. + + Returns: + value (str): Value of the item. + + Raises: + ValueError: If item doesn't exist. + + """ + return self._get_item(name) + + @abstractmethod + def _get_item(self, name): + # type: (str) -> str + # Implement it + pass + + def __getitem__(self, name): + return self._get_item(name) + + def delete_item(self, name): + # type: (str) -> None + """Delete item from settings registry. + + Args: + name (str): Name of the item. + + """ + self._delete_item(name) + + @abstractmethod + def _delete_item(self, name): + # type: (str) -> None + """Delete item from settings. + + Note: + see :meth:`pype.lib.user_settings.ARegistrySettings.delete_item` + + """ + pass + + def __delitem__(self, name): + del self._items[name] + self._delete_item(name) + + def set_secure_item(self, name, value): + # type: (str, str) -> None + """Set sensitive item into system's keyring. + + This uses `Keyring module`_ to save sensitive stuff into system's + keyring. + + Args: + name (str): Name of the item. + value (str): Value of the item. + + .. _Keyring module: + https://github.com/jaraco/keyring + + """ + if six.PY2: + raise NotImplementedError( + "Keyring not available on Python 2 hosts") + import keyring + keyring.set_password(self._name, name, value) + + @lru_cache(maxsize=32) + def get_secure_item(self, name): + # type: (str) -> str + """Get value of sensitive item from system's keyring. + + See also `Keyring module`_ + + Args: + name (str): Name of the item. + + Returns: + value (str): Value of the item. + + Raises: + ValueError: If item doesn't exist. + + .. _Keyring module: + https://github.com/jaraco/keyring + + """ + if six.PY2: + raise NotImplementedError( + "Keyring not available on Python 2 hosts") + import keyring + value = keyring.get_password(self._name, name) + if not value: + raise ValueError( + "Item {}:{} does not exist in keyring.".format( + self._name, name)) + return value + + def delete_secure_item(self, name): + # type: (str) -> None + """Delete value stored in system's keyring. + + See also `Keyring module`_ + + Args: + name (str): Name of the item to be deleted. + + .. _Keyring module: + https://github.com/jaraco/keyring + + """ + if six.PY2: + raise NotImplementedError( + "Keyring not available on Python 2 hosts") + import keyring + self.get_secure_item.cache_clear() + keyring.delete_password(self._name, name) + + +class IniSettingRegistry(ASettingRegistry): + """Class using :mod:`configparser`. + + This class is using :mod:`configparser` (ini) files to store items. + + """ + + def __init__(self, name, path): + # type: (str, str) -> IniSettingRegistry + super(IniSettingRegistry, self).__init__(name) + # get registry file + version = os.getenv("PYPE_VERSION", "N/A") + self._registry_file = os.path.join(path, "{}.ini".format(name)) + if not os.path.exists(self._registry_file): + with open(self._registry_file, mode="w") as cfg: + print("# Settings registry", cfg) + print("# Generated by Pype {}".format(version), cfg) + now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + print("# {}".format(now), cfg) + + def set_item_section( + self, section, name, value): + # type: (str, str, str) -> None + """Set item to specific section of ini registry. + + If section doesn't exists, it is created. + + Args: + section (str): Name of section. + name (str): Name of the item. + value (str): Value of the item. + + """ + value = str(value) + config = configparser.ConfigParser() + + config.read(self._registry_file) + if not config.has_section(section): + config.add_section(section) + current = config[section] + current[name] = value + + with open(self._registry_file, mode="w") as cfg: + config.write(cfg) + + def _set_item(self, name, value): + # type: (str, str) -> None + self.set_item_section("MAIN", name, value) + + def set_item(self, name, value): + # type: (str, str) -> None + """Set item to settings ini file. + + This saves item to ``DEFAULT`` section of ini as each item there + must reside in some section. + + Args: + name (str): Name of the item. + value (str): Value of the item. + + """ + # this does the some, overridden just for different docstring. + # we cast value to str as ini options values must be strings. + super(IniSettingRegistry, self).set_item(name, str(value)) + + def get_item(self, name): + # type: (str) -> str + """Gets item from settings ini file. + + This gets settings from ``DEFAULT`` section of ini file as each item + there must reside in some section. + + Args: + name (str): Name of the item. + + Returns: + str: Value of item. + + Raises: + ValueError: If value doesn't exist. + + """ + return super(IniSettingRegistry, self).get_item(name) + + @lru_cache(maxsize=32) + def get_item_from_section(self, section, name): + # type: (str, str) -> str + """Get item from section of ini file. + + This will read ini file and try to get item value from specified + section. If that section or item doesn't exist, :exc:`ValueError` + is risen. + + Args: + section (str): Name of ini section. + name (str): Name of the item. + + Returns: + str: Item value. + + Raises: + ValueError: If value doesn't exist. + + """ + config = configparser.ConfigParser() + config.read(self._registry_file) + try: + value = config[section][name] + except KeyError: + raise ValueError( + "Registry doesn't contain value {}:{}".format(section, name)) + return value + + def _get_item(self, name): + # type: (str) -> str + return self.get_item_from_section("MAIN", name) + + def delete_item_from_section(self, section, name): + # type: (str, str) -> None + """Delete item from section in ini file. + + Args: + section (str): Section name. + name (str): Name of the item. + + Raises: + ValueError: If item doesn't exist. + + """ + self.get_item_from_section.cache_clear() + config = configparser.ConfigParser() + config.read(self._registry_file) + try: + _ = config[section][name] + except KeyError: + raise ValueError( + "Registry doesn't contain value {}:{}".format(section, name)) + config.remove_option(section, name) + + # if section is empty, delete it + if len(config[section].keys()) == 0: + config.remove_section(section) + + with open(self._registry_file, mode="w") as cfg: + config.write(cfg) + + def _delete_item(self, name): + """Delete item from default section. + + Note: + See :meth:`~pype.lib.IniSettingsRegistry.delete_item_from_section` + + """ + self.delete_item_from_section("MAIN", name) + + +class JSONSettingRegistry(ASettingRegistry): + """Class using json file as storage.""" + + def __init__(self, name, path): + # type: (str, str) -> JSONSettingRegistry + super(JSONSettingRegistry, self).__init__(name) + #: str: name of registry file + self._registry_file = os.path.join(path, "{}.json".format(name)) + now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + header = { + "__metadata__": { + "pype-version": os.getenv("PYPE_VERSION", "N/A"), + "generated": now + }, + "registry": {} + } + + if not os.path.exists(os.path.dirname(self._registry_file)): + os.makedirs(os.path.dirname(self._registry_file), exist_ok=True) + if not os.path.exists(self._registry_file): + with open(self._registry_file, mode="w") as cfg: + json.dump(header, cfg, indent=4) + + @lru_cache(maxsize=32) + def _get_item(self, name): + # type: (str) -> object + """Get item value from registry json. + + Note: + See :meth:`pype.lib.JSONSettingRegistry.get_item` + + """ + with open(self._registry_file, mode="r") as cfg: + data = json.load(cfg) + try: + value = data["registry"][name] + except KeyError: + raise ValueError( + "Registry doesn't contain value {}".format(name)) + return value + + def get_item(self, name): + # type: (str) -> object + """Get item value from registry json. + + Args: + name (str): Name of the item. + + Returns: + value of the item + + Raises: + ValueError: If item is not found in registry file. + + """ + return self._get_item(name) + + def _set_item(self, name, value): + # type: (str, object) -> None + """Set item value to registry json. + + Note: + See :meth:`pype.lib.JSONSettingRegistry.set_item` + + """ + with open(self._registry_file, "r+") as cfg: + data = json.load(cfg) + data["registry"][name] = value + cfg.truncate(0) + cfg.seek(0) + json.dump(data, cfg, indent=4) + + def set_item(self, name, value): + # type: (str, object) -> None + """Set item and its value into json registry file. + + Args: + name (str): name of the item. + value (Any): value of the item. + + """ + self._set_item(name, value) + + def _delete_item(self, name): + # type: (str) -> None + self._get_item.cache_clear() + with open(self._registry_file, "r+") as cfg: + data = json.load(cfg) + del data["registry"][name] + cfg.truncate(0) + cfg.seek(0) + json.dump(data, cfg, indent=4) + + +class PypeSettingsRegistry(JSONSettingRegistry): + """Class handling Pype general settings registry. + + Attributes: + vendor (str): Name used for path construction. + product (str): Additional name used for path construction. + + """ + + def __init__(self): + self.vendor = "pypeclub" + self.product = "pype" + path = appdirs.user_data_dir(self.product, self.vendor) + super(PypeSettingsRegistry, self).__init__("pype_settings", path) diff --git a/pype/lib/terminal_splash.py b/pype/lib/terminal_splash.py index 0d148bd6e2..1a7645571e 100644 --- a/pype/lib/terminal_splash.py +++ b/pype/lib/terminal_splash.py @@ -27,11 +27,12 @@ def play_animation(): animation_length = int(len(animation) / frame_size) current_frame = 0 for _ in range(animation_length): - frame = "" - y = 0 - for scanline in animation[current_frame:current_frame + frame_size]: - frame += scanline - y += 1 + frame = "".join( + scanline + for y, scanline in enumerate( + animation[current_frame : current_frame + frame_size] + ) + ) with term.location(0, 0): # term.aquamarine3_bold(frame) diff --git a/pype/lib/user_settings.py b/pype/lib/user_settings.py index 0b40eccb65..00ce68cb0b 100644 --- a/pype/lib/user_settings.py +++ b/pype/lib/user_settings.py @@ -28,8 +28,6 @@ import platform import appdirs import six -from ..version import __version__ - @six.add_metaclass(ABCMeta) class ASettingRegistry(): @@ -213,11 +211,12 @@ class IniSettingRegistry(ASettingRegistry): # type: (str, str) -> IniSettingRegistry super(IniSettingRegistry, self).__init__(name) # get registry file + version = os.getenv("PYPE_VERSION", "N/A") self._registry_file = os.path.join(path, "{}.ini".format(name)) if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: print("# Settings registry", cfg) - print("# Generated by Pype {}".format(__version__), cfg) + print("# Generated by Pype {}".format(version), cfg) now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") print("# {}".format(now), cfg) @@ -368,7 +367,7 @@ class JSONSettingRegistry(ASettingRegistry): now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") header = { "__metadata__": { - "pype-version": __version__, + "pype-version": os.getenv("PYPE_VERSION", "N/A"), "generated": now }, "registry": {} @@ -459,9 +458,9 @@ class PypeSettingsRegistry(JSONSettingRegistry): product (str): Additional name used for path construction. """ - vendor = "pypeclub" - product = "pype" def __init__(self): + self.vendor = "pypeclub" + self.product = "pype" path = appdirs.user_data_dir(self.product, self.vendor) super(PypeSettingsRegistry, self).__init__("pype_settings", path) diff --git a/setup.py b/setup.py index fbf8f3ec1d..0924f1070d 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,8 @@ build_options = dict( includes=includes, excludes=excludes, bin_includes=bin_includes, - include_files=include_files + include_files=include_files, + optimize=0 ) icon_path = pype_root / "igniter" / "pype.ico" diff --git a/start.py b/start.py index eb8bb7cd42..d47dc1080c 100644 --- a/start.py +++ b/start.py @@ -99,7 +99,16 @@ import traceback import subprocess from pathlib import Path -import acre +# add dependencies folder to sys.pat for frozen code +if getattr(sys, 'frozen', False): + frozen_libs = os.path.normpath( + os.path.join(os.path.dirname(sys.executable), "dependencies")) + sys.path.append(frozen_libs) + # add stuff from `/dependencies` to PYTHONPATH. + pythonpath = os.getenv("PYTHONPATH", "") + paths = pythonpath.split(os.pathsep) + paths.append(frozen_libs) + os.environ["PYTHONPATH"] = os.pathsep.join(paths) from igniter import BootstrapRepos from igniter.tools import load_environments @@ -116,6 +125,15 @@ def set_environments() -> None: better handling of environments """ + try: + import acre + except ImportError: + if getattr(sys, 'frozen', False): + sys.path.append(os.path.join( + os.path.dirname(sys.executable), + "dependencies" + )) + import acre try: env = load_environments(["global"]) except OSError as e: @@ -163,6 +181,7 @@ def set_modules_environments(): """ from pype.modules import ModulesManager + import acre modules_manager = ModulesManager() @@ -267,7 +286,8 @@ def _find_frozen_pype(use_version: str = None, """ pype_version = None - pype_versions = bootstrap.find_pype(include_zips=True) + pype_versions = bootstrap.find_pype(include_zips=True, + staging=use_staging) try: # use latest one found (last in the list is latest) pype_version = pype_versions[-1] @@ -281,16 +301,6 @@ def _find_frozen_pype(use_version: str = None, if not pype_versions: raise RuntimeError("No Pype versions found.") - # find only staging versions - if use_staging: - staging_versions = [v for v in pype_versions if v.is_staging()] - if not staging_versions: - raise RuntimeError("No Pype staging versions found.") - - staging_versions.sort() - # get latest staging version (last in the list is latest) - pype_version = staging_versions[-1] - # get path of version specified in `--use-version` version_path = BootstrapRepos.get_version_path_from_list( use_version, pype_versions) @@ -322,18 +332,11 @@ def _find_frozen_pype(use_version: str = None, print(">>> Extracting zip file ...") version_path = bootstrap.extract_pype(pype_version) + os.environ["PYPE_VERSION"] = pype_version.version # inject version to Python environment (sys.path, ...) print(">>> Injecting Pype version to running environment ...") bootstrap.add_paths_from_directory(version_path) - # add stuff from `/lib` to PYTHONPATH. - pythonpath = os.getenv("PYTHONPATH", "") - paths = pythonpath.split(os.pathsep) - frozen_libs = os.path.normpath( - os.path.join(os.path.dirname(sys.executable), "lib")) - paths.append(frozen_libs) - os.environ["PYTHONPATH"] = os.pathsep.join(paths) - # set PYPE_ROOT to point to currently used Pype version. os.environ["PYPE_ROOT"] = os.path.normpath(version_path.as_posix()) @@ -347,7 +350,7 @@ def boot(): # Play animation # ------------------------------------------------------------------------ - from pype.lib.terminal_splash import play_animation + from igniter.terminal_splash import play_animation # don't play for silenced commands if all(item not in sys.argv for item in silent_commands): @@ -363,7 +366,6 @@ def boot(): # Determine mongodb connection # ------------------------------------------------------------------------ - pype_mongo = "" try: pype_mongo = _determine_mongodb() except RuntimeError as e: @@ -385,7 +387,6 @@ def boot(): if getattr(sys, 'frozen', False): # find versions of Pype to be used with frozen code - version_path = None try: version_path = _find_frozen_pype(use_version, use_staging) except RuntimeError as e: @@ -399,6 +400,7 @@ def boot(): os.path.dirname(os.path.realpath(__file__))) # get current version of Pype local_version = bootstrap.get_local_live_version() + os.environ["PYPE_VERSION"] = local_version if use_version and use_version != local_version: pype_versions = bootstrap.find_pype(include_zips=True) version_path = BootstrapRepos.get_version_path_from_list( @@ -406,7 +408,9 @@ def boot(): if version_path: # use specified bootstrap.add_paths_from_directory(version_path) - + os.environ["PYPE_VERSION"] = use_version + else: + version_path = pype_root 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] @@ -434,6 +438,8 @@ def boot(): del sys.modules["pype.version"] except AttributeError: pass + except KeyError: + pass from pype import cli from pype.lib import terminal as t @@ -442,10 +448,9 @@ def boot(): set_modules_environments() info = get_info() - info.insert(0, ">>> Using Pype from [ {} ]".format( - os.path.dirname(cli.__file__))) + info.insert(0, f">>> Using Pype from [ {version_path} ]") - t_width = os.get_terminal_size().columns + t_width = os.get_terminal_size().columns - 2 _header = f"*** Pype [{__version__}] " info.insert(0, _header + "-" * (t_width - len(_header))) diff --git a/tests/igniter/test_bootstrap_repos.py b/tests/igniter/test_bootstrap_repos.py index 34ddc12550..59469b0687 100644 --- a/tests/igniter/test_bootstrap_repos.py +++ b/tests/igniter/test_bootstrap_repos.py @@ -33,7 +33,8 @@ def test_pype_version(): assert str(v3) == "1.2.3-staging" v4 = PypeVersion(1, 2, 3, variant="staging", client="client") - assert str(v4) == "1.2.3-staging-client" + assert str(v4) == "1.2.3-client-staging" + assert v3 < v4 v5 = PypeVersion(1, 2, 3, variant="foo", client="x") assert str(v5) == "1.2.3-x" @@ -100,7 +101,7 @@ def test_pype_version(): with pytest.raises(ValueError): _ = PypeVersion(version="booobaa") - v11 = PypeVersion(version="4.6.7-staging-client") + v11 = PypeVersion(version="4.6.7-client-staging") assert v11.major == 4 assert v11.minor == 6 assert v11.subversion == 7 @@ -131,7 +132,7 @@ def test_search_string_for_pype_version(printer): ("foo-3.0", False), ("foo-3.0.1", True), ("3", False), - ("foo-3.0.1-staging-client", True), + ("foo-3.0.1-client-staging", True), ("foo-3.0.1-bar-baz", True) ] for ver_string in strings: @@ -178,7 +179,7 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): suffix=".zip", type="zip", valid=True), test_pype(prefix="bum-v", version="5.5.4-staging", suffix=".zip", type="zip", valid=True), - test_pype(prefix="zum-v", version="5.5.5-staging-client", + test_pype(prefix="zum-v", version="5.5.5-client-staging", suffix=".zip", type="zip", valid=True), test_pype(prefix="fam-v", version="5.6.3", suffix=".zip", type="zip", valid=True), @@ -201,7 +202,7 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): suffix=".zip", type="zip", valid=True), test_pype(prefix="woo-v", version="7.2.8-client-strange", suffix=".zip", type="zip", valid=True), - test_pype(prefix="loo-v", version="7.2.10-staging-client", + test_pype(prefix="loo-v", version="7.2.10-client-staging", suffix=".zip", type="zip", valid=True), test_pype(prefix="kok-v", version="7.0.1", suffix=".zip", type="zip", valid=True) @@ -222,7 +223,7 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): suffix=".zip", type="zip", valid=True), test_pype(prefix="foo-v", version="3.0.1-staging", suffix=".zip", type="zip", valid=True), - test_pype(prefix="foo-v", version="3.0.1-staging-client", + test_pype(prefix="foo-v", version="3.0.1-client-staging", suffix=".zip", type="zip", valid=True), test_pype(prefix="foo-v", version="3.2.0", suffix=".zip", type="zip", valid=True) @@ -254,8 +255,8 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): fp.write("invalid") def _create_valid_dir(path: Path, version: str): - pype_path = path / "pype" - version_path = path / "pype" / "version.py" + pype_path = path / "pype" / "pype" + version_path = pype_path / "version.py" pype_path.mkdir(parents=True, exist_ok=True) with open(version_path, "w") as fp: fp.write(f"__version__ = '{version}'\n\n") @@ -306,7 +307,7 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): _build_test_item(dir_path, test_file) printer("testing finding Pype in given path ...") - result = fix_bootstrap.find_pype(g_path, True) + result = fix_bootstrap.find_pype(g_path, include_zips=True) # we should have results as file were created assert result is not None, "no Pype version found" # latest item in `result` should be latest version found. @@ -317,6 +318,7 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): test_versions_3[3].suffix ) ) + assert result, "nothing found" assert result[-1].path == expected_path, "not a latest version of Pype 3" monkeypatch.setenv("PYPE_PATH", e_path.as_posix()) @@ -332,6 +334,7 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): test_versions_1[5].suffix ) ) + assert result, "nothing found" assert result[-1].path == expected_path, "not a latest version of Pype 1" monkeypatch.delenv("PYPE_PATH", raising=False) @@ -350,14 +353,15 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): # latest item in `result` should be latest version found. expected_path = Path( d_path / "{}{}{}".format( - test_versions_2[4].prefix, - test_versions_2[4].version, - test_versions_2[4].suffix + test_versions_2[3].prefix, + test_versions_2[3].version, + test_versions_2[3].suffix ) ) + assert result, "nothing found" assert result[-1].path == expected_path, "not a latest version of Pype 2" - result = fix_bootstrap.find_pype(e_path, True) + result = fix_bootstrap.find_pype(e_path, include_zips=True) assert result is not None, "no Pype version found" expected_path = Path( e_path / "{}{}{}".format( @@ -368,12 +372,13 @@ def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): ) assert result[-1].path == expected_path, "not a latest version of Pype 1" - result = fix_bootstrap.find_pype(dir_path, True) + result = fix_bootstrap.find_pype(dir_path, include_zips=True) assert result is not None, "no Pype versions found" expected_path = Path( - e_path / "{}{}{}".format( + dir_path / "{}{}{}".format( test_versions_4[0].prefix, test_versions_4[0].version, test_versions_4[0].suffix ) ) + assert result[-1].path == expected_path, "not a latest version of Pype 4" diff --git a/tools/build.ps1 b/tools/build.ps1 index 389846d8ea..f2e6b3ba0d 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -168,6 +168,7 @@ catch { Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Cleaning cache files ... " -NoNewline Get-ChildItem $pype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force +Get-ChildItem $pype_root -Filter "*.pyo" -Force -Recurse | Remove-Item -Force Get-ChildItem $pype_root -Filter "__pycache__" -Force -Recurse | Remove-Item -Force -Recurse Write-Host "OK" -ForegroundColor green @@ -176,6 +177,7 @@ Write-Host "Building Pype ..." $out = & python setup.py build 2>&1 Set-Content -Path "$($pype_root)\build\build.log" -Value $out +& python -B "$($pype_root)\tools\build_dependencies.py" Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "deactivating venv ..." diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py new file mode 100644 index 0000000000..f85331a787 --- /dev/null +++ b/tools/build_dependencies.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +"""Script to fix frozen dependencies. + +Because Pype code needs to run under different versions of Python interpreter +(yes, even Python 2) we need to include all dependencies as source code +without Python's system stuff. Cx-freeze puts everything into lib and compile +it as .pyc/.pyo files and that doesn't work for hosts like Maya 2020 with +their own Python interpreter and libraries. + +This script will take ``site-packages`` and copy them to built Pype under +``dependencies`` directory. It will then compare stuff inside with ``lib`` folder +in frozen Pype, removing duplicities from there. + +This must be executed after build finished and it is done by build PowerShell +script. + +Note: Speedcopy can be used for copying if server-side copy is important for +speed. + +""" +import os +import sys +import site +from distutils.util import get_platform +from pathlib import Path +import shutil +import blessed +import time + + +term = blessed.Terminal() + + +def _print(msg: str, type: int = 0) -> None: + """Print message to console. + + Args: + msg (str): message to print + type (int): type of message (0 info, 1 error, 2 note) + + """ + if type == 0: + header = term.aquamarine3(">>> ") + elif type == 1: + header = term.orangered2("!!! ") + elif type == 2: + header = term.tan1("... ") + else: + header = term.darkolivegreen3("--- ") + + print("{}{}".format(header, msg)) + + +_print("Starting dependency cleanup ...") +start_time = time.time_ns() + +# path to venv site packages +sites = site.getsitepackages() + +# WARNING: this assumes that all we've got is path to venv itself and +# another path ending with 'site-packages' as is default. But because +# this must run under different platform, we cannot easily check if this path +# is the one, because under Linux and macOS site-packages are in different +# location. +site_pkg = None +for s in sites: + site_pkg = Path(s) + if site_pkg.name == "site-packages": + break + +_print("Getting venv site-packages ...") +assert site_pkg, "No venv site-packages are found." +_print(f"Working with: {site_pkg}", 2) + +# now, copy it to build directory +build_dir = None +if sys.platform.startswith("linux"): + # TODO: what is it under linux? + raise NotImplementedError("not implemented for linux yet") +elif sys.platform == "darwin": + # TODO: what is it under macOS? + raise NotImplementedError("not implemented for macOS yet") +elif sys.platform == "win32": + # string is formatted as cx_freeze is doing it + build_dir = "exe.{}-{}".format(get_platform(), sys.version[0:3]) + +# create full path +build_dir = Path(os.path.dirname(__file__)).parent / "build" / build_dir + +_print(f"Using build at {build_dir}", 2) +assert build_dir.exists(), "Build directory doesn't exist" + +deps_dir = build_dir / "dependencies" + +# copy all files +_print("Copying dependencies ...") +shutil.copytree(site_pkg.as_posix(), deps_dir.as_posix()) + +# iterate over frozen libs and create list to delete +libs_dir = build_dir / "lib" + +to_delete = [] +_print("Finding duplicates ...") +for d in libs_dir.iterdir(): + if (deps_dir / d.name) in deps_dir.iterdir(): + to_delete.append(d) + _print(f"found {d}", 3) + +# delete duplicates +_print(f"Deleting {len(to_delete)} duplicates ...") +for d in to_delete: + if d.is_dir(): + shutil.rmtree(d) + else: + d.unlink() + +end_time = time.time_ns() +total_time = (end_time - start_time) / 1000000000 +_print(f"Dependency cleanup done in {total_time} secs.") diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index b2d0319534..a78b06bb20 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -90,7 +90,7 @@ Write-Host "OK [ $p ]" -ForegroundColor green Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Entering venv ..." try { - . (".\venv\Scripts\Activate.ps1") + . ("$($pype_root)\venv\Scripts\Activate.ps1") } catch { Write-Host "!!! Failed to activate" -ForegroundColor red @@ -99,6 +99,9 @@ catch { } Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Generating zip from current sources ..." +Write-Host "... " -NoNewline -ForegroundColor Magenta +Write-Host "arguments: " -NoNewline -ForegroundColor Gray +Write-Host $ARGS -ForegroundColor White & python "$($pype_root)\start.py" generate-zip $ARGS Write-Host ">>> " -NoNewline -ForegroundColor green