Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Expand default functionality to enable fallback lookups. #180

Merged
merged 1 commit into from
Nov 26, 2024

Conversation

DanCardin
Copy link
Owner

@DanCardin DanCardin commented Nov 22, 2024

  • Add Default object with associated fallback semantics for sequences of default handlers.
  • Add ValueFrom for handling default_factory lazily, as well as arbitrary function dispatch
  • Add State as object accessible to invoke, Arg.parse, and ValueFrom.callable for sharing state amongst different stages of argument parsing.

@pawamoy
Copy link
Contributor

pawamoy commented Nov 23, 2024

Trying it out locally. The classes (default, defaulttype, env, prompt, confirm, valuefrom) should probably implement __str__ to allow displaying them in CLI help message. Might be tricky 🤔 For example I would like the following default:

cappa.Env("SOME_ENV_VAR") | cappa.ValueFrom(from_config, "some_config_value")

...to display as:

Default: `SOME_ENV_VAR` or configuration value `some.config_value`.

...and there's no way to get this result automatically, of course. But I don't see how the user could provide their own string to ValueFrom, unless the class accepts explicit args and kwargs params to avoid name shadowing 🤔

# If `ValueFrom` accepts `helptext`, the same name cannot be used in `some_callable`.
cappa.ValueFrom(some_callable, "some_config_value", helptext="configuration value `some.config_value`")

# Instead, maybe accept this?
cappa.ValueFrom(
    some_callable,
    args=["some_config_value"],

    # These two don't clash anymore.
    kwargs={"helptext": "whatever"},
    helptext="configration value `some.config_value`",
)

Or maybe you have another idea on how to customize the display of defaults ☺️? Maybe the user could subclass ValueFrom to provide its own __str__ method?

class FromConfig(ValueFrom):
    def __init__(self, attr_name: str, conf_name: str) -> None:
        super().__init__(from_config, attr_name)
        self.conf_name = conf_name

    def __str__(self) -> str:
        return f"configuration value `{self.conf_name}`"

@pawamoy
Copy link
Contributor

pawamoy commented Nov 23, 2024

Actually it looks like such defaults are not displayed at all in the CLI help. And it turns out I'd need a default_format parameter on each Arg because in really depends on the default value. I'll open a feature request.

@pawamoy
Copy link
Contributor

pawamoy commented Nov 24, 2024

Is there a reason why exceptions raised when evaluating a default are ignored?

    def __call__(self) -> tuple[bool, Any | None]:
        for default in self.sequence:
            try:
                value = default()
            except Exception:  # noqa: S112
                continue

I got bit by this. My Config object didn't declare a specific attribute, so my from_config callable doing getattr(config, attr_name) raised an exception, but it was silently ignored and the final value of the command option was None instead of the actual default, an empty dict built with the default factory.

Personally I'd let exceptions bubble up to fail early 🙂

@pawamoy
Copy link
Contributor

pawamoy commented Nov 24, 2024

There seems to be an issue with recognizing defaults, because my default value obtained from ValueFrom is passed again to parse. Probably somewhere in map_result, like:

assert isinstance(arg.default, Default), arg
is_parsed, value = arg.default()

Or it simply comes from

            if value is not None:
                return False, value

in Default.__call__. Should it ever return False for is_parsed since it's a default? Changing it to return True, value fixes the issue on my end ☺️

@DanCardin
Copy link
Owner Author

i have a bunch more changes pending, not yet passing all tests.

but stuff like Env and Prompt always return strings akin to raw values from cli parsing, and should be parsed. I agree that ValueFrom is userspace code that should just be returning the correct type.

@pawamoy
Copy link
Contributor

pawamoy commented Nov 25, 2024

but stuff like Env and Prompt always return strings akin to raw values from cli parsing, and should be parsed

Yes that makes sense. We must make sure it's documented in the "defaults are not parsed" paragraph 🙂

@DanCardin DanCardin force-pushed the dc/fancy-default branch 2 times, most recently from 946be14 to 10e67bb Compare November 26, 2024 02:03
Copy link

codecov bot commented Nov 26, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (de0fe5b) to head (c46749e).
Report is 2 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main      #180      +/-   ##
===========================================
+ Coverage   99.35%   100.00%   +0.64%     
===========================================
  Files          25        26       +1     
  Lines        2310      2422     +112     
  Branches      465       522      +57     
===========================================
+ Hits         2295      2422     +127     
+ Misses         12         0      -12     
+ Partials        3         0       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@DanCardin DanCardin marked this pull request as ready for review November 26, 2024 02:26
@DanCardin
Copy link
Owner Author

  • Updated docs
  • default_factory should now be lazy (by using ValueFrom)
  • parse shouldn't be called on ValueFroms
  • State is an object you should be able to use for sharing state rather than using a global/classvar. In your case, you should just be able to depend on it without directly constructing one.

This ended up being a much larger diff than I'd originally thought it would be, but it also simplified a bunch of the default handling for Env/Prompt and made doing default_factory properly trivial (implemented it in literally 2 minutes).

So I think it was clearly an overdue concept/refactor even if I trashed the whole State concept.

I think State ends up being reasonably analagous to click.pass_context (or at least the only reason i've ever used pass_context was to stash data), so I dont feel bad about it, but i've also never really had a reason for something like this (I always just use Deps), so this bit i'm less certain about the ergonomics of.

But it seems like it should have utility any time someone wants to pass data from the calling context into something inside cappa's handling anyways, so 🤷

@pawamoy
Copy link
Contributor

pawamoy commented Nov 26, 2024

Thank you so much! I'll try to test and report back, but do merge if you think it's ready (don't wait for me) 😄

@DanCardin DanCardin changed the title Dc/fancy default feat: Expand default functionality to enable fallback lookups. Nov 26, 2024
@DanCardin DanCardin merged commit 98f5bfa into main Nov 26, 2024
10 checks passed
@DanCardin
Copy link
Owner Author

I'll merge now just because it'll otherwise block other work, but i'd love if you were able to test before I cut a release

@DanCardin DanCardin deleted the dc/fancy-default branch November 26, 2024 14:24
@pawamoy
Copy link
Contributor

pawamoy commented Nov 26, 2024

I just read the docs, they are great! 🚀 It's a big PR indeed 😂 State looks cleaner than my private class variable, I'll try it too.

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