diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 999a6daa19..448282b30b 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- """Bootstrap Pype repositories.""" -import sys +import functools +import logging as log import os import re -import logging as log import shutil +import sys import tempfile -from typing import Union, Callable, List -from zipfile import ZipFile from pathlib import Path -import functools - -from speedcopy import copyfile +from typing import Union, Callable, List, Tuple +from zipfile import ZipFile, BadZipFile from appdirs import user_data_dir -from pype.version import __version__ +from speedcopy import copyfile + from pype.lib import PypeSettingsRegistry +from pype.version import __version__ from .tools import load_environments @@ -39,6 +39,9 @@ class PypeVersion: client = None path = None + _version_regex = re.compile( + r"(?P\d+)\.(?P\d+)\.(?P\d+)(-?((?Pstaging)|(?P.+))(-(?P.+))?)?") # noqa: E501 + @property def version(self): """return formatted version string.""" @@ -58,8 +61,6 @@ class PypeVersion: variant: str = "production", client: str = None, path: Path = None): self.path = path - self._version_regex = re.compile( - r"(?P\d+)\.(?P\d+)\.(?P\d+)(-?((?Pstaging)|(?P.+))(-(?P.+))?)?") # noqa: E501 if major is None or minor is None or subversion is None: if version is None: @@ -91,8 +92,9 @@ class PypeVersion: return version - def _decompose_version(self, version_string: str) -> tuple: - m = re.match(self._version_regex, version_string) + @classmethod + def _decompose_version(cls, version_string: str) -> tuple: + m = re.search(cls._version_regex, version_string) if not m: raise ValueError( "Cannot parse version string: {}".format(version_string)) @@ -138,6 +140,27 @@ class PypeVersion: return False + @staticmethod + def version_in_str(string: str) -> Tuple: + """Find Pype version in given string. + + Args: + string (str): string to search. + + Returns: + tuple: True/False and PypeVersion if found. + + """ + try: + result = PypeVersion._decompose_version(string) + except ValueError: + return False, None + return True, PypeVersion(major=result[0], + minor=result[1], + subversion=result[2], + variant=result[3], + client=result[4]) + class BootstrapRepos: """Class for bootstrapping local Pype installation. @@ -163,6 +186,7 @@ class BootstrapRepos: self._log = log.getLogger(str(__class__)) self.data_dir = Path(user_data_dir(self._app, self._vendor)) self.registry = PypeSettingsRegistry() + self.zip_filter = [".pyc", "__pycache__"] # dummy progress reporter def empty_progress(x: int): @@ -225,7 +249,7 @@ class BootstrapRepos: """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 + or in user provided path. Then it will zip it in temporary directory and finally it will move it to destination which is user data directory. Existing files will be replaced. @@ -252,7 +276,7 @@ class BootstrapRepos: # create zip inside temporary directory. with tempfile.TemporaryDirectory() as temp_dir: temp_zip = \ - Path(temp_dir) / f"pype-repositories-v{version}.zip" + Path(temp_dir) / f"pype-v{version}.zip" self._log.info(f"creating zip: {temp_zip}") self._create_pype_zip(temp_zip, repo_dir) @@ -275,7 +299,7 @@ class BootstrapRepos: except shutil.Error as e: self._log.error(e) return None - return self.data_dir / temp_zip.name + return destination def _create_pype_zip( self, @@ -287,9 +311,6 @@ class BootstrapRepos: to later implement file filter to skip git related stuff to make it into archive. - Todo: - Implement file filter - Args: zip_path (str): path to zip file. include_dir (Path): repo directories to include. @@ -310,18 +331,46 @@ class BootstrapRepos: with ZipFile(zip_path, "w") as zip_file: for root, _, files in os.walk(include_dir.as_posix()): for file in files: + progress += repo_inc + self._progress_callback(int(progress)) + + # skip all starting with '.' + if file.startswith("."): + continue + + # skip if direct parent starts with '.' + if Path(root).parts[-1].startswith("."): + continue + + # filter + if file in self.zip_filter: + continue + zip_file.write( os.path.relpath(os.path.join(root, file), os.path.join(include_dir, '..')), os.path.relpath(os.path.join(root, file), os.path.join(include_dir)) ) - progress += repo_inc - self._progress_callback(int(progress)) # add pype itself if include_pype: for root, _, files in os.walk("pype"): for file in files: + progress += pype_inc + self._progress_callback(int(progress)) + + # skip all starting with '.' + if file.startswith("."): + continue + + # skip if direct parent starts with '.' + if Path(root).parts[-1].startswith("."): + continue + + # filter + if file in self.zip_filter: + continue + zip_file.write( os.path.relpath(os.path.join(root, file), os.path.join('pype', '..')), @@ -330,8 +379,7 @@ class BootstrapRepos: os.path.relpath(os.path.join(root, file), os.path.join('pype', '..'))) ) - progress += pype_inc - self._progress_callback(int(progress)) + zip_file.testzip() self._progress_callback(100) @@ -342,8 +390,10 @@ class BootstrapRepos: This will enable Python to import modules is second-level directories in zip file. + Adding to both `sys.path` and `PYTHONPATH`, skipping duplicates. + Args: - archive (str): path to archive. + archive (Path): path to archive. """ with ZipFile(archive, "r") as zip_file: @@ -362,8 +412,37 @@ class BootstrapRepos: os.environ["PYTHONPATH"] = os.pathsep.join(paths) + @staticmethod + def add_paths_from_directory(directory: Path) -> None: + """Add first level directories as paths to :mod:`sys.path`. + + This works the same as :meth:`add_paths_from_archive` but in + specified directory. + + Adding to both `sys.path` and `PYTHONPATH`, skipping duplicates. + + Args: + directory (Path): path to directory. + + """ + roots = [] + for item in directory.iterdir(): + if item.is_dir(): + root = item.as_posix() + if root not in roots: + roots.append(root) + sys.path.insert(0, root) + + pythonpath = os.getenv("PYTHONPATH", "") + paths = pythonpath.split(os.pathsep) + paths += roots + + os.environ["PYTHONPATH"] = os.pathsep.join(paths) + def find_pype( - self, pype_path: Path = None) -> Union[List[PypeVersion], None]: + self, + pype_path: Path = None, + include_zips: bool = False) -> Union[List[PypeVersion], None]: """Get ordered dict of detected Pype version. Resolution order for Pype is following: @@ -374,6 +453,8 @@ class BootstrapRepos: Args: pype_path (Path, optional): Try to find Pype on the given path. + include_zips (bool, optional): If set True it will try to find + Pype in zip files in given directory. Returns: dict of Path: Dictionary of detected Pype version. @@ -383,42 +464,87 @@ class BootstrapRepos: """ dir_to_search = self.data_dir - if os.getenv("PYPE_PATH"): - if Path(os.getenv("PYPE_PATH")).exists(): - dir_to_search = Path(os.getenv("PYPE_PATH")) - else: - try: - registry_dir = Path(self.registry.get_item("pypePath")) - if registry_dir.exists(): - dir_to_search = registry_dir - except ValueError: - # nothing found in registry, we'll use data dir - pass - - # if we have pyp_path specified, search only there. + # if we have pype_path specified, search only there. if pype_path: dir_to_search = pype_path + else: + if os.getenv("PYPE_PATH"): + if Path(os.getenv("PYPE_PATH")).exists(): + dir_to_search = Path(os.getenv("PYPE_PATH")) + else: + try: + registry_dir = Path(str(self.registry.get_item("pypePath"))) + if registry_dir.exists(): + dir_to_search = registry_dir + + except ValueError: + # nothing found in registry, we'll use data dir + pass # pype installation dir doesn't exists if not dir_to_search.exists(): return None _pype_versions = [] - file_pattern = re.compile(r"^pype-repositories-v(?P\d+\.\d+\.\d*.+?).zip$") # noqa: E501 + # iterate over directory in first level and find all that might + # contain Pype. for file in dir_to_search.iterdir(): - m = re.match( - file_pattern, - file.name) - if m: - try: - _pype_versions.append( - PypeVersion( - version=m.group("version"), path=file)) - except ValueError: - # cannot parse version string - print(m) - pass + + result = PypeVersion.version_in_str(file.stem) + + if result[0]: + detected_version = result[1] + + if file.is_dir(): + # if item is directory that might (based on it's name) + # contain Pype version, check if it really does contain + # Pype and that their versions matches. + version_check = PypeVersion( + version=BootstrapRepos.get_version(file)) + if version_check != detected_version: + self._log.error( + (f"dir version ({detected_version}) and " + f"its content version ({version_check}) " + "doesn't match. Skipping.")) + continue + + if file.is_file(): + + if not include_zips: + continue + + # skip non-zip files + if file.suffix.lower() != ".zip": + continue + + # open zip file, look inside and parse version from Pype + # inside it. If there is none, or it is different from + # version specified in file name, skip it. + try: + with ZipFile(file, "r") as zip_file: + with zip_file.open( + "pype/pype/version.py") as version_file: + zip_version = {} + exec(version_file.read(), zip_version) + version_check = PypeVersion( + version=zip_version["__version__"]) + + if version_check != detected_version: + self._log.error( + (f"zip version ({detected_version}) and " + f"its content version ({version_check}) " + "doesn't match. Skipping.")) + continue + except BadZipFile: + self._log.error(f"{file} is not zip file") + continue + except KeyError: + self._log.error("Zip not containing Pype") + continue + + detected_version.path = file + _pype_versions.append(detected_version) return sorted(_pype_versions) @@ -426,16 +552,16 @@ class BootstrapRepos: def _get_pype_from_mongo(mongo_url: str) -> Union[Path, None]: """Get path from Mongo database. - This sets environment variable ``AVALON_MONGO`` for + This sets environment variable ``PYPE_MONGO`` for :mod:`pype.settings` to be able to read data from database. It will then retrieve environment variables and among them - must be ``PYPE_ROOT``. + must be ``PYPE_PATH``. Args: mongo_url (str): mongodb connection url Returns: - Path: if path from ``PYPE_ROOT`` is found. + Path: if path from ``PYPE_PATH`` is found. None: if not. """ @@ -479,11 +605,18 @@ class BootstrapRepos: 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. + # test if entered path isn't user data dir + if self.data_dir == pype_path: + self._log.error(f"cannot point to user data dir") + return None + + # find pype zip files in location. There can be + # either "live" Pype repository, or multiple zip files or even + # multiple pype version directories. This process looks into zip + # files and directories and tries to parse `version.py` file. versions = self.find_pype(pype_path) if versions: - self._log.info(f"found Pype zips in [ {pype_path} ].") + self._log.info(f"found Pype in [ {pype_path} ]") self._log.info(f"latest version found is [ {versions[-1]} ]") destination = self.data_dir / versions[-1].path.name @@ -503,13 +636,43 @@ class BootstrapRepos: if not destination.parent.exists(): destination.parent.mkdir(parents=True) + # latest version found is directory + if versions[-1].path.is_dir(): + # zip it, copy it and extract it + # create zip inside temporary directory. + self._log.info("Creating zip from directory ...") + with tempfile.TemporaryDirectory() as temp_dir: + temp_zip = \ + Path(temp_dir) / f"pype-v{versions[-1]}.zip" + self._log.info(f"creating zip: {temp_zip}") + + self._create_pype_zip(temp_zip, versions[-1].path) + if not os.path.exists(temp_zip): + self._log.error("make archive failed.") + return None + + destination = self.data_dir / temp_zip.name + + elif versions[-1].path.is_file(): + # in this place, it must be zip file as `find_pype()` is + # checking just that. + assert versions[-1].path.suffix.lower() == ".zip", ( + "Invalid file format" + ) try: + self._log.info("Copying zip to destination ...") copyfile(versions[-1].path.as_posix(), destination.as_posix()) except OSError: self._log.error( "cannot copy detected version to user data directory", exc_info=True) return None + + # extract zip there + self._log.info("extracting zip to destination ...") + with ZipFile(versions[-1].path, "r") as zip_ref: + zip_ref.extractall(destination) + return destination # if we got here, it means that location is "live" Pype repository. @@ -518,4 +681,22 @@ class BootstrapRepos: if not repo_file.exists(): self._log.error(f"installing zip {repo_file} failed.") return None - return repo_file + + destination = self.data_dir / repo_file.stem + if destination.exists(): + try: + destination.unlink() + except OSError: + self._log.error( + f"cannot remove already existing {destination}", + exc_info=True) + return None + + destination.mkdir(parents=True) + + # extract zip there + self._log.info("extracting zip to destination ...") + with ZipFile(versions[-1].path, "r") as zip_ref: + zip_ref.extractall(destination) + + return destination diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 278877bdf7..e4253958e5 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- """Working thread for installer.""" import os +from zipfile import ZipFile from Qt.QtCore import QThread, Signal from .bootstrap_repos import BootstrapRepos +from .bootstrap_repos import PypeVersion from .tools import validate_mongo_connection @@ -68,14 +70,62 @@ class InstallThread(QThread): os.environ["PYPE_MONGO"] = self._mongo + self.message.emit( + f"Detecting installed Pype versions in {bs.data_dir}", False) + detected = bs.find_pype() + + if detected: + if PypeVersion(version=local_version) < detected[-1]: + self.message.emit(( + f"Latest installed version {detected[-1]} is newer " + f"then currently running {local_version}" + ), False) + self.message.emit("Skipping Pype install ...", False) + return + + if PypeVersion(version=local_version) == detected[-1]: + self.message.emit(( + f"Latest installed version is the same as " + f"currently running {local_version}" + ), False) + self.message.emit("Skipping Pype install ...", False) + return + + self.message.emit(( + "All installed versions are older then " + f"currently running one {local_version}" + ), False) + else: + self.message.emit("None detected.", False) + self.message.emit( f"We will use local Pype version {local_version}", False) + repo_file = bs.install_live_repos() if not repo_file: self.message.emit( - f"!!! install failed - {repo_file}", True) + f"!!! Install failed - {repo_file}", True) return - self.message.emit(f"installed as {repo_file}", False) + + destination = bs.data_dir / repo_file.stem + if destination.exists(): + try: + destination.unlink() + except OSError as e: + self.message.emit( + f"!!! Cannot remove already existing {destination}", + True) + self.message.emit(e.strerror, True) + return + + destination.mkdir(parents=True) + + # extract zip there + self.message.emit("Extracting zip to destination ...", False) + with ZipFile(repo_file, "r") as zip_ref: + zip_ref.extractall(destination) + + 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. diff --git a/pype.py b/pype.py index 769e8c8f6f..d613e8e0fc 100644 --- a/pype.py +++ b/pype.py @@ -6,22 +6,24 @@ Bootstrapping process of Pype is as follows: `PYPE_PATH` is checked for existence - either one from environment or from user settings. Precedence takes the one set by environment. -On this path we try to find zip files with `pype-repositories-v3.x.x.zip` -format. +On this path we try to find pype in directories version string in their names. +For example: `pype-v3.0.1-foo` is valid name, or even `foo_3.0.2` - as long +as version can be determined from its name _AND_ file `pype/pype/version.py` +can be found inside, it is considered Pype installation. -If no Pype repositories are found in `PYPE_PATH (user data dir) +If no Pype repositories are found in `PYPE_PATH` (user data dir) then **Igniter** (Pype setup tool) will launch its GUI. It can be used to specify `PYPE_PATH` or if it is _not_ specified, current -*"live"* repositories will be used to create such zip file and copy it to -appdata dir in user home. Version will be determined by version specified -in Pype module. +*"live"* repositories will be used to create zip file and copy it to +appdata dir in user home and extract it there. Version will be determined by +version specified in Pype module. -If Pype repositories zip file is found in default install location -(user data dir) or in `PYPE_PATH`, it will get list of those zips there and +If Pype repository directories are found in default install location +(user data dir) or in `PYPE_PATH`, it will get list of those dirs there and use latest one or the one specified with optional `--use-version` command line argument. If the one specified doesn't exist then latest available -version will be used. All repositories in that zip will be added +version will be used. All repositories in that dir will be added to `sys.path` and `PYTHONPATH`. If Pype is live (not frozen) then current version of Pype module will be @@ -29,9 +31,56 @@ used. All directories under `repos` will be added to `sys.path` and `PYTHONPATH`. Pype depends on connection to `MongoDB`_. You can specify MongoDB connection -string via `AVALON_MONGO` set in environment or it can be set in user +string via `PYPE_MONGO` set in environment or it can be set in user settings or via **Igniter** GUI. +So, bootstrapping Pype looks like this:: + +.. code-block:: bash + ++-------------------------------------------------------+ +| Determine MongoDB connection: | +| Use `PYPE_MONGO`, system keyring `pypeMongo` | ++--------------------------|----------------------------+ + .--- Found? --. + YES NO + | | + | +------v--------------+ + | | Fire up Igniter GUI |<---------\ + | | and ask User | | + | +---------------------+ | + | | + | | ++-----------------v------------------------------------+ | +| Get location of Pype: | | +| 1) Test for `PYPE_PATH` environment variable | | +| 2) Test `pypePath` in registry setting | | +| 3) Test user data directory | | +| ................................................... | | +| If running from frozen code: | | +| - Use latest one found in user data dir | | +| If running from live code: | | +| - Use live code and install it to user data dir | | +| * can be overridden with `--use-version` argument | | ++-------------------------|----------------------------+ | + .-- Is Pype found? --. | + YES NO | + | | | + | +--------------v------------------+ | + | | Look in `PYPE_PATH`, find | | + | | latest version and install it | | + | | to user data dir. | | + | +--------------|------------------+ | + | .-- Is Pype found? --. | + | YES NO ---------/ + | | + |<--------/ + | ++-------------v------------+ +| Run Pype | ++--------------------------+ + + Todo: Move or remove bootstrapping environments out of the code. @@ -44,9 +93,8 @@ import re import sys import traceback -from igniter.tools import load_environments, add_acre_to_sys_path - from igniter import BootstrapRepos +from igniter.tools import load_environments, add_acre_to_sys_path try: import acre @@ -94,7 +142,7 @@ def set_modules_environments(): _publish_paths.add(os.path.normpath(path)) module_envs["PYBLISHPLUGINPATH"] = os.pathsep.join(_publish_paths) - # Metge environments with current environments and update values + # Merge environments with current environments and update values if module_envs: parsed_envs = acre.parse(module_envs) env = acre.merge(parsed_envs, dict(os.environ)) @@ -143,7 +191,7 @@ def boot(): use_version, pype_versions) if version_path: # use specified - bootstrap.add_paths_from_archive(version_path) + bootstrap.add_paths_from_directory(version_path) else: if use_version is not None: @@ -151,7 +199,7 @@ def boot(): "latest available")) # use latest version_path = pype_versions[-1].path - bootstrap.add_paths_from_archive(version_path) + bootstrap.add_paths_from_directory(version_path) use_version = str(pype_versions[-1]) os.environ["PYPE_ROOT"] = version_path.as_posix() @@ -164,7 +212,7 @@ def boot(): use_version, pype_versions) if version_path: # use specified - bootstrap.add_paths_from_archive(version_path) + bootstrap.add_paths_from_directory(version_path) os.environ["PYPE_ROOT"] = pype_root repos = os.listdir(os.path.join(pype_root, "repos")) diff --git a/setup.py b/setup.py index 5e32a1afe1..fcc2ddaf2f 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,24 @@ # -*- coding: utf-8 -*- """Setup info for building Pype 3.0.""" -import sys import os +import sys + from cx_Freeze import setup, Executable from sphinx.setup_command import BuildDoc version = {} with open(os.path.join("pype", "version.py")) as fp: exec(fp.read(), version) -__version__ = version['__version__'] +__version__ = version["__version__"] +base = None +if sys.platform == "win32": + # base = "Win32GUI" + ... + +# ----------------------------------------------------------------------- +# build_exe +# Build options for cx_Freeze. Manually add/exclude packages and binaries install_requires = [ "appdirs", @@ -21,40 +30,47 @@ install_requires = [ "pathlib2", "PIL", "pymongo", + "pynput", + "jinxed", + "blessed", "Qt", - "speedcopy", - "win32ctypes" + "speedcopy" ] -base = None -if sys.platform == "win32": - base = "Win32GUI" +includes = [ + "repos/acre/acre", + "repos/avalon-core/avalon", + "repos/pyblish-base/pyblish", + "repos/maya-look-assigner/mayalookassigner" +] + +excludes = [] +bin_includes = [] +include_files = [ + "igniter", + "pype", + "repos", + "schema", + "setup", + "vendor", + "LICENSE", + "README.md", + "pype/version.py" +] + +if sys.platform == "win32": + install_requires.append("win32ctypes") -# Build options for cx_Freeze. Manually add/exclude packages and binaries buildOptions = dict( packages=install_requires, - includes=[ - 'repos/acre/acre', - 'repos/avalon-core/avalon', - 'repos/pyblish-base/pyblish', - 'repos/maya-look-assigner/mayalookassigner' - ], - excludes=[], - bin_includes=[], - include_files=[ - "igniter", - "pype", - "repos", - "schema", - "setup", - "vendor", - "LICENSE", - "README.md", - "pype/version.py"] + includes=includes, + excludes=excludes, + bin_includes=bin_includes, + include_files=include_files ) -executables = [Executable("pype.py", base=None, targetName="pype")] +executables = [Executable("pype.py", base=base, targetName="pype")] setup( name="pype", diff --git a/tests/igniter/test_bootstrap_repos.py b/tests/igniter/test_bootstrap_repos.py index 868b356771..a04632c3cb 100644 --- a/tests/igniter/test_bootstrap_repos.py +++ b/tests/igniter/test_bootstrap_repos.py @@ -2,9 +2,13 @@ """Test suite for repos bootstrapping (install).""" import os import sys +from collections import namedtuple from pathlib import Path -import pytest +from zipfile import ZipFile + import appdirs +import pytest + from igniter.bootstrap_repos import BootstrapRepos from igniter.bootstrap_repos import PypeVersion from pype.lib import PypeSettingsRegistry @@ -116,6 +120,20 @@ def test_get_version_path_from_list(): assert path == Path("/bar/baz") +def test_search_string_for_pype_version(printer): + strings = [ + ("3.0.1", True), + ("foo-3.0", False), + ("foo-3.0.1", True), + ("3", False), + ("foo-3.0.1-staging-client", True), + ("foo-3.0.1-bar-baz", True) + ] + for ver_string in strings: + printer(f"testing {ver_string[0]} should be {ver_string[1]}") + assert PypeVersion.version_in_str(ver_string[0])[0] == ver_string[1] + + def test_install_live_repos(fix_bootstrap, printer): rf = fix_bootstrap.install_live_repos() sep = os.path.sep @@ -125,8 +143,7 @@ def test_install_live_repos(fix_bootstrap, printer): f"{rf}{sep}avalon-unreal-integration", f"{rf}{sep}maya-look-assigner", f"{rf}{sep}pyblish-base", - f"{rf}{sep}pype", - f"{rf}{sep}pype-config" + f"{rf}{sep}pype" ] printer("testing zip creation") assert os.path.exists(rf), "zip archive was not created" @@ -147,92 +164,186 @@ def test_install_live_repos(fix_bootstrap, printer): def test_find_pype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): + test_pype = namedtuple("Pype", "prefix version suffix type valid") + test_versions_1 = [ - "pype-repositories-v5.5.1.zip", - "pype-repositories-v5.5.2-client.zip", - "pype-repositories-v5.5.3-client-strange.zip", - "pype-repositories-v5.5.4-staging.zip", - "pype-repositories-v5.5.5-staging-client.zip", - "pype-repositories-v5.6.3.zip", - "pype-repositories-v5.6.3-staging.zip" + test_pype(prefix="foo-v", version="5.5.1", + suffix=".zip", type="zip", valid=False), + test_pype(prefix="bar-v", version="5.5.2-client", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="baz-v", version="5.5.3-client-strange", + 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", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="fam-v", version="5.6.3", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="foo-v", version="5.6.3-staging", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="fim-v", version="5.6.3", + suffix=".zip", type="zip", valid=False), + test_pype(prefix="foo-v", version="5.6.4", + suffix=".txt", type="txt", valid=False), + test_pype(prefix="foo-v", version="5.7.1", + suffix="", type="dir", valid=False), ] test_versions_2 = [ - "pype-repositories-v7.2.6.zip", - "pype-repositories-v7.2.7-client.zip", - "pype-repositories-v7.2.8-client-strange.zip", - "pype-repositories-v7.2.9-staging.zip", - "pype-repositories-v7.2.10-staging-client.zip", - "pype-repositories-v7.0.1.zip", + test_pype(prefix="foo-v", version="10.0.0", + suffix=".txt", type="txt", valid=False), + test_pype(prefix="lom-v", version="7.2.6", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="bom-v", version="7.2.7-client", + 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", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="kok-v", version="7.0.1", + suffix=".zip", type="zip", valid=True) ] test_versions_3 = [ - "pype-repositories-v3.0.0.zip", - "pype-repositories-v3.0.1.zip", - "pype-repositories-v4.1.0.zip", - "pype-repositories-v4.1.2.zip", - "pype-repositories-v3.0.1-client.zip", - "pype-repositories-v3.0.1-client-strange.zip", - "pype-repositories-v3.0.1-staging.zip", - "pype-repositories-v3.0.1-staging-client.zip", - "pype-repositories-v3.2.0.zip", + test_pype(prefix="foo-v", version="3.0.0", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="goo-v", version="3.0.1", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="hoo-v", version="4.1.0", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="foo-v", version="4.1.2", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="foo-v", version="3.0.1-client", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="foo-v", version="3.0.1-client-strange", + 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", + suffix=".zip", type="zip", valid=True), + test_pype(prefix="foo-v", version="3.2.0", + suffix=".zip", type="zip", valid=True) ] + + def _create_invalid_zip(path: Path): + with ZipFile(path, "w") as zf: + zf.writestr("test.foo", "test") + + def _create_valid_zip(path: Path, version: str): + with ZipFile(path, "w") as zf: + zf.writestr( + "pype/pype/version.py", f"__version__ = '{version}'\n\n") + + def _create_invalid_dir(path: Path): + path.mkdir(parents=True, exist_ok=True) + with open(path / "invalid", "w") as fp: + fp.write("invalid") + + def _create_valid_dir(path: Path, version: str): + pype_path = path / "pype" + version_path = path / "pype" / "version.py" + pype_path.mkdir(parents=True, exist_ok=True) + with open(version_path, "w") as fp: + fp.write(f"__version__ = '{version}'\n\n") + + def _build_test_item(path, item): + test_path = path / "{}{}{}".format(item.prefix, + item.version, + item.suffix) + if item.type == "zip": + if item.valid: + _create_valid_zip(test_path, item.version) + else: + _create_invalid_zip(test_path) + elif item.type == "dir": + if item.valid: + _create_valid_dir(test_path, item.version) + else: + _create_invalid_dir(test_path) + else: + with open(test_path, "w") as fp: + fp.write("foo") # in PYPE_PATH e_path = tmp_path_factory.mktemp("environ") + + # create files and directories for test for test_file in test_versions_1: - with open(e_path / test_file, "w") as fp: - fp.write(test_file) + _build_test_item(e_path, test_file) # in pypePath registry - r_path = tmp_path_factory.mktemp("pypePath") + p_path = tmp_path_factory.mktemp("pypePath") for test_file in test_versions_2: - with open(r_path / test_file, "w") as fp: - fp.write(test_file) + _build_test_item(p_path, test_file) # in data dir - for test_file in test_versions_3: - with open(os.path.join(fix_bootstrap.data_dir, test_file), "w") as fp: - fp.write(test_file) + d_path = tmp_path_factory.mktemp("dataPath") + for test_file in test_versions_2: + _build_test_item(d_path, test_file) - result = fix_bootstrap.find_pype() + # in provided path + g_path = tmp_path_factory.mktemp("providedPath") + for test_file in test_versions_3: + _build_test_item(g_path, test_file) + + result = fix_bootstrap.find_pype(g_path, 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. - assert result[-1].path == Path( - fix_bootstrap.data_dir / test_versions_3[3] - ), "not a latest version of Pype 3" + expected_path = Path( + g_path / "{}{}{}".format( + test_versions_3[3].prefix, + test_versions_3[3].version, + test_versions_3[3].suffix + ) + ) + assert result[-1].path == expected_path, "not a latest version of Pype 3" monkeypatch.setenv("PYPE_PATH", e_path.as_posix()) - result = fix_bootstrap.find_pype() + result = fix_bootstrap.find_pype(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. - assert result[-1].path == Path( - e_path / test_versions_1[5] - ), "not a latest version of Pype 1" + expected_path = Path( + e_path / "{}{}{}".format( + test_versions_1[5].prefix, + test_versions_1[5].version, + test_versions_1[5].suffix + ) + ) + assert result[-1].path == expected_path, "not a latest version of Pype 1" monkeypatch.delenv("PYPE_PATH", raising=False) # mock appdirs user_data_dir def mock_user_data_dir(*args, **kwargs): - return r_path.as_posix() + return d_path.as_posix() monkeypatch.setattr(appdirs, "user_data_dir", mock_user_data_dir) fix_bootstrap.registry = PypeSettingsRegistry() - fix_bootstrap.registry.set_item("pypePath", r_path.as_posix()) + fix_bootstrap.registry.set_item("pypePath", d_path.as_posix()) - result = fix_bootstrap.find_pype() + result = fix_bootstrap.find_pype(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. - assert result[-1].path == Path( - r_path / test_versions_2[4] - ), "not a latest version of Pype 2" + expected_path = Path( + d_path / "{}{}{}".format( + test_versions_2[4].prefix, + test_versions_2[4].version, + test_versions_2[4].suffix + ) + ) + assert result[-1].path == expected_path, "not a latest version of Pype 2" - result = fix_bootstrap.find_pype(e_path) + result = fix_bootstrap.find_pype(e_path, True) assert result is not None, "no Pype version found" - assert result[-1].path == Path( - e_path / test_versions_1[5] - ), "not a latest version of Pype 1" + expected_path = Path( + e_path / "{}{}{}".format( + test_versions_1[5].prefix, + test_versions_1[5].version, + test_versions_1[5].suffix + ) + ) + assert result[-1].path == expected_path, "not a latest version of Pype 1"