From 17c19f25187e4951472b85034279bf4afb76a95d Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 4 Nov 2024 21:51:08 -0500 Subject: [PATCH] remove shim and renderable, much cleanup --- autotest/test_codegen.py | 3 +- flopy/mf6/utils/codegen/__init__.py | 82 ++- flopy/mf6/utils/codegen/context.py | 67 -- flopy/mf6/utils/codegen/dfn.py | 268 ++++---- flopy/mf6/utils/codegen/dfn2toml.py | 13 +- flopy/mf6/utils/codegen/jinja.py | 619 ++++++++++-------- flopy/mf6/utils/codegen/renderable.py | 129 ---- flopy/mf6/utils/codegen/shim.py | 53 -- .../utils/codegen/templates/__init__.py.jinja | 2 +- .../utils/codegen/templates/exchange.py.jinja | 17 +- .../mf6/utils/codegen/templates/macros.jinja | 14 +- .../utils/codegen/templates/model.py.jinja | 21 +- .../utils/codegen/templates/package.py.jinja | 37 +- .../codegen/templates/simulation.py.jinja | 15 +- flopy/mf6/utils/codegen/utils.py | 3 - 15 files changed, 602 insertions(+), 741 deletions(-) delete mode 100644 flopy/mf6/utils/codegen/renderable.py delete mode 100644 flopy/mf6/utils/codegen/shim.py delete mode 100644 flopy/mf6/utils/codegen/utils.py diff --git a/autotest/test_codegen.py b/autotest/test_codegen.py index 7d5b58df7..34ab9d814 100644 --- a/autotest/test_codegen.py +++ b/autotest/test_codegen.py @@ -4,6 +4,7 @@ from flopy.mf6.utils.codegen import make_all, make_targets from flopy.mf6.utils.codegen.context import Context from flopy.mf6.utils.codegen.dfn import Dfn +from flopy.mf6.utils.codegen.jinja import Filters PROJ_ROOT = get_project_root_path() MF6_PATH = PROJ_ROOT / "flopy" / "mf6" @@ -38,7 +39,7 @@ def test_make_targets(dfn_name, function_tmpdir): make_targets(dfn, function_tmpdir, verbose=True) assert all( - (function_tmpdir / name.target).is_file() + (function_tmpdir / f"mf{Filters.Cls.title(name)}.py").is_file() for name in Context.Name.from_dfn(dfn) ) diff --git a/flopy/mf6/utils/codegen/__init__.py b/flopy/mf6/utils/codegen/__init__.py index 0439f68c0..f90df170f 100644 --- a/flopy/mf6/utils/codegen/__init__.py +++ b/flopy/mf6/utils/codegen/__init__.py @@ -1,61 +1,93 @@ +from dataclasses import asdict +from itertools import chain from os import PathLike from pathlib import Path from flopy.utils import import_optional_dependency -__all__ = ["make_targets", "make_all"] +__all__ = ["make_init", "make_targets", "make_all"] def _get_template_env(): - from flopy.mf6.utils.codegen.jinja import Filters - + # import here instead of module so we don't + # expect optional deps at module init time jinja = import_optional_dependency("jinja2") loader = jinja.PackageLoader("flopy", "mf6/utils/codegen/templates/") env = jinja.Environment(loader=loader) - env.filters["parent"] = Filters.parent - env.filters["prefix"] = Filters.prefix - env.filters["skip"] = Filters.skip + + from flopy.mf6.utils.codegen.jinja import Filters + + env.filters["base"] = Filters.Cls.base + env.filters["title"] = Filters.Cls.title + env.filters["description"] = Filters.Cls.description + env.filters["prefix"] = Filters.Cls.prefix + env.filters["parent"] = Filters.Cls.parent + env.filters["skip"] = Filters.Cls.skip + + env.filters["attrs"] = Filters.Vars.attrs + env.filters["init"] = Filters.Vars.init + + env.filters["type"] = Filters.Var.type + + env.filters["nokw"] = Filters.nokw + env.filters["escape_trailing"] = Filters.escape_trailing + env.filters["value"] = Filters.value + return env def make_init(dfns: dict, outdir: PathLike, verbose: bool = False): """Generate a Python __init__.py file for the given input definitions.""" - from flopy.mf6.utils.codegen.context import Context - env = _get_template_env() outdir = Path(outdir).expanduser() - contexts = [ - c - for cc in [ - [ctx for ctx in Context.from_dfn(dfn)] for dfn in dfns.values() - ] - for c in cc - ] # ugly, but it's the fastest way to flatten the list + + from flopy.mf6.utils.codegen.context import Context + + contexts = list( + chain( + *[[ctx for ctx in Context.from_dfn(dfn)] for dfn in dfns.values()] + ) + ) target_name = "__init__.py" - target = outdir / target_name + target_path = outdir / target_name template = env.get_template(f"{target_name}.jinja") - with open(target, "w") as f: + with open(target_path, "w") as f: f.write(template.render(contexts=contexts)) if verbose: - print(f"Wrote {target}") + print(f"Wrote {target_path}") def make_targets(dfn, outdir: PathLike, verbose: bool = False): """Generate Python source file(s) from the given input definition.""" - from flopy.mf6.utils.codegen.context import Context - env = _get_template_env() outdir = Path(outdir).expanduser() + + from flopy.mf6.utils.codegen.context import Context + from flopy.mf6.utils.codegen.jinja import Filters + + def _get_template_name(ctx_name) -> str: + """The template file to use.""" + base = Filters.Cls.base(ctx_name) + if base == "MFSimulationBase": + return "simulation.py.jinja" + elif base == "MFModel": + return "model.py.jinja" + elif base == "MFPackage": + if ctx_name.l == "exg": + return "exchange.py.jinja" + return "package.py.jinja" + for context in Context.from_dfn(dfn): name = context.name - target = outdir / name.target - template = env.get_template(name.template) - with open(target, "w") as f: - f.write(template.render(**context.render())) + target_path = outdir / f"mf{Filters.Cls.title(name)}.py" + template_name = _get_template_name(name) + template = env.get_template(template_name) + with open(target_path, "w") as f: + f.write(template.render(**asdict(context))) if verbose: - print(f"Wrote {target}") + print(f"Wrote {target_path}") def make_all(dfndir: Path, outdir: Path, verbose: bool = False): diff --git a/flopy/mf6/utils/codegen/context.py b/flopy/mf6/utils/codegen/context.py index 34c320123..0c30cecb0 100644 --- a/flopy/mf6/utils/codegen/context.py +++ b/flopy/mf6/utils/codegen/context.py @@ -9,11 +9,8 @@ ) from flopy.mf6.utils.codegen.dfn import Dfn, Ref, Vars -from flopy.mf6.utils.codegen.renderable import renderable -from flopy.mf6.utils.codegen.shim import SHIM -@renderable(**SHIM) @dataclass class Context: """ @@ -62,70 +59,6 @@ class Name(NamedTuple): l: str r: 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 file name and class name. - """ - l, r = 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 - - @property - def base(self) -> str: - """Base class from which the input context should inherit.""" - _, r = self - if self == ("sim", "nam"): - return "MFSimulationBase" - if r is None: - return "MFModel" - return "MFPackage" - - @property - def target(self) -> str: - """The source file name to generate.""" - return f"mf{self.title}.py" - - @property - def template(self) -> str: - """The template file to use.""" - if self.base == "MFSimulationBase": - return "simulation.py.jinja" - elif self.base == "MFModel": - return "model.py.jinja" - elif self.base == "MFPackage": - if self.l == "exg": - return "exchange.py.jinja" - return "package.py.jinja" - - @property - def description(self) -> str: - """A description of the input context.""" - l, r = self - title = self.title.title() - if self.base == "MFPackage": - return f"Modflow{title} defines a {r.upper()} package." - elif self.base == "MFModel": - return f"Modflow{title} defines a {l.upper()} model." - elif self.base == "MFSimulationBase": - return ( - "MFSimulation is used to load, build, and/or save a MODFLOW 6 simulation." - " A MFSimulation object must be created before creating any of the MODFLOW" - " 6 model objects." - ) - @staticmethod def from_dfn(dfn: Dfn) -> List["Context.Name"]: """ diff --git a/flopy/mf6/utils/codegen/dfn.py b/flopy/mf6/utils/codegen/dfn.py index cf6aac5df..577ab4d91 100644 --- a/flopy/mf6/utils/codegen/dfn.py +++ b/flopy/mf6/utils/codegen/dfn.py @@ -16,9 +16,6 @@ from boltons.dictutils import OMD -from flopy.mf6.utils.codegen.renderable import renderable -from flopy.mf6.utils.codegen.utils import try_literal_eval, try_parse_bool - _SCALARS = { "keyword", "integer", @@ -27,7 +24,7 @@ } -def try_literal_eval(value: str) -> Any: +def _try_literal_eval(value: str) -> Any: """ Try to parse a string as a literal. If this fails, return the value unaltered. @@ -38,7 +35,7 @@ def try_literal_eval(value: str) -> Any: return value -def try_parse_bool(value: Any) -> Any: +def _try_parse_bool(value: Any) -> Any: """ Try to parse a boolean from a string as represented in a DFN file, otherwise return the value unaltered. @@ -58,22 +55,11 @@ def try_parse_bool(value: Any) -> Any: class Var(TypedDict): """MODFLOW 6 input variable specification.""" - class Kind(Enum): - """ - An input variable's kind. This is an enumeration - of the general shapes of data MODFLOW 6 accepts. - """ - - Array = "array" - Scalar = "scalar" - Record = "record" - Union = "union" - List = "list" - name: str - kind: Kind type: Optional[str] = None + shape: Optional[Any] = None block: Optional[str] = None + fkey: Optional["Ref"] = None default: Optional[Any] = None children: Optional[Vars] = None description: Optional[str] = None @@ -107,11 +93,6 @@ def from_dfn(cls, dfn: "Dfn") -> Optional["Ref"]: referenced by other contexts. """ - # TODO: all this won't be necessary once we - # structure DFN format; we can then support - # subpackage references directly instead of - # by making assumptions about `dfn.meta` - if not dfn.meta or "dfn" not in dfn.meta: return None @@ -168,7 +149,6 @@ def _parent(): ) -@renderable class Dfn(UserDict): """ MODFLOW 6 input definition. An input definition @@ -192,6 +172,9 @@ def parse(cls, v: str) -> "Dfn.Name": except: raise ValueError(f"Bad DFN name format: {v}") + def __str__(self) -> str: + return "-".join(self) + name: Optional[Name] meta: Optional[Dict[str, Any]] @@ -208,14 +191,10 @@ def __init__( @staticmethod def _load(f, common: Optional[dict] = None) -> Tuple[OMD, List[str]]: """ - Internal use only. Loads the DFN as an ordered multi-dictionary* and - a list of string metadata. This is later parsed into more structured - form. We also store the original representation for now so it can be - used by the shim. + Internal use only. Loads the DFN as a flat multi-dictionary* with a + list of string metadata, which are then parsed into structured form. - *The point of the OMD is to handle duplicate variable names; the only - case of this right now is 'auxiliary' which can appear in the options - block and again as a keyword in a record in a package data variable. + *The point of this is to losslessly handle duplicate variable names. """ var = dict() @@ -304,11 +283,18 @@ def load( refs: Optional[Dfns] = None, **kwargs, ) -> "Dfn": - """Load an input definition.""" + """ + Load an input definition from a DFN file. + + Notes + ----- + Loads the DFN as a flat multidict with `_load()` + then walks composite variables and builds a tree. + """ - refs = refs or dict() - referenced = dict() flat, meta = Dfn._load(f, **kwargs) + refs = refs or dict() + fkeys = dict() def _map(spec: Dict[str, Any]) -> Var: """ @@ -317,16 +303,11 @@ def _map(spec: Dict[str, Any]) -> Var: Notes ----- - This involves expanding nested type hierarchies, mapping - types to roughly equivalent Python primitives/composites. - The composite inflation step will not be necessary after - DFNs move to a structured format. - If a variable does not have a `default` attribute, it will default to `False` if it is a keyword, otherwise to `None`. - Any filepath variable whose name functions as a foreign key - for another context will be given a pointer to the context. + A filepath variable whose name functions as a foreign key + for a separate context will be given a reference to it. """ @@ -334,7 +315,7 @@ def _map(spec: Dict[str, Any]) -> Var: # stay a string except default values, which we'll # try to parse as arbitrary literals below, and at # some point types, once we introduce type hinting - spec = {k: try_parse_bool(v) for k, v in spec.items()} + spec = {k: _try_parse_bool(v) for k, v in spec.items()} _name = spec["name"] _type = spec.get("type", None) @@ -342,18 +323,29 @@ def _map(spec: Dict[str, Any]) -> Var: shape = spec.get("shape", None) shape = None if shape == "" else shape default = spec.get("default", None) + default = ( + _try_literal_eval(default) if _type != "string" else default + ) description = spec.get("description", "") + tagged = spec.get("tagged", False) children = dict() - # if var is a foreign key, register the reference - ref = refs.get(_name, None) - if ref: - referenced[_name] = ref + # if var is a foreign key, register it + fkey = refs.get(_name, None) + if fkey: + fkeys[_name] = fkey + + def _choices() -> Vars: + """Load a union's children (choices).""" + names = _type.split()[1:] + return { + v["name"]: _map(v) + for v in flat.values(multi=True) + if v["name"] in names and v.get("in_record", False) + } - def _fields(record_name: str) -> Vars: - """Recursively load/convert a record's fields.""" - record = next(iter(flat.getlist(record_name)), None) - assert record + def _fields() -> Vars: + """Load a record's children (fields).""" names = _type.split()[1:] fields = { v["name"]: _map(v) @@ -375,14 +367,8 @@ def _fields(record_name: str) -> Vars: if keyword: fields.pop(keyword) - # set the type - n = list(fields.keys())[0] - path_field = fields[n] - path_field["kind"] = Var.Kind.Scalar - fields[n] = path_field - # if tagged, remove the leading keyword - elif record.get("tagged", False): + elif tagged: keyword = next(iter(fields), None) if keyword: fields.pop(keyword) @@ -391,52 +377,48 @@ def _fields(record_name: str) -> Vars: # list, child is the item type if _type.startswith("recarray"): - # make sure columns are defined names = _type.split()[1:] + types = [ + v["type"] + for v in flat.values(multi=True) + if v["name"] in names and v.get("in_record", False) + ] + n_names = len(names) if n_names < 1: raise ValueError(f"Missing recarray definition: {_type}") - # list input can have records or unions as rows. - # lists which have a consistent record type are - # regular, inconsistent record types irregular. - - # regular tabular/columnar data (1 record type) can be - # defined with a nested record (i.e. explicit) or with - # fields directly inside the recarray (implicit). list - # data for unions/keystrings necessarily comes nested. - - is_explicit_record = n_names == 1 and flat[names[0]][ - "type" - ].startswith("record") - - def _is_implicit_scalar_record(): - # if the record is defined implicitly and it has - # only scalar fields - types = [ - v["type"] - for v in flat.values(multi=True) - if v["name"] in names and v.get("in_record", False) - ] - return all(t in _SCALARS for t in types) - - if is_explicit_record: - record = next(iter(flat.getlist(names[0])), None) - children = {names[0]: _map(record)} - kind = Var.Kind.List - elif _is_implicit_scalar_record(): - fields = _fields(_name) + # list input can have records or unions as rows. lists + # that have a consistent item type can be considered + # tabular. lists that can possess multiple item types + # (unions) are considered irregular. regular lists can + # be defined with a nested record (explicit) or with a + # set of fields directly in the recarray (implicit). an + # irregular list is always defined with a nested union. + is_explicit_composite = n_names == 1 and ( + types[0].startswith("record") + or types[0].startswith("keystring") + ) + is_implicit_scalar_record = all(t in _SCALARS for t in types) + + if is_explicit_composite: + child = next(iter(flat.getlist(names[0]))) + children = {names[0]: _map(child)} + _type = "list" + elif is_implicit_scalar_record: + fields = _fields() children = { _name: Var( name=_name, - kind=Var.Kind.Record, + type="record", block=block, children=fields, - description=description, - type=f"[{', '.join([f['type'] for f in fields.values()])}]", + description=description.replace( + "is the list of", "is the record of" + ), ) } - kind = Var.Kind.List + _type = "list" else: # implicit complex record (i.e. some fields are records or unions) fields = { @@ -447,75 +429,67 @@ def _is_implicit_scalar_record(): first = list(fields.values())[0] single = len(fields) == 1 name_ = first["name"] if single else _name + child_type = ( + "union" + if single and "keystring" in first["type"] + else "record" + ) children = { name_: Var( name=name_, - kind=Var.Kind.Record, + type=child_type, block=block, children=first["children"] if single else fields, - description=description, - type=f"[{', '.join([v['type'] for v in fields.values()])}]", + description=description.replace( + "is the list of", f"is the {child_type} of" + ), ) } - kind = Var.Kind.List - type_ = ( - f"[{', '.join([v['name'] for v in children.values()])}]" - ) + _type = "list" - # union (product), children are choices + # union (product) type elif _type.startswith("keystring"): - names = _type.split()[1:] - children = { - v["name"]: _map(v) - for v in flat.values(multi=True) - 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()])}]" - ) + children = _choices() + _type = "union" - # record (sum), children are fields + # record (sum) type elif _type.startswith("record"): - children = _fields(_name) - kind = Var.Kind.Record - type_ = ( - f"[{', '.join([v['type'] for v in children.values()])}]" - ) + children = _fields() + _type = "record" - # at this point, if it has a shape, it's an array + # at this point, if it has a shape, it's an array. check its type elif shape is not None: if _type not in _SCALARS: raise TypeError(f"Unsupported array type: {_type}") - elif _type == "string": - kind = Var.Kind.List - else: - kind = Var.Kind.Array - type_ = f"[{_type}]" - - # finally scalars - else: - kind = Var.Kind.Scalar - type_ = _type - # create var + # if the var is a foreign key, swap in the referenced variable + ref = refs.get(_name, None) + if not ref: + return Var( + name=_name, + type=_type, + shape=shape, + block=block, + description=description, + default=default, + children=children, + meta={"ref": fkey}, + ) return Var( - # if name is a reserved keyword, add a trailing underscore to it. - # convert dashes to underscores since it may become a class attr. - name=(f"{_name}_" if _name in kwlist else _name).replace( - "-", "_" - ), - kind=kind, - type=type_, + name=ref["param" if name == ("sim", "nam") else "val"], + type=_type, + shape=shape, block=block, - description=description, - default=( - try_literal_eval(default) if _type != "string" else default + description=( + f"* Contains data for the {ref['abbr']} package. Data can be " + f"stored in a dictionary containing data for the {ref['abbr']} " + "package with variable names as keys and package data as " + f"values. Data just for the {ref['val']} variable is also " + f"acceptable. See {ref['abbr']} package documentation for more " + "information" ), - children=children, - # type is a string for now, when - # introducing type hints make it - # a proper type... + default=None, + children=None, meta={"ref": ref}, ) @@ -526,29 +500,25 @@ def _is_implicit_scalar_record(): # convert input variable specs to # structured form, descending into # composites recursively as needed - flat = { + vars_ = { 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, + vars_, name, { "dfn": (_vars, meta), - "refs": referenced, + "refs": fkeys, }, ) @staticmethod def load_all(dfndir: PathLike) -> Dict[str, "Dfn"]: """Load all input definitions from the given directory.""" + # find definition files paths = [ p diff --git a/flopy/mf6/utils/codegen/dfn2toml.py b/flopy/mf6/utils/codegen/dfn2toml.py index 771be3509..198e61d9d 100644 --- a/flopy/mf6/utils/codegen/dfn2toml.py +++ b/flopy/mf6/utils/codegen/dfn2toml.py @@ -1,4 +1,5 @@ import argparse +from collections.abc import Mapping from pathlib import Path from flopy.utils import import_optional_dependency @@ -8,6 +9,14 @@ _TOML_PATH = _MF6_PATH / "data" / "toml" +def _drop_none(d: dict) -> dict: + return ( + {k: _drop_none(v) for k, v in d.items() if v is not None} + if isinstance(d, Mapping) + else d + ) + + if __name__ == "__main__": """Convert DFN files to TOML.""" @@ -31,5 +40,5 @@ outdir = Path(args.outdir) outdir.mkdir(exist_ok=True, parents=True) for dfn in Dfn.load_all(dfndir).values(): - with open(Path(outdir) / f"{'-'.join(dfn.name)}.toml", "w") as f: - tomlkit.dump(dfn.render(), f) + with open(Path(outdir) / f"{dfn.name}.toml", "w") as f: + tomlkit.dump(_drop_none(dfn), f) diff --git a/flopy/mf6/utils/codegen/jinja.py b/flopy/mf6/utils/codegen/jinja.py index a6ac4ad36..cb0b3f764 100644 --- a/flopy/mf6/utils/codegen/jinja.py +++ b/flopy/mf6/utils/codegen/jinja.py @@ -5,6 +5,8 @@ from jinja2 import pass_context +from flopy.mf6.utils.codegen.dfn import _SCALARS + def try_get_enum_value(v: Any) -> Any: """ @@ -15,38 +17,178 @@ def try_get_enum_value(v: Any) -> Any: class Filters: - @pass_context - def cls_attrs(ctx, ctx_name) -> List[str]: - def _attr(var: dict) -> Optional[str]: - var_name = var["name"] - 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) - - 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 ( - var_name != "packages" - and (ctx_name.l is not None and ctx_name.r == "nam") + class Cls: + def base(ctx_name) -> str: + """Base class from which the input context should inherit.""" + _, r = ctx_name + if ctx_name == ("sim", "nam"): + return "MFSimulationBase" + if r is None: + return "MFModel" + return "MFPackage" + + def title(ctx_name) -> 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 file name and class name. + """ + l, r = ctx_name + if (l, r) == ("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 + + def description(ctx_name) -> str: + """A description of the input context.""" + l, r = ctx_name + base = Filters.Cls.base(ctx_name) + title = Filters.Cls.title(ctx_name).title() + if base == "MFPackage": + return f"Modflow{title} defines a {r.upper()} package." + elif base == "MFModel": + return f"Modflow{title} defines a {l.upper()} model." + elif base == "MFSimulationBase": + return ( + "MFSimulation is used to load, build, and/or save a MODFLOW 6 simulation." + " A MFSimulation object must be created before creating any of the MODFLOW" + " 6 model objects." ) - ): - return None - if var_kind in ["list", "record", "union", "array"]: - if not var_block: - raise ValueError("Need block") + def prefix(ctx_name) -> str: + base = Filters.Cls.base(ctx_name) + return "MF" if base == "MFSimulationBase" else "Modflow" - if var_kind != "array": - if var_ref: - # if the variable is a subpackage reference, use the original key - # (which has been replaced already with the referenced variable) + @pass_context + def parent(ctx, ctx_name) -> str: + ref = ctx["meta"].get("ref", None) + if ref: + return ref["parent"] + if ctx_name == ("sim", "nam"): + return None + elif ( + ctx_name.l is None + or ctx_name.r is None + or ctx_name.l in ["sim", "exg", "sln"] + ): + return "simulation" + elif ref: + if ctx_name.l == "utl" and ctx_name.r == "hpc": + return "simulation" + return "package" + return "model" + + @pass_context + def skip(ctx, ctx_name) -> List[str]: + base = Filters.Cls.base(ctx_name) + if base == "MFSimulationBase": + return [ + "tdis6", + "models", + "exchanges", + "mxiter", + "solutiongroup", + ] + 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"): + for k in refs.keys(): + if ctx["vars"].get(k, None): + skip.append(k) + return skip + else: + if ctx_name.r == "nam": + return ["export_netcdf", "nc_filerecord"] + elif ctx_name == ("utl", "ts"): + return ["method", "interpolation_method_single", "sfac"] + return [] + + class Var: + def type(var: dict) -> str: + _type = var["type"] + shape = var.get("shape", None) + children = var.get("children", None) + if children: + if _type == "list": + children = ", ".join( + [v["name"] for v in children.values()] + ) + return f"[{children}]" + elif _type == "record": + children = ", ".join( + [v["name"] for v in children.values()] + ) + return f"({children})" + elif _type == "union": + return " | ".join([v["name"] for v in children.values()]) + if shape: + return f"[{_type}]" + return var["type"] + + class Vars: + @pass_context + def attrs(ctx, vars) -> List[str]: + ctx_name = ctx["name"] + base = Filters.Cls.base(ctx_name) + + def _attr(var: dict) -> Optional[str]: + var_name = var["name"] + var_type = var["type"] + var_shape = var.get("shape", None) + var_block = var.get("block", None) + var_ref = var.get("meta", dict()).get("ref", None) + + if ( + (var_type in _SCALARS and not var_shape) + or var_name in ["cvoptions", "output"] + or (ctx_name.r == "dis" and var_name == "packagedata") + or ( + var_name != "packages" + and (ctx_name.l is not None and ctx_name.r == "nam") + ) + ): + return None + + is_array = ( + var_type in ["integer", "double precision"] and var_shape + ) + is_composite = var_type in ["list", "record", "union"] + if is_array or is_composite: + if not var_block: + raise ValueError("Need block") + + if not is_array: + if var_ref: + # 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"'{var_block}'", + f"'{var_ref['key']}'", + ] + if ctx_name.l is not None and ctx_name.l not in [ + "sim", + "sln", + "utl", + "exg", + ]: + args.insert(0, f"'{ctx_name.l}6'") + return f"{var_ref['key']} = ListTemplateGenerator(({', '.join(args)}))" + + def _args(): args = [ f"'{ctx_name.r}'", f"'{var_block}'", - f"'{var_ref['key']}'", + f"'{var_name}'", ] if ctx_name.l is not None and ctx_name.l not in [ "sim", @@ -55,255 +197,210 @@ def _attr(var: dict) -> Optional[str]: "exg", ]: args.insert(0, f"'{ctx_name.l}6'") - return f"{var_ref['key']} = ListTemplateGenerator(({', '.join(args)}))" + return 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 [ - "sim", - "sln", - "utl", - "exg", - ]: - args.insert(0, f"'{ctx_name.l}6'") - return args - - kind = var_kind if var_kind == "array" else "list" - return f"{var_name} = {kind.title()}TemplateGenerator(({', '.join(_args())}))" - - return None - - def _dfn() -> List[List[str]]: - dfn, meta = ctx["meta"]["dfn"] - - def _meta(): - exclude = ["subpackage", "parent_name_type"] - return [v for v in meta if not any(p in v for p in exclude)] - - def _dfn(): - def _var(var: dict) -> List[str]: - exclude = ["longname", "description"] - name = var["name"] - var_ = ctx["vars"].get(name, None) - keys = [ - "construct_package", - "construct_data", - "parameter_name", - ] - if var_ and keys[0] in var_: - for k in keys: - var[k] = var_[k] + kind = "array" if is_array else "list" + return f"{var_name} = {kind.title()}TemplateGenerator(({', '.join(_args())}))" + + return None + + def _dfn() -> List[List[str]]: + dfn, meta = ctx["meta"]["dfn"] + + def _meta(): + exclude = ["subpackage", "parent_name_type"] return [ - " ".join([k, v]).strip() - for k, v in var.items() - if k not in exclude + v for v in meta if not any(p in v for p in exclude) ] - return [_var(var) for var in dfn] - - return [["header"] + _meta()] + _dfn() - - attrs = list(filter(None, [_attr(v) for v in ctx["vars"].values()])) - - if ctx_name.base == "MFModel": - attrs.append(f"model_type = {ctx_name.l}") - elif ctx_name.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"dfn = {pformat(_dfn(), indent=10)}", - ] - ) + def _dfn(): + def _var(var: dict) -> List[str]: + exclude = ["longname", "description"] + name = var["name"] + var_ = vars.get(name, None) + keys = [ + "construct_package", + "construct_data", + "parameter_name", + ] + if var_ and keys[0] in var_: + for k in keys: + var[k] = var_[k] + return [ + " ".join([k, v]).strip() + for k, v in var.items() + if k not in exclude + ] - return attrs + return [_var(var) for var in dfn] - @pass_context - def init_body(ctx, ctx_name) -> List[str]: - def _statements() -> Optional[List[str]]: - base = ctx_name.base - if base == "MFSimulationBase": + return [["header"] + _meta()] + _dfn() + + attrs = list(filter(None, [_attr(v) for v in vars.values()])) - def _should_set(var: dict) -> bool: - return var["name"] not in [ - "tdis6", - "models", - "exchanges", - "mxiter", - "solutiongroup", - "hpc_data", + if base == "MFModel": + attrs.append(f"model_type = {ctx_name.l}") + 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"dfn = {pformat(_dfn(), indent=10)}", ] + ) - stmts = [] - refs = {} - for var in ctx["vars"].values(): - ref = var.get("meta", dict()).get("ref", None) - if not var.get("kind", None): - continue - - name = var["name"] - if name in kwlist: - name = f"{name}_" - - if _should_set(var): - stmts.append(f"self.name_file.{name}.set_data({name})") - stmts.append(f"self.{name} = self.name_file.{name}") - if ref and ref["key"] not in refs: - refs[ref["key"]] = ref - stmts.append( - f"self.{ref['param']} = self._create_package('{ref['abbr']}', {ref['param']})" - ) - elif base == "MFModel": + return attrs - def _should_set(var: dict) -> bool: - return var["name"] not in [ - "export_netcdf", - "nc_filerecord", - "packages", - ] + @pass_context + def init(ctx, vars) -> List[str]: + ctx_name = ctx["name"] + base = Filters.Cls.base(ctx_name) - stmts = [] - refs = {} - for var in ctx["vars"].values(): - ref = var.get("meta", dict()).get("ref", None) - if not var.get("kind", None): - continue - - name = var["name"] - if name in kwlist: - name = f"{name}_" - - if _should_set(var): - stmts.append(f"self.name_file.{name}.set_data({name})") - stmts.append(f"self.{name} = self.name_file.{name}") - if ref and ref["key"] not in refs: - refs[ref["key"]] = ref - stmts.append( - f"self.{ref['param']} = self._create_package('{ref['abbr']}', {ref['param']})" - ) - elif base == "MFPackage": + def _statements() -> Optional[List[str]]: + if base == "MFSimulationBase": + + def _should_set(var: dict) -> bool: + return var["name"] not in [ + "tdis6", + "models", + "exchanges", + "mxiter", + "solutiongroup", + "hpc_data", + ] + + stmts = [] + refs = {} + for var in vars.values(): + ref = var.get("meta", dict()).get("ref", None) + name = var["name"] + if name in kwlist: + name = f"{name}_" - def _should_build(var: dict) -> bool: - if var.get("meta", dict()).get("ref", None) and ctx[ - "name" - ] != ( - None, - "nam", - ): - return False - name = var["name"] - if name in [ - "simulation", - "model", - "package", - "parent_model", - "parent_package", - "parent_model_or_package", - "parent_file", - "modelname", - "model_nam_file", - "export_netcdf", - "nc_filerecord", - "method", - "interpolation_method_single", - "sfac", - "output", - ]: - return False - return True - - stmts = [] - refs = {} - for var in ctx["vars"].values(): - name = var["name"] - ref = var.get("meta", dict()).get("ref", None) - if name in kwlist: - name = f"{name}_" - - if _should_build(var): - if ref and ctx["name"] == (None, "nam"): + if _should_set(var): stmts.append( - f"self.{'_' if ref else ''}{ref['key']} = self.build_mfdata('{ref['key']}', None)" + f"self.name_file.{name}.set_data({name})" ) - else: - # hack... - _name = name[:-1] if name.endswith("_") else name - if _name == "steady_state": - _name = "steady-state" stmts.append( - f"self.{'_' if ref else ''}{name} = self.build_mfdata('{_name}', {name if var.get('init_param', True) else 'None'})" + f"self.{name} = self.name_file.{name}" ) + if ref and ref["key"] not in refs: + refs[ref["key"]] = ref + stmts.append( + f"self.{ref['param']} = self._create_package('{ref['abbr']}', {ref['param']})" + ) + elif base == "MFModel": - if ( - ref - and ref["key"] not in refs - and ctx["name"].r != "nam" - ): - refs[ref["key"]] = ref - stmts.append( - f"self._{ref['key']} = self.build_mfdata('{ref['key']}', None)" - ) - stmts.append( - f"self._{ref['abbr']}_package = self.build_child_package('{ref['abbr']}', {ref['val']}, '{ref['param']}', self._{ref['key']})" - ) - - return stmts - - return list(filter(None, _statements())) - - @pass_context - def parent(ctx, ctx_name): - ref = ctx["meta"].get("ref", None) - if ref: - return ref["parent"] - if ctx_name == ("sim", "nam"): - return None - elif ( - ctx_name.l is None - or ctx_name.r is None - or ctx_name.l in ["sim", "exg", "sln"] - ): - return "simulation" - elif ref: - if ctx_name.l == "utl" and ctx_name.r == "hpc": - return "simulation" - return "package" - return "model" - - def prefix(ctx_name): - return "MF" if ctx_name.base == "MFSimulationBase" else "Modflow" - - @pass_context - def skip(ctx, ctx_name): - base = ctx_name.base - if base == "MFSimulationBase": - return [ - "tdis6", - "models", - "exchanges", - "mxiter", - "solutiongroup", - ] - 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"): - for k in refs.keys(): - if ctx["vars"].get(k, None): - skip.append(k) - return skip - else: - if ctx_name.r == "nam": - return ["export_netcdf", "nc_filerecord"] - elif ctx_name == ("utl", "ts"): - return ["method", "interpolation_method_single", "sfac"] - return [] + def _should_set(var: dict) -> bool: + return var["name"] not in [ + "export_netcdf", + "nc_filerecord", + "packages", + ] + + stmts = [] + refs = {} + for var in vars.values(): + ref = var.get("meta", dict()).get("ref", None) + name = var["name"] + if name in kwlist: + name = f"{name}_" + + if _should_set(var): + stmts.append( + f"self.name_file.{name}.set_data({name})" + ) + stmts.append( + f"self.{name} = self.name_file.{name}" + ) + if ref and ref["key"] not in refs: + refs[ref["key"]] = ref + stmts.append( + f"self.{ref['param']} = self._create_package('{ref['abbr']}', {ref['param']})" + ) + elif base == "MFPackage": + + def _should_build(var: dict) -> bool: + if var.get("meta", dict()).get( + "ref", None + ) and ctx_name != ( + None, + "nam", + ): + return False + name = var["name"] + if name in [ + "simulation", + "model", + "package", + "parent_model", + "parent_package", + "parent_model_or_package", + "parent_file", + "modelname", + "model_nam_file", + "export_netcdf", + "nc_filerecord", + "method", + "interpolation_method_single", + "sfac", + "output", + ]: + return False + return True + + stmts = [] + refs = {} + for var in vars.values(): + name = var["name"] + ref = var.get("meta", dict()).get("ref", None) + if name in kwlist: + name = f"{name}_" + + if _should_build(var): + if ref and ctx["name"] == (None, "nam"): + stmts.append( + f"self.{'_' if ref else ''}{ref['key']} = self.build_mfdata('{ref['key']}', None)" + ) + else: + _name = ( + name[:-1] if name.endswith("_") else name + ) + name = name.replace("-", "_") + stmts.append( + f"self.{'_' if ref else ''}{name} = self.build_mfdata('{_name}', {name})" + ) + + if ( + ref + and ref["key"] not in refs + and ctx["name"].r != "nam" + ): + refs[ref["key"]] = ref + stmts.append( + f"self._{ref['key']} = self.build_mfdata('{ref['key']}', None)" + ) + stmts.append( + f"self._{ref['abbr']}_package = self.build_child_package('{ref['abbr']}', {ref['val']}, '{ref['param']}', self._{ref['key']})" + ) + + return stmts + + return list(filter(None, _statements())) + + def nokw(v: str) -> str: + return (f"{v}_" if v in kwlist else v).replace("-", "_") + + def escape_trailing(v: str) -> str: + return f"{v[:-1]}\\\\_" if v.endswith("_") else v + + def value(v: Any) -> str: + v = try_get_enum_value(v) + if isinstance(v, str) and v[0] not in ["'", '"']: + v = f"'{v}'" + return v diff --git a/flopy/mf6/utils/codegen/renderable.py b/flopy/mf6/utils/codegen/renderable.py deleted file mode 100644 index 6e1384fd1..000000000 --- a/flopy/mf6/utils/codegen/renderable.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -This module contains a decorator intended to -allow modifying dataclass instances to make -them more palatable for templates. It also -keeps implementation details incidental to -the current design of MF6 input framework -cleanly isolated from the reimplementation -of which this code is a part, which aims -for a more general approach. -""" - -from dataclasses import asdict, is_dataclass -from typing import Any, Callable, Dict, Iterable, Optional, Tuple - -from flopy.mf6.utils.codegen.utils import try_get_enum_value - -Predicate = Callable[[Any], bool] -Transform = Callable[[Any], Dict[str, str]] -Pair = Tuple[str, Any] -Pairs = Iterable[Pair] - - -def renderable( - maybe_cls=None, - *, - keep_none: Optional[Iterable[str]] = None, - drop_keys: Optional[Iterable[str]] = None, - quote_str: Optional[Iterable[str]] = None, - transform: Optional[Iterable[Tuple[Predicate, Transform]]] = None, -): - """ - Decorator for dataclasses which are meant - to be passed into a Jinja template. The - decorator adds a `.render()` method to - the decorated class, which recursively - converts the instance to a dictionary - with (by default) the `asdict()` builtin - `dataclasses` module function, plus a - few modifications to make the instance - easier to work with from the template. - - By default, attributes with value `None` - are dropped before conversion to a `dict`. - To specify that a given attribute should - remain even with a `None` value, use the - `keep_none` parameter. - - When a string value is to become the RHS - of an assignment or an argument-passing - expression, it needs to be wrapped with - quotation marks before insertion into - the template. To indicate an attribute's - value should be wrapped with quotation - marks, use the `quote_str` parameter. - - Arbitrary transformations can be configured - via the `transform` parameter, which accepts - an iterable of predicate / function tuples. - Each of these specifies a condition in which - an instance of a context should be modified, - and a function to make the alteration. - - Notes - ----- - Because a transformation function accepts an - instance of a dataclass and converts it to a - dictionary, only one transformation function - (of the first matching predicate) is applied. - - This was inspired by `attrs` class decorators. - """ - - quote_str = quote_str or list() - keep_none = keep_none or list() - drop_keys = drop_keys or list() - transform = transform or list() - - def __renderable(cls): - def _render(d: dict) -> dict: - def _render_val(k, v): - v = try_get_enum_value(v) - if ( - k in quote_str - and isinstance(v, str) - and v[0] not in ["'", '"'] - ): - v = f"'{v}'" - elif isinstance(v, dict): - v = _render(v) - return v - - def _keep(k, v): - return k in keep_none or (v and not isinstance(v, bool)) - - def _drop(k, v): - return k in drop_keys - - return { - k: _render_val(k, v) - for k, v in d.items() - if (_keep(k, v) and not _drop(k, v)) - } - - def _dict(o): - d = dict(o) - for p, t in transform: - if p(o): - d = t(o) - break - return d - - def _dict_factory(o): - return _render(_dict(o)) - - def render(self) -> dict: - """Recursively render the dataclass instance.""" - return _render( - asdict(self, dict_factory=_dict_factory) - if is_dataclass(self) - else self - ) - - setattr(cls, "render", render) - return cls - - # first arg value depends on the decorator usage: - # class if `@renderable`, `None` if `@renderable()`. - # referenced from https://github.com/python-attrs/attrs/blob/a59c5d7292228dfec5480388b5f6a14ecdf0626c/src/attr/_next_gen.py#L405C4-L406C65 - return __renderable if maybe_cls is None else __renderable(maybe_cls) diff --git a/flopy/mf6/utils/codegen/shim.py b/flopy/mf6/utils/codegen/shim.py deleted file mode 100644 index f94c55576..000000000 --- a/flopy/mf6/utils/codegen/shim.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -The purpose of this module is to keep special handling -necessary to support the current `flopy.mf6` generated -classes separate from more general templating and code -generation infrastructure. -""" - - -def _is_context(o) -> bool: - d = dict(o) - return "name" in d and "vars" in d - - -def _replace_refs(ctx: dict, name_param: str = "val") -> 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[name_param], - "description": ( - f"* Contains data for the {ref['abbr']} package. Data can be " - f"stored in a dictionary containing data for the {ref['abbr']} " - "package with variable names as keys and package data as " - f"values. Data just for the {ref['val']} variable is also " - f"acceptable. See {ref['abbr']} package documentation for more " - "information" - ), - "ref": ref, - "default": None, - "children": None, - } - return ctx - - -def _transform_context(o): - ctx = dict(o) - ctx_name = ctx["name"] - ctx_base = ctx_name.base - if ctx_base == "MFSimulationBase": - return _replace_refs(ctx, name_param="param") - else: - return _replace_refs(ctx) - - -SHIM = { - "keep_none": ["default", "vars"], - "quote_str": ["default"], - "transform": [(_is_context, _transform_context)], -} diff --git a/flopy/mf6/utils/codegen/templates/__init__.py.jinja b/flopy/mf6/utils/codegen/templates/__init__.py.jinja index 1db6d034d..2eb7d436c 100644 --- a/flopy/mf6/utils/codegen/templates/__init__.py.jinja +++ b/flopy/mf6/utils/codegen/templates/__init__.py.jinja @@ -1,3 +1,3 @@ {% for ctx in contexts %} -from .mf{{ ctx.name.title }} import {{ ctx.name|prefix }}{{ ctx.name.title.title() }} +from .mf{{ ctx.name|title }} import {{ ctx.name|prefix }}{{ (ctx.name|title).title() }} {% endfor %} \ No newline at end of file diff --git a/flopy/mf6/utils/codegen/templates/exchange.py.jinja b/flopy/mf6/utils/codegen/templates/exchange.py.jinja index fb52a2d49..bf052f1a1 100644 --- a/flopy/mf6/utils/codegen/templates/exchange.py.jinja +++ b/flopy/mf6/utils/codegen/templates/exchange.py.jinja @@ -1,4 +1,5 @@ {% import 'macros.jinja' as macros %} +{% set title = (name|title).title() %} from os import PathLike, curdir from typing import Union @@ -6,16 +7,16 @@ from typing import Union from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator from flopy.mf6.mfpackage import MFPackage -class Modflow{{ name.title.title() }}(MFPackage): +class Modflow{{ title }}(MFPackage): """ - {{ name.description }} + {{ name|description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(vars, indent=4) }} """ - {% for attr in cls_attrs %} + {% for attr in vars|attrs %} {{ attr }} {%- endfor %} @@ -26,13 +27,13 @@ class Modflow{{ name.title.title() }}(MFPackage): exgtype="{{ name.r[:3].upper() }}6-{{ name.r[3:].upper() }}6", exgmnamea=None, exgmnameb=None, - {{ macros.init_vars(vars, skip=name|skip) }} + {{ macros.init_vars(vars, indent=8, skip=name|skip) }} filename=None, pname=None, **kwargs, ): """ - {{ name.description }} + {{ name|description }} simulation : MFSimulation Simulation that this package is a part of. Package is automatically @@ -63,7 +64,7 @@ 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(vars, indent=8) }} """ super().__init__( @@ -80,7 +81,7 @@ class Modflow{{ name.title.title() }}(MFPackage): self.exgmnameb = exgmnameb simulation.register_exchange_file(self) - {% for statement in init_body %} + {% for statement in vars|init %} {{ statement }} {%- endfor %} diff --git a/flopy/mf6/utils/codegen/templates/macros.jinja b/flopy/mf6/utils/codegen/templates/macros.jinja index 3befc515b..ee7a038c9 100644 --- a/flopy/mf6/utils/codegen/templates/macros.jinja +++ b/flopy/mf6/utils/codegen/templates/macros.jinja @@ -1,17 +1,17 @@ -{% macro init_vars(vars, alias=false, skip=none) %} -{%- for n, var in vars.items() if n not in skip %} -{% if alias %}{{ n }}{% else %}{{ var.name }}{% endif %}{%- if var.default is defined %}={{ var.default }}{%- endif -%}, +{% macro init_vars(vars, alias=false, indent=0, skip=none) %} +{%- for n, v in vars.items() if n not in skip %} +{{ ""|indent(indent, first=true) }}{% if alias %}{{ n|nokw }}{% else %}{{ v.name|nokw }}{% endif %}{%- if v.default is defined %}={{ v.default|value }}{%- endif -%}, {%- endfor %} {% endmacro %} -{% macro vars_docs(vars, start_indent=0) %} +{% macro vars_docs(vars, 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.type is defined %} : {{ v.type }}{% endif %} +{{ ""|indent(indent, first=true) }}{% if loop.depth > 1 %}* {% endif %}{{ v.name|nokw|escape_trailing }} : {{ v|type }} {%- if v.description is defined and v.description is not none %} -{{ v.description|wordwrap|indent(start_indent + (loop.depth * 4), first=true) }} +{{ v.description|wordwrap|indent(indent + (loop.depth * 4), first=true) }} {%- endif %} {%- if v.children is defined and v.children is not none -%} -{{ loop(v.children.values())|indent(start_indent, first=true) }} +{{ loop(v.children.values())|indent(indent, first=true) }} {%- 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 079485deb..651dcf6a6 100644 --- a/flopy/mf6/utils/codegen/templates/model.py.jinja +++ b/flopy/mf6/utils/codegen/templates/model.py.jinja @@ -1,4 +1,5 @@ {% import 'macros.jinja' as macros %} +{% set title = (name|title).title() %} from os import PathLike, curdir from typing import Union @@ -7,13 +8,13 @@ from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenera from flopy.mf6.mfmodel import MFModel -class Modflow{{ name.title.title() }}(MFModel): +class Modflow{{ title }}(MFModel): """ - {{ name.description }} + {{ name|description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(vars, indent=4) }} Methods ------- @@ -23,7 +24,7 @@ class Modflow{{ name.title.title() }}(MFModel): a class method that loads a model from files """ - model_type = "{{ name.title }}" + model_type = "{{ title.lower() }}" def __init__( self, @@ -33,11 +34,11 @@ class Modflow{{ name.title.title() }}(MFModel): version="mf6", exe_name="mf6", model_rel_path=".", - {{ macros.init_vars(vars, skip=name|skip) }} + {{ macros.init_vars(vars, indent=8, skip=name|skip) }} **kwargs, ): """ - {{ name.description }} + {{ name|description }} Parameters ---------- @@ -60,12 +61,12 @@ 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(vars, indent=8) }} """ super().__init__( simulation, - model_type="{{ name.title }}6", + model_type="{{ title.lower() }}6", modelname=modelname, model_nam_file=model_nam_file, version=version, @@ -74,7 +75,7 @@ class Modflow{{ name.title.title() }}(MFModel): **kwargs, ) - {% for statement in init_body %} + {% for statement in vars|init %} {{ statement }} {%- endfor %} @@ -97,7 +98,7 @@ class Modflow{{ name.title.title() }}(MFModel): structure, modelname, model_nam_file, - "{{ name.title }}6", + "{{ title.lower() }}6", version, exe_name, strict, diff --git a/flopy/mf6/utils/codegen/templates/package.py.jinja b/flopy/mf6/utils/codegen/templates/package.py.jinja index cbc86319e..a763e81eb 100644 --- a/flopy/mf6/utils/codegen/templates/package.py.jinja +++ b/flopy/mf6/utils/codegen/templates/package.py.jinja @@ -1,4 +1,5 @@ {% import 'macros.jinja' as macros %} +{% set title = (name|title).title() %} from os import PathLike, curdir from typing import Union @@ -7,16 +8,16 @@ from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenera from flopy.mf6.mfpackage import MFPackage, MFChildPackages -class Modflow{{ name.title.title() }}(MFPackage): +class Modflow{{ title }}(MFPackage): """ - {{ name.description }} + {{ name|description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(vars, indent=4) }} """ - {% for attr in cls_attrs %} + {% for attr in vars|attrs %} {{ attr }} {%- endfor %} @@ -24,13 +25,13 @@ class Modflow{{ name.title.title() }}(MFPackage): self, {{ name|parent }}, loading_package=False, - {{ macros.init_vars(vars, skip=name|skip) }} + {{ macros.init_vars(vars, indent=8, skip=name|skip) }} filename=None, pname=None, **kwargs, ): """ - {{ name.description }} + {{ name|description }} Parameters ---------- @@ -42,7 +43,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(vars, indent=8) }} filename : str File name for this package. @@ -65,39 +66,39 @@ class Modflow{{ name.title.title() }}(MFPackage): **kwargs ) - {% for statement in init_body %} + {% for statement in vars|init %} {{ statement }} {%- endfor %} self._init_complete = True {% if "ref" in meta and name.r != "hpc" %} -class {{ name.title.title() }}Packages(MFChildPackages): +class {{ title }}Packages(MFChildPackages): """ - {{ name.title.title() }}Packages is a container class for the Modflow{{ name.title.title() }} class. + {{ title }}Packages is a container class for the Modflow{{ title }} class. Methods ------- initialize - Initializes a new Modflow{{ name.title.title() }} package removing any sibling child - packages attached to the same parent package. See Modflow{{ name.title.title() }} init + Initializes a new Modflow{{ title }} package removing any sibling child + packages attached to the same parent package. See Modflow{{ title }} init documentation for definition of parameters. append_package - Adds a new Modflow{{ name.title.title() }} package to the container. See Modflow{{ name.title.title() }} + Adds a new Modflow{{ title }} package to the container. See Modflow{{ title }} init documentation for definition of parameters. """ - package_abbr = "{{ name.title.lower() }}packages" + package_abbr = "{{ title.lower() }}packages" def initialize( self, - {{ macros.init_vars(vars, alias=true, skip=name|skip) }} + {{ macros.init_vars(vars, alias=true, indent=8, skip=name|skip) }} filename=None, pname=None, ): - new_package = Modflow{{ name.title.title() }}( + new_package = Modflow{{ title }}( self._cpparent, {%- for n, var in vars.items() if n not in name|skip %} {{ n }}={{ n }}, @@ -111,11 +112,11 @@ class {{ name.title.title() }}Packages(MFChildPackages): {% if name.r != "obs" %} def append_package( self, - {{ macros.init_vars(vars, alias=true, skip=name|skip) }} + {{ macros.init_vars(vars, alias=true, indent=8, skip=name|skip) }} filename=None, pname=None, ): - new_package = Modflow{{ name.title.title() }}( + new_package = Modflow{{ title }}( self._cpparent, {%- for n, var in vars.items() if n not in name|skip %} {{ n }}={{ n }}, diff --git a/flopy/mf6/utils/codegen/templates/simulation.py.jinja b/flopy/mf6/utils/codegen/templates/simulation.py.jinja index 76c15057b..bdfbf0547 100644 --- a/flopy/mf6/utils/codegen/templates/simulation.py.jinja +++ b/flopy/mf6/utils/codegen/templates/simulation.py.jinja @@ -1,4 +1,5 @@ {% import 'macros.jinja' as macros %} +{% set title = (name|title).title() %} from os import PathLike, curdir from typing import Union @@ -6,13 +7,13 @@ from typing import Union from flopy.mf6.data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator from flopy.mf6.mfsimbase import MFSimulationBase -class MF{{ name.title.title() }}(MFSimulationBase): +class MF{{ title }}(MFSimulationBase): """ - {{ name.description }} + {{ name|description }} Parameters ---------- - {{ macros.vars_docs(vars, start_indent=4) }} + {{ macros.vars_docs(vars, indent=4) }} Methods ------- @@ -34,10 +35,10 @@ class MF{{ name.title.title() }}(MFSimulationBase): write_headers: bool = True, use_pandas: bool = True, lazy_io: bool = False, - {{ macros.init_vars(vars, skip=name|skip) }} + {{ macros.init_vars(vars, indent=8, skip=name|skip) }} ): """ - {{ name.description }} + {{ name|description }} Parameters ---------- @@ -65,7 +66,7 @@ class MF{{ name.title.title() }}(MFSimulationBase): lazy_io Whether to use lazy IO - {{ macros.vars_docs(vars, start_indent=8) }} + {{ macros.vars_docs(vars, indent=8) }} """ super().__init__( @@ -79,7 +80,7 @@ class MF{{ name.title.title() }}(MFSimulationBase): use_pandas=use_pandas ) - {% for statement in init_body %} + {% for statement in vars|init %} {{ statement }} {%- endfor %} diff --git a/flopy/mf6/utils/codegen/utils.py b/flopy/mf6/utils/codegen/utils.py deleted file mode 100644 index e663835be..000000000 --- a/flopy/mf6/utils/codegen/utils.py +++ /dev/null @@ -1,3 +0,0 @@ -from ast import literal_eval -from enum import Enum -from typing import Any