SCons / scons

SCons - a software construction tool
http://scons.org
MIT License
2.04k stars 312 forks source link

Check if option was set when default value is used #3125

Open dmoody256 opened 6 years ago

dmoody256 commented 6 years ago

I sent this to the user mailing list and also wrote a stack overflow question: https://stackoverflow.com/questions/50396802/checking-if-arg-was-passed-when-default-is-set-with-python-optparse

In my case I am interested in num_jobs options, but this could apply to any option.

I want to determine if the option was set by the user even in the default case. So far I can only figure out how to do this by looking through the sys.argv, but considering that the option parser is already doing this, maybe it should save this information at the time it is parsing the args.

Since Scons already extends pythons optparse class, this should be pretty easy to add.

Here is how I did it by manually checking sys.argv:

###################################################
# Determine number of Jobs
# start by assuming num_jobs was not set
NUM_JOBS_SET = False
if GetOption("num_jobs") == 1:
    # if num_jobs is the default we need to check sys.argv
    # to see if the user happened to set the default
    for arg in sys.argv:
        if arg.startswith("-j") or arg.startswith("--jobs"):
            if arg == "-j" or arg == "--jobs":
                if(int(sys.argv[sys.argv.index(arg)+1]) == 1):
                    NUM_JOBS_SET = True
            else:
                if arg.startswith("-j"):
                    if(int(arg[2:]) == 1):
                        NUM_JOBS_SET = True
else:
    # user must have set something if it wasn't default
    NUM_JOBS_SET = True

# num_jobs wasn't specified so lets use the
# max number since the user doesn't seem to care
if not NUM_JOBS_SET:
    NUM_CPUS = get_num_cpus()
    print("Building with " + str(NUM_CPUS) + " parallel jobs")
    MAIN_ENV.SetOption("num_jobs", NUM_CPUS)
else:
    # user wants a certain number of jobs so do that
    print("Building with " + str(GetOption('num_jobs')) + " parallel jobs")

Here's how I did by using optparse:

###################################################
# Determine number of Jobs
# start by assuming num_jobs was not set
opts_no_defaults = optparse.Values()
parser = Parser(MAIN_ENV._get_major_minor_revision(SCons.__version__))
__, args = parser.parse_args(values=opts_no_defaults)
opts = optparse.Values(parser.get_default_values().__dict__)
opts._update_careful(opts_no_defaults.__dict__)

# num_jobs wasn't specificed so let use the
# max number since the user doesn't seem to care
if not hasattr(opts_no_defaults, parser.get_option('--jobs').dest):
    NUM_CPUS = get_num_cpus()
    ColorPrinter().InfoPrint("Building with " + str(NUM_CPUS) + " parallel jobs")
    MAIN_ENV.SetOption("num_jobs", NUM_CPUS)
else:
    # user wants a certain number of jobs so do that
    ColorPrinter().InfoPrint(
        "Building with " + str(GetOption('num_jobs')) + " parallel jobs")
mwichmann commented 5 years ago

Does the normal trick for this kind of thing work here? Make the default be None, and if the value is still None when processing, set the "real" default.

mwichmann commented 3 years ago

SCons "knows" which source a value came from, since it walks through the list (cli param, SetOption, defaults from option definitions) in SConsValues.__getattr__, but it doesn't record or have a clean way to return that info. Maybe there's a way to solve that?

mwichmann commented 2 years ago

I see there was a kind of solution in the linked StackOverflow article. Is there anything from that we can bring back to SCons itself? Or alternatively add as a recipe for someone else with the same need at https://scons-cookbook.readthedocs.io/en/latest ?

mwichmann commented 2 years ago

Can't remember if this got mentioned somewhere else. How about creating an options object that holds the "real" defaults, and put dummy ones (some kind of sentinel) in the add_option calls; then add a processing step where if any options have the dummy default you fill them in from the defaults object - that way there's a difference between an option not set, and an option set by the user to the default value.

mwichmann commented 2 months ago

Leaving a note here that argparse has something for this, sort of... you can cause an attribute not to be added to the parsed object if the argument was not present in the parsed input (cli, usually). optparse does not have this.

>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo', default=argparse.SUPPRESS)
>>> parser.add_argument('--bar', default=22)
>>> parser.parse_args([])  # input source is empty...
Namespace(bar=22)   # 'bar' took its default, 'foo' omitted
mwichmann commented 2 months ago

I think the recipe in the original submission is about the only way to do this in optparse. Given the relative complexity, is it actually worth putting this in and wrapping it up in something that can be queried? I almost lean to the "write it up in Cookbook" approach. The behavior of passing an existing Values object to parse_args to avoid it populating with defaults is documented in the optparse "manpage". But the behavior of calling the parser's get_default_values method does not seem to be, that I can find, nor is passing a dict of defaults to the Values constructor. And _update_careful is clearly intended to be an internal method, and is also not in the docs. Some maybe should have been, but the status of optparse (deprecated but not planned for removal) means it won't get those kinds of doc updates.