Skip to content

Commit 1895f32

Browse files
committed
iron out some inconsistencies in planning docs
1 parent e31bc55 commit 1895f32

3 files changed

Lines changed: 242 additions & 170 deletions

File tree

docs/md/dev/dfns.md

Lines changed: 58 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ Each source has:
124124
- `registry_path`: Path within the repository to the registry metadata file (defaults to `.registry/dfns.toml`)
125125
- `refs`: List of git refs (branches, tags, or commit hashes) to sync by default
126126

127+
#### User config overlay
128+
129+
Users can customize or extend the bundled bootstrap configuration by creating a user config file at:
130+
- Linux/macOS: `~/.config/modflow-devtools/dfn-bootstrap.toml` (respects `$XDG_CONFIG_HOME`)
131+
- Windows: `%APPDATA%/modflow-devtools/dfn-bootstrap.toml`
132+
133+
The user config follows the same format as the bundled bootstrap file. Sources defined in the user config will override or extend those in the bundled config, allowing users to:
134+
- Add custom DFN repositories
135+
- Point to forks of existing repositories (useful for testing experimental schema versions)
136+
- Override default refs for existing sources
137+
138+
**Implementation note**: The user config path logic (`get_user_config_path("dfn")`) is shared across all three APIs (Models, Programs, DFNs) via `modflow_devtools.config`, but each API implements its own `merge_bootstrap()` function using API-specific bootstrap schemas.
139+
127140
#### Sample bootstrap file
128141

129142
```toml
@@ -190,13 +203,13 @@ Or for v1/v1.1, no spec file needed - everything inferred.
190203

191204
#### Registry file format
192205

193-
A `dfns.toml` registry file for **discovery and distribution**:
206+
A **`dfns.toml`** registry file for **discovery and distribution** (the specific naming distinguishes it from `models.toml` and `programs.toml`):
194207

195208
```toml
196-
# Registry metadata (optional)
209+
# Registry metadata (top-level, optional)
210+
schema_version = "1.0"
197211
generated_at = "2025-01-02T10:30:00Z"
198212
devtools_version = "1.9.0"
199-
registry_schema_version = "1.0"
200213

201214
[metadata]
202215
ref = "6.6.0" # Optional, known from discovery context
@@ -533,50 +546,23 @@ Examples:
533546

534547
### Registry classes
535548

536-
#### DfnRegistry (abstract base)
537-
538-
Similar to `ModelRegistry` and `ProgramRegistry`, defines the contract:
539-
540-
```python
541-
class DfnRegistry(ABC):
542-
@property
543-
@abstractmethod
544-
def spec(self) -> DfnSpec:
545-
"""Get the full DFN specification."""
546-
pass
547-
548-
@property
549-
@abstractmethod
550-
def ref(self) -> str:
551-
"""Get the git ref for this registry."""
552-
pass
549+
The registry class hierarchy is based on a Pydantic `DfnRegistry` base class:
553550

554-
def get_dfn(self, component: str) -> Dfn:
555-
"""
556-
Get a parsed DFN for the specified component.
557-
Convenience method for spec[component].
558-
"""
559-
return self.spec[component]
551+
**`DfnRegistry` (base class)**:
552+
- Pydantic model with optional `meta` field for registry metadata
553+
- Provides access to a `DfnSpec` (the full parsed specification)
554+
- Can be instantiated directly for data-only use (e.g., loading/parsing TOML files)
555+
- Key properties:
556+
- `spec` - The full DFN specification (lazy-loaded)
557+
- `ref` - Git ref for this registry
558+
- `get_dfn(component)` - Convenience for `spec[component]`
559+
- `get_dfn_path(component)` - Get local path to DFN file
560+
- `schema_version` - Convenience for `spec.schema_version`
561+
- `components` - Convenience for `dict(spec.items())`
560562

561-
@abstractmethod
562-
def get_dfn_path(self, component: str) -> Path:
563-
"""Get the local path to a DFN file (fetching if needed)."""
564-
pass
563+
**`RemoteDfnRegistry(DfnRegistry)`**:
565564

566-
@property
567-
def schema_version(self) -> str:
568-
"""Get the schema version. Convenience for spec.schema_version."""
569-
return self.spec.schema_version
570-
571-
@property
572-
def components(self) -> dict[str, Dfn]:
573-
"""Get all components. Convenience for dict(spec.items())."""
574-
return dict(self.spec.items())
575-
```
576-
577-
#### RemoteDfnRegistry
578-
579-
Handles remote registry discovery, caching, and DFN fetching:
565+
Handles remote registry discovery, caching, and DFN fetching. Constructs DFN file URLs dynamically from bootstrap metadata:
580566

581567
```python
582568
class RemoteDfnRegistry(DfnRegistry):
@@ -590,45 +576,13 @@ class RemoteDfnRegistry(DfnRegistry):
590576
self._cache_dir = None
591577
self._load()
592578

593-
def _load(self):
594-
# Load bootstrap metadata for this source
595-
self._bootstrap_meta = self._load_bootstrap(self.source)
596-
597-
# Check cache for registry
598-
if cached := self._load_from_cache():
599-
self._registry_meta = cached
600-
else:
601-
# Sync from remote
602-
self._sync()
603-
604-
# Set up Pooch for file fetching
605-
self._setup_pooch()
606-
607-
# Ensure all files are cached (lazy fetching on access)
608-
# Don't load spec until needed
609-
610-
@property
611-
def spec(self) -> DfnSpec:
612-
"""Lazy-load the DfnSpec from cached files."""
613-
if self._spec is None:
614-
# Ensure all DFN files are cached
615-
self._ensure_cached()
616-
# Load spec from cache directory
617-
self._spec = DfnSpec.load(self._cache_dir)
618-
return self._spec
619-
620-
def _ensure_cached(self):
621-
"""Ensure all DFN files are fetched and cached."""
622-
for filename in self._registry_meta["files"]:
623-
self._pooch.fetch(filename)
624-
625579
def _setup_pooch(self):
626580
# Create Pooch instance with dynamically constructed URLs
627581
import pooch
628582

629583
self._cache_dir = self._get_cache_dir()
630584

631-
# Construct base URL from bootstrap metadata
585+
# Construct base URL from bootstrap metadata (NOT stored in registry)
632586
repo = self._bootstrap_meta["repo"]
633587
dfn_path = self._bootstrap_meta.get("dfn_path", "doc/mf6io/mf6ivar/dfn")
634588
base_url = f"https://raw.githubusercontent.com/{repo}/{self._ref}/{dfn_path}/"
@@ -641,18 +595,18 @@ class RemoteDfnRegistry(DfnRegistry):
641595

642596
def get_dfn_path(self, component: str) -> Path:
643597
# Use Pooch to fetch file (from cache or remote)
644-
# Pooch constructs full URL from base_url + filename
598+
# Pooch constructs full URL from base_url + filename at runtime
645599
filename = self._get_filename(component)
646600
return Path(self._pooch.fetch(filename))
647601
```
648602

649603
**Benefits of dynamic URL construction**:
650-
- Registry files are smaller and simpler
651-
- Users can substitute personal forks by modifying bootstrap file
604+
- Registry files are smaller and simpler (no URLs stored)
605+
- Users can test against personal forks by modifying bootstrap file
652606
- Single source of truth for repository location
653607
- URLs adapt automatically when repo/path changes
654608

655-
#### LocalDfnRegistry
609+
**`LocalDfnRegistry(DfnRegistry)`**:
656610

657611
For developers working with local DFN files:
658612

@@ -680,6 +634,11 @@ class LocalDfnRegistry(DfnRegistry):
680634
raise ValueError(f"Component {component} not found in {self.path}")
681635
```
682636

637+
**Design decisions**:
638+
- **Pydantic-based** (not ABC) - allows direct instantiation for data-only use cases
639+
- **Dynamic URL construction** - DFN file URLs constructed at runtime, not stored in registry
640+
- **No `MergedRegistry`** - users typically work with one MODFLOW 6 version at a time, so merging across versions doesn't make sense
641+
683642
### Module-level API
684643

685644
Convenient module-level functions:
@@ -1383,6 +1342,24 @@ The DFNs API deliberately mirrors the Models and Programs API architecture for c
13831342

13841343
This consistency benefits both developers and users with a familiar experience across all three APIs.
13851344

1345+
## Cross-API Consistency
1346+
1347+
The DFNs API follows the same design patterns as the Models and Programs APIs for consistency. See the **Cross-API Consistency** section in `models.md` for full details.
1348+
1349+
**Key shared patterns**:
1350+
- Pydantic-based registry classes (not ABCs)
1351+
- Dynamic URL construction (URLs built at runtime, not stored in registries)
1352+
- Separate bootstrap and user config files (`dfn-bootstrap.toml`)
1353+
- Top-level `schema_version` metadata field
1354+
- Distinctly named registry file (`dfns.toml`)
1355+
- Shared config utility: `get_user_config_path("dfn")`
1356+
1357+
**Unique to DFNs API**:
1358+
- Discovery via version control (release assets mode planned for future)
1359+
- Extra `dfn_path` bootstrap field (location of DFN files within repo)
1360+
- Schema versioning and mapping capabilities
1361+
- No `MergedRegistry` (users work with one MF6 version at a time)
1362+
13861363
## Design Decisions
13871364

13881365
### Use Pooch for fetching

0 commit comments

Comments
 (0)