diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c18594..4738d35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,7 +167,8 @@ jobs: BIN_PATH: ~/.local/bin/modflow REPOS_PATH: ${{ github.workspace }} GITHUB_TOKEN: ${{ github.token }} - run: pytest -v -n auto --durations 0 --ignore modflow_devtools/test/test_download.py + # use --dist loadfile to so tests requiring pytest-virtualenv run on the same worker + run: pytest -v -n auto --dist loadfile --durations 0 --ignore modflow_devtools/test/test_download.py - name: Run network-dependent tests # only invoke the GH API on one OS and Python version diff --git a/conftest.py b/conftest.py index 74bcb56..d952441 100644 --- a/conftest.py +++ b/conftest.py @@ -1 +1,4 @@ +from pathlib import Path + pytest_plugins = ["modflow_devtools.fixtures"] +project_root_path = Path(__file__).parent diff --git a/modflow_devtools/misc.py b/modflow_devtools/misc.py index 31d540d..7dd9210 100644 --- a/modflow_devtools/misc.py +++ b/modflow_devtools/misc.py @@ -398,21 +398,47 @@ def has_exe(exe): return _has_exe_cache[exe] -def has_pkg(pkg): +def has_pkg(pkg: str, strict: bool = False) -> bool: """ Determines if the given Python package is installed. + Parameters + ---------- + pkg : str + Name of the package to check. + strict : bool + If False, only check if package metadata is available. + If True, try to import the package (all dependencies must be present). + + Returns + ------- + bool + True if the package is installed, otherwise False. + + Notes + ----- Originally written by Mike Toews (mwtoews@gmail.com) for FloPy. """ - if pkg not in _has_pkg_cache: - found = True + + def try_import(): + try: # import name, e.g. "import shapefile" + importlib.import_module(pkg) + return True + except ModuleNotFoundError: + return False + + def try_metadata() -> bool: try: # package name, e.g. pyshp metadata.distribution(pkg) + return True except metadata.PackageNotFoundError: - try: # import name, e.g. "import shapefile" - importlib.import_module(pkg) - except ModuleNotFoundError: - found = False - _has_pkg_cache[pkg] = found + return False + + found = False + if not strict: + found = pkg in _has_pkg_cache or try_metadata() + if not found: + found = try_import() + _has_pkg_cache[pkg] = found return _has_pkg_cache[pkg] diff --git a/modflow_devtools/test/test_misc.py b/modflow_devtools/test/test_misc.py index fe6d2cb..5db6388 100644 --- a/modflow_devtools/test/test_misc.py +++ b/modflow_devtools/test/test_misc.py @@ -2,14 +2,17 @@ import shutil from os import environ from pathlib import Path +from pprint import pprint from typing import List import pytest +from conftest import project_root_path from modflow_devtools.misc import ( get_model_paths, get_namefile_paths, get_packages, has_package, + has_pkg, set_dir, set_env, ) @@ -249,3 +252,34 @@ def test_get_namefile_paths_select_patterns(): def test_get_namefile_paths_select_packages(): paths = get_namefile_paths(_examples_path, packages=["wel"]) assert len(paths) >= 43 + + +@pytest.mark.slow +def test_has_pkg(virtualenv): + python = virtualenv.python + venv = Path(python).parent + pkg = "pytest" + dep = "pluggy" + print( + f"Using temp venv at {venv} with python {python} to test has_pkg('{pkg}') with and without '{dep}'" + ) + + # install a package and remove one of its dependencies + virtualenv.run(f"pip install {project_root_path}") + virtualenv.run(f"pip install {pkg}") + virtualenv.run(f"pip uninstall -y {dep}") + + # check with/without strict mode + for strict in [False, True]: + cmd = ( + f"from modflow_devtools.misc import has_pkg; print(has_pkg('{pkg}'" + + (", strict=True))'" if strict else "))'") + ) + exp = "False" if strict else "True" + assert ( + virtualenv.run( + f'{python} -c "{cmd}"', + capture=True, + ).strip() + == exp + ) diff --git a/pyproject.toml b/pyproject.toml index 3dedbcb..c3630b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ test = [ "pytest-cases", "pytest-cov", "pytest-dotenv", + "pytest-virtualenv", "pytest-xdist", "PyYaml" ]