Skip to content

Commit 1f2f340

Browse files
committed
feat: add_config flag #26
1 parent 04c3aea commit 1f2f340

File tree

6 files changed

+85
-14
lines changed

6 files changed

+85
-14
lines changed

docs/Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## unreleased
4+
* feat: [`run`][mininterface.run] add_config flag
5+
36
## 1.1.4 (2025-10-13)
47
* enh: Python 3.14 compatible
58

docs/Config-file.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
# Config file
22
Any settings you see in the `--help` command can be modified via a YAML config file.
33

4-
By default, we try to find one in the current working dir, whose name stem is the same as the program's. Ex: program.py will search for program.yaml. This behaviour can be changed via the [run][mininterface.run] method.
4+
By default, we try to find one in the current working dir, whose name stem is the same as the program's. Ex: program.py will search for program.yaml. This behaviour can be changed via the [run][mininterface.run] method `config_file` or `add_config` parameters or via `MININTERFACE_CONFIG` environment variable.
55

66
!!! Tip
77
You do not have to re-define all the settings in the config file, you can choose a few.
88

9+
## Search order by highest priority
10+
11+
* `$ program.py --config PATH` with `run(add_config=True)` will load `PATH`
12+
* `$ MININTERFACE_CONFIG=PATH program.py` will load `PATH`
13+
* `$ program.py` with `run(config_file=PATH)` will load `PATH`
14+
* `$ program.py` with `run(config_file=True)` will load `program.yaml`
15+
916
## Basic example
1017

1118
We have this nested structure:

mininterface/_lib/cli_flags.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ class CliFlags:
1515
default_verbosity: int = logging.WARNING
1616
_verbosity_sequence: Optional[Sequence[int]] = None
1717

