From 3719e7beef9a4d171e439ce379937f3de3d8080e Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sun, 31 May 2026 19:30:55 -0700 Subject: [PATCH 1/6] mf6 synchronization --- flopy4/cli.py | 173 +++++++++++++++ flopy4/mf6/__init__.py | 11 + flopy4/mf6/_compat.py | 53 +++++ flopy4/mf6/_contract.py | 3 + flopy4/mf6/utils/codegen/generate_classes.py | 218 ++++++++++++------- flopy4/mf6/utils/codegen/make.py | 27 +-- pixi.lock | 50 ++--- pyproject.toml | 17 +- 8 files changed, 432 insertions(+), 120 deletions(-) create mode 100644 flopy4/cli.py create mode 100644 flopy4/mf6/_compat.py create mode 100644 flopy4/mf6/_contract.py diff --git a/flopy4/cli.py b/flopy4/cli.py new file mode 100644 index 00000000..6a5c6d43 --- /dev/null +++ b/flopy4/cli.py @@ -0,0 +1,173 @@ +"""Top-level CLI entry point for flopy4.""" + +import argparse +import logging +import shutil +import sys +import warnings + + +def _resolve_release_id(release_id: str | None, verbose: bool = False) -> str: + """Resolve a release_id, falling back to the discovered binary's version + or the latest release if no release_id is given. + """ + if release_id is not None: + return release_id + + # Try to read the version from a binary on PATH. + from flopy4.mf6._compat import _query_mf6_version + + for name in ("mf6", "mf6.exe"): + exe = shutil.which(name) + if exe: + version = _query_mf6_version(exe) + if version: + if verbose: + print(f"Detected MF6 {version} at {exe}") + return f"MODFLOW-ORG/modflow6@{version}" + + # Fall back to @latest. + if verbose: + print("No MF6 binary found on PATH; resolving to latest release.") + return "MODFLOW-ORG/modflow6@latest" + + +def _cmd_sync(args: argparse.Namespace) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=".*modflow_devtools.programs.*experimental.*") + from modflow_devtools.programs import install_program + + from flopy4.mf6.utils.codegen.generate_classes import generate_classes + + release_id = _resolve_release_id(args.release_id, verbose=args.verbose) + + # For remote release IDs, install the matching binary unless --no-install. + from pathlib import Path + + is_local = Path(release_id).expanduser().is_dir() + if not is_local and not args.no_install: + _, tag = release_id.split("@", 1) + if tag not in ("latest",): + version_arg = tag + else: + version_arg = None # let install_program resolve latest + if args.verbose: + label = version_arg or "latest" + print(f"Installing mf6 {label} ...") + try: + installed = install_program( + "mf6", + version=version_arg, + force=args.force, + verbose=args.verbose, + ) + if args.verbose and installed: + print(f"Installed: {', '.join(str(p) for p in installed)}") + except Exception as exc: + print(f"Warning: binary installation failed: {exc}", file=sys.stderr) + print("Continuing with class generation only.", file=sys.stderr) + + if args.verbose: + print(f"Generating flopy4.mf6 from {release_id} ...") + + synced_version = generate_classes( + release_id=release_id, + mf6_version=args.mf6_version, + existing_only=not args.all_packages, + makedirs=args.all_packages, + fmt=not args.no_format, + force=args.force, + ) + + print(f"Synced flopy4.mf6 to MF6 {synced_version}.") + + +def _cmd_status(args: argparse.Namespace) -> None: + try: + from flopy4.mf6._contract import DFN_SCHEMA_VERSION, MF6_VERSION + except ImportError: + MF6_VERSION = "unknown" + DFN_SCHEMA_VERSION = "unknown" + + from flopy4.mf6._compat import _query_mf6_version + + exe = shutil.which("mf6") or shutil.which("mf6.exe") + binary_version = _query_mf6_version(exe) if exe else None + + synced = binary_version is not None and binary_version == MF6_VERSION + status = "(✓ in sync)" if synced else "(! mismatch)" if binary_version else "(not found)" + + print(f"flopy4.mf6 synced to : {MF6_VERSION}") + print(f"DFN schema version : {DFN_SCHEMA_VERSION}") + if exe: + print(f"Discovered binary : {binary_version} [{exe}] {status}") + else: + print("Discovered binary : none (not found on PATH)") + + +def main() -> None: + parser = argparse.ArgumentParser(prog="flopy4") + sub = parser.add_subparsers(dest="command") + + mf6_p = sub.add_parser("mf6", help="MF6 sync and status commands.") + mf6_sub = mf6_p.add_subparsers(dest="subcommand") + + # flopy4 mf6 sync + sync_p = mf6_sub.add_parser( + "sync", + help="Sync flopy4.mf6 to an MF6 release: install binary and regenerate classes.", + ) + sync_p.add_argument( + "release_id", + nargs="?", + default=None, + help="Remote release ID (owner/repo@tag) or local path to .dfn files. " + "Defaults to the discovered binary's version, or latest if no binary found.", + ) + sync_p.add_argument( + "--mf6-version", + default=None, + dest="mf6_version", + help="Override MF6 version in _contract.py (useful with local DFN paths).", + ) + sync_p.add_argument( + "--no-install", + action="store_true", + help="Skip binary installation; regenerate classes only.", + ) + sync_p.add_argument( + "--all-packages", + action="store_true", + dest="all_packages", + help="Generate all packages, including ones not yet on disk. " + "By default sync only updates already-generated files.", + ) + sync_p.add_argument( + "--no-format", + action="store_true", + help="Skip ruff formatting of generated files.", + ) + sync_p.add_argument("--force", action="store_true", help="Force binary reinstallation.") + sync_p.add_argument("--verbose", action="store_true") + sync_p.set_defaults(func=_cmd_sync) + + # flopy4 mf6 status + status_p = mf6_sub.add_parser( + "status", + help="Show current sync state: synced version vs. discovered binary.", + ) + status_p.set_defaults(func=_cmd_status) + + args = parser.parse_args() + if not hasattr(args, "func"): + parser.print_help() + sys.exit(1) + + if getattr(args, "verbose", False): + logging.basicConfig(level=logging.INFO) + + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/flopy4/mf6/__init__.py b/flopy4/mf6/__init__.py index a36a1f3d..4599d84d 100644 --- a/flopy4/mf6/__init__.py +++ b/flopy4/mf6/__init__.py @@ -5,8 +5,15 @@ from tomli import load as load_toml from tomli_w import dump as dump_toml +try: + from flopy4.mf6._contract import DFN_SCHEMA_VERSION, MF6_VERSION +except ImportError: + DFN_SCHEMA_VERSION = "unknown" + MF6_VERSION = "unknown" + # Import submodules to make them accessible via flopy4.mf6.* from flopy4.mf6 import gwe, gwf, gwt, prt, simulation, solution, utils +from flopy4.mf6._compat import check_mf6_compatibility from flopy4.mf6.codec import dump as dump_mf6 from flopy4.mf6.codec import load as load_mf6 from flopy4.mf6.component import Component @@ -36,6 +43,8 @@ "NetCDFModel", "Tdis", "Simulation", + "MF6_VERSION", + "DFN_SCHEMA_VERSION", ] @@ -98,3 +107,5 @@ def _write_toml(component: Component) -> None: DEFAULT_REGISTRY.register_writer(Component, "mf6", _write_mf6) DEFAULT_REGISTRY.register_writer(Component, "json", _write_json) DEFAULT_REGISTRY.register_writer(Component, "toml", _write_toml) + +check_mf6_compatibility() diff --git a/flopy4/mf6/_compat.py b/flopy4/mf6/_compat.py new file mode 100644 index 00000000..63b6ec84 --- /dev/null +++ b/flopy4/mf6/_compat.py @@ -0,0 +1,53 @@ +import re +import shutil +import subprocess +import warnings + +_VERSION_RE = re.compile(r"version\s+([\d]+\.[\d]+\.[\d]+(?:\.\S+)?)", re.I) + + +def _query_mf6_version(exe: str) -> str | None: + try: + out = subprocess.check_output([exe, "-v"], text=True, stderr=subprocess.STDOUT) + m = _VERSION_RE.search(out) + return m.group(1) if m else None + except Exception: + return None + + +def check_mf6_compatibility(exe: str | None = None) -> None: + """Warn if a discovered MF6 binary doesn't match the synced version. + + Does nothing when the synced version is ``"unknown"`` or a branch + name (non-semver). + + Parameters + ---------- + exe : + Path to an MF6 executable. If None, searches PATH for ``mf6`` + or ``mf6.exe``. Does nothing if no binary is found. + """ + try: + from flopy4.mf6._contract import MF6_VERSION + except ImportError: + return + + # Skip if version is unknown or a branch name rather than a semver tag. + if not MF6_VERSION or not MF6_VERSION[0].isdigit(): + return + + if exe is None: + exe = shutil.which("mf6") or shutil.which("mf6.exe") + if exe is None: + return + + binary_version = _query_mf6_version(exe) + if binary_version is None or binary_version == MF6_VERSION: + return + + warnings.warn( + f"flopy4.mf6 is synced to MF6 {MF6_VERSION} but the binary at '{exe}' " + f"reports {binary_version}. Run `flopy4 mf6 sync` to re-sync.", + UserWarning, + stacklevel=3, + ) diff --git a/flopy4/mf6/_contract.py b/flopy4/mf6/_contract.py new file mode 100644 index 00000000..4a5aa81a --- /dev/null +++ b/flopy4/mf6/_contract.py @@ -0,0 +1,3 @@ +# autogenerated file, do not modify +MF6_VERSION = "6.7.0" +DFN_SCHEMA_VERSION = "2.0.0.dev1" diff --git a/flopy4/mf6/utils/codegen/generate_classes.py b/flopy4/mf6/utils/codegen/generate_classes.py index 4838071c..8d695fb6 100644 --- a/flopy4/mf6/utils/codegen/generate_classes.py +++ b/flopy4/mf6/utils/codegen/generate_classes.py @@ -3,19 +3,19 @@ Usage (CLI):: - pixi run python -m flopy4.mf6.utils.codegen.generate_classes --ref 6.6.0 - pixi run python -m flopy4.mf6.utils.codegen.generate_classes --dfnpath /path/to/dfns + generate-mf6-classes MODFLOW-ORG/modflow6@6.6.0 + generate-mf6-classes /path/to/local/dfns --mf6-version 6.8.0.dev0+abc1234 """ import argparse import logging -import shutil import sys -import tempfile +import warnings from pathlib import Path -from modflow_devtools.dfn import get_dfns -from modflow_devtools.dfn2toml import convert as dfn2toml +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=".*modflow_devtools.dfns.*experimental.*") + from modflow_devtools.dfns.registry import LocalDfnRegistry, RemoteDfnRegistry from .make import make_all @@ -23,8 +23,7 @@ _PROJ_ROOT = Path(__file__).parents[4].expanduser().resolve() _MF6_ROOT = _PROJ_ROOT / "flopy4" / "mf6" -_MF6_REPO_OWNER = "MODFLOW-ORG" -_MF6_REPO_NAME = "modflow6" +_DFN_SCHEMA_VERSION = "2.0.0.dev1" # DFN names that require special handling beyond simple package generation. # These are skipped until their generation tier is implemented. @@ -54,84 +53,147 @@ } +def _parse_release_id( + release_id: str, +) -> tuple[LocalDfnRegistry | RemoteDfnRegistry, str | None]: + """Parse a release_id into a registry and resolved MF6 version. + + Parameters + ---------- + release_id : + Either a local directory path or a remote release ID in + ``owner/repo@tag`` format (e.g. ``MODFLOW-ORG/modflow6@6.7.0`` + or ``MODFLOW-ORG/modflow6@latest``). + + Returns + ------- + tuple + (registry, mf6_version) where mf6_version is the resolved concrete + tag for remote registries (``@latest`` is resolved via the GitHub + API) or None for local paths. + """ + path = Path(release_id).expanduser() + if path.exists() and path.is_dir(): + return LocalDfnRegistry(path=path), None + if "@" not in release_id or "/" not in release_id.split("@")[0]: + raise ValueError( + f"release_id must be 'owner/repo@tag' or a local path; got: {release_id!r}" + ) + registry = RemoteDfnRegistry(release_id=release_id) + return registry, registry.latest_tag() + + +def _populate_remote_cache( + registry: RemoteDfnRegistry, release_id: str, force: bool = False +) -> None: + """Populate the registry's DFN cache. + + Tries ``registry.sync()`` first (requires a ``dfns.zip`` release asset). + Falls back to fetching DFNs from the repository tree when that asset is + not yet available — writes directly into the registry's cache path so + ``registry.spec()`` finds the files in the expected location. + + Parameters + ---------- + force : + Re-fetch even if the cache already exists. Should be True for + branch refs (mutable) and False for pinned release tags. + """ + if not force and registry.cache_path.exists() and any(registry.cache_path.iterdir()): + return + try: + registry.sync() + except Exception: + # dfns.zip not yet a release asset; fetch from the git tree instead. + from modflow_devtools.dfn import get_dfns + + owner_repo, tag = release_id.split("@", 1) + owner, repo = owner_repo.split("/") + registry.cache_path.mkdir(parents=True, exist_ok=True) + get_dfns(owner=owner, repo=repo, ref=tag, outdir=registry.cache_path) + + +def _write_contract(outdir: Path, mf6_version: str) -> None: + (outdir / "_contract.py").write_text( + "# autogenerated file, do not modify\n" + f'MF6_VERSION = "{mf6_version}"\n' + f'DFN_SCHEMA_VERSION = "{_DFN_SCHEMA_VERSION}"\n' + ) + + def generate_classes( - owner: str = _MF6_REPO_OWNER, - repo: str = _MF6_REPO_NAME, - ref: str | None = None, - dfnpath: str | None = None, + release_id: str, outdir: str | Path = _MF6_ROOT, + mf6_version: str | None = None, developmode: bool = False, fmt: bool = True, makedirs: bool = False, existing_only: bool = False, + force: bool = False, ) -> None: """Generate Python classes for MODFLOW 6 packages. - Fetches (or reads) v1 DFN files, converts them to v2 TOML, then - generates a Python source file for each component into ``outdir``. + Fetches (or reads) DFN files via the registry, generates a Python source + file for each component into ``outdir``, and writes ``_contract.py`` + recording the MF6 version and DFN schema version. Parameters ---------- - owner : - GitHub organisation that owns the MODFLOW 6 repository. - repo : - Repository name. - ref : - Branch name, tag, or commit hash to fetch DFNs from. - Required when ``dfnpath`` is None. - dfnpath : - Path to a local directory of v1 ``.dfn`` files. Takes precedence - over remote fetching when supplied. + release_id : + Remote release ID (``owner/repo@tag``) or local path to a directory + of ``.dfn`` files. outdir : - Root output directory. Defaults to ``flopy4/mf6/`` inside the + Root output directory. Defaults to ``flopy4/mf6/`` inside the project so generated files land alongside hand-written ones. + mf6_version : + Override the MF6 version written to ``_contract.py``. For remote + release IDs the tag is used automatically; for local paths this + defaults to ``"unknown"`` unless provided here. developmode : Include fields marked ``developmode`` in the DFN. Default False. fmt : Run ``ruff format`` / ``ruff check --fix`` on each generated file. + makedirs : + Create output subdirectories if missing. + existing_only : + Only (re)generate files that already exist on disk. + force : + Force re-fetch of DFNs even if already cached. Automatically + enabled for branch refs (non-semver tags), which are mutable. """ - if dfnpath is None and ref is None: - raise ValueError("Provide either 'ref' (remote) or 'dfnpath' (local).") - - with tempfile.TemporaryDirectory() as tmp: - tmpdir = Path(tmp) - v1dir = tmpdir / "v1" - v2dir = tmpdir / "v2" - v1dir.mkdir() - v2dir.mkdir() - - if dfnpath is not None: - src = Path(dfnpath).expanduser().resolve() - if not src.is_dir(): - raise FileNotFoundError(f"dfnpath '{src}' is not a directory.") - shutil.copytree(src, v1dir, dirs_exist_ok=True) - logger.info(f"Using local DFNs from {src}") - else: - logger.info(f"Fetching DFNs from {owner}/{repo}@{ref}") - get_dfns( - owner=owner, - repo=repo, - ref=ref, - outdir=v1dir, - verbose=logger.isEnabledFor(logging.INFO), - ) - - logger.info("Converting v1 DFNs to v2 TOML") - dfn2toml(v1dir, v2dir) - - outdir = Path(outdir).expanduser().resolve() - generated = make_all( - dfndir=v2dir, - outdir=outdir, - developmode=developmode, - fmt=fmt, - skip=_SKIP, - makedirs=makedirs, - existing_only=existing_only, - v1dfndir=v1dir, + registry, derived_version = _parse_release_id(release_id) + effective_version = mf6_version or derived_version or "unknown" + + if effective_version == "unknown": + logger.warning( + "MF6 version is unknown for local DFN path '%s'. " + "Pass --mf6-version to record it explicitly in _contract.py.", + release_id, ) - logger.info(f"Generated {len(generated)} files.") + if isinstance(registry, RemoteDfnRegistry): + # Branch refs are mutable: always re-fetch unless the caller explicitly + # passes a pinned semver tag (first char is a digit). + is_branch = not effective_version[0].isdigit() if effective_version else True + _populate_remote_cache(registry, release_id, force=force or is_branch) + + logger.info(f"Loading DFNs from {release_id!r} (schema {_DFN_SCHEMA_VERSION})") + dfns = registry.spec(schema_version=_DFN_SCHEMA_VERSION) + + outdir = Path(outdir).expanduser().resolve() + generated = make_all( + dfns=dfns, + outdir=outdir, + developmode=developmode, + fmt=fmt, + skip=_SKIP, + makedirs=makedirs, + existing_only=existing_only, + ) + + _write_contract(outdir, effective_version) + logger.info(f"Generated {len(generated)} files. Contract: MF6 {effective_version}.") + return effective_version def cli_main() -> None: @@ -139,17 +201,16 @@ def cli_main() -> None: parser = argparse.ArgumentParser( description="Generate MF6 Python classes from DFN specification files.", ) - parser.add_argument("--owner", default=_MF6_REPO_OWNER) - parser.add_argument("--repo", default=_MF6_REPO_NAME) parser.add_argument( - "--ref", - default=None, - help="Git ref (branch, tag, or commit) to fetch DFNs from.", + "release_id", + help="Remote release ID (owner/repo@tag) or local path to .dfn files.", ) parser.add_argument( - "--dfnpath", + "--mf6-version", default=None, - help="Path to a local directory of v1 .dfn files.", + dest="mf6_version", + help="MF6 version string to record in _contract.py. " + "Inferred from the release tag for remote IDs; defaults to 'unknown' for local paths.", ) parser.add_argument( "--outdir", @@ -176,6 +237,12 @@ def cli_main() -> None: action="store_true", help="Only regenerate files that already exist on disk.", ) + parser.add_argument( + "--force", + action="store_true", + help="Force re-fetch of DFNs even if already cached. " + "Implied for branch refs (e.g. @develop).", + ) parser.add_argument("--verbose", action="store_true") args = parser.parse_args() @@ -184,15 +251,14 @@ def cli_main() -> None: try: generate_classes( - owner=args.owner, - repo=args.repo, - ref=args.ref, - dfnpath=args.dfnpath, + release_id=args.release_id, outdir=args.outdir, + mf6_version=args.mf6_version, developmode=args.developmode, fmt=not args.no_format, makedirs=args.makedirs, existing_only=args.existing_only, + force=args.force, ) except (EOFError, KeyboardInterrupt): sys.exit(f"Cancelled '{sys.argv[0]}'") diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index 558ec87c..335f9a74 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -949,21 +949,24 @@ def make_component( def make_all( *, - dfndir: PathLike, + dfns: dict[str, "Dfn"] | None = None, + dfndir: PathLike | None = None, outdir: PathLike, developmode: bool = False, fmt: bool = True, skip: set[str] | None = None, makedirs: bool = False, existing_only: bool = False, - v1dfndir: PathLike | None = None, ) -> list[ComponentSpec]: - """Generate Python source files for all DFNs in dfndir. + """Generate Python source files for all DFNs. Parameters ---------- + dfns : + Pre-loaded DFN dict from a registry's spec() call. Takes precedence + over dfndir when both are provided. dfndir : - Directory containing v2 TOML DFN files. + Directory containing DFN files. Used only when dfns is not provided. outdir : Root output directory for generated Python files. developmode : @@ -976,30 +979,24 @@ def make_all( If True, create output subdirectories as needed. existing_only : If True, only (re)generate files that already exist on disk. - v1dfndir : - Optional directory containing v1 DFN files (.dfn). When provided, - numeric_index is read from the v1 DFN to auto-detect cellid columns - in extra_list_blocks without requiring explicit ``cellid=true`` in - dfn_overrides.toml. Returns ------- list[ComponentSpec] Specs for all components that were generated. """ - dfndir = Path(dfndir) + if dfns is None: + if dfndir is None: + raise ValueError("Provide either 'dfns' or 'dfndir'.") + dfns = Dfn.load_all(Path(dfndir), schema_version="2.0.0.dev1") outdir = Path(outdir) skip = skip or set() env = _get_env() - dfns = Dfn.load_all(dfndir, schema_version="2.0.0.dev1") - v1_dfns = Dfn.load_all(v1dfndir, schema_version="2.0.0.dev1") if v1dfndir else {} specs = [] for name, dfn in dfns.items(): if name in skip: continue - spec = build_component_spec( - dfn, root=outdir, developmode=developmode, v1_dfn=v1_dfns.get(name) - ) + spec = build_component_spec(dfn, root=outdir, developmode=developmode, v1_dfn=dfn) if existing_only and not spec.outpath.exists(): logger.info(f"{spec.outpath} does not exist — skipping {name} (existing_only)") continue diff --git a/pixi.lock b/pixi.lock index 874744dc..5721cd2b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -88,7 +88,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -229,7 +229,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -378,7 +378,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz @@ -523,7 +523,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl @@ -687,7 +687,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -957,7 +957,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1233,7 +1233,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -1505,7 +1505,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -1796,7 +1796,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl @@ -2055,7 +2055,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -2323,7 +2323,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl @@ -2586,7 +2586,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl @@ -2868,7 +2868,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -3105,7 +3105,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -3351,7 +3351,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -3593,7 +3593,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -3853,7 +3853,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -4087,7 +4087,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -4330,7 +4330,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -4569,7 +4569,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -4825,7 +4825,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -5061,7 +5061,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -5306,7 +5306,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -5547,7 +5547,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda + - pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -9648,7 +9648,7 @@ packages: - build ; extra == 'build' - twine ; extra == 'build' requires_python: '>=3.11,<3.14' -- pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda +- pypi: git+https://github.com/modflow-org/modflow-devtools#abec244c96a3595f7774b279dea5d10ab64663d7 name: modflow-devtools version: 1.10.0.dev1 requires_dist: diff --git a/pyproject.toml b/pyproject.toml index 25eaa2ab..54a0c2e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ ignore = [ ] [project.scripts] +flopy4 = "flopy4.cli:main" dfn2lark = "flopy4.mf6.codec.reader.dfn2lark:main" generate-mf6-classes = "flopy4.mf6.utils.codegen.generate_classes:cli_main" @@ -164,12 +165,20 @@ update-ghpages = { cmd = "python scripts/update_ghpages.py", description = "Sync build = { cmd = "python -m build" } [tool.pixi.tasks.generate-classes] -cmd = "generate-mf6-classes --ref develop --existing-only --verbose" -description = "Regenerate existing MF6 classes in flopy4/mf6/ from upstream DFNs" +cmd = "generate-mf6-classes MODFLOW-ORG/modflow6@latest --existing-only --verbose" +description = "Regenerate existing MF6 classes in flopy4/mf6/ from the latest release DFNs" [tool.pixi.tasks.generate-classes-preview] -cmd = "generate-mf6-classes --ref develop --outdir ./tmp/mf6-preview --makedirs --verbose" -description = "Generate MF6 classes into /tmp/mf6-preview (safe preview)" +cmd = "generate-mf6-classes MODFLOW-ORG/modflow6@latest --outdir ./tmp/mf6-preview --makedirs --verbose" +description = "Generate MF6 classes into ./tmp/mf6-preview (safe preview)" + +[tool.pixi.tasks.sync] +cmd = "flopy4 mf6 sync --verbose" +description = "Sync flopy4.mf6 to the discovered or latest MF6 release" + +[tool.pixi.tasks.sync-status] +cmd = "flopy4 mf6 status" +description = "Show flopy4.mf6 sync status vs. discovered MF6 binary" [tool.pixi.feature.test.tasks] test = { cmd = "pytest -v -n auto", env = { MPLBACKEND = "Agg" } } From 04cfbc39ce9d056ccb75739bff90000b377548cb Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 1 Jun 2026 08:51:36 -0700 Subject: [PATCH 2/6] mypy --- flopy4/mf6/utils/codegen/generate_classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flopy4/mf6/utils/codegen/generate_classes.py b/flopy4/mf6/utils/codegen/generate_classes.py index 8d695fb6..7d4ab69c 100644 --- a/flopy4/mf6/utils/codegen/generate_classes.py +++ b/flopy4/mf6/utils/codegen/generate_classes.py @@ -130,7 +130,7 @@ def generate_classes( makedirs: bool = False, existing_only: bool = False, force: bool = False, -) -> None: +) -> str: """Generate Python classes for MODFLOW 6 packages. Fetches (or reads) DFN files via the registry, generates a Python source From 23044feef6e45706b66a5e24ceaf68878c865756 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 1 Jun 2026 09:40:28 -0700 Subject: [PATCH 3/6] cleanup --- flopy4/cli.py | 76 +++++- flopy4/mf6/codec/reader/dfn2lark.py | 9 +- flopy4/mf6/utils/codegen/__init__.py | 4 +- flopy4/mf6/utils/codegen/dfn2py.py | 128 +++++++++ flopy4/mf6/utils/codegen/generate_classes.py | 268 ------------------- flopy4/mf6/utils/codegen/make.py | 10 +- pyproject.toml | 2 - test/test_mf6_codegen.py | 14 +- 8 files changed, 216 insertions(+), 295 deletions(-) create mode 100644 flopy4/mf6/utils/codegen/dfn2py.py delete mode 100644 flopy4/mf6/utils/codegen/generate_classes.py diff --git a/flopy4/cli.py b/flopy4/cli.py index 6a5c6d43..6a37e16a 100644 --- a/flopy4/cli.py +++ b/flopy4/cli.py @@ -6,6 +6,8 @@ import sys import warnings +_DFN_SCHEMA_VERSION = "2.0.0.dev1" + def _resolve_release_id(release_id: str | None, verbose: bool = False) -> str: """Resolve a release_id, falling back to the discovered binary's version @@ -35,9 +37,11 @@ def _resolve_release_id(release_id: str | None, verbose: bool = False) -> str: def _cmd_sync(args: argparse.Namespace) -> None: with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=".*modflow_devtools.programs.*experimental.*") + warnings.filterwarnings("ignore", message=".*modflow_devtools.dfns.*experimental.*") + from modflow_devtools.dfns import LocalDfnRegistry, RemoteDfnRegistry from modflow_devtools.programs import install_program - from flopy4.mf6.utils.codegen.generate_classes import generate_classes + from flopy4.mf6.utils.codegen.dfn2py import make release_id = _resolve_release_id(args.release_id, verbose=args.verbose) @@ -70,16 +74,76 @@ def _cmd_sync(args: argparse.Namespace) -> None: if args.verbose: print(f"Generating flopy4.mf6 from {release_id} ...") - synced_version = generate_classes( - release_id=release_id, - mf6_version=args.mf6_version, + def _populate_remote_cache( + registry: RemoteDfnRegistry, release_id: str, force: bool = False + ) -> None: + """Populate the registry's DFN cache. + + Tries ``registry.sync()`` first (requires a ``dfns.zip`` release asset). + Falls back to fetching DFNs from the repository tree when that asset is + not yet available — writes directly into the registry's cache path so + ``registry.spec()`` finds the files in the expected location. + + Parameters + ---------- + force : + Re-fetch even if the cache already exists. Should be True for + branch refs (mutable) and False for pinned release tags. + """ + if not force and registry.cache_path.exists() and any(registry.cache_path.iterdir()): + return + try: + registry.sync() + except Exception: + # dfns.zip not yet a release asset; fetch from the git tree instead. + from modflow_devtools.dfn import get_dfns + + owner_repo, tag = release_id.split("@", 1) + owner, repo = owner_repo.split("/") + registry.cache_path.mkdir(parents=True, exist_ok=True) + get_dfns(owner=owner, repo=repo, ref=tag, outdir=registry.cache_path) + + def _write_contract(outdir: Path, mf6_version: str) -> None: + (outdir / "_contract.py").write_text( + "# autogenerated file, do not modify\n" + f'MF6_VERSION = "{mf6_version}"\n' + f'DFN_SCHEMA_VERSION = "{_DFN_SCHEMA_VERSION}"\n' + ) + + path = Path(release_id).expanduser() + if path.exists() and path.is_dir(): + registry = LocalDfnRegistry(path=path) + effective_version = "unknown" + else: + if "@" not in release_id or "/" not in release_id.split("@")[0]: + raise ValueError( + f"release_id must be 'owner/repo@tag' or a local path; got: {release_id!r}" + ) + registry = RemoteDfnRegistry(release_id=release_id) + effective_version = args.mf6_version or registry.latest_tag() or "unknown" + + if effective_version == "unknown": + warnings.warn( + f"MF6 version is unknown for local DFN path '{release_id}'. " + "Pass --mf6-version to record it explicitly in _contract.py." + ) + + if isinstance(registry, RemoteDfnRegistry): + # Branch refs are mutable: always re-fetch unless the caller explicitly + # passes a pinned semver tag (first char is a digit). + is_branch = not effective_version[0].isdigit() if effective_version else True + _populate_remote_cache(registry, release_id, force=args.force or is_branch) + + make( + dfndir=registry.cache_path, + outdir=args.outdir, existing_only=not args.all_packages, makedirs=args.all_packages, fmt=not args.no_format, - force=args.force, ) + _write_contract(args.outdir, effective_version) - print(f"Synced flopy4.mf6 to MF6 {synced_version}.") + print(f"Synced flopy4.mf6 to MF6 {effective_version}.") def _cmd_status(args: argparse.Namespace) -> None: diff --git a/flopy4/mf6/codec/reader/dfn2lark.py b/flopy4/mf6/codec/reader/dfn2lark.py index ff56a620..2a1f5161 100644 --- a/flopy4/mf6/codec/reader/dfn2lark.py +++ b/flopy4/mf6/codec/reader/dfn2lark.py @@ -1,10 +1,10 @@ -"""Convert (TOML/v2) DFNs to Lark grammars.""" +"""Generate Lark grammars from DFN files.""" import argparse from os import PathLike from pathlib import Path -from modflow_devtools.dfn import load_flat, map +import modflow_devtools.dfn as dfn from flopy4.mf6.codec.reader.grammar import make_grammars @@ -17,9 +17,8 @@ def make(dfndir: str | PathLike, outdir: str | PathLike): dfndir = Path(dfndir).expanduser().absolute() outdir = Path(outdir).expanduser().absolute() outdir.mkdir(exist_ok=True, parents=True) - dfns_v1 = load_flat(dfndir) - dfns_v2 = {name: map(dfn, schema_version=2) for name, dfn in dfns_v1.items()} - make_grammars(dfns_v2, outdir) + dfns = dfn.Dfn.load_all(dfndir, schema_version="2.0.0.dev1") + make_grammars(dfns, outdir) def main(): diff --git a/flopy4/mf6/utils/codegen/__init__.py b/flopy4/mf6/utils/codegen/__init__.py index 6b2a501e..c2e8b023 100644 --- a/flopy4/mf6/utils/codegen/__init__.py +++ b/flopy4/mf6/utils/codegen/__init__.py @@ -1,3 +1,3 @@ -from .generate_classes import generate_classes +from flopy4.mf6.utils.codegen.dfn2py import make -__all__ = ["generate_classes"] +__all__ = ["make"] diff --git a/flopy4/mf6/utils/codegen/dfn2py.py b/flopy4/mf6/utils/codegen/dfn2py.py new file mode 100644 index 00000000..125b360f --- /dev/null +++ b/flopy4/mf6/utils/codegen/dfn2py.py @@ -0,0 +1,128 @@ +"""Generate an MF6 module from DFN files.""" + +import argparse +import sys +from os import PathLike +from pathlib import Path + +import modflow_devtools.dfn as dfn + +from flopy4.mf6.utils.codegen.make import make_modules + +_PROJ_ROOT = Path(__file__).parents[4].expanduser().resolve() +_MF6_ROOT = _PROJ_ROOT / "flopy4" / "mf6" + +# DFN names that require special handling beyond simple package generation. +# These are skipped until their generation tier is implemented. +# +# nam files: model/simulation name files need special model-level handling. +# dis/disv: require DisBase + grid conversion methods (dis tier). +# tdis/ims: top-level hand-written files, not yet templated. +# *g / *a variants: gridded/array package variants, deferred. +# +# TODO (subpackage tier): detect `# flopy subpackage` DFN annotations and emit +# a typed child attrs field (e.g. ncf: Optional[Ncf]) alongside the existing path +# field; DisBase.write() already establishes the write pattern for NCF. +# utl-ts also needs period values referencing timeseries by name written as strings. +_SKIP = { + # discretization tier (require DisBase + grid methods) + "gwf-dis", + "gwf-disv", + "gwt-dis", + "gwe-dis", + "prt-dis", + # time discretization (hand-written tdis.py) + "sim-tdis", + # hand-written: wkt field type override + Ncf.from_grid() factory. + # TODO: move factory to NcfBase (utl/ncf_base.py) so codegen can own utl/ncf.py, + # matching the DisBase pattern used for discretization packages. + "utl-ncf", +} + + +def make( + dfndir: str | PathLike, + outdir: str | PathLike = _MF6_ROOT, + developmode: bool = False, + fmt: bool = True, + makedirs: bool = False, + existing_only: bool = False, +): + """Generate an MF6 module from DFNs.""" + dfndir = Path(dfndir).expanduser().resolve() + outdir = Path(outdir).expanduser().resolve() + outdir.mkdir(exist_ok=True, parents=True) + dfns = dfn.Dfn.load_all(dfndir, schema_version="2.0.0.dev1") + components = make_modules( + dfns=dfns, + outdir=outdir, + developmode=developmode, + fmt=fmt, + skip=_SKIP, + makedirs=makedirs, + existing_only=existing_only, + ) + print(f"Generated {len(components)} component modules") + + +def cli_main() -> None: + """Command-line entry point.""" + parser = argparse.ArgumentParser( + description="Generate MF6 Python classes from DFN specification files.", + ) + parser.add_argument( + "--dfndir", + "-d", + type=str, + help="Directory containing DFN files.", + ) + parser.add_argument( + "--outdir", + default=str(_MF6_ROOT), + help="Root output directory (default: flopy4/mf6/ in the project).", + ) + parser.add_argument( + "--mf6-version", + default=None, + dest="mf6_version", + help="MF6 version string to record in _contract.py. " + "Inferred from the release tag for remote IDs; defaults to 'unknown' for local paths.", + ) + parser.add_argument( + "--developmode", + action="store_true", + help="Include developmode fields.", + ) + parser.add_argument( + "--no-format", + action="store_true", + help="Skip ruff formatting.", + ) + parser.add_argument( + "--makedirs", + action="store_true", + help="Create missing output subdirectories (useful for preview paths).", + ) + parser.add_argument( + "--existing-only", + action="store_true", + help="Only regenerate files that already exist on disk.", + ) + parser.add_argument("--verbose", action="store_true") + args = parser.parse_args() + + try: + make( + dfndir=args.dfndir, + outdir=args.outdir, + developmode=args.developmode, + fmt=not args.no_format, + makedirs=args.makedirs, + existing_only=args.existing_only, + ) + except (EOFError, KeyboardInterrupt): + sys.exit(f"Cancelled '{sys.argv[0]}'") + + +if __name__ == "__main__": + cli_main() diff --git a/flopy4/mf6/utils/codegen/generate_classes.py b/flopy4/mf6/utils/codegen/generate_classes.py deleted file mode 100644 index 7d4ab69c..00000000 --- a/flopy4/mf6/utils/codegen/generate_classes.py +++ /dev/null @@ -1,268 +0,0 @@ -""" -Orchestrate MF6 Python class generation from DFN specification files. - -Usage (CLI):: - - generate-mf6-classes MODFLOW-ORG/modflow6@6.6.0 - generate-mf6-classes /path/to/local/dfns --mf6-version 6.8.0.dev0+abc1234 -""" - -import argparse -import logging -import sys -import warnings -from pathlib import Path - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message=".*modflow_devtools.dfns.*experimental.*") - from modflow_devtools.dfns.registry import LocalDfnRegistry, RemoteDfnRegistry - -from .make import make_all - -logger = logging.getLogger(__name__) - -_PROJ_ROOT = Path(__file__).parents[4].expanduser().resolve() -_MF6_ROOT = _PROJ_ROOT / "flopy4" / "mf6" -_DFN_SCHEMA_VERSION = "2.0.0.dev1" - -# DFN names that require special handling beyond simple package generation. -# These are skipped until their generation tier is implemented. -# -# nam files: model/simulation name files need special model-level handling. -# dis/disv: require DisBase + grid conversion methods (dis tier). -# tdis/ims: top-level hand-written files, not yet templated. -# *g / *a variants: gridded/array package variants, deferred. -# -# TODO (subpackage tier): detect `# flopy subpackage` DFN annotations and emit -# a typed child attrs field (e.g. ncf: Optional[Ncf]) alongside the existing path -# field; DisBase.write() already establishes the write pattern for NCF. -# utl-ts also needs period values referencing timeseries by name written as strings. -_SKIP = { - # discretization tier (require DisBase + grid methods) - "gwf-dis", - "gwf-disv", - "gwt-dis", - "gwe-dis", - "prt-dis", - # time discretization (hand-written tdis.py) - "sim-tdis", - # hand-written: wkt field type override + Ncf.from_grid() factory. - # TODO: move factory to NcfBase (utl/ncf_base.py) so codegen can own utl/ncf.py, - # matching the DisBase pattern used for discretization packages. - "utl-ncf", -} - - -def _parse_release_id( - release_id: str, -) -> tuple[LocalDfnRegistry | RemoteDfnRegistry, str | None]: - """Parse a release_id into a registry and resolved MF6 version. - - Parameters - ---------- - release_id : - Either a local directory path or a remote release ID in - ``owner/repo@tag`` format (e.g. ``MODFLOW-ORG/modflow6@6.7.0`` - or ``MODFLOW-ORG/modflow6@latest``). - - Returns - ------- - tuple - (registry, mf6_version) where mf6_version is the resolved concrete - tag for remote registries (``@latest`` is resolved via the GitHub - API) or None for local paths. - """ - path = Path(release_id).expanduser() - if path.exists() and path.is_dir(): - return LocalDfnRegistry(path=path), None - if "@" not in release_id or "/" not in release_id.split("@")[0]: - raise ValueError( - f"release_id must be 'owner/repo@tag' or a local path; got: {release_id!r}" - ) - registry = RemoteDfnRegistry(release_id=release_id) - return registry, registry.latest_tag() - - -def _populate_remote_cache( - registry: RemoteDfnRegistry, release_id: str, force: bool = False -) -> None: - """Populate the registry's DFN cache. - - Tries ``registry.sync()`` first (requires a ``dfns.zip`` release asset). - Falls back to fetching DFNs from the repository tree when that asset is - not yet available — writes directly into the registry's cache path so - ``registry.spec()`` finds the files in the expected location. - - Parameters - ---------- - force : - Re-fetch even if the cache already exists. Should be True for - branch refs (mutable) and False for pinned release tags. - """ - if not force and registry.cache_path.exists() and any(registry.cache_path.iterdir()): - return - try: - registry.sync() - except Exception: - # dfns.zip not yet a release asset; fetch from the git tree instead. - from modflow_devtools.dfn import get_dfns - - owner_repo, tag = release_id.split("@", 1) - owner, repo = owner_repo.split("/") - registry.cache_path.mkdir(parents=True, exist_ok=True) - get_dfns(owner=owner, repo=repo, ref=tag, outdir=registry.cache_path) - - -def _write_contract(outdir: Path, mf6_version: str) -> None: - (outdir / "_contract.py").write_text( - "# autogenerated file, do not modify\n" - f'MF6_VERSION = "{mf6_version}"\n' - f'DFN_SCHEMA_VERSION = "{_DFN_SCHEMA_VERSION}"\n' - ) - - -def generate_classes( - release_id: str, - outdir: str | Path = _MF6_ROOT, - mf6_version: str | None = None, - developmode: bool = False, - fmt: bool = True, - makedirs: bool = False, - existing_only: bool = False, - force: bool = False, -) -> str: - """Generate Python classes for MODFLOW 6 packages. - - Fetches (or reads) DFN files via the registry, generates a Python source - file for each component into ``outdir``, and writes ``_contract.py`` - recording the MF6 version and DFN schema version. - - Parameters - ---------- - release_id : - Remote release ID (``owner/repo@tag``) or local path to a directory - of ``.dfn`` files. - outdir : - Root output directory. Defaults to ``flopy4/mf6/`` inside the - project so generated files land alongside hand-written ones. - mf6_version : - Override the MF6 version written to ``_contract.py``. For remote - release IDs the tag is used automatically; for local paths this - defaults to ``"unknown"`` unless provided here. - developmode : - Include fields marked ``developmode`` in the DFN. Default False. - fmt : - Run ``ruff format`` / ``ruff check --fix`` on each generated file. - makedirs : - Create output subdirectories if missing. - existing_only : - Only (re)generate files that already exist on disk. - force : - Force re-fetch of DFNs even if already cached. Automatically - enabled for branch refs (non-semver tags), which are mutable. - """ - registry, derived_version = _parse_release_id(release_id) - effective_version = mf6_version or derived_version or "unknown" - - if effective_version == "unknown": - logger.warning( - "MF6 version is unknown for local DFN path '%s'. " - "Pass --mf6-version to record it explicitly in _contract.py.", - release_id, - ) - - if isinstance(registry, RemoteDfnRegistry): - # Branch refs are mutable: always re-fetch unless the caller explicitly - # passes a pinned semver tag (first char is a digit). - is_branch = not effective_version[0].isdigit() if effective_version else True - _populate_remote_cache(registry, release_id, force=force or is_branch) - - logger.info(f"Loading DFNs from {release_id!r} (schema {_DFN_SCHEMA_VERSION})") - dfns = registry.spec(schema_version=_DFN_SCHEMA_VERSION) - - outdir = Path(outdir).expanduser().resolve() - generated = make_all( - dfns=dfns, - outdir=outdir, - developmode=developmode, - fmt=fmt, - skip=_SKIP, - makedirs=makedirs, - existing_only=existing_only, - ) - - _write_contract(outdir, effective_version) - logger.info(f"Generated {len(generated)} files. Contract: MF6 {effective_version}.") - return effective_version - - -def cli_main() -> None: - """Command-line entry point.""" - parser = argparse.ArgumentParser( - description="Generate MF6 Python classes from DFN specification files.", - ) - parser.add_argument( - "release_id", - help="Remote release ID (owner/repo@tag) or local path to .dfn files.", - ) - parser.add_argument( - "--mf6-version", - default=None, - dest="mf6_version", - help="MF6 version string to record in _contract.py. " - "Inferred from the release tag for remote IDs; defaults to 'unknown' for local paths.", - ) - parser.add_argument( - "--outdir", - default=str(_MF6_ROOT), - help="Root output directory (default: flopy4/mf6/ in the project).", - ) - parser.add_argument( - "--developmode", - action="store_true", - help="Include developmode fields.", - ) - parser.add_argument( - "--no-format", - action="store_true", - help="Skip ruff formatting.", - ) - parser.add_argument( - "--makedirs", - action="store_true", - help="Create missing output subdirectories (useful for preview paths).", - ) - parser.add_argument( - "--existing-only", - action="store_true", - help="Only regenerate files that already exist on disk.", - ) - parser.add_argument( - "--force", - action="store_true", - help="Force re-fetch of DFNs even if already cached. " - "Implied for branch refs (e.g. @develop).", - ) - parser.add_argument("--verbose", action="store_true") - args = parser.parse_args() - - if args.verbose: - logging.basicConfig(level=logging.INFO) - - try: - generate_classes( - release_id=args.release_id, - outdir=args.outdir, - mf6_version=args.mf6_version, - developmode=args.developmode, - fmt=not args.no_format, - makedirs=args.makedirs, - existing_only=args.existing_only, - force=args.force, - ) - except (EOFError, KeyboardInterrupt): - sys.exit(f"Cancelled '{sys.argv[0]}'") - - -if __name__ == "__main__": - cli_main() diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index 335f9a74..52670903 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -929,13 +929,13 @@ def _format(path: Path) -> None: ) -def make_component( +def make_module( spec: ComponentSpec, env: jinja2.Environment, *, fmt: bool = True, ) -> None: - """Render and write a single component file.""" + """Generate a single component module.""" template = env.get_template(spec.template) rendered = template.render(spec=spec) spec.outpath.write_text(rendered, newline="\n") @@ -947,7 +947,7 @@ def make_component( logger.warning(f"Failed to format {spec.outpath}: {e.stderr.decode().strip()}") -def make_all( +def make_modules( *, dfns: dict[str, "Dfn"] | None = None, dfndir: PathLike | None = None, @@ -958,7 +958,7 @@ def make_all( makedirs: bool = False, existing_only: bool = False, ) -> list[ComponentSpec]: - """Generate Python source files for all DFNs. + """Generate Python modules for all components. Parameters ---------- @@ -1002,6 +1002,6 @@ def make_all( continue if makedirs: spec.outpath.parent.mkdir(parents=True, exist_ok=True) - make_component(spec, env, fmt=fmt) + make_module(spec, env, fmt=fmt) specs.append(spec) return specs diff --git a/pyproject.toml b/pyproject.toml index 54a0c2e9..a5f80511 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,8 +118,6 @@ ignore = [ [project.scripts] flopy4 = "flopy4.cli:main" -dfn2lark = "flopy4.mf6.codec.reader.dfn2lark:main" -generate-mf6-classes = "flopy4.mf6.utils.codegen.generate_classes:cli_main" [project.entry-points.flopy4] plot = "flopy4.singledispatch.plot_int" diff --git a/test/test_mf6_codegen.py b/test/test_mf6_codegen.py index c9bb7a32..83382a4d 100644 --- a/test/test_mf6_codegen.py +++ b/test/test_mf6_codegen.py @@ -32,7 +32,7 @@ safe_name, spec_call, ) -from flopy4.mf6.utils.codegen.make import build_component_spec, make_all +from flopy4.mf6.utils.codegen.make import build_component_spec, make_modules # Shared fixtures @@ -600,7 +600,7 @@ def test_simple_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): (tmp_path / "gwf").mkdir() skip = {n for n in all_dfns if n not in SIMPLE_TIER} - specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) + specs = make_modules(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} @@ -638,7 +638,7 @@ def test_solution_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): from flopy4.mf6.solution import Solution skip = {n for n in all_dfns if n not in SOLUTION_TIER} - specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) + specs = make_modules(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} @@ -697,7 +697,7 @@ def test_transport_tier_generates_importable_files(dfn_path, tmp_path, all_dfns) target = {n for n in TRANSPORT_TIER if n in all_dfns} skip = {n for n in all_dfns if n not in target} - specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) + specs = make_modules(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package @@ -719,7 +719,7 @@ def test_oc_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): target = {n for n in OC_TIER if n in all_dfns} skip = {n for n in all_dfns if n not in target} - specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) + specs = make_modules(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package @@ -743,7 +743,7 @@ def test_utl_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): target = {n for n in UTL_TIER if n in all_dfns} skip = {n for n in all_dfns if n not in target} - specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) + specs = make_modules(dfndir=dfn_path, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package @@ -764,7 +764,7 @@ def test_exg_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): target = {n for n in EXG_TIER if n in all_dfns} skip = {n for n in all_dfns if n not in target} - specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) + specs = make_modules(dfndir=dfn_path, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package From 20ce78d9ad04fb3a79ea3900bf9ba0a20e255ce7 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 1 Jun 2026 09:49:59 -0700 Subject: [PATCH 4/6] more cleanup --- flopy4/cli.py | 12 ++++++++---- flopy4/mf6/_contract.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/flopy4/cli.py b/flopy4/cli.py index 6a37e16a..81314be4 100644 --- a/flopy4/cli.py +++ b/flopy4/cli.py @@ -5,7 +5,10 @@ import shutil import sys import warnings +from pathlib import Path +_PROJ_ROOT = Path(__file__).parents[1].expanduser().resolve() +_MF6_ROOT = _PROJ_ROOT / "flopy4" / "mf6" _DFN_SCHEMA_VERSION = "2.0.0.dev1" @@ -135,15 +138,16 @@ def _write_contract(outdir: Path, mf6_version: str) -> None: _populate_remote_cache(registry, release_id, force=args.force or is_branch) make( - dfndir=registry.cache_path, - outdir=args.outdir, + dfndir=registry.cache_path if isinstance(registry, RemoteDfnRegistry) else registry.path, + outdir=_MF6_ROOT, existing_only=not args.all_packages, makedirs=args.all_packages, fmt=not args.no_format, ) - _write_contract(args.outdir, effective_version) + _write_contract(_MF6_ROOT, effective_version) - print(f"Synced flopy4.mf6 to MF6 {effective_version}.") + if effective_version != "unknown": + print(f"Synced flopy4.mf6 to MF6 version: {effective_version}") def _cmd_status(args: argparse.Namespace) -> None: diff --git a/flopy4/mf6/_contract.py b/flopy4/mf6/_contract.py index 4a5aa81a..520d9a71 100644 --- a/flopy4/mf6/_contract.py +++ b/flopy4/mf6/_contract.py @@ -1,3 +1,3 @@ # autogenerated file, do not modify -MF6_VERSION = "6.7.0" +MF6_VERSION = "unknown" DFN_SCHEMA_VERSION = "2.0.0.dev1" From 1431b392f83f2f1936bceacc29f54f1610fcfec1 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 1 Jun 2026 09:55:20 -0700 Subject: [PATCH 5/6] skip exe install for now --- flopy4/cli.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/flopy4/cli.py b/flopy4/cli.py index 81314be4..6f3c24cd 100644 --- a/flopy4/cli.py +++ b/flopy4/cli.py @@ -51,28 +51,28 @@ def _cmd_sync(args: argparse.Namespace) -> None: # For remote release IDs, install the matching binary unless --no-install. from pathlib import Path - is_local = Path(release_id).expanduser().is_dir() - if not is_local and not args.no_install: - _, tag = release_id.split("@", 1) - if tag not in ("latest",): - version_arg = tag - else: - version_arg = None # let install_program resolve latest - if args.verbose: - label = version_arg or "latest" - print(f"Installing mf6 {label} ...") - try: - installed = install_program( - "mf6", - version=version_arg, - force=args.force, - verbose=args.verbose, - ) - if args.verbose and installed: - print(f"Installed: {', '.join(str(p) for p in installed)}") - except Exception as exc: - print(f"Warning: binary installation failed: {exc}", file=sys.stderr) - print("Continuing with class generation only.", file=sys.stderr) + # is_local = Path(release_id).expanduser().is_dir() + # if not is_local and not args.no_install: + # _, tag = release_id.split("@", 1) + # if tag not in ("latest",): + # version_arg = tag + # else: + # version_arg = None # let install_program resolve latest + # if args.verbose: + # label = version_arg or "latest" + # print(f"Installing mf6 {label} ...") + # try: + # installed = install_program( + # "mf6", + # version=version_arg, + # force=args.force, + # verbose=args.verbose, + # ) + # if args.verbose and installed: + # print(f"Installed: {', '.join(str(p) for p in installed)}") + # except Exception as exc: + # print(f"Warning: binary installation failed: {exc}", file=sys.stderr) + # print("Continuing with class generation only.", file=sys.stderr) if args.verbose: print(f"Generating flopy4.mf6 from {release_id} ...") From e5b26988b24d2669cd946d526fa4622d1158ad32 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 1 Jun 2026 09:58:08 -0700 Subject: [PATCH 6/6] ruff --- flopy4/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flopy4/cli.py b/flopy4/cli.py index 6f3c24cd..d2af30d9 100644 --- a/flopy4/cli.py +++ b/flopy4/cli.py @@ -42,7 +42,7 @@ def _cmd_sync(args: argparse.Namespace) -> None: warnings.filterwarnings("ignore", message=".*modflow_devtools.programs.*experimental.*") warnings.filterwarnings("ignore", message=".*modflow_devtools.dfns.*experimental.*") from modflow_devtools.dfns import LocalDfnRegistry, RemoteDfnRegistry - from modflow_devtools.programs import install_program + # from modflow_devtools.programs import install_program from flopy4.mf6.utils.codegen.dfn2py import make