Ejemplo n.º 1
0
    def test_module_and_local_dir_have_same_name(self, worker_id):
        """If a pip-installed module and a local directory share a name, the module is collected.

        If a user can import a package "foo" in their environment, and uses
        custom modules to find "foo", we will prefer that package over a
        directory/file "foo" in the cwd. Otherwise, it is very difficult or
        impossible to force the installed package.

        """
        name = worker_id

        # avoid using an existing package name
        hypothesis.assume(not CustomModules.is_importable(name))

        with utils.chtempdir():
            # create local directory with same name as package
            local_dir = os.path.abspath(name)
            os.mkdir(local_dir)
            with open(os.path.join(local_dir, "empty.json"), "w") as f:
                json.dump({}, f)

            # create package in another directory and install
            with utils.tempdir() as tempd:
                with contexts.installable_package(name, dir=tempd) as pkg_dir:
                    with contexts.installed_local_package(pkg_dir, name):
                        # collect and validate custom modules
                        custom_modules = _DeployableEntity._custom_modules_as_artifact(
                            [name], )
                        self.assert_in_custom_modules(custom_modules, name)
Ejemplo n.º 2
0
    def test_deploy_module_and_local_pkg_have_same_name(
        self,
        deployable_entity,
        endpoint,
        worker_id,
    ):
        """Deployment analogue to ``custom_modules/test_custom_modules.py::TestPipInstalledModule::test_module_and_local_pkg_have_same_name``.

        A success means that custom modules successfully collected the
        pip-installed module, and the deployed model could import it.

        """
        name = worker_id

        # avoid using an existing package name
        hypothesis.assume(not CustomModules.is_importable(name))

        with utils.chtempdir():
            # create package in *current* directory and install
            with contexts.installable_package(name, dir=".") as pkg_dir:
                with contexts.installed_local_package(pkg_dir, name):
                    Model = models.create_custom_module_model(name)
                    assert Model().predict("") == name  # sanity check

                    deployable_entity.log_model(Model, custom_modules=[name])
        deployable_entity.log_environment(Python([]))

        endpoint.update(deployable_entity, wait=True)

        assert endpoint.get_deployed_model().predict("") == name
Ejemplo n.º 3
0
    def test_deploy_pip_installed_custom_modules(
        self,
        deployable_entity,
        endpoint,
        worker_id,
    ):
        """A pip-installed custom module can be specified by name (instead of path).

        A success means that custom modules successfully collected the
        pip-installed module, and the deployed model could import it.

        """
        name = worker_id

        # avoid using an existing package nam
        hypothesis.assume(not CustomModules.is_importable(name))

        with utils.tempdir() as tempd:
            # create package in another directory and install
            with contexts.installable_package(name, dir=tempd) as pkg_dir:
                with contexts.installed_local_package(pkg_dir, name):
                    Model = models.create_custom_module_model(name)
                    assert Model().predict("") == name  # sanity check

                    deployable_entity.log_model(Model, custom_modules=[name])
        deployable_entity.log_environment(Python([]))

        endpoint.update(deployable_entity, wait=True)

        assert endpoint.get_deployed_model().predict("") == name
Ejemplo n.º 4
0
def installed_local_package(pkg_dir, name):
    """pip install a locally-defined package.

    Parameters
    ----------
    pkg_dir : str
        Absolute path to the package.
    name : str
        Name of the package. This is used to uninstall the package on
        cleanup.

    Warnings
    --------
    While packages are uninstalled on cleanup, their dependencies might not be.

    """
    subprocess.check_call(
        [
            sys.executable,
            "-m",
            "pip",
            "--no-python-version-warning",
            "install",
            "-qq",
            pkg_dir,
        ],
    )
    assert CustomModules.is_importable(name)  # verify installation

    try:
        yield
    finally:
        subprocess.check_call(
            [
                sys.executable,
                "-m",
                "pip",
                "--no-python-version-warning",
                "uninstall",
                "-y",
                name,
            ],
        )

        # delete cached module to disallow subsequent imports
        if name in sys.modules:
            del sys.modules[name]
Ejemplo n.º 5
0
    def test_module_and_local_pkg_have_same_name(self, worker_id):
        """A specific case of :meth:`test_module_and_local_dir_have_same_name`.

        The local directory *is* a Python package repository (but not directly
        importable without ``cd``ing one level into it).

        A user may have a monolithic project with model management scripts
        alongside Python package directories (that may *also* be installed
        into the environment).

        """
        name = worker_id

        # avoid using an existing package name
        hypothesis.assume(not CustomModules.is_importable(name))

        with utils.chtempdir():
            # create package in *current* directory and install
            with contexts.installable_package(name, dir=".") as pkg_dir:
                with contexts.installed_local_package(pkg_dir, name):
                    # collect and validate custom modules
                    custom_modules = _DeployableEntity._custom_modules_as_artifact(
                        [name], )
                    self.assert_in_custom_modules(custom_modules, name)
