Skip to content

Commit

Permalink
Merge pull request MODFLOW-USGS#93 from MODFLOW-USGS/v0.2.0
Browse files Browse the repository at this point in the history
Release 0.2.0
  • Loading branch information
wpbonelli committed Jul 26, 2023
2 parents 590f27e + f86994a commit ab06557
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 107 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,10 @@ jobs:
reset_branch="post-release-${{ steps.latest_tag.outputs.tag }}-reset"
git switch -c $reset_branch
# increment patch version
# increment minor version
major_version=$(echo "${{ steps.latest_tag.outputs.tag }}" | cut -d. -f1)
minor_version=$(echo "${{ steps.latest_tag.outputs.tag }}" | cut -d. -f2)
patch_version=$(echo "${{ steps.latest_tag.outputs.tag }}" | cut -d. -f3)
version="$major_version.$minor_version.$((patch_version + 1))"
version="$major_version.$((minor_version + 1)).0.dev0"
python scripts/update_version.py -v "$version"
python scripts/lint.py
Expand Down
6 changes: 6 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### Version 0.2.0

#### New features

* [feat(set_env)](https://github.com/MODFLOW-USGS/modflow-devtools/commit/53b31cce34d221bade4c842efe3b5ed3034b2742): Add set_env contextmanager utility (#87). Committed by w-bonelli on 2023-07-26.

### Version 0.1.8

#### New features
Expand Down
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 = "0.1.8"
release = "0.2.0"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
41 changes: 38 additions & 3 deletions docs/md/doctoc.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,46 @@
# Generating TOCs

The [`doctoc`](https://www.npmjs.com/package/doctoc) tool can be used to automatically generate table of contents sections for markdown files. `doctoc` is distributed with the [Node Package Manager](https://docs.npmjs.com/cli/v7/configuring-npm/install). With Node installed use `npm install -g doctoc` to install `doctoc` globally. Then just run `doctoc <file>`, e.g.:
The [`doctoc`](https://www.npmjs.com/package/doctoc) tool generates table of contents sections for markdown files.

## Installing Node.js, `npm` and `doctoc``

`doctoc` is distributed with the [Node Package Manager](https://docs.npmjs.com/cli/v7/configuring-npm/install). [Node](https://nodejs.org/en) is a JavaScript runtime environment.

On Ubuntu, Node can be installed with:

```shell
sudo apt update
sudo apt install nodejs
```

On Windows, with [Chocolatey](https://community.chocolatey.org/packages/nodejs):

```shell
choco install nodejs
```

Installers and binaries for Windows and macOS are [available for download](https://nodejs.org/en/download).

Once Node is installed, install `doctoc` with:

```shell
npm install -g doctoc
```

## Using `doctoc`

Then TOCs can be generated with `doctoc <file>`, e.g.:

```shell
doctoc DEVELOPER.md
```

This will insert HTML comments surrounding an automatically edited region, scanning for headers and creating an appropriately indented TOC tree. Subsequent runs are idempotent, updating if the file has changed or leaving it untouched if not.
This will insert HTML comments surrounding an automatically edited region, in which `doctoc` will create an appropriately indented TOC tree. Subsequent runs are idempotent, scanning for headers and only updating the TOC if the file header structure has changed.

To run `doctoc` for all markdown files in a particular directory (recursive), use `doctoc some/path`.

By default `doctoc` inserts a self-descriptive comment

> **Table of Contents** *generated with DocToc*
To run `doctoc` for all markdown files in a particular directory (recursive), use `doctoc some/path`.
This can be removed (and other content within the TOC region edited) &mdash; `doctoc` will not overwrite it, only the table.
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__ = "Apr 21, 2023"
__version__ = "0.1.8"
__date__ = "Jul 26, 2023"
__version__ = "0.2.0"
__maintainer__ = "Joseph D. Hughes"
__email__ = "[email protected]"
__status__ = "Production"
Expand Down
33 changes: 33 additions & 0 deletions modflow_devtools/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@ def set_dir(path: PathLike):
print(f"Returned to previous directory: {origin}")


@contextmanager
def set_env(*remove, **update):
"""
Temporarily updates the ``os.environ`` dictionary in-place.
Referenced from https://stackoverflow.com/a/34333710/6514033.
The ``os.environ`` dictionary is updated in-place so that the modification
is sure to work in all situations.
:param remove: Environment variables to remove.
:param update: Dictionary of environment variables and values to add/update.
"""
env = environ
update = update or {}
remove = remove or []

# List of environment variables being updated or removed.
stomped = (set(update.keys()) | set(remove)) & set(env.keys())
# Environment variables and values to restore on exit.
update_after = {k: env[k] for k in stomped}
# Environment variables and values to remove on exit.
remove_after = frozenset(k for k in update if k not in env)

try:
env.update(update)
[env.pop(k, None) for k in remove]
yield
finally:
env.update(update_after)
[env.pop(k) for k in remove_after]


class add_sys_path:
"""
Context manager for temporarily editing the system path
Expand Down
25 changes: 21 additions & 4 deletions modflow_devtools/test/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
get_packages,
has_package,
set_dir,
set_env,
)


Expand All @@ -21,6 +22,22 @@ def test_set_dir(tmp_path):
assert Path(os.getcwd()) != tmp_path


def test_set_env(tmp_path):
# test adding a variable
key = "TEST_ENV"
val = "test"
assert environ.get(key) is None
with set_env(**{key: val}):
assert environ.get(key) == val
with set_env(TEST_ENV=val):
assert environ.get(key) == val

# test removing a variable
with set_env(**{key: val}):
with set_env(key):
assert environ.get(key) is None


_repos_path = environ.get("REPOS_PATH")
if _repos_path is None:
_repos_path = Path(__file__).parent.parent.parent.parent
Expand Down Expand Up @@ -167,7 +184,7 @@ def test_get_model_paths_largetestmodels():
def test_get_model_paths_exclude_patterns(models):
path, expected_count = models
paths = get_model_paths(path, excluded=["gwt"])
assert len(paths) == expected_count
assert len(paths) >= expected_count


@pytest.mark.skipif(
Expand Down Expand Up @@ -210,7 +227,7 @@ def test_get_namefile_paths_largetestmodels():
def test_get_namefile_paths_exclude_patterns(models):
path, expected_count = models
paths = get_namefile_paths(path, excluded=["gwf"])
assert len(paths) == expected_count
assert len(paths) >= expected_count


@pytest.mark.skipif(not any(_example_paths), reason="examples not found")
Expand All @@ -225,10 +242,10 @@ def test_get_namefile_paths_select_prefix():
@pytest.mark.skipif(not any(_example_paths), reason="examples not found")
def test_get_namefile_paths_select_patterns():
paths = get_namefile_paths(_examples_path, selected=["gwf"])
assert len(paths) == 70
assert len(paths) >= 70


@pytest.mark.skipif(not any(_example_paths), reason="examples not found")
def test_get_namefile_paths_select_packages():
paths = get_namefile_paths(_examples_path, packages=["wel"])
assert len(paths) == 43
assert len(paths) >= 43
106 changes: 13 additions & 93 deletions scripts/update_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,77 +7,25 @@
from typing import NamedTuple

from filelock import FileLock
from packaging.version import Version

_project_name = "modflow-devtools"
_project_root_path = Path(__file__).parent.parent
_version_txt_path = _project_root_path / "version.txt"
_package_init_path = _project_root_path / "modflow_devtools" / "__init__.py"
_readme_path = _project_root_path / "README.md"
_docs_config_path = _project_root_path / "docs" / "conf.py"
_initial_version = Version("0.0.1")
_current_version = Version(_version_txt_path.read_text().strip())


class Version(NamedTuple):
"""Semantic version number"""

major: int = 0
minor: int = 0
patch: int = 0

def __repr__(self):
return f"{self.major}.{self.minor}.{self.patch}"

@classmethod
def from_string(cls, version: str) -> "Version":
t = version.split(".")

vmajor = int(t[0])
vminor = int(t[1])
vpatch = int(t[2])

return cls(major=vmajor, minor=vminor, patch=vpatch)

@classmethod
def from_file(cls, path: PathLike) -> "Version":
lines = [
line.rstrip("\n")
for line in open(Path(path).expanduser().absolute(), "r")
]
vmajor = vminor = vpatch = None
for line in lines:
line = line.strip()
if not any(line):
continue
t = line.split(".")
vmajor = int(t[0])
vminor = int(t[1])
vpatch = int(t[2])

assert (
vmajor is not None and vminor is not None and vpatch is not None
), "version string must follow semantic version format: major.minor.patch"
return cls(major=vmajor, minor=vminor, patch=vpatch)


class ReleaseType(Enum):
CANDIDATE = "Release Candidate"
RELEASE = "Release"


_initial_version = Version(0, 0, 1)
_current_version = Version.from_file(_version_txt_path)


def update_version_txt(
release_type: ReleaseType, timestamp: datetime, version: Version
):
def update_version_txt(version: Version):
with open(_version_txt_path, "w") as f:
f.write(str(version))
print(f"Updated {_version_txt_path} to version {version}")


def update_init_py(
release_type: ReleaseType, timestamp: datetime, version: Version
):
def update_init_py(timestamp: datetime, version: Version):
lines = _package_init_path.read_text().rstrip().split("\n")
with open(_package_init_path, "w") as f:
for line in lines:
Expand All @@ -89,23 +37,7 @@ def update_init_py(
print(f"Updated {_package_init_path} to version {version}")


def update_readme_markdown(
release_type: ReleaseType, timestamp: datetime, version: Version
):
lines = _readme_path.read_text().rstrip().split("\n")
with open(_readme_path, "w") as f:
for line in lines:
if "### Version " in line:
line = f"### Version {version}"
if release_type != ReleaseType.RELEASE:
line += f" &mdash; {release_type.value.lower()}"
f.write(f"{line}\n")
print(f"Updated {_readme_path} to version {version}")


def update_docs_config(
release_type: ReleaseType, timestamp: datetime, version: Version
):
def update_docs_config(timestamp: datetime, version: Version):
lines = _docs_config_path.read_text().rstrip().split("\n")
with open(_docs_config_path, "w") as f:
for line in lines:
Expand All @@ -116,25 +48,23 @@ def update_docs_config(


def update_version(
release_type: ReleaseType,
timestamp: datetime = datetime.now(),
version: Version = None,
):
lock_path = Path(_version_txt_path.name + ".lock")
try:
lock = FileLock(lock_path)
previous = Version.from_file(_version_txt_path)
previous = Version(_version_txt_path.read_text().strip())
version = (
version
if version
else Version(previous.major, previous.minor, previous.patch)
else Version(previous.major, previous.minor, previous.micro)
)

with lock:
update_version_txt(release_type, timestamp, version)
update_init_py(release_type, timestamp, version)
# update_readme_markdown(release_type, timestamp, version)
update_docs_config(release_type, timestamp, version)
update_version_txt(version)
update_init_py(timestamp, version)
update_docs_config(timestamp, version)
finally:
try:
lock_path.unlink()
Expand Down Expand Up @@ -162,13 +92,6 @@ def update_version(
required=False,
help="Specify the release version",
)
parser.add_argument(
"-a",
"--approve",
required=False,
action="store_true",
help="Indicate release is approved (defaults to false for preliminary/development distributions)",
)
parser.add_argument(
"-g",
"--get",
Expand All @@ -179,14 +102,11 @@ def update_version(
args = parser.parse_args()

if args.get:
print(_current_version)
print(Version(_version_txt_path.read_text().strip()))
else:
update_version(
release_type=ReleaseType.RELEASE
if args.approve
else ReleaseType.CANDIDATE,
timestamp=datetime.now(),
version=Version.from_string(args.version)
version=Version(args.version)
if args.version
else _current_version,
)
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.8
0.2.0

0 comments on commit ab06557

Please sign in to comment.