Skip to content

Commit

Permalink
Merge pull request #108 from MODFLOW-USGS/v1.1.0
Browse files Browse the repository at this point in the history
Release 1.1.0
  • Loading branch information
wpbonelli authored Aug 12, 2023
2 parents e92682a + cacd944 commit 79c8f3b
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### Version 1.1.0

#### Refactoring

* [refactor](https://github.com/MODFLOW-USGS/modflow-devtools/commit/582d48a4d72f18a787216ada5befb7543cebdfcf): Deprecate misc functions, add ostags alternatives (#105). Committed by w-bonelli on 2023-08-08.
* [refactor(has_pkg)](https://github.com/MODFLOW-USGS/modflow-devtools/commit/03ea04157190480b455e174de64c692ff3bb86a3): Introduce strict flag (#106). Committed by w-bonelli on 2023-08-12.

### Version 1.0.0

#### New features
Expand Down
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from pathlib import Path

pytest_plugins = ["modflow_devtools.fixtures"]
project_root_path = Path(__file__).parent
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

project = "modflow-devtools"
author = "MODFLOW Team"
release = "1.0.0"
release = "1.1.0"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
11 changes: 11 additions & 0 deletions docs/md/ostags.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ OSTag.convert(platform.system(), "py2mf")
The second argument specifies the mapping in format `<source>2<target>`, where `<source>` and `<target>` may take values `py`, `mf`, or `gh`.

**Note**: source and target must be different.

## Getting suffixes

A convenience function is available to get the appropriate binary file extensions for a given operating system, identified by tag (or the current operating system if no tag is provided). The return value is a 2-tuple containing the executable and library extensions, respectively.

```python
get_binary_suffixes() # get extensions for current OS
get_binary_suffixes("linux") # returns ("", ".so")
get_binary_suffixes("linux") # returns ("", ".so")
get_binary_suffixes("win64") # returns (".exe", ".dll")
```
4 changes: 2 additions & 2 deletions modflow_devtools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__author__ = "Joseph D. Hughes"
__date__ = "Aug 05, 2023"
__version__ = "1.0.0"
__date__ = "Aug 12, 2023"
__version__ = "1.1.0"
__maintainer__ = "Joseph D. Hughes"
__email__ = "[email protected]"
__status__ = "Production"
Expand Down
2 changes: 1 addition & 1 deletion modflow_devtools/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def requires_exe(*exes):


def requires_pkg(*pkgs):
missing = {pkg for pkg in pkgs if not has_pkg(pkg)}
missing = {pkg for pkg in pkgs if not has_pkg(pkg, strict=True)}
return pytest.mark.skipif(
missing,
reason=f"missing package{'s' if len(missing) != 1 else ''}: "
Expand Down
89 changes: 75 additions & 14 deletions modflow_devtools/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ def __exit__(self, exc_type, exc_value, traceback):


def get_ostag() -> str:
"""Determine operating system tag from sys.platform."""
"""
Determine operating system tag from sys.platform.
.. deprecated:: 1.1.0
Use ``ostags.get_modflow_ostag()`` instead.
"""
if sys.platform.startswith("linux"):
return "linux"
elif sys.platform.startswith("win"):
Expand All @@ -93,7 +98,12 @@ def get_ostag() -> str:


def get_suffixes(ostag) -> Tuple[str, str]:
"""Returns executable and library suffixes for the given OS (as returned by sys.platform)"""
"""
Returns executable and library suffixes for the given OS (as returned by sys.platform)
.. deprecated:: 1.1.0
Use ``ostags.get_binary_suffixes()`` instead.
"""

tag = ostag.lower()

Expand Down Expand Up @@ -136,6 +146,23 @@ def run_py_script(script, *args, verbose=False):


def get_current_branch() -> str:
"""
Tries to determine the name of the current branch, first by the GITHUB_REF
environent variable, then by asking ``git`` if GITHUB_REF is not set.
Returns
-------
str
name of the current branch
Raises
------
RuntimeError
if ``git`` is not available
ValueError
if the current branch could not be determined
"""

# check if on GitHub Actions CI
ref = environ.get("GITHUB_REF")
if ref is not None:
Expand All @@ -160,6 +187,7 @@ def get_packages(namefile_path: PathLike) -> List[str]:
----------
namefile_path : PathLike
path to MODFLOW 6 simulation or model name file
Returns
-------
a list of packages used by the simulation or model
Expand Down Expand Up @@ -215,7 +243,9 @@ def parse_model_namefile(line):


def has_package(namefile_path: PathLike, package: str) -> bool:
"""Determines whether the model with the given namefile contains the selected package"""
"""
Determines whether the model with the given namefile contains the selected package.
"""
packages = get_packages(namefile_path)
return package.lower() in packages

Expand Down Expand Up @@ -306,7 +336,9 @@ def get_model_paths(
def is_connected(hostname):
"""
Tests whether the given URL is accessible.
See https://stackoverflow.com/a/20913928/."""
See https://stackoverflow.com/a/20913928/.
"""

try:
host = socket.gethostbyname(hostname)
s = socket.create_connection((host, 80), 2)
Expand All @@ -318,7 +350,10 @@ def is_connected(hostname):


def is_in_ci():
"""Determines whether the current process is running GitHub Actions CI"""
"""
Determines whether the current process is running GitHub Actions CI
by checking for the "CI" environment variable.
"""

# if running in GitHub Actions CI, "CI" variable always set to true
# https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
Expand All @@ -328,7 +363,7 @@ def is_in_ci():
def is_github_rate_limited() -> Optional[bool]:
"""
Determines if a GitHub API rate limit is applied to the current IP.
Running this function will consume an API request!
Calling this function will consume an API request!
Returns
-------
Expand Down Expand Up @@ -363,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 ([email protected]) 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]
46 changes: 43 additions & 3 deletions modflow_devtools/ostags.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
"""
MODFLOW 6, Python3, and build servers may all refer to operating
systems by different names. This module contains conversion utilities.
MODFLOW 6, Python3, and GitHub Actions refer to operating
systems differently. This module contains conversion utilities.
"""


import sys
from enum import Enum
from platform import system
from typing import Tuple

_system = system()


def get_modflow_ostag() -> str:
if _system == "Windows":
return "win64"
return "win" + ("64" if sys.maxsize > 2**32 else "32")
elif _system == "Linux":
return "linux"
elif _system == "Darwin":
Expand All @@ -30,6 +32,44 @@ def get_github_ostag() -> str:
raise NotImplementedError(f"Unsupported system: {_system}")


def get_binary_suffixes(ostag: str = None) -> Tuple[str, str]:
"""
Returns executable and library suffixes for the given OS tag, if provided,
otherwise for the current operating system.
Parameters
----------
ostag : str, optional
The OS tag. May be provided in modflow, python, or github format.
Returns
-------
Tuple[str, str]
The executable and library suffixes, respectively.
"""

if ostag is None:
ostag = get_modflow_ostag()

def _suffixes(tag):
if tag in ["win32", "win64"]:
return ".exe", ".dll"
elif tag == "linux":
return "", ".so"
elif tag == "mac" or tag == "darwin":
return "", ".dylib"
else:
raise KeyError(f"unrecognized OS tag: {tag!r}")

try:
return _suffixes(ostag.lower())
except:
try:
return _suffixes(python_to_modflow_ostag(ostag))
except:
return _suffixes(github_to_modflow_ostag(ostag))


def python_to_modflow_ostag(tag: str) -> str:
"""
Convert a platform.system() string to an ostag as expected
Expand Down
34 changes: 34 additions & 0 deletions modflow_devtools/test/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
)
Loading

0 comments on commit 79c8f3b

Please sign in to comment.