Skip to content

Conversation

@nh13
Copy link
Contributor

@nh13 nh13 commented May 27, 2025

The following (incomplete) pull request adds a new argument to the run command to specify a mapping of defaults for function arguments. It may be useful to have the defaults be set at runtime, for example if we want to read them from a YAML or other configuration file. The below is a toy example of how I'd use the argument.

If you like the path this PR is going, the next steps would be:

  1. solve how to pass defaults to nested commands
  2. add tests
  3. add to docs and examples
import defopt

def foo(*, bar: int) -> None:
    print(bar)

def foo2(*, bar: int) -> None:
    print(bar)


if __name__ == '__main__':
    defaults = {
        "foo": {
            "bar": 5
        },
        "sub": {
            "foo2": {
                "bar": 6
            }
        }
    }

    defopt.run({"foo": foo, "sub": [foo2]}, defaults=defaults)

hasdefault = param.default is not param.empty
default = param.default if hasdefault else SUPPRESS

if not hasdefault and defaults is not None and name in defaults:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: This makes the function argument take precedence. Perhaps it should be the other way around?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would seem that if you pass an explicit default, that one should be preferred?

Command line arguments to parse (default: ``sys.argv[1:]``).
:param defaults:
Mapping for argument defaults passed to
`~argparse.ArgumentParser.set_defaults`. Key must be the command name,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: fixme, this isn't true anymore

@anntzer
Copy link
Owner

anntzer commented May 27, 2025

I don't know how exactly does your want your yaml-loading works (e.g. how do you specify the funcs arg?) but I suspect I'd rather add proper support for functools.partial so that you can write

import defopt
from functools import partial

def foo(*, bar: int) -> None:
    print(bar)

def foo2(*, bar: int) -> None:
    print(bar)

if __name__ == '__main__':
    defopt.run({"foo": partial(foo, bar=5), "sub": [partial(foo2, bar=6)]})

Thoughts?

@nh13 nh13 closed this May 28, 2025
@nh13
Copy link
Contributor Author

nh13 commented May 28, 2025

proper support for functools.partial Would be great.

@nh13 nh13 reopened this May 28, 2025
@anntzer
Copy link
Owner

anntzer commented May 28, 2025

From a quick test the following patch seems enough? Still needs tests and docs of course.

diff --git i/src/defopt.py w/src/defopt.py
index 8855c05..df9e386 100644
--- i/src/defopt.py
+++ w/src/defopt.py
@@ -330,7 +330,8 @@ def _recurse_functions(funcs, subparsers):
         # If this iterable is not a mapping, then convert it to one using the
         # function name itself as the key, but replacing _ with -.
         try:
-            funcs = {func.__name__.replace('_', '-'): func for func in funcs}
+            funcs = {_unwrap_partial(func).__name__.replace('_', '-'): func
+                     for func in funcs}
         except AttributeError as exc:
             # Do not allow a mapping inside of a list
             raise ValueError(
@@ -468,12 +469,16 @@ def signature(func: Union[Callable, str]):
         inspect_sig = _preprocess_inspect_signature(
             func, inspect.signature(func))
         doc_sig = _preprocess_doc_signature(
-            func, signature(inspect.getdoc(func)))
+            func, signature(inspect.getdoc(_unwrap_partial(func))))
         return _merge_signatures(inspect_sig, doc_sig)
 
 
+def _unwrap_partial(func):
+    return func.func if isinstance(func, functools.partial) else func
+
+
 def _preprocess_inspect_signature(func, sig):
-    hints = typing.get_type_hints(func)
+    hints = typing.get_type_hints(_unwrap_partial(func))
     parameters = []
     for name, param in sig.parameters.items():
         if param.name.startswith('_'):

@nh13
Copy link
Contributor Author

nh13 commented May 29, 2025

Closing in favor of #130

@nh13 nh13 closed this May 29, 2025
@nh13 nh13 deleted the feat/defaults branch May 29, 2025 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants