lebrice / SimpleParsing

Simple, Elegant, Typed Argument Parsing with argparse
MIT License
427 stars 52 forks source link

Get final flag values without parsing #101

Closed pbarker closed 2 years ago

pbarker commented 2 years ago

Is your feature request related to a problem? Please describe. A way to get flag values once they are merged. This comes from using Amazon Sagemaker where they want CLI arguments formatted as a dictionary, which they then apply to the container.

Describe the solution you'd like

    parser = ArgumentParser()

    parser.add_arguments(TrainArgs, dest="train")
    parser.add_arguments(SagemakerArgs, dest="sagemaker")
    parser.add_arguments(EvaluationArgs, dest="eval")

    parser.values()

    outputs:
    {"foo": "bar", "train.baz": "qux"}

Describe alternatives you've considered I've been digging through the code and don't see an easy way of doing this today but please let me know if I missed something!

pbarker commented 2 years ago

Found a way to do this, and in case anyone needs it in the future:

import logging
import os
from datetime import datetime
from logging import Logger
from typing import Dict, List, Tuple, Any
from simple_parsing import ArgumentParser

def hyperparameters_from_parser(parser: ArgumentParser, ns_map: Dict[str, object]) -> Dict[str, Any]:
    """Generate Sagemaker style hyperparameters from a parser. This exists because the simple_parser
       will do conflict resolution across dataclasses and change flag names. Sagemaker then needs a 
       dictionary of those new flag names

    Args:
        parser (ArgumentParser): parser to use, should not have called parse_args() at this time
        ns_map (Dict[str, object]): a map of parser namespace to corresponding object e.g. {'train': MyTrainArgsDataclass}

    Returns:
        Dict[str, Any]: A map of arguments to their values
    """

    # get the final argument names
    parser._resolve_conflicts()

    # dynamically create a variable by the same name as the argparse namespace, set equal to the corresponding object
    for ns, obj in ns_map.items():
        # check that the namespace exists in the parser
        ns_exists = False
        for wrapper in parser._wrappers:
            if wrapper.name == ns:
                ns_exists = True
                break
        if not ns_exists:
            raise ValueError(f"namespace: {ns} - does not exist in parser")
        globals()[ns] = obj

    hyperparameters = {}
    for wrapper in parser._wrappers:
        for f in wrapper.fields:
            # get the command line argument and clean
            arg = f.option_strings[0]
            plain_arg = arg.lstrip("--")

            # from the dynamically create variable above, access its properties dynamically and pull out the value
            # which corresponds to the command line argument
            dest = f.arg_options["dest"]
            e = f"val = {dest}"
            l = locals()
            exec(e, globals(), l)
            val = l["val"]

            hyperparameters[plain_arg] = val

    return hyperparameters

parser = ArgumentParser()

parser.add_arguments(SagemakerArgs, dest="sagemaker")
parser.add_arguments(TrainArgs, dest="train")
parser.add_arguments(EvaluationArgs, dest="eval")

sm_args: SagemakerArgs = args.sagemaker
train_args: TrainArgs = args.train
eval_args: EvaluationArgs = args.eval

hyperparameters = hyperparameters_from_parser(
    parser, {"sagemaker": sm_args, "train": train_args, "eval": eval_args}
)