18+
config: bool = False
19+
1820
def __init__(
1921
self,
2022
add_verbose: bool | int | Sequence[int] = False,
2123
add_version: Optional[str] = None,
2224
add_version_package: Optional[str] = None,
2325
add_quiet: bool = False,
26+
add_config: bool = False,
2427
):
25-
self._enabled = {"verbose": True, "version": True, "quiet": True}
28+
self._enabled = {"verbose": True, "version": True, "quiet": True, "config": True}
2629
# verbosity
2730
match add_verbose:
2831
case bool():
@@ -50,13 +53,17 @@ def __init__(
5053
except PackageNotFoundError:
5154
self.version = f"package {add_version_package} not found"
5255

56+
# config
57+
self.config = add_config
58+
5359
def should_add(self, env_classes: list[EnvClass]) -> bool:
5460
# Flags are added only if neither the env_class nor any of the subcommands have the same-name flag already
5561
self._enabled["verbose"] = self._add_verbose and self._attr_not_present("verbose", env_classes)
5662
self._enabled["quiet"] = self._add_quiet and self._attr_not_present("quiet", env_classes)
5763
self._enabled["version"] = self.version and self._attr_not_present("version", env_classes)
64+
self._enabled["config"] = self.config and self._attr_not_present("config", env_classes)
5865

59-
return self.add_verbose or self.add_version or self.add_quiet
66+
return self.add_verbose or self.add_version or self.add_quiet or self.add_config
6067

6168
def _attr_not_present(self, flag, env_classes):
6269
return all(flag not in cl.__annotations__ for cl in env_classes)
@@ -73,6 +80,10 @@ def add_version(self):
7380
def add_quiet(self):
7481
return self._add_quiet and self._enabled["quiet"]
7582

83+
@property
84+
def add_config(self):
85+
return self.config and self._enabled["config"]
86+
7687
def get_log_level(self, count):
7788
"""
7889
Ex.

mininterface/_lib/run.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def run(
3232
title: str = "",
3333
config_file: Path | str | bool = True,
3434
*,
35+
add_config: bool = False,
3536
add_help: bool = True,
3637
add_verbose: bool|int|Sequence[int] = True,
3738
add_version: Optional[str] = None,
@@ -83,9 +84,13 @@ class Env:
8384
whose name stem is the same as the program's.
8485
Ex: `program.py` will search for `program.yaml`.
8586
If False, no config file is used.
87+
88+
Might be superseded by `add_config` or `MININTERFACE_CONFIG` environment variable.
89+
8690
See the [Config file](Config-file.md) section.
87-
add_help: Adds the help flag.
88-
add_verbose: The default base Python verbosity logging level is `logging.WARNING`. Here you can add the verbose flag that automatically increases the level to `logging.INFO` (*-v*) or `logging.DEBUG` (*-vv*).
91+
add_config: Adds the `--config` flag to change the config file location.
92+
add_help: Adds the `--help` flag.
93+
add_verbose: Adds the `--verbose` flag. The default base Python verbosity logging level is `logging.WARNING`. Here you can add the verbose flag that automatically increases the level to `logging.INFO` (*-v*) or `logging.DEBUG` (*-vv*).
8994
Either, the value is `True` (the default) which means the base logging level stays at `logging.WARNING` and the flag is added. `False` means no flag is added.
9095
Also, it can be `int` to determine the default logging state (i.g. some programs prefer to show INFO by default) or a sequnce of `int`s for even finer control.
9196
@@ -140,7 +145,7 @@ class Env:
140145
141146
When user writes more `-v` than defined, the level sets to `logging.NOTSET`.
142147
143-
add_version: Your program version. Adds the version flag.
148+
add_version: Adds the `--version` flag to print out your program's version.
144149
```python
145150
run(add_version="1.2.5")
146151
```
@@ -155,7 +160,7 @@ class Env:
155160
╰─────────────────────────────────────────────────────────────────────╯
156161
```
157162
158-
add_quiet: Decrease verbosity, only print warnings and errors.
163+
add_quiet: Adds the `--quiet` flag to decrease the verbosity and only print warnings and errors.
159164
```python
160165
import logging
161166
logger = logging.getLogger(__name__)
@@ -236,7 +241,27 @@ class Env:
236241
if not add_help:
237242
kwargs["add_help"] = False
238243

244+
# Assure args
245+
if isinstance(assure_args, DependencyRequired) and not env_or_list:
246+
# Basic dependencies missing, we have no CLI capacities
247+
# Since the user needs no CLI, we return a bare interface.
248+
return get_interface(interface, title)
249+
args = assure_args(args)
250+
239251
# Prepare the config file
252+
if cf:= environ.get("MININTERFACE_CONFIG"):
253+
config_file = cf
254+
if add_config and "--config" in args:
255+
# Detect the `--config` and pop it.
256+
# `--flag --config FILE --flag2` -> `--flag --flag2`
257+
args = list(args)
258+
idx = args.index("--config")
259+
try:
260+
config_file = args[idx + 1]
261+
except IndexError:
262+
raise ValueError("Missing value after --config")
263+
else:
264+
del args[idx:idx + 2]
240265
if config_file is True and not kwargs.get("default"):
241266
# Undocumented feature. User put a namespace into kwargs["default"]
242267
# that already serves for defaults. We do not fetch defaults yet from a config file.
@@ -260,12 +285,6 @@ class Env:
260285
if environ.get("MININTERFACE_ENFORCED_WEB"):
261286
interface = "web"
262287

263-
if isinstance(assure_args, DependencyRequired) and not env_or_list:
264-
# Basic dependencies missing, we have no CLI capacities
265-
# Since the user needs no CLI, we return a bare interface.
266-
return get_interface(interface, title)
267-
args = assure_args(args)
268-
269288
# Hidden meta-commands in args
270289
if environ.get("MININTERFACE_INTEGRATE_TO_SYSTEM"):
271290
del environ["MININTERFACE_INTEGRATE_TO_SYSTEM"]
@@ -306,7 +325,7 @@ class Env:
306325
args[0] = to_kebab_case(choose_subcommand(env_or_list, m).__name__)
307326

308327
# Parse CLI arguments, possibly merged from a config file.
309-
cf = _CliFlags(add_verbose, add_version, add_version_package, add_quiet)
328+
cf = _CliFlags(add_verbose, add_version, add_version_package, add_quiet, add_config)
310329
if env_or_list:
311330
# A single Env object, or a list of such objects (with one is not/being selected via args)
312331
# Load configuration from CLI and a config file

mininterface/_lib/tyro_patches.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,13 @@ def _(self: TyroArgumentParser, *args, **kwargs):
347347
help="suppress warnings, display only errors",
348348
)
349349

350+
if cf.add_config:
351+
self.add_argument(
352+
default_prefix * 2 + "config",
353+
help=f"path to config file to fetch the defaults from",
354+
metavar="PATH"
355+
)
356+
350357
return _
351358

352359

@@ -369,6 +376,11 @@ def _(self: TyroArgumentParser, args=None, namespace=None):
369376
# raise SystemExit(0)
370377
# delattr(namespace, "version")
371378

379+
# Note that we do not parse --config here as it is parsed at `run.py`, before CLI parsing.
380+
# Since config file serves as default fo CLI parsing.
381+
if cf.add_config and hasattr(namespace, "config"):
382+
delattr(namespace, "config")
383+
372384
if cf.add_quiet and hasattr(namespace, "quiet"):
373385
if namespace.quiet:
374386
logging.basicConfig(level=cf.get_log_level(-1), format="%(message)s", force=True)

tests/test_run.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from tempfile import NamedTemporaryFile
22
from unittest import skipUnless
33
from mininterface import Mininterface
4+
from mininterface._lib import config_file
45
from mininterface._lib.config_file import ensure_settings_inheritance
56
from mininterface._lib.run import run
67
from mininterface.settings import UiSettings, MininterfaceSettings as MSOrig
@@ -258,6 +259,24 @@ def r(model):
258259
r(model)
259260
self.assertIn("Unknown fields in the configuration file", str(w[0].message))
260261

262+
def test_add_config(self):
263+
for add_config, config_file, expected, args, env_vars in [
264+
(True, True, 4, [], {}),
265+
(True, True, 10, ["--config", "tests/SimpleEnv.yaml"], {"MININTERFACE_CONFIG": "SimpleEnv.yaml"}),
266+
(True, True, 20, ["--config", "tests/SimpleEnv2.yaml"], {"MININTERFACE_CONFIG": "SimpleEnv.yaml"}),
267+
(True, True, 10, [], {"MININTERFACE_CONFIG": "tests/SimpleEnv.yaml"}),
268+
(True, True, 20, [], {"MININTERFACE_CONFIG": "tests/SimpleEnv2.yaml"}),
269+
(False, True, 20, [], {"MININTERFACE_CONFIG": "tests/SimpleEnv2.yaml"}),
270+
(False, False, 20, [], {"MININTERFACE_CONFIG": "tests/SimpleEnv2.yaml"}),
271+
(True, False, 20, ["--config", "tests/SimpleEnv2.yaml"], {}),
272+
]:
273+
with self.subTest(args=args):
274+
with patch.dict(os.environ, env_vars):
275+
self.assertEqual(
276+
expected,
277+
runm(SimpleEnv, args, add_config=add_config, config_file=config_file).env.important_number,
278+
)
279+
261280
def test_settings(self):
262281
# NOTE
263282
# The settings had little params at the moment of the test writing.

0 commit comments

Comments
 (0)