From 07985b5c70ec8daee2d7cd490a52c4b69916810a Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 30 Oct 2024 12:23:45 -0400 Subject: [PATCH] wip --- .github/workflows/mf6.yml | 4 +- flopy/mf6/utils/codegen/__init__.py | 61 ++--- flopy/mf6/utils/codegen/context.py | 85 +++---- flopy/mf6/utils/codegen/dfn.py | 160 +++++++------ flopy/mf6/utils/codegen/shim.py | 223 ++++++++---------- .../utils/codegen/templates/exchange.py.jinja | 14 +- .../mf6/utils/codegen/templates/macros.jinja | 8 +- .../utils/codegen/templates/model.py.jinja | 10 +- .../utils/codegen/templates/package.py.jinja | 24 +- .../codegen/templates/simulation.py.jinja | 10 +- 10 files changed, 281 insertions(+), 318 deletions(-) diff --git a/.github/workflows/mf6.yml b/.github/workflows/mf6.yml index 62c1d8ed3..ba094552c 100644 --- a/.github/workflows/mf6.yml +++ b/.github/workflows/mf6.yml @@ -37,7 +37,7 @@ jobs: pip install https://github.com/modflowpy/pymake/zipball/master pip install https://github.com/Deltares/xmipy/zipball/develop pip install https://github.com/MODFLOW-USGS/modflowapi/zipball/develop - pip install .[codegen,test,optional] + pip install .[dev] pip install meson ninja - name: Setup GNU Fortran @@ -120,7 +120,7 @@ jobs: pip install https://github.com/modflowpy/pymake/zipball/master pip install https://github.com/Deltares/xmipy/zipball/develop pip install https://github.com/MODFLOW-USGS/modflowapi/zipball/develop - pip install .[codegen,test,optional] + pip install .[dev] pip install meson ninja pip install -r modflow6-examples/etc/requirements.pip.txt diff --git a/flopy/mf6/utils/codegen/__init__.py b/flopy/mf6/utils/codegen/__init__.py index 78ba16e23..0e31f3f16 100644 --- a/flopy/mf6/utils/codegen/__init__.py +++ b/flopy/mf6/utils/codegen/__init__.py @@ -1,29 +1,26 @@ from pathlib import Path -from warnings import warn from flopy.utils import import_optional_dependency __all__ = ["make_targets", "make_all"] - -jinja = import_optional_dependency("jinja2", errors="ignore") -if jinja: - _TEMPLATES_PATH = "mf6/utils/codegen/templates/" - _TEMPLATE_LOADER = jinja.PackageLoader("flopy", _TEMPLATES_PATH) - _TEMPLATE_ENV = jinja.Environment(loader=_TEMPLATE_LOADER) +__jinja = import_optional_dependency("jinja2", errors="ignore") def make_targets(dfn, outdir: Path, verbose: bool = False): - """Generate Python source file(s) from the given input definition.""" + """Generate Python source file(s) from an input definition.""" + + if not __jinja: + raise RuntimeError("Need Jinja2 for code generation") from flopy.mf6.utils.codegen.context import Context - if not jinja: - raise RuntimeError("Jinja2 not installed, can't make targets") + loader = __jinja.PackageLoader("flopy", "mf6/utils/codegen/templates/") + env = __jinja.Environment(loader=loader) for context in Context.from_dfn(dfn): name = context.name target = outdir / name.target - template = _TEMPLATE_ENV.get_template(name.template) + template = env.get_template(name.template) with open(target, "w") as f: f.write(template.render(**context.render())) if verbose: @@ -31,44 +28,16 @@ def make_targets(dfn, outdir: Path, verbose: bool = False): def make_all(dfndir: Path, outdir: Path, verbose: bool = False): - """Generate Python source files from the DFN files in the given location.""" - - from flopy.mf6.utils.codegen.context import Context - from flopy.mf6.utils.codegen.dfn import Dfn, Dfns, Ref, Refs + """Generate Python source files from DFN files.""" - if not jinja: - raise RuntimeError("Jinja2 not installed, can't make targets") + if not __jinja: + raise RuntimeError("Need Jinja2 for code generation") - # find definition files - paths = [ - p for p in dfndir.glob("*.dfn") if p.stem not in ["common", "flopy"] - ] - - # try to load common variables - common_path = dfndir / "common.dfn" - if not common_path.is_file: - common = None - else: - with open(common_path, "r") as f: - common, _ = Dfn._load(f) - - # load subpackage references first - refs: Refs = {} - for path in paths: - name = Dfn.Name(*path.stem.split("-")) - with open(path) as f: - dfn = Dfn.load(f, name=name, common=common) - ref = Ref.from_dfn(dfn) - if ref: - refs[ref.key] = ref + from flopy.mf6.utils.codegen.context import Context + from flopy.mf6.utils.codegen.dfn import Dfn - # load all the input definitions - dfns: Dfns = {} - for path in paths: - name = Dfn.Name(*path.stem.split("-")) - with open(path) as f: - dfn = Dfn.load(f, name=name, refs=refs, common=common) - dfns[name] = dfn + # load definition files + dfns = Dfn.load_all(dfndir) # make target files for dfn in dfns.values(): diff --git a/flopy/mf6/utils/codegen/context.py b/flopy/mf6/utils/codegen/context.py index d874edf40..28fb17a4a 100644 --- a/flopy/mf6/utils/codegen/context.py +++ b/flopy/mf6/utils/codegen/context.py @@ -8,7 +8,7 @@ Optional, ) -from flopy.mf6.utils.codegen.dfn import Dfn, Ref, Vars +from flopy.mf6.utils.codegen.dfn import Dfn, Ref from flopy.mf6.utils.codegen.renderable import renderable from flopy.mf6.utils.codegen.shim import SHIM @@ -31,8 +31,7 @@ class Context: becomes the first `__init__` method parameter). The context class may reference other contexts via foreign key - relations held by its variables, and may itself be referenced - by other contexts if desired. + relations held by its variables, and may itself be referenced. """ @@ -59,37 +58,35 @@ class Name(NamedTuple): """ - l: str - r: Optional[str] + component: str + subcomponent: Optional[str] @property def title(self) -> str: """ - The input context's unique title. This is not - identical to `f"{l}{r}` in some cases, but it - remains unique. The title is substituted into + The input context's title. Substituted into the file name and class name. """ - l, r = self + comp, sub = self if self == ("sim", "nam"): return "simulation" - if l is None: - return r - if r is None: - return l - if l == "sim": - return r - if l in ["sln", "exg"]: - return r - return l + r + if comp is None: + return sub + if sub is None: + return comp + if comp == "sim": + return sub + if comp in ["sln", "exg"]: + return sub + return comp + sub @property def base(self) -> str: """Base class from which the input context should inherit.""" - _, r = self + _, sub = self if self == ("sim", "nam"): return "MFSimulationBase" - if r is None: + if sub is None: return "MFModel" return "MFPackage" @@ -106,19 +103,19 @@ def template(self) -> str: elif self.base == "MFModel": return "model.py.jinja" elif self.base == "MFPackage": - if self.l == "exg": + if self.component == "exg": return "exchange.py.jinja" return "package.py.jinja" @property def description(self) -> str: """A description of the input context.""" - l, r = self + comp, sub = self title = self.title.title() if self.base == "MFPackage": - return f"Modflow{title} defines a {r.upper()} package." + return f"Modflow{title} defines a {sub.upper()} package." elif self.base == "MFModel": - return f"Modflow{title} defines a {l.upper()} model." + return f"Modflow{title} defines a {comp.upper()} model." elif self.base == "MFSimulationBase": return ( "MFSimulation is used to load, build, and/or save a MODFLOW 6 simulation." @@ -129,7 +126,7 @@ def description(self) -> str: @staticmethod def from_dfn(dfn: Dfn) -> List["Context.Name"]: """ - Returns a list of context names this definition produces. + Get a list of context names this definition produces. Notes ----- @@ -140,50 +137,42 @@ def from_dfn(dfn: Dfn) -> List["Context.Name"]: definition files. All other definition files produce a single context. """ - if dfn.name.r == "nam": - if dfn.name.l == "sim": + if dfn.name.subcomponent == "nam": + if dfn.name.component == "sim": return [ - Context.Name(None, dfn.name.r), # nam pkg + Context.Name(None, dfn.name.subcomponent), # nam pkg Context.Name(*dfn.name), # simulation ] else: return [ Context.Name(*dfn.name), # nam pkg - Context.Name(dfn.name.l, None), # model + Context.Name(dfn.name.component, None), # model ] - elif dfn.name in [ + elif (dfn.name.component, dfn.name.subcomponent) in [ ("gwf", "mvr"), ("gwf", "gnc"), ("gwt", "mvt"), ]: + # TODO: remove these special cases and deduplicate + # mfmvr.py/mfgwfmvr.py etc return [ Context.Name(*dfn.name), - Context.Name(None, dfn.name.r), + Context.Name(None, dfn.name.subcomponent), ] return [Context.Name(*dfn.name)] name: Name - vars: Vars - base: Optional[type] = None - description: Optional[str] = None + dfn: Dfn meta: Optional[Dict[str, Any]] = None @classmethod def from_dfn(cls, dfn: Dfn) -> Iterator["Context"]: """ - Extract context class descriptor(s) from an input definition. - These are structured representations of input context classes. - Each input definition yields one or more input contexts. + Create context class descriptor(s) from an input definition. + These are structured representations of input context class + definitions. Each input definition yields one or more class + definitions. """ - meta = dfn.meta.copy() - ref = Ref.from_dfn(dfn) - if ref: - meta["ref"] = ref + for name in Context.Name.from_dfn(dfn): - yield Context( - name=name, - vars=dfn.data, - base=name.base, - description=name.description, - meta=meta, - ) + yield Context(name=name, dfn=dfn, meta=dfn.meta) diff --git a/flopy/mf6/utils/codegen/dfn.py b/flopy/mf6/utils/codegen/dfn.py index b6fcef441..a74210eb1 100644 --- a/flopy/mf6/utils/codegen/dfn.py +++ b/flopy/mf6/utils/codegen/dfn.py @@ -1,9 +1,9 @@ from ast import literal_eval from collections import UserDict -from dataclasses import dataclass from enum import Enum from keyword import kwlist from os import PathLike +from pathlib import Path from typing import ( Any, Dict, @@ -11,7 +11,7 @@ NamedTuple, Optional, Tuple, - Union, + TypedDict, ) from warnings import warn @@ -32,8 +32,7 @@ Refs = Dict[str, "Ref"] -@dataclass -class Var: +class Var(TypedDict): """MODFLOW 6 input variable specification.""" class Kind(Enum): @@ -47,6 +46,7 @@ class Kind(Enum): Record = "record" Union = "union" List = "list" + Path = "path" name: str kind: Kind @@ -70,8 +70,8 @@ class Name(NamedTuple): Consists of a left term and a right term. """ - l: str - r: str + component: str + subcomponent: str @classmethod def parse(cls, v: str) -> "Dfn.Name": @@ -155,7 +155,11 @@ def _load(f, common: Optional[dict] = None) -> Tuple[OMD, List[str]]: # remove backslashes, TODO: generate/insert citations. descr = var.get("description", None) if descr: - descr = descr.replace("\\", "") + descr = ( + descr.replace("\\", "") + .replace("``", "'") + .replace("''", "'") + ) _, replace, tail = descr.strip().partition("REPLACE") if replace: key, _, subs = tail.strip().partition(" ") @@ -190,8 +194,8 @@ def load( ) -> "Dfn": """Load an input definition.""" - refs = refs or dict() referenced = dict() + refs = refs or dict() flat, meta = Dfn._load(f, **kwargs) def _map(spec: Dict[str, Any]) -> Var: @@ -227,10 +231,10 @@ def _map(spec: Dict[str, Any]) -> Var: shape = None if shape == "" else shape default = spec.get("default", None) description = spec.get("description", "") + ref = refs.get(_name, None) children = dict() # if var is a foreign key, register the reference - ref = refs.get(_name, None) if ref: referenced[_name] = ref @@ -262,7 +266,7 @@ def _fields(record_name: str) -> Vars: # set the type n = list(fields.keys())[0] path_field = fields[n] - path_field._type = Union[str, PathLike] + path_field["kind"] = Var.Kind.Path fields[n] = path_field # if tagged, remove the leading keyword @@ -318,7 +322,7 @@ def _is_implicit_scalar_record(): children=fields, description=description, meta={ - "type": f"[{', '.join([f.meta['type'] for f in fields.values()])}]" + "type": f"[{', '.join([f["name"] for f in fields.values()])}]" }, ) } @@ -332,21 +336,23 @@ def _is_implicit_scalar_record(): } first = list(fields.values())[0] single = len(fields) == 1 - name_ = first.name if single else _name + name_ = first["name"] if single else _name children = { name_: Var( name=name_, kind=Var.Kind.Record, block=block, - children=first.children if single else fields, + children=first["children"] if single else fields, description=description, meta={ - "type": f"[{', '.join([v.meta['type'] for v in fields.values()])}]" + "type": f"[{', '.join([v["name"] for v in fields.values()])}]" }, ) } kind = Var.Kind.List - type_ = f"[{', '.join([v.name for v in children.values()])}]" + type_ = ( + f"[{', '.join([v["name"] for v in children.values()])}]" + ) # union (product), children are choices elif _type.startswith("keystring"): @@ -357,15 +363,20 @@ def _is_implicit_scalar_record(): if v["name"] in names and v.get("in_record", False) } kind = Var.Kind.Union - type_ = f"[{', '.join([v.name for v in children.values()])}]" + type_ = ( + f"[{', '.join([v["name"] for v in children.values()])}]" + ) # record (sum), children are fields elif _type.startswith("record"): children = _fields(_name) kind = Var.Kind.Record - type_ = f"[{', '.join([v.meta['type'] for v in children.values()])}]" + type_ = ( + f"[{', '.join([v["name"] for v in children.values()])}]" + ) - # at this point, if it has a shape, it's an array + # if it has a shape, it's an array + # (unless str, in which case list) elif shape is not None: if _type not in _SCALARS: raise TypeError(f"Unsupported array type: {_type}") @@ -400,63 +411,78 @@ def _is_implicit_scalar_record(): meta={"ref": ref, "type": type_}, ) - # pass the original DFN representation as - # metadata so the shim can use it for now - _vars = list(flat.values(multi=True)) - - # convert input variable specs to - # structured form, descending into - # composites recursively as needed - flat = { - var["name"]: _map(var) - for var in flat.values(multi=True) - if not var.get("in_record", False) - } - - # reset the var name. we may have altered - # it when converting the variable e.g. to - # avoid collision with a reserved keyword - flat = {v.name: v for v in flat.values()} - - return cls( - flat, + dfn = cls( + # convert input variable specs to + # structured form, descending into + # composites recursively as needed + { + var["name"]: _map(var) + for var in flat.values(multi=True) + if not var.get("in_record", False) + }, name, { - "dfn": (_vars, meta), + # pass the original DFN representation as + # metadata so the shim can use it for now + "dfn": (list(flat.values(multi=True)), meta), "refs": referenced, }, ) - -@dataclass -class Ref: + ref = Ref.from_dfn(dfn) + if ref: + dfn.meta["ref"] = ref + + return dfn + + def load_all(dfndir: PathLike) -> List["Dfn"]: + # find definition files + dfndir = Path(dfndir) + paths = [ + p + for p in dfndir.glob("*.dfn") + if p.stem not in ["common", "flopy"] + ] + + # try to load common variables + common_path = dfndir / "common.dfn" + if not common_path.is_file: + common = None + else: + with open(common_path, "r") as f: + common, _ = Dfn._load(f) + + # load subpackage references first + refs: Refs = {} + for path in paths: + name = Dfn.Name(*path.stem.split("-")) + with open(path) as f: + dfn = Dfn.load(f, name=name, common=common) + ref = Ref.from_dfn(dfn) + if ref: + refs[ref["key"]] = ref + + # load definitions + dfns: Dfns = {} + for path in paths: + name = Dfn.Name(*path.stem.split("-")) + with open(path) as f: + dfn = Dfn.load(f, name=name, refs=refs, common=common) + dfns[name] = dfn + + return dfns + + +class Ref(TypedDict): """ A foreign-key-like reference between a file input variable and another input definition. This allows an input context to refer to another input context, by including a filepath - variable whose name acts as a foreign key for a different - input context. The referring context's `__init__` method - is modified such that the variable named `val` replaces - the `key` variable. - - Notes - ----- - This class is used to represent subpackage references. - - Parameters - ---------- - key : str - The name of the foreign key file input variable. - val : str - The name of the data variable in the referenced context. - abbr : str - An abbreviation of the referenced context's name. - param : str - The referenced parameter name. - parents : List[str] - The referenced context's supported parents. - description : Optional[str] - The reference's description. + variable whose name acts as a foreign key. This mechanism + is used to represent subpackages. + + The referring context's `__init__` method is modified such + that the variable named `val` replaces the `key` variable. """ key: str @@ -506,14 +532,14 @@ def from_dfn(cls, dfn: Dfn) -> Optional["Ref"]: def _subpkg(): line = lines["subpkg"] _, key, abbr, param, val = line.split() - matches = [v for v in dfn.values() if v.name == val] + matches = [v for v in dfn.values() if v["name"] == val] if not any(matches): descr = None else: if len(matches) > 1: warn(f"Multiple matches for referenced variable {val}") match = matches[0] - descr = match.description + descr = match["description"] return { "key": key, diff --git a/flopy/mf6/utils/codegen/shim.py b/flopy/mf6/utils/codegen/shim.py index 2b02ecd02..18afce7ca 100644 --- a/flopy/mf6/utils/codegen/shim.py +++ b/flopy/mf6/utils/codegen/shim.py @@ -9,24 +9,28 @@ from pprint import pformat from typing import List, Optional +from flopy.mf6.utils.codegen.utils import try_get_enum_value + def _cls_attrs(ctx: dict) -> List[str]: - ctx_name = ctx["name"] + name = ctx["name"] + base = name.base + dfn = ctx["dfn"] def _attr(var: dict) -> Optional[str]: var_name = var["name"] - var_kind = var.get("kind", None) + var_kind = try_get_enum_value(var.get("kind", None)) var_block = var.get("block", None) - var_ref = var.get("meta", dict()).get("ref", None) + var_ref = var.get("ref", None) if ( var_kind is None or var_kind == "scalar" or var_name in ["cvoptions", "output"] - or (ctx_name.r == "dis" and var_name == "packagedata") + or (name.subcomponent == "dis" and var_name == "packagedata") or ( var_name != "packages" - and (ctx_name.l is not None and ctx_name.r == "nam") + and (name.component is not None and name.subcomponent == "nam") ) ): return None @@ -40,28 +44,34 @@ def _attr(var: dict) -> Optional[str]: # if the variable is a subpackage reference, use the original key # (which has been replaced already with the referenced variable) args = [ - f"'{ctx_name.r}'", + f"'{name.subcomponent}'", f"'{var_block}'", f"'{var_ref['key']}'", ] - if ctx_name.l is not None and ctx_name.l not in [ + if name.component not in [ + None, "sim", "sln", "utl", "exg", ]: - args.insert(0, f"'{ctx_name.l}6'") + args.insert(0, f"'{name.component}6'") return f"{var_ref['key']} = ListTemplateGenerator(({', '.join(args)}))" def _args(): - args = [f"'{ctx_name.r}'", f"'{var_block}'", f"'{var_name}'"] - if ctx_name.l is not None and ctx_name.l not in [ + args = [ + f"'{name.subcomponent}'", + f"'{var_block}'", + f"'{var_name}'", + ] + if name.component not in [ + None, "sim", "sln", "utl", "exg", ]: - args.insert(0, f"'{ctx_name.l}6'") + args.insert(0, f"'{name.component}6'") return args kind = var_kind if var_kind == "array" else "list" @@ -70,7 +80,7 @@ def _args(): return None def _dfn() -> List[List[str]]: - dfn, meta = ctx["meta"]["dfn"] + dfn_, meta = ctx["meta"]["dfn"] def _meta(): exclude = ["subpackage", "parent_name_type"] @@ -80,7 +90,7 @@ def _dfn(): def _var(var: dict) -> List[str]: exclude = ["longname", "description"] name = var["name"] - var_ = ctx["vars"].get(name, None) + var_ = dfn.get(name, None) keys = [ "construct_package", "construct_data", @@ -95,24 +105,24 @@ def _var(var: dict) -> List[str]: if k not in exclude ] - return [_var(var) for var in dfn] + return [_var(var) for var in dfn_] return [["header"] + _meta()] + _dfn() - attrs = list(filter(None, [_attr(v) for v in ctx["vars"].values()])) + attrs = list(filter(None, [_attr(v) for v in dfn.values()])) - if ctx["base"] == "MFModel": - attrs.append(f"model_type = {ctx_name.l}") - elif ctx["base"] == "MFPackage": + if base == "MFModel": + attrs.append(f"model_type = {name.component}") + elif base == "MFPackage": attrs.extend( [ - f"package_abbr = '{ctx_name.r}'" - if ctx_name.l == "exg" - else f"package_abbr = '{'' if ctx_name.l in ['sln', 'sim', 'exg', None] else ctx_name.l}{ctx_name.r}'", - f"_package_type = '{ctx_name.r}'", - f"dfn_file_name = '{ctx_name.l}-{ctx_name.r}.dfn'" - if ctx_name.l == "exg" - else f"dfn_file_name = '{ctx_name.l or 'sim'}-{ctx_name.r}.dfn'", + f"package_abbr = '{name.subcomponent}'" + if name.component == "exg" + else f"package_abbr = '{'' if name.component in ['sln', 'sim', 'exg', None] else name.component}{name.subcomponent}'", + f"_package_type = '{name.subcomponent}'", + f"dfn_file_name = '{'-'.join(name)}.dfn'" + if name.component == "exg" + else f"dfn_file_name = '{name.component or 'sim'}-{name.subcomponent}.dfn'", f"dfn = {pformat(_dfn(), indent=10)}", ] ) @@ -121,8 +131,11 @@ def _var(var: dict) -> List[str]: def _init_body(ctx: dict) -> List[str]: + name = ctx["name"] + base = name.base + dfn = ctx["dfn"] + def _statements() -> Optional[List[str]]: - base = ctx["base"] if base == "MFSimulationBase": def _should_set(var: dict) -> bool: @@ -137,8 +150,8 @@ def _should_set(var: dict) -> bool: stmts = [] refs = {} - for var in ctx["vars"].values(): - ref = var.get("meta", dict()).get("ref", None) + for var in dfn.values(): + ref = var.get("ref", None) if not var.get("kind", None): continue @@ -165,8 +178,8 @@ def _should_set(var: dict) -> bool: stmts = [] refs = {} - for var in ctx["vars"].values(): - ref = var.get("meta", dict()).get("ref", None) + for var in dfn.values(): + ref = var.get("ref", None) if not var.get("kind", None): continue @@ -185,9 +198,7 @@ def _should_set(var: dict) -> bool: elif base == "MFPackage": def _should_build(var: dict) -> bool: - if var.get("meta", dict()).get("ref", None) and ctx[ - "name" - ] != ( + if var.get("ref", None) and ctx["name"] != ( None, "nam", ): @@ -215,9 +226,9 @@ def _should_build(var: dict) -> bool: stmts = [] refs = {} - for var in ctx["vars"].values(): + for var in dfn.values(): name = var["name"] - ref = var.get("meta", dict()).get("ref", None) + ref = var.get("ref", None) if name in kwlist: name = f"{name}_" @@ -235,7 +246,11 @@ def _should_build(var: dict) -> bool: f"self.{'_' if ref else ''}{name} = self.build_mfdata('{_name}', {name if var.get('init_param', True) else 'None'})" ) - if ref and ref["key"] not in refs and ctx["name"].r != "nam": + if ( + ref + and ref["key"] not in refs + and ctx["name"].subcomponent != "nam" + ): refs[ref["key"]] = ref stmts.append( f"self._{ref['key']} = self.build_mfdata('{ref['key']}', None)" @@ -252,26 +267,26 @@ def _should_build(var: dict) -> bool: def _init_skip(ctx: dict) -> List[str]: name = ctx["name"] base = name.base + dfn = ctx["dfn"] + refs = ctx["meta"]["refs"] + if base == "MFSimulationBase": - skip = [ + return [ "tdis6", "models", "exchanges", "mxiter", "solutiongroup", ] - refs = ctx.get("meta", dict()).get("refs", dict()) - return skip elif base == "MFModel": skip = ["packages", "export_netcdf", "nc_filerecord"] - refs = ctx.get("meta", dict()).get("refs", dict()) - if any(refs) and ctx["name"] != (None, "nam"): + if any(refs) and name != (None, "nam"): for key in refs.keys(): - if ctx["vars"].get(key, None): + if dfn.get(key, None): skip.append(key) return skip elif base == "MFPackage": - if name.r == "nam": + if name.subcomponent == "nam": return ["export_netcdf", "nc_filerecord"] elif name == ("utl", "ts"): return ["method", "interpolation_method_single", "sfac"] @@ -281,110 +296,74 @@ def _init_skip(ctx: dict) -> List[str]: def _is_context(o) -> bool: d = dict(o) - return "name" in d and "base" in d + return "name" in d and "dfn" in d def _parent(ctx: dict) -> str: + name = ctx["name"] ref = ctx["meta"].get("ref", None) if ref: return ref["parent"] - name = ctx["name"] - ref = ctx["meta"].get("ref", None) if name == ("sim", "nam"): return None - elif name.l is None or name.r is None or name.l in ["sim", "exg", "sln"]: + elif ( + name.component is None + or name.subcomponent is None + or name.component in ["sim", "exg", "sln"] + ): return "simulation" - elif ref: - if name.l == "utl" and name.r == "hpc": - return "simulation" - return "package" return "model" -def _replace_refs_exg(ctx: dict) -> dict: - refs = ctx.get("meta", dict()).get("refs", dict()) - if any(refs): - for key, ref in refs.items(): - key_var = ctx["vars"].get(key, None) - if not key_var: - continue - ctx["vars"][key] = { - **key_var, - "name": ref["val"], - "description": ref.get("description", None), - "ref": ref, - "default": None, - } - return ctx - +def _replace_refs(ctx: dict, name_param: str = "val") -> dict: + # swap subpkg reference params + name = ctx["name"] + refs = ctx["meta"]["refs"].copy() -def _replace_refs_pkg(ctx: dict) -> dict: - refs = ctx.get("meta", dict()).get("refs", dict()) - if any(refs): + if name == (None, "nam") or not any(refs): + return ctx + + def _try_add_ref(var): for key, ref in refs.items(): - key_var = ctx["vars"].get(key, None) - if not key_var: - continue - ctx["vars"][key] = { - **key_var, - "name": ref["val"], - "description": ref.get("description", None), - "ref": ref, - "default": None, - } - return ctx + key_var = ctx["dfn"].get(var["name"], None) + if key_var: + del refs[key] + return { + **key_var, + "name": ref[name_param], + "description": ref.get("description", None), + "ref": ref, + "default": None, + } + return var + + ctx["dfn"] = {n: _try_add_ref(var) for n, var in ctx["dfn"].items()} - -def _replace_refs_mdl(ctx: dict) -> dict: - refs = ctx.get("meta", dict()).get("refs", dict()) - if any(refs): - for key, ref in refs.items(): - key_var = ctx["vars"].get(key, None) - if not key_var: - continue - ctx["vars"][key] = { - **key_var, - "name": ref["val"], - "description": ref.get("description", None), - "ref": ref, - } return ctx -def _replace_refs_sim(ctx: dict) -> dict: - refs = ctx.get("meta", dict()).get("refs", dict()) - if any(refs) and ctx["name"] != (None, "nam"): - for key, ref in refs.items(): - key_var = ctx["vars"].get(key, None) - if not key_var: - continue - ctx["vars"][key] = { - **key_var, - "name": ref["param"], - "description": ref.get("description", None), - "ref": ref, - "default": None, - } - return ctx +def _rename_vars(ctx: dict) -> dict: + # avoid reserved keyword / name collisions + return { + "dfn": { + name: {"name": f"{name}_" if name in kwlist else name, **var} + for name, var in ctx["dfn"].items() + }, + **ctx, + } def _transform_context(o): ctx = dict(o) - ctx_name = ctx["name"] - ctx_base = ctx_name.base - if ctx_base == "MFSimulationBase": - return _replace_refs_sim(ctx) - elif ctx_base == "MFModel": - return _replace_refs_mdl(ctx) - elif ctx_base == "MFPackage": - if ctx_name.l == "exg": - return _replace_refs_exg(ctx) - else: - return _replace_refs_pkg(ctx) + ctx = _rename_vars(ctx) + ctx = _replace_refs( + ctx, "param" if ctx["name"].base == "MFSimulationBase" else "val" + ) + return ctx SHIM = { - "keep_none": ["default", "block", "metadata"], + "keep_none": ["default"], "quote_str": ["default"], "set_pairs": [ ( diff --git a/flopy/mf6/utils/codegen/templates/exchange.py.jinja b/flopy/mf6/utils/codegen/templates/exchange.py.jinja index 23158aa45..8a88e4465 100644 --- a/flopy/mf6/utils/codegen/templates/exchange.py.jinja +++ b/flopy/mf6/utils/codegen/templates/exchange.py.jinja @@ -8,11 +8,11 @@ from flopy.mf6.mfpackage import MFPackage class Modflow{{ name.title.title() }}(MFPackage): """ - {{ description }} + {{ name.description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(dfn, start_indent=4) }} """ {% for attr in cls_attrs %} @@ -23,10 +23,10 @@ class Modflow{{ name.title.title() }}(MFPackage): self, simulation, loading_package=False, - exgtype="{{ name.r[:3].upper() }}6-{{ name.r[3:].upper() }}6", + exgtype="{{ name.subcomponent[:3].upper() }}6-{{ name.subcomponent[3:].upper() }}6", exgmnamea=None, exgmnameb=None, - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ var.name }}{%- if var.default is defined %}={{ var.default }}{%- endif -%}, {%- endfor %} filename=None, @@ -34,7 +34,7 @@ class Modflow{{ name.title.title() }}(MFPackage): **kwargs, ): """ - {{ description }} + {{ name.description }} simulation : MFSimulation Simulation that this package is a part of. Package is automatically @@ -65,12 +65,12 @@ class Modflow{{ name.title.title() }}(MFPackage): GWE Model with the name exgmnameb must correspond to the GWF Model with the name gwfmodelname2. - {{ macros.vars_docs(vars, start_indent=8) }} + {{ macros.vars_docs(dfn, start_indent=8) }} """ super().__init__( {{ parent }}, - "{{ name.r }}", + "{{ name.subcomponent }}", filename, pname, loading_package, diff --git a/flopy/mf6/utils/codegen/templates/macros.jinja b/flopy/mf6/utils/codegen/templates/macros.jinja index 4d5c6310f..be5b1ab1a 100644 --- a/flopy/mf6/utils/codegen/templates/macros.jinja +++ b/flopy/mf6/utils/codegen/templates/macros.jinja @@ -1,11 +1,11 @@ {% macro vars_docs(vars, start_indent=0) %} {%- for v in vars.values() recursive %} {{ ""|indent(start_indent, first=true) }}{% if loop.depth > 1 %}* {% endif %}{% if v.name|last == "_" %}{{ v.name.replace("_", "\\\_") }}{% else %}{{ v.name }}{% endif %}{% if v.meta is defined and v.meta.type is defined %} : {{ v.meta.type }}{% endif %} - {%- if v.description is defined and v.description is not none %} +{%- if v.description is defined and v.description is not none %} {{ v.description|wordwrap|indent(start_indent + (loop.depth * 4), first=true) }} - {%- endif %} - {%- if v.children is defined and v.children is not none -%} +{%- endif %} +{%- if v.children is defined and v.children is not none -%} {{ loop(v.children.values())|indent(start_indent, first=true) }} - {%- endif %} +{%- endif %} {% endfor -%} {% endmacro %} \ No newline at end of file diff --git a/flopy/mf6/utils/codegen/templates/model.py.jinja b/flopy/mf6/utils/codegen/templates/model.py.jinja index ed32c3749..8f5ea1202 100644 --- a/flopy/mf6/utils/codegen/templates/model.py.jinja +++ b/flopy/mf6/utils/codegen/templates/model.py.jinja @@ -9,11 +9,11 @@ from flopy.mf6.mfmodel import MFModel class Modflow{{ name.title.title() }}(MFModel): """ - {{ description }} + {{ name.description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(dfn, start_indent=4) }} Methods ------- @@ -33,13 +33,13 @@ class Modflow{{ name.title.title() }}(MFModel): version="mf6", exe_name="mf6", model_rel_path=".", - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ var.name }}{%- if var.default is defined %}={{ var.default }}{%- endif -%}, {%- endfor %} **kwargs, ): """ - {{ description }} + {{ name.description }} Parameters ---------- @@ -62,7 +62,7 @@ class Modflow{{ name.title.title() }}(MFModel): Simulation that this model is a part of. Model is automatically added to simulation when it is initialized. - {{ macros.vars_docs(vars, start_indent=8) }} + {{ macros.vars_docs(dfn, start_indent=8) }} """ super().__init__( diff --git a/flopy/mf6/utils/codegen/templates/package.py.jinja b/flopy/mf6/utils/codegen/templates/package.py.jinja index 857a0d416..225edfe6e 100644 --- a/flopy/mf6/utils/codegen/templates/package.py.jinja +++ b/flopy/mf6/utils/codegen/templates/package.py.jinja @@ -9,11 +9,11 @@ from flopy.mf6.mfpackage import MFPackage, MFChildPackages class Modflow{{ name.title.title() }}(MFPackage): """ - {{ description }} + {{ name.description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(dfn, start_indent=4) }} """ {% for attr in cls_attrs %} @@ -24,7 +24,7 @@ class Modflow{{ name.title.title() }}(MFPackage): self, {{ parent }}, loading_package=False, - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ var.name }}{%- if var.default is defined %}={{ var.default }}{%- endif -%}, {%- endfor %} filename=None, @@ -32,7 +32,7 @@ class Modflow{{ name.title.title() }}(MFPackage): **kwargs, ): """ - {{ description }} + {{ name.description }} Parameters ---------- @@ -44,7 +44,7 @@ class Modflow{{ name.title.title() }}(MFPackage): Do not set this parameter. It is intended for debugging and internal processing purposes only. - {{ macros.vars_docs(vars, start_indent=8) }} + {{ macros.vars_docs(dfn, start_indent=8) }} filename : str File name for this package. @@ -60,7 +60,7 @@ class Modflow{{ name.title.title() }}(MFPackage): super().__init__( {{ parent }}, - "{{ name.r }}", + "{{ name.subcomponent }}", filename, pname, loading_package, @@ -73,7 +73,7 @@ class Modflow{{ name.title.title() }}(MFPackage): self._init_complete = True -{% if "ref" in meta and name.r != "hpc" %} +{% if "ref" in meta and name.subcomponent != "hpc" %} class {{ name.title.title() }}Packages(MFChildPackages): """ {{ name.title.title() }}Packages is a container class for the Modflow{{ name.title.title() }} class. @@ -95,7 +95,7 @@ class {{ name.title.title() }}Packages(MFChildPackages): def initialize( self, - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ n }}{%- if var.default is defined %}={{ var.default }}{% endif -%}, {%- endfor %} filename=None, @@ -103,7 +103,7 @@ class {{ name.title.title() }}Packages(MFChildPackages): ): new_package = Modflow{{ name.title.title() }}( self._cpparent, - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ n }}={{ n }}, {%- endfor %} filename=filename, @@ -112,10 +112,10 @@ class {{ name.title.title() }}Packages(MFChildPackages): ) self.init_package(new_package, filename) - {% if name.r != "obs" %} + {% if name.subcomponent != "obs" %} def append_package( self, - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ n }}{%- if var.default is defined %}={{ var.default }}{% endif -%}, {%- endfor %} filename=None, @@ -123,7 +123,7 @@ class {{ name.title.title() }}Packages(MFChildPackages): ): new_package = Modflow{{ name.title.title() }}( self._cpparent, - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ n }}={{ n }}, {%- endfor %} filename=filename, diff --git a/flopy/mf6/utils/codegen/templates/simulation.py.jinja b/flopy/mf6/utils/codegen/templates/simulation.py.jinja index aab32746b..6eea281ee 100644 --- a/flopy/mf6/utils/codegen/templates/simulation.py.jinja +++ b/flopy/mf6/utils/codegen/templates/simulation.py.jinja @@ -8,11 +8,11 @@ from flopy.mf6.mfsimbase import MFSimulationBase class MF{{ name.title.title() }}(MFSimulationBase): """ - {{ description }} + {{ name.description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(dfn, start_indent=4) }} Methods ------- @@ -34,12 +34,12 @@ class MF{{ name.title.title() }}(MFSimulationBase): write_headers: bool = True, use_pandas: bool = True, lazy_io: bool = False, - {%- for n, var in vars.items() if n not in init_skip %} + {%- for n, var in dfn.items() if n not in init_skip %} {{ var.name }}{%- if var.default is defined %}={{ var.default }}{%- endif -%}, {%- endfor %} ): """ - {{ description }} + {{ name.description }} Parameters ---------- @@ -67,7 +67,7 @@ class MF{{ name.title.title() }}(MFSimulationBase): lazy_io Whether to use lazy IO - {{ macros.vars_docs(vars, start_indent=8) }} + {{ macros.vars_docs(dfn, start_indent=8) }} """ super().__init__(