Ejemplo n.º 6
0
    def _custom_modules_as_artifact(paths=None):
        if isinstance(paths, six.string_types):
            paths = [paths]

        # If we include a path that is actually a module, then we _must_ add its parent to the
        # adjusted sys.path in the end so that we can re-import with the same name.
        forced_local_sys_paths = []
        if paths is not None:
            new_paths = []
            for p in paths:
                abspath = os.path.abspath(os.path.expanduser(p))
                if CustomModules.is_importable(p):
                    mod_path = CustomModules.get_module_path(p)
                    new_paths.append(mod_path)
                    forced_local_sys_paths.append(os.path.dirname(mod_path))
                else:
                    if os.path.exists(abspath):
                        new_paths.append(abspath)
                    else:
                        raise ValueError(
                            "custom module {} does not correspond to an existing folder or module"
                            .format(p))

            paths = new_paths

        forced_local_sys_paths = sorted(list(set(forced_local_sys_paths)))

        # collect local sys paths
        local_sys_paths = copy.copy(sys.path)
        ## replace empty first element with cwd
        ##     https://docs.python.org/3/library/sys.html#sys.path
        if local_sys_paths[0] == "":
            local_sys_paths[0] = os.getcwd()
        ## convert to absolute paths
        local_sys_paths = list(map(os.path.abspath, local_sys_paths))
        ## remove paths that don't exist
        local_sys_paths = list(filter(os.path.exists, local_sys_paths))
        ## remove .ipython
        local_sys_paths = list(
            filter(lambda path: not path.endswith(".ipython"),
                   local_sys_paths))
        ## remove virtual (and real) environments
        local_sys_paths = list(
            filter(lambda path: not _utils.is_in_venv(path), local_sys_paths))

        # get paths to files within
        if paths is None:
            # Python files within filtered sys.path dirs
            paths = local_sys_paths
            extensions = ['py', 'pyc', 'pyo']
        else:
            # all user-specified files
            paths = paths
            extensions = None
        local_filepaths = _utils.find_filepaths(
            paths,
            extensions=extensions,
            include_hidden=True,
            include_venv=False,  # ignore virtual environments nested within
        )
        ## remove .git
        local_filepaths = set(
            filter(
                lambda path: not path.endswith(".git") and ".git/" not in path,
                local_filepaths))

        # obtain deepest common directory
        #     This directory on the local system will be mirrored in `_CUSTOM_MODULES_DIR` in
        #     deployment.
        curr_dir = os.path.join(os.getcwd(), "")
        paths_plus = list(local_filepaths) + [curr_dir]
        common_prefix = os.path.commonprefix(paths_plus)
        common_dir = os.path.dirname(common_prefix)

        # replace `common_dir` with `_CUSTOM_MODULES_DIR` for deployment sys.path
        depl_sys_paths = list(
            map(lambda path: os.path.relpath(path, common_dir),
                local_sys_paths + forced_local_sys_paths))
        depl_sys_paths = list(
            map(lambda path: os.path.join(_CUSTOM_MODULES_DIR, path),
                depl_sys_paths))

        bytestream = six.BytesIO()
        with zipfile.ZipFile(bytestream, 'w') as zipf:
            for filepath in local_filepaths:
                arcname = os.path.relpath(
                    filepath, common_dir)  # filepath relative to archive root
                try:
                    zipf.write(filepath, arcname)
                except:
                    # maybe file has corrupt metadata; try reading then writing contents
                    with open(filepath, 'rb') as f:
                        zipf.writestr(
                            _artifact_utils.global_read_zipinfo(arcname),
                            f.read(),
                        )

            # add verta config file for sys.path and chdir
            working_dir = os.path.join(_CUSTOM_MODULES_DIR,
                                       os.path.relpath(curr_dir, common_dir))
            zipf.writestr(
                _artifact_utils.global_read_zipinfo("_verta_config.py"),
                six.ensure_binary('\n'.join([
                    "import os, sys",
                    "",
                    "",
                    "sys.path = sys.path[:1] + {} + sys.path[1:]".format(
                        depl_sys_paths),
                    "",
                    "try:",
                    "    os.makedirs(\"{}\")".format(working_dir),
                    "except OSError:  # already exists",
                    "    pass",
                    "os.chdir(\"{}\")".format(working_dir),
                ])))

            # add __init__.py
            init_filename = "__init__.py"
            if init_filename not in zipf.namelist():
                zipf.writestr(
                    _artifact_utils.global_read_zipinfo(init_filename),
                    b"",
                )

        bytestream.seek(0)

        return bytestream