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

How to create required CLI config options? #828

Open
metaperl opened this issue Jan 24, 2023 · 5 comments
Open

How to create required CLI config options? #828

metaperl opened this issue Jan 24, 2023 · 5 comments

Comments

@metaperl
Copy link
Contributor

In #490 this issue was raised and then closed by @rmorshea.

However,

So:

  1. How does one flag an attribute as mandatory?
  2. Is this documented and did I miss it?

Here is a code sample for you to elide with what is necessary to make the req attribute mandatory:

class App(Application):
    optional_arg = Int(config=True)
    req = Unicode(config=True)

I'm expecting an exception to be thrown when I invoke this app without providing req:

python my_app.py --App.optional_arg=5
@rmorshea rmorshea changed the title How to flag an attribute as required / mandatory? How to create required CLI config options? Jan 24, 2023
@rmorshea
Copy link
Contributor

If the solution in the linked issue is not sufficient, then it seems you're asking a slightly different question. Specifically, whether there's some way to mark configurable traits in such a way that they will be treated as required when generating CLI options. As it happens, the logic within the argparse config loader has no way to accomplish this.

With that said, I don't think it would be especially difficult to allow for this. Before jumping to any particular implementation it would be useful to get some input from other maintainers, but my approach would be as follows... instead of adding more metadata to interpret, we could simply handle a generic argparse_kwargs tag that would allow users to configure a trait's generated argparse option however they'd like (relevant logic is here). An example usage would then be:

class App(Application):
    optional_arg = Int(config=True)
    req = Unicode(config=True).tag(argparse_kwargs={"required": True})

@metaperl
Copy link
Contributor Author

First thing is that the concept of required is not just for the CLI: it also applies to simply making objects programmatically.

Next, I find the syntax argparse_kwargs={"required": True} very verbose and it exposes too many low level implementation details. It is not a simple high-level key-value pair.

The @validate decorator as previously discussed is interesting, but I think it is also possible to use @default and raise an Exception is there is an attempt to set the value of an attribute via default:

class App(Application):
    optional_arg = Int(config=True)
    req = Unicode(config=True)

@default('req')
def _default_req(self):
    raise TraitError('value must be set on CLI or at object construction time')

@metaperl
Copy link
Contributor Author

Another thing regarding this sample -

class MyS3Class(HasTraits):
    
    s3_bucket_name = Unicode()

    @validate("s3_bucket_name")
    def _validate_s3_bucket_name(self, proposal):
        if not proposal.value:
            raise ValueError("S3 bucket name cannot be empty.")

    def __init__(self, s3_bucket_name, **kwargs):
        super().__init__(**kwargs)
        self.s3_bucket_name = s3_bucket_name

The positional argument to __init__ does not necessarily map to s3_bucket_name... this is a failing of positional arguments.

@azjps
Copy link
Collaborator

azjps commented Jan 28, 2023

instead of adding more metadata to interpret, we could simply handle a generic argparse_kwargs tag that would allow users to configure a trait's generated argparse option however they'd like

FYI unfortunately this is not possible with the current implementation, there are no arguments added to the ArgumentParser instance corresponding to arguments of the form --Class.trait. There's just a catch-all which tries to find arguments that look like this and they're then assigned later by traitlets itself. So its not very possible currently to customize command-line parsing, which leads to problems like #690.

Regarding the original issue, using @validate is reasonable, although it wouldn't work if the default value is a valid input. One workaround is just to set the default_value to something invalid, e.g. default_value=None, allow_none=False:

>>> import traitlets
>>> from traitlets.config import Configurable
>>> class A(Configurable):
...   x = traitlets.Int(default_value=None, allow_none=False, config=True)
...
>>> A().x
Traceback (most recent call last):
  File "/home/azhu/anaconda3/envs/2021_mit/lib/python3.9/site-packages/traitlets
    value = obj._trait_values[self.name]
KeyError: 'x'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "$venv/lib/python3.9/site-packages/traitlets
    return self.get(obj, cls)
  File "$venv/lib/python3.9/site-packages/traitlets
    value = self._validate(obj, default)
  File "$venv/lib/python3.9/site-packages/traitlets
    value = self.validate(obj, value)
  File "$venv/lib/python3.9/site-packages/traitlets
    self.error(obj, value)
  File "$venv/lib/python3.9/site-packages/traitlets
    raise TraitError(e)
traitlets.traitlets.TraitError: The 'x' trait of an A instance expected an int,

>>> from traitlets.config import Config
>>> a = A(config=Config({"A": {"x": 1}}))
>>> a.x

(Using a positional argument in __init__() is also a totally reasonable way to do it, if you're directly constructing the objects and not using Configurable.)

@metaperl
Copy link
Contributor Author

metaperl commented Mar 6, 2023

I really like default_value=None, allow_none=False. It would be nice if there were a dictionary:

REQUIRED = default_value=None, allow_none=False

Then we could just go:

x = traitlets.Int(**REQUIRED, config=True)

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

No branches or pull requests

3 participants