diff --git a/docs/api_reference.md b/docs/api_reference.md new file mode 100644 index 00000000..78859003 --- /dev/null +++ b/docs/api_reference.md @@ -0,0 +1,29 @@ +# API Reference + +This section provides an auto-generated API reference for the `simple_parsing` library. + +## Core Parsing Logic + +::: simple_parsing.parsing + +## Serializable Objects + +::: simple_parsing.helpers.serialization.serializable.Serializable + +## Dataclass Utilities + +::: simple_parsing.wrappers.dataclass_wrapper + +## Field Wrappers + +::: simple_parsing.wrappers.field_wrapper + +## ArgumentParser + +::: simple_parsing.ArgumentParser + +## Other Helpers + +You can add more specific modules or classes here as needed. +For example, to document the `simple_parsing.helpers.hparams` module: +::: simple_parsing.helpers.hparams diff --git a/docs/examples/ML/README.md b/docs/examples/ML/README.md new file mode 100644 index 00000000..f9baa69a --- /dev/null +++ b/docs/examples/ML/README.md @@ -0,0 +1,90 @@ +## Use-Case Example: ML Scripts + +Let's look at a great use-case for `simple-parsing`: ugly ML code: + +### Before: + +```python +import argparse + +parser = argparse.ArgumentParser() + +# hyperparameters +parser.add_argument("--learning_rate", type=float, default=0.05) +parser.add_argument("--momentum", type=float, default=0.01) +# (... other hyperparameters here) + +# args for training config +parser.add_argument("--data_dir", type=str, default="/data") +parser.add_argument("--log_dir", type=str, default="/logs") +parser.add_argument("--checkpoint_dir", type=str, default="checkpoints") + +args = parser.parse_args() + +learning_rate = args.learning_rate +momentum = args.momentum +# (...) dereference all the variables here, without any typing +data_dir = args.data_dir +log_dir = args.log_dir +checkpoint_dir = args.checkpoint_dir + +class MyModel(): + def __init__(self, data_dir, log_dir, checkpoint_dir, learning_rate, momentum, *args): + # config: + self.data_dir = data_dir + self.log_dir = log_dir + self.checkpoint_dir = checkpoint_dir + + # hyperparameters: + self.learning_rate = learning_rate + self.momentum = momentum + +m = MyModel(data_dir, log_dir, checkpoint_dir, learning_rate, momentum) +# Ok, what if we wanted to add a new hyperparameter?! +``` + +### After: + +```python +from dataclasses import dataclass +from simple_parsing import ArgumentParser + +# create a parser, as usual +parser = ArgumentParser() + +@dataclass +class MyModelHyperParameters: + """Hyperparameters of MyModel""" + # Learning rate of the Adam optimizer. + learning_rate: float = 0.05 + # Momentum of the optimizer. + momentum: float = 0.01 + +@dataclass +class TrainingConfig: + """Training configuration settings""" + data_dir: str = "/data" + log_dir: str = "/logs" + checkpoint_dir: str = "checkpoints" + + +# automatically add arguments for all the fields of the classes above: +parser.add_arguments(MyModelHyperParameters, dest="hparams") +parser.add_arguments(TrainingConfig, dest="config") + +args = parser.parse_args() + +# Create an instance of each class and populate its values from the command line arguments: +hyperparameters: MyModelHyperParameters = args.hparams +config: TrainingConfig = args.config + +class MyModel(): + def __init__(self, hyperparameters: MyModelHyperParameters, config: TrainingConfig): + # hyperparameters: + self.hyperparameters = hyperparameters + # config: + self.config = config + +m = MyModel(hyperparameters, config) + +``` diff --git a/docs/examples/ML/ml_example_after.py b/docs/examples/ML/ml_example_after.py new file mode 100644 index 00000000..c69ad76c --- /dev/null +++ b/docs/examples/ML/ml_example_after.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + +# create a parser, as usual +parser = ArgumentParser() + + +@dataclass +class MyModelHyperParameters: + """Hyperparameters of MyModel.""" + + # Learning rate of the Adam optimizer. + learning_rate: float = 0.05 + # Momentum of the optimizer. + momentum: float = 0.01 + + +@dataclass +class TrainingConfig: + """Training configuration settings.""" + + data_dir: str = "/data" + log_dir: str = "/logs" + checkpoint_dir: str = "checkpoints" + + +# automatically add arguments for all the fields of the classes above: +parser.add_arguments(MyModelHyperParameters, dest="hparams") +parser.add_arguments(TrainingConfig, dest="config") + +args = parser.parse_args() + +# Create an instance of each class and populate its values from the command line arguments: +hyperparameters: MyModelHyperParameters = args.hparams +config: TrainingConfig = args.config + + +class MyModel: + def __init__(self, hyperparameters: MyModelHyperParameters, config: TrainingConfig): + # hyperparameters: + self.hyperparameters = hyperparameters + # config: + self.config = config + + +m = MyModel(hyperparameters, config) diff --git a/docs/examples/ML/ml_example_before.py b/docs/examples/ML/ml_example_before.py new file mode 100644 index 00000000..95a72ac6 --- /dev/null +++ b/docs/examples/ML/ml_example_before.py @@ -0,0 +1,38 @@ +from argparse import ArgumentParser + +parser = ArgumentParser() + +# hyperparameters +parser.add_argument("--learning_rate", type=float, default=0.05) +parser.add_argument("--momentum", type=float, default=0.01) +# (... other hyperparameters here) + +# args for training config +parser.add_argument("--data_dir", type=str, default="/data") +parser.add_argument("--log_dir", type=str, default="/logs") +parser.add_argument("--checkpoint_dir", type=str, default="checkpoints") + +args = parser.parse_args() + +learning_rate = args.learning_rate +momentum = args.momentum +# (...) dereference all the variables here, without any typing +data_dir = args.data_dir +log_dir = args.log_dir +checkpoint_dir = args.checkpoint_dir + + +class MyModel: + def __init__(self, data_dir, log_dir, checkpoint_dir, learning_rate, momentum, *args): + # config: + self.data_dir = data_dir + self.log_dir = log_dir + self.checkpoint_dir = checkpoint_dir + + # hyperparameters: + self.learning_rate = learning_rate + self.momentum = momentum + + +m = MyModel(data_dir, log_dir, checkpoint_dir, learning_rate, momentum) +# Ok, what if we wanted to add a new hyperparameter?! diff --git a/docs/examples/ML/other_ml_example.py b/docs/examples/ML/other_ml_example.py new file mode 100644 index 00000000..089cadef --- /dev/null +++ b/docs/examples/ML/other_ml_example.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass + +import simple_parsing + +# create a parser, +parser = simple_parsing.ArgumentParser() + + +@dataclass +class MyModelHyperParameters: + """Hyperparameters of MyModel.""" + + # Batch size (per-GPU) + batch_size: int = 32 + # Learning rate of the Adam optimizer. + learning_rate: float = 0.05 + # Momentum of the optimizer. + momentum: float = 0.01 + + +@dataclass +class TrainingConfig: + """Settings related to Training.""" + + data_dir: str = "data" + log_dir: str = "logs" + checkpoint_dir: str = "checkpoints" + + +@dataclass +class EvalConfig: + """Settings related to evaluation.""" + + eval_dir: str = "eval_data" + + +# automatically add arguments for all the fields of the classes above: +parser.add_arguments(MyModelHyperParameters, "hparams") +parser.add_arguments(TrainingConfig, "train_config") +parser.add_arguments(EvalConfig, "eval_config") + +# NOTE: `ArgumentParser` is just a subclass of `argparse.ArgumentParser`, +# so we could add some other arguments as usual: +# parser.add_argument(...) +# parser.add_argument(...) +# (...) +# parser.add_argument(...) +# parser.add_argument(...) + +args = parser.parse_args() + +# Retrieve the objects from the parsed args! +hparams: MyModelHyperParameters = args.hparams +train_config: TrainingConfig = args.train_config +eval_config: EvalConfig = args.eval_config + +print(hparams, train_config, eval_config, sep="\n") +expected = """ +MyModelHyperParameters(batch_size=32, learning_rate=0.05, momentum=0.01) +TrainingConfig(data_dir='data', log_dir='logs', checkpoint_dir='checkpoints') +EvalConfig(eval_dir='eval_data') +""" diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 00000000..f957fc5e --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,25 @@ +# Examples + +- [dataclasses intro](dataclasses/README.md): Quick intro to Python's new [dataclasses](https://docs.python.org/3.7/library/dataclasses.html) module. + +- **[Simple example](simple/basic.py)**: Simple use-case example with a before/after comparison. + +- [ML-related Examples](ML/README.md) + +- **NEW**: [Subgroups Example](subgroups/README.md) + + + +- [Serialization to `json`/`yaml`](serialization/README.md) + +- [Attribute Docstrings Example](docstrings/README.md) + +- [Parsing of lists and tuples](container_types/README.md) + +- [**Nesting**!!](nesting/README.md) + +- [Prefixing](prefixing/README.md) + +- [Enums Example](enums/README.md) + +- [Subparsers Example](subparsers/README.md) diff --git a/docs/examples/__init__.py b/docs/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docs/examples/aliases/README.md b/docs/examples/aliases/README.md new file mode 100644 index 00000000..79b251b1 --- /dev/null +++ b/docs/examples/aliases/README.md @@ -0,0 +1,62 @@ +# Using Aliases + +## Notes about `option_strings`: + +Additional names for the same argument can be added via the `alias` argument +of the `field` function (see [the custom_args Example](../custom_args/README.md) for more info). + +The `simple_parsing.ArgumentParser` accepts an argument (currently called `add_option_string_dash_variants`, which defaults to False) which adds additional variants to allow using either dashes or underscores to refer to an argument: + +- Whenever the name of an attribute includes underscores ("\_"), the same + argument can be passed by using dashes ("-") instead. This also includes + aliases. +- If an alias contained leading dashes, either single or double, the + same number of dashes will be used, even in the case where a prefix is + added. + For instance, consider the following example. + Here we have two prefixes: `"train"` and `"valid"`. + The corresponding option_strings for each argument will be + `["--train.debug", "-train.d"]` and `["--valid.debug", "-valid.d"]`, + respectively, as shown here: + +```python +from dataclasses import dataclass +from simple_parsing import ArgumentParser, field + +@dataclass +class RunSettings: + ''' Parameters for a run. ''' + # whether or not to execute in debug mode. + debug: bool = field(alias=["-d"], default=False) + some_value: int = field(alias=["-v"], default=123) + +parser = ArgumentParser(add_option_string_dash_variants=True) +parser.add_arguments(RunSettings, dest="train") +parser.add_arguments(RunSettings, dest="valid") +parser.print_help() + +# This prints: +''' +usage: test.py [-h] [--train.debug [bool]] [--train.some_value int] + [--valid.debug [bool]] [--valid.some_value int] + +optional arguments: + -h, --help show this help message and exit + +RunSettings ['train']: + Parameters for a run. + + --train.debug [bool], --train.d [bool] + whether or not to execute in debug mode. (default: + False) + --train.some_value int, --train.v int + +RunSettings ['valid']: + Parameters for a run. + + --valid.debug [bool], --valid.d [bool] + whether or not to execute in debug mode. (default: + False) + --valid.some_value int, --valid.v int +''' +``` diff --git a/docs/examples/aliases/aliases_example.py b/docs/examples/aliases/aliases_example.py new file mode 100644 index 00000000..85014205 --- /dev/null +++ b/docs/examples/aliases/aliases_example.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser, field + + +@dataclass +class RunSettings: + """Parameters for a run.""" + + # whether or not to execute in debug mode. + debug: bool = field(alias=["-d"], default=False) + # whether or not to add a lot of logging information. + verbose: bool = field(alias=["-v"], action="store_true") + + +parser = ArgumentParser(add_option_string_dash_variants=True) +parser.add_arguments(RunSettings, dest="train") +parser.add_arguments(RunSettings, dest="valid") +args = parser.parse_args() +print(args) +# This prints: +expected = """ +Namespace(train=RunSettings(debug=False, verbose=False), valid=RunSettings(debug=False, verbose=False)) +""" + +parser.print_help() +expected += """\ +usage: aliases_example.py [-h] [-train.d bool] [-train.v] [-valid.d bool] + [-valid.v] + +optional arguments: + -h, --help show this help message and exit + +RunSettings ['train']: + Parameters for a run. + + -train.d bool, --train.debug bool, --train.nod bool, --train.nodebug bool + whether or not to execute in debug mode. (default: + False) + -train.v, --train.verbose + whether or not to add a lot of logging information. + (default: False) + +RunSettings ['valid']: + Parameters for a run. + + -valid.d bool, --valid.debug bool, --valid.nod bool, --valid.nodebug bool + whether or not to execute in debug mode. (default: + False) + -valid.v, --valid.verbose + whether or not to add a lot of logging information. + (default: False) +""" diff --git a/docs/examples/config_files/README.md b/docs/examples/config_files/README.md new file mode 100644 index 00000000..2464bb90 --- /dev/null +++ b/docs/examples/config_files/README.md @@ -0,0 +1,32 @@ +# Using config files + +Simple-Parsing can use default values from one or more configuration files. + +The `config_path` argument can be passed to the ArgumentParser constructor. The values read from +that file will overwrite the default values from the dataclass definitions. + +Additionally, when the `add_config_path_arg` argument of the `ArgumentParser` constructor is set, +a `--config_path` argument will be added to the parser. This argument accepts one or more paths to configuration +files, whose contents will be read, and used to update the defaults, in the same manner as with the +`config_path` argument above. + +When using both options (the `config_path` parameter of `ArgumentParser.__init__`, as well as the `--config_path` command-line argument), the defaults are first updated using `ArgumentParser.config_path`, and then +updated with the contents of the `--config_path` file(s). + +In other words, the default values are set like so, in increasing priority: + +1. normal defaults (e.g. from the dataclass definitions) +2. updated with the contents of the `config_path` file(s) of `ArgumentParser.__init__` +3. updated with the contents of the `--config_path` file(s) from the command-line. + +## [Single Config example](one_config.py) + +When using a single config dataclass, the `simple_parsing.parse` function can then be used to simplify the argument parsing setup a bit. + +## [Multiple Configs](many_configs.py) + +Config files can also be used when defining multiple config dataclasses with the same parser. + +## [Composition (WIP)](composition.py) + +(Coming soon): Multiple config files can be composed together à-la Hydra! diff --git a/docs/examples/config_files/composition.py b/docs/examples/config_files/composition.py new file mode 100644 index 00000000..1e7e22aa --- /dev/null +++ b/docs/examples/config_files/composition.py @@ -0,0 +1,65 @@ +"""Example where we compose different configurations!""" + +import shlex +from dataclasses import dataclass + +import simple_parsing + + +@dataclass +class Foo: + a: str = "default value for `a` (from the dataclass definition of Foo)" + + +@dataclass +class Bar: + b: str = "default value for `b` (from the dataclass definition of Bar)" + + +@dataclass +class Baz: + c: str = "default value for `c` (from the dataclass definition of Baz)" + + +def main(args=None) -> None: + """Example using composition of different configurations.""" + parser = simple_parsing.ArgumentParser( + add_config_path_arg=True, config_path="composition_defaults.yaml" + ) + + parser.add_arguments(Foo, dest="foo") + parser.add_arguments(Bar, dest="bar") + parser.add_arguments(Baz, dest="baz") + + if isinstance(args, str): + args = shlex.split(args) + args = parser.parse_args(args) + + foo: Foo = args.foo + bar: Bar = args.bar + baz: Baz = args.baz + print(f"foo: {foo}") + print(f"bar: {bar}") + print(f"baz: {baz}") + + +main() +expected = """ +foo: Foo(a="default value for `a` from the Parser's `config_path` (composition_defaults.yaml)") +bar: Bar(b="default value for `b` from the Parser's `config_path` (composition_defaults.yaml)") +baz: Baz(c="default value for `c` from the Parser's `config_path` (composition_defaults.yaml)") +""" + +main("--a 'Value passed from the command-line.' --config_path config_b.yaml") +expected += """\ +foo: Foo(a='Value passed from the command-line.') +bar: Bar(b='default value for `b` from the config_b.yaml file') +baz: Baz(c="default value for `c` from the Parser's `config_path` (composition_defaults.yaml)") +""" + +main("--a 'Value passed from the command-line.' --config_path config_a.yaml config_b.yaml") +expected += """\ +foo: Foo(a='Value passed from the command-line.') +bar: Bar(b='default value for `b` from the config_b.yaml file') +baz: Baz(c="default value for `c` from the Parser's `config_path` (composition_defaults.yaml)") +""" diff --git a/docs/examples/config_files/composition_defaults.yaml b/docs/examples/config_files/composition_defaults.yaml new file mode 100644 index 00000000..481f83bd --- /dev/null +++ b/docs/examples/config_files/composition_defaults.yaml @@ -0,0 +1,8 @@ +foo: + a: "default value for `a` from the Parser's `config_path` (composition_defaults.yaml)" + +bar: + b: "default value for `b` from the Parser's `config_path` (composition_defaults.yaml)" + +baz: + c: "default value for `c` from the Parser's `config_path` (composition_defaults.yaml)" diff --git a/docs/examples/config_files/config_a.yaml b/docs/examples/config_files/config_a.yaml new file mode 100644 index 00000000..494910b0 --- /dev/null +++ b/docs/examples/config_files/config_a.yaml @@ -0,0 +1,2 @@ +foo: + a: "default value for `a` from the config_a.yaml file" diff --git a/docs/examples/config_files/config_b.yaml b/docs/examples/config_files/config_b.yaml new file mode 100644 index 00000000..39cb3e74 --- /dev/null +++ b/docs/examples/config_files/config_b.yaml @@ -0,0 +1,2 @@ +bar: + b: "default value for `b` from the config_b.yaml file" diff --git a/docs/examples/config_files/many_configs.py b/docs/examples/config_files/many_configs.py new file mode 100644 index 00000000..41807306 --- /dev/null +++ b/docs/examples/config_files/many_configs.py @@ -0,0 +1,56 @@ +import shlex +from dataclasses import dataclass + +import simple_parsing + + +@dataclass +class TrainConfig: + """Training config for Machine Learning.""" + + workers: int = 8 # The number of workers for training + exp_name: str = "default_exp" # The experiment name + + +@dataclass +class EvalConfig: + """Evaluation config.""" + + n_batches: int = 8 # The number of batches for evaluation + checkpoint: str = "best.pth" # The checkpoint to use + + +def main(args=None) -> None: + parser = simple_parsing.ArgumentParser(add_config_path_arg=True) + + parser.add_arguments(TrainConfig, dest="train") + parser.add_arguments(EvalConfig, dest="eval") + + if isinstance(args, str): + args = shlex.split(args) + args = parser.parse_args(args) + + train_config: TrainConfig = args.train + eval_config: EvalConfig = args.eval + print(f"Training {train_config.exp_name} with {train_config.workers} workers...") + print(f"Evaluating '{eval_config.checkpoint}' with {eval_config.n_batches} batches...") + + +main() +expected = """ +Training default_exp with 8 workers... +Evaluating 'best.pth' with 8 batches... +""" + + +main("") +expected += """\ +Training default_exp with 8 workers... +Evaluating 'best.pth' with 8 batches... +""" + +main("--config_path many_configs.yaml --exp_name my_first_exp") +expected += """\ +Training my_first_exp with 42 workers... +Evaluating 'best.pth' with 100 batches... +""" diff --git a/docs/examples/config_files/many_configs.yaml b/docs/examples/config_files/many_configs.yaml new file mode 100644 index 00000000..cfeca267 --- /dev/null +++ b/docs/examples/config_files/many_configs.yaml @@ -0,0 +1,6 @@ +train: + exp_name: my_yaml_exp + workers: 42 +eval: + checkpoint: best.pth + n_batches: 100 diff --git a/docs/examples/config_files/one_config.py b/docs/examples/config_files/one_config.py new file mode 100644 index 00000000..d09733a7 --- /dev/null +++ b/docs/examples/config_files/one_config.py @@ -0,0 +1,37 @@ +"""Example adapted from https://github.com/eladrich/pyrallis#my-first-pyrallis-example-""" + +from dataclasses import dataclass + +import simple_parsing + + +@dataclass +class TrainConfig: + """Training configuration.""" + + workers: int = 8 # The number of workers for training + exp_name: str = "default_exp" # The experiment name + + +def main(args=None) -> None: + cfg = simple_parsing.parse( + config_class=TrainConfig, args=args, add_config_path_arg="config-file" + ) + print(f"Training {cfg.exp_name} with {cfg.workers} workers...") + + +main() +expected = """ +Training default_exp with 8 workers... +""" + +main("") +expected += """\ +Training default_exp with 8 workers... +""" + +# NOTE: When running as in the readme: +main("--config-file one_config.yaml --exp_name my_first_exp") +expected += """\ +Training my_first_exp with 42 workers... +""" diff --git a/docs/examples/config_files/one_config.yaml b/docs/examples/config_files/one_config.yaml new file mode 100644 index 00000000..5b255e06 --- /dev/null +++ b/docs/examples/config_files/one_config.yaml @@ -0,0 +1,2 @@ +exp_name: my_yaml_exp +workers: 42 diff --git a/docs/examples/container_types/README.md b/docs/examples/container_types/README.md new file mode 100644 index 00000000..7959c029 --- /dev/null +++ b/docs/examples/container_types/README.md @@ -0,0 +1,11 @@ +# Parsing Container-type Arguments (list, tuple) + +In "vanilla" argparse, it is usually difficult to parse lists and tuples. + +`simple-parsing` makes it easier, by leveraging the type-annotations of the builtin `typing` module. Simply mark you attributes using the corresponding annotation, and the item types will be automatically converted for you: + + + +```python + +``` diff --git a/docs/examples/container_types/lists_example.py b/docs/examples/container_types/lists_example.py new file mode 100644 index 00000000..7eca3c0a --- /dev/null +++ b/docs/examples/container_types/lists_example.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass, field + +from simple_parsing import ArgumentParser +from simple_parsing.helpers import list_field + + +@dataclass +class Example: + some_integers: list[int] = field( + default_factory=list + ) # This is a list of integers (empty by default) + """This list is empty, by default. + + when passed some parameters, they are automatically converted to integers, since we annotated + the attribute with a type (typing.List[]). + """ + + # When using a list attribute, the dataclasses module requires us to use `dataclass.field()`, + # so each instance of this class has a different list, rather than them sharing the same list. + # To simplify this, you can use `MutableField(value)` which is just a shortcut for `field(default_factory=lambda: value)`. + some_floats: list[float] = list_field(3.14, 2.56) + + some_list_of_strings: list[str] = list_field("default_1", "default_2") + """This list has a default value of ["default_1", "default_2"].""" + + +parser = ArgumentParser() +parser.add_arguments(Example, "example") +args = parser.parse_args() + +example: Example = args.example +print(example) +expected = "Example(some_integers=[], some_floats=[3.14, 2.56], some_list_of_strings=['default_1', 'default_2'])" diff --git a/docs/examples/custom_args/README.md b/docs/examples/custom_args/README.md new file mode 100644 index 00000000..2c8d5398 --- /dev/null +++ b/docs/examples/custom_args/README.md @@ -0,0 +1,75 @@ +# Custom Argparse Arguments + +The `dataclasses.field()` function is used to customize the declaration of +fields on a dataclass. It accepts, among others, the `default`, +`default_factory`, arguments used to set the default instance values to fields +(please take a look at the [official documentation](https://docs.python.org/3/library/dataclasses.html#dataclasses.field) For more +information). + +`simple-parsing` provides an overloaded version of this function: +`simple_parsing.field()`, which, in addition to all the above-mentioned keyword +arguments of the `field()` method, **can be passed any of the arguments +of the usual `add_argument(*option_strings, **kwargs)` function!**. + +The values passed this way take precedence over those auto-generated by +`simple-parsing`, allowing you to do pretty much anything you want. + +## Examples + +- ### List of choices + + For example, here is how you would create a list of choices, whereby any + of the passed arguments can only be contained within the choices: + + ```python + + from dataclasses import dataclass + from simple_parsing import ArgumentParser, field + from typing import List + + @dataclass + class Foo: + """ Some class Foo """ + + # A sequence of tasks. + task_sequence: List[str] = field(choices=["train", "test", "ood"]) + + parser = ArgumentParser() + parser.add_arguments(Foo, "foo") + + args = parser.parse_args("--task_sequence train train ood".split()) + foo: Foo = args.foo + print(foo) + assert foo.task_sequence == ["train", "train", "ood"] + + ``` + +- ### Adding additional aliases for an argument + + By passing the + + ```python + @dataclass + class Foo(TestSetup): + """ Some example Foo. """ + # The output directory. (can be passed using any of "-o" or --out or ) + output_dir: str = field( + default="/out", + alias=["-o", "--out"], + choices=["/out", "/bob"] + ) + + foo = Foo.setup("--output_dir /bob") + assert foo.output_dir == "/bob" + + with raises(): + foo = Foo.setup("-o /cat") + assert foo.output_dir == "/cat" + + foo = Foo.setup("--out /bob") + assert foo.output_dir == "/bob" + ``` + +- ### Adding Flags with "store-true" or "store-false" + + Additionally, diff --git a/docs/examples/custom_args/custom_args_example.py b/docs/examples/custom_args/custom_args_example.py new file mode 100644 index 00000000..79b708fe --- /dev/null +++ b/docs/examples/custom_args/custom_args_example.py @@ -0,0 +1,85 @@ +"""Example of overwriting auto-generated argparse options with custom ones.""" + +from dataclasses import dataclass + +from simple_parsing import ArgumentParser, field +from simple_parsing.helpers import list_field + + +def parse(cls, args: str = ""): + """Removes some boilerplate code from the examples.""" + parser = ArgumentParser() # Create an argument parser + parser.add_arguments(cls, "example") # add arguments for the dataclass + ns = parser.parse_args(args.split()) # parse the given `args` + return ns.example # return the dataclass instance + + +# Example 1: List of Choices: + + +@dataclass +class Example1: + # A list of animals to take on a walk. (can only be passed 'cat' or 'dog') + pets_to_walk: list[str] = list_field(default=["dog"], choices=["cat", "dog"]) + + +# passing no arguments uses the default values: +assert parse(Example1, "") == Example1(pets_to_walk=["dog"]) +assert parse(Example1, "--pets_to_walk") == Example1(pets_to_walk=[]) +assert parse(Example1, "--pets_to_walk cat") == Example1(pets_to_walk=["cat"]) +assert parse(Example1, "--pets_to_walk dog dog cat") == Example1( + pets_to_walk=["dog", "dog", "cat"] +) + + +# # Passing a value not in 'choices' produces an error: +# with pytest.raises(SystemExit): +# example = parse(Example1, "--pets_to_walk racoon") +# expected = """ +# usage: custom_args_example.py [-h] [--pets_to_walk [{cat,dog,horse} [{cat,dog} ...]]] +# custom_args_example.py: error: argument --pets_to_walk: invalid choice: 'racoon' (choose from 'cat', 'dog') +# """ + + +# Example 2: Additional Option Strings + + +@dataclass +class Example2: + # (This argument can be passed either as "-i" or "--input_dir") + input_dir: str = field("./in", alias="-i") + # (This argument can be passed either as "-o", "--out", or "--output_dir") + output_dir: str = field("./out", alias=["-o", "--out"]) + + +assert parse(Example2, "-i tmp/data") == Example2(input_dir="tmp/data") +assert parse(Example2, "-o tmp/data") == Example2(output_dir="tmp/data") +assert parse(Example2, "--out louise") == Example2(output_dir="louise") +assert parse(Example2, "--input_dir louise") == Example2(input_dir="louise") +assert parse(Example2, "--output_dir joe/annie") == Example2(output_dir="joe/annie") +assert parse(Example2, "-i input -o output") == Example2(input_dir="input", output_dir="output") + + +# Example 3: Using other actions (store_true, store_false, store_const, etc.) + + +@dataclass +class Example3: + """Examples with other actions.""" + + b: bool = False + debug: bool = field(alias="-d", action="store_true") + verbose: bool = field(alias="-v", action="store_true") + + cache: bool = False + # cache: bool = field(default=True, "--no_cache", "store_false") + # no_cache: bool = field(dest=cache, action="store_false") + + +parser = ArgumentParser() +parser.add_arguments(Example3, "example") +args = parser.parse_args() +example = args.example +print(example) +delattr(args, "example") +assert not vars(args) diff --git a/docs/examples/dataclasses/README.md b/docs/examples/dataclasses/README.md new file mode 100644 index 00000000..47b953cd --- /dev/null +++ b/docs/examples/dataclasses/README.md @@ -0,0 +1,19 @@ +# Dataclasses + +These are simple examples showing how to use `@dataclass` to create argument classes. + +First, take a look at the official [dataclasses module documentation](https://docs.python.org/3.7/library/dataclasses.html). + +With `simple-parsing`, groups of attributes are defined directly in code as dataclasses, each holding a set of related parameters. Methods can also be added to these dataclasses, which helps to promote the "Separation of Concerns" principle by keeping all the logic related to argument parsing in the same place as the arguments themselves. + +## Examples: + +- [dataclass_example.py](dataclass_example.py): a simple toy example showing an example of a dataclass + +- [hyperparameters_example.py](hyperparameters_example.py): Shows an example of an argument dataclass which also defines serialization methods. + + + +NOTE: For attributes of a mutable type (a type besides `int`, `float`, `bool` or `str`, such as `list`, `tuple`, or `object` or any of their subclasses), it is highly recommended (and often required) to use the `field` function of the dataclasses module, and to specify either a default value or a default factory function. + +To simplify this, `simple-parsing` provides `MutableField`, a convenience function, which directly sets the passed argument as the return value of an anonymous factory function. diff --git a/docs/examples/dataclasses/dataclass_example.py b/docs/examples/dataclasses/dataclass_example.py new file mode 100644 index 00000000..f55c8f45 --- /dev/null +++ b/docs/examples/dataclasses/dataclass_example.py @@ -0,0 +1,31 @@ +import math +from dataclasses import dataclass, fields + + +@dataclass +class Point: + """Simple class Point.""" + + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + + def distance(self, other: "Point") -> float: + return math.sqrt( + (self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2 + ) + + +p1 = Point(x=1, y=3) +p2 = Point(x=1.0, y=3.0) + +assert p1 == p2 + +for field in fields(p1): + print(f"Field {field.name} has type {field.type} and a default value if {field.default}.") + +expected = """ +Field x has type and a default value if 0.0. +Field y has type and a default value if 0.0. +Field z has type and a default value if 0.0. +""" diff --git a/docs/examples/dataclasses/hyperparameters_example.py b/docs/examples/dataclasses/hyperparameters_example.py new file mode 100644 index 00000000..b590dde4 --- /dev/null +++ b/docs/examples/dataclasses/hyperparameters_example.py @@ -0,0 +1,48 @@ +""" - Argument dataclasses can also have methods! """ +import json +import os +from dataclasses import asdict, dataclass + +from simple_parsing import ArgumentParser + +parser = ArgumentParser() + + +@dataclass +class HyperParameters: + batch_size: int = 32 + optimizer: str = "ADAM" + learning_rate: float = 1e-4 + max_epochs: int = 100 + l1_reg: float = 1e-5 + l2_reg: float = 1e-5 + + def save(self, path: str): + with open(path, "w") as f: + config_dict = asdict(self) + json.dump(config_dict, f, indent=1) + + @classmethod + def load(cls, path: str): + with open(path) as f: + config_dict = json.load(f) + return cls(**config_dict) + + +parser.add_arguments(HyperParameters, dest="hparams") + +args = parser.parse_args() + +hparams: HyperParameters = args.hparams +print(hparams) +expected = """ +HyperParameters(batch_size=32, optimizer='ADAM', learning_rate=0.0001, max_epochs=100, l1_reg=1e-05, l2_reg=1e-05) +""" + +# save and load from a json file: +hparams.save("hyperparameters.json") +_hparams = HyperParameters.load("hyperparameters.json") +assert hparams == _hparams + + +os.remove("hyperparameters.json") diff --git a/docs/examples/demo.py b/docs/examples/demo.py new file mode 100644 index 00000000..679fc34f --- /dev/null +++ b/docs/examples/demo.py @@ -0,0 +1,22 @@ +# examples/demo.py +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + +parser = ArgumentParser() +parser.add_argument("--foo", type=int, default=123, help="foo help") + + +@dataclass +class Options: + """Help string for this group of command-line arguments.""" + + log_dir: str # Help string for a required str argument + learning_rate: float = 1e-4 # Help string for a float argument + + +parser.add_arguments(Options, dest="options") + +args = parser.parse_args() +print("foo:", args.foo) +print("options:", args.options) diff --git a/docs/examples/demo_simple.py b/docs/examples/demo_simple.py new file mode 100644 index 00000000..15f0e31b --- /dev/null +++ b/docs/examples/demo_simple.py @@ -0,0 +1,16 @@ +# examples/demo_simple.py +from dataclasses import dataclass + +import simple_parsing + + +@dataclass +class Options: + """Help string for this group of command-line arguments.""" + + log_dir: str # Help string for a required str argument + learning_rate: float = 1e-4 # Help string for a float argument + + +options = simple_parsing.parse(Options) +print(options) diff --git a/docs/examples/docstrings/README.md b/docs/examples/docstrings/README.md new file mode 100644 index 00000000..1ee2c893 --- /dev/null +++ b/docs/examples/docstrings/README.md @@ -0,0 +1,65 @@ +# Docstrings + +A docstring can either be: + +- A comment on the same line as the attribute definition +- A single or multi-line comment on the line(s) preceding the attribute definition +- A single or multi-line docstring on the line(s) following the attribute + definition, starting with either `"""` or `'''` and ending with the same token. + +When more than one docstring options are present, one of them is chosen to +be used as the '--help' text of the attribute, according to the following ordering: + +1. docstring below the attribute +2. comment above the attribute +3. inline comment + +NOTE: It is recommended to add blank lines between consecutive attribute +assignments when using either the 'comment above' or 'docstring below' +style, just for clarity. This doesn't change anything about the output of +the "--help" command. + +```python +""" +A simple example to demonstrate the 'attribute docstrings' mechanism of simple-parsing. + +""" +from dataclasses import dataclass, field +from typing import List, Tuple + +from simple_parsing import ArgumentParser + +parser = ArgumentParser() + +@dataclass +class DocStringsExample(): + """NOTE: This block of text is the class docstring, and it will show up under + the name of the class in the --help group for this set of parameters. + """ + + attribute1: float = 1.0 + """docstring below, When used, this always shows up in the --help text for this attribute""" + + # Comment above only: this shows up in the help text, since there is no docstring below. + attribute2: float = 1.0 + + attribute3: float = 1.0 # inline comment only (this shows up in the help text, since none of the two other options are present.) + + # comment above 42 + attribute4: float = 1.0 # inline comment + """docstring below (this appears in --help)""" + + # comment above (this appears in --help) 46 + attribute5: float = 1.0 # inline comment + + attribute6: float = 1.0 # inline comment (this appears in --help) + + attribute7: float = 1.0 # inline comment + """docstring below (this appears in --help)""" + + +parser.add_arguments(DocStringsExample, "example") +args = parser.parse_args() +ex = args.example +print(ex) +``` diff --git a/docs/examples/docstrings/docstrings_example.py b/docs/examples/docstrings/docstrings_example.py new file mode 100644 index 00000000..d2911d4c --- /dev/null +++ b/docs/examples/docstrings/docstrings_example.py @@ -0,0 +1,42 @@ +"""A simple example to demonstrate the 'attribute docstrings' mechanism of simple-parsing.""" +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + +parser = ArgumentParser() + + +@dataclass +class DocStringsExample: + """NOTE: This block of text is the class docstring, and it will show up under + the name of the class in the --help group for this set of parameters. + """ + + attribute1: float = 1.0 + """Docstring below, When used, this always shows up in the --help text for this attribute.""" + + # Comment above only: this shows up in the help text, since there is no docstring below. + attribute2: float = 1.0 + + attribute3: float = 1.0 # inline comment only (this shows up in the help text, since none of the two other options are present.) + + # comment above 42 + attribute4: float = 1.0 # inline comment + """Docstring below (this appears in --help)""" + + # comment above (this appears in --help) 46 + attribute5: float = 1.0 # inline comment + + attribute6: float = 1.0 # inline comment (this appears in --help) + + attribute7: float = 1.0 # inline comment + """Docstring below (this appears in --help)""" + + +parser.add_arguments(DocStringsExample, "example") +args = parser.parse_args() +ex = args.example +print(ex) +expected = """ +DocStringsExample(attribute1=1.0, attribute2=1.0, attribute3=1.0, attribute4=1.0, attribute5=1.0, attribute6=1.0, attribute7=1.0) +""" diff --git a/docs/examples/enums/README.md b/docs/examples/enums/README.md new file mode 100644 index 00000000..25ecaaf6 --- /dev/null +++ b/docs/examples/enums/README.md @@ -0,0 +1,37 @@ +# Parsing Enums + +Parsing enums can be done quite simply, like so: + +```python +import enum +from dataclasses import dataclass, field + +from simple_parsing import ArgumentParser + +parser = ArgumentParser() + +class Color(enum.Enum): + RED = "RED" + ORANGE = "ORANGE" + BLUE = "BLUE" + +class Temperature(enum.Enum): + HOT = 1 + WARM = 0 + COLD = -1 + MONTREAL = -35 + +@dataclass +class MyPreferences: + """You can use Enums""" + color: Color = Color.BLUE # my favorite colour + temp: Temperature = Temperature.WARM + +parser.add_arguments(MyPreferences, "my_preferences") +args = parser.parse_args() +prefs: MyPreferences = args.my_preferences +print(prefs) + +``` + +You parse most datatypes using `simple-parsing`, as the type annotation on an argument is called as a conversion function in case the type of the attribute is not a builtin type or a dataclass. diff --git a/docs/examples/enums/enums_example.py b/docs/examples/enums/enums_example.py new file mode 100644 index 00000000..1cafff84 --- /dev/null +++ b/docs/examples/enums/enums_example.py @@ -0,0 +1,36 @@ +import enum +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + +parser = ArgumentParser() + + +class Color(enum.Enum): + RED = "RED" + ORANGE = "ORANGE" + BLUE = "BLUE" + + +class Temperature(enum.Enum): + HOT = 1 + WARM = 0 + COLD = -1 + MONTREAL = -35 + + +@dataclass +class MyPreferences: + """You can use Enums.""" + + color: Color = Color.BLUE # my favorite colour + temp: Temperature = Temperature.WARM + + +parser.add_arguments(MyPreferences, "my_preferences") +args = parser.parse_args() +prefs: MyPreferences = args.my_preferences +print(prefs) +expected = """ +MyPreferences(color=, temp=) +""" diff --git a/docs/examples/inheritance/README.md b/docs/examples/inheritance/README.md new file mode 100644 index 00000000..73fa83cd --- /dev/null +++ b/docs/examples/inheritance/README.md @@ -0,0 +1,55 @@ +# Inheritance + +Say you are working on a new research project, building on top of some previous work. + +Let's suppose that the previous authors were smart enough to use `simple-parsing` to define their `HyperParameters` as a dataclass, potentially saving you and others a lot of work. All the model hyperparameters can therefore be provided directly as command-line arguments. + +You have a set of new hyperparameters or command-line arguments you want to add to your model. Rather than redefining the same HyperParameters over and over, wouldn't it be nice to be able to just add a few new arguments to an existing arguments dataclass? + +Behold, inheritance: + +```python +from simple_parsing import ArgumentParser +from simple_parsing.helpers import JsonSerializable + + +from dataclasses import dataclass +from typing import Optional + +@dataclass +class GANHyperParameters(JsonSerializable): + batch_size: int = 32 # batch size + d_steps: int = 1 # number of generator updates + g_steps: int = 1 # number of discriminator updates + learning_rate: float = 1e-4 + optimizer: str = "ADAM" + + +@dataclass +class WGANHyperParameters(GANHyperParameters): + lambda_coeff: float = 10 # the lambda penalty coefficient. + + +@dataclass +class WGANGPHyperParameters(WGANHyperParameters): + gp_penalty: float = 1e-6 # Gradient penalty coefficient + + +parser = ArgumentParser() +parser.add_argument( + "--load_path", + type=str, + default=None, + help="If given, the HyperParameters are read from the given file instead of from the command-line." +) +parser.add_arguments(WGANGPHyperParameters, dest="hparams") + +args = parser.parse_args() + +load_path: str = args.load_path +if load_path is None: + hparams: WGANGPHyperParameters = args.hparams +else: + hparams = WGANGPHyperParameters.load_json(load_path) +print(hparams) +``` diff --git a/docs/examples/inheritance/inheritance_example.py b/docs/examples/inheritance/inheritance_example.py new file mode 100644 index 00000000..b43926a0 --- /dev/null +++ b/docs/examples/inheritance/inheritance_example.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser +from simple_parsing.helpers import Serializable + + +@dataclass +class GANHyperParameters(Serializable): + batch_size: int = 32 # batch size + d_steps: int = 1 # number of generator updates + g_steps: int = 1 # number of discriminator updates + learning_rate: float = 1e-4 + optimizer: str = "ADAM" + + +@dataclass +class WGANHyperParameters(GANHyperParameters): + lambda_coeff: float = 10 # the lambda penalty coefficient. + + +@dataclass +class WGANGPHyperParameters(WGANHyperParameters): + gp_penalty: float = 1e-6 # Gradient penalty coefficient + + +parser = ArgumentParser() +parser.add_argument( + "--load_path", + type=str, + default=None, + help="If given, the HyperParameters are read from the given file instead of from the command-line.", +) +parser.add_arguments(WGANGPHyperParameters, dest="hparams") + +args = parser.parse_args() + +load_path: str = args.load_path +if load_path is None: + hparams: WGANGPHyperParameters = args.hparams +else: + hparams = WGANGPHyperParameters.load_json(load_path) + +assert hparams == WGANGPHyperParameters( + batch_size=32, + d_steps=1, + g_steps=1, + learning_rate=0.0001, + optimizer="ADAM", + lambda_coeff=10, + gp_penalty=1e-06, +) diff --git a/docs/examples/inheritance/ml_inheritance.py b/docs/examples/inheritance/ml_inheritance.py new file mode 100644 index 00000000..e0a2f75d --- /dev/null +++ b/docs/examples/inheritance/ml_inheritance.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser, choice +from simple_parsing.helpers import Serializable + +# import tensorflow as tf + + +class GAN: + @dataclass + class HyperParameters(Serializable): + """Hyperparameters of the Generator and Discriminator networks.""" + + learning_rate: float = 1e-4 + optimizer: str = choice("ADAM", "RMSPROP", "SGD", default="ADAM") + n_disc_iters_per_g_iter: int = ( + 1 # Number of Discriminator iterations per Generator iteration. + ) + + def __init__(self, hparams: HyperParameters): + self.hparams = hparams + + +class WGAN(GAN): + """Wasserstein GAN.""" + + @dataclass + class HyperParameters(GAN.HyperParameters): + e_drift: float = 1e-4 + """Coefficient from the progan authors which penalizes critic outputs for having a large + magnitude.""" + + def __init__(self, hparams: HyperParameters): + self.hparams = hparams + + +class WGANGP(WGAN): + """Wasserstein GAN with Gradient Penalty.""" + + @dataclass + class HyperParameters(WGAN.HyperParameters): + e_drift: float = 1e-4 + """Coefficient from the progan authors which penalizes critic outputs for having a large + magnitude.""" + gp_coefficient: float = 10.0 + """Multiplying coefficient for the gradient penalty term of the loss equation. + + (10.0 is the default value, and was used by the PROGAN authors.) + """ + + def __init__(self, hparams: HyperParameters): + self.hparams: WGANGP.HyperParameters = hparams + print(self.hparams.gp_coefficient) + + +parser = ArgumentParser() +parser.add_arguments(WGANGP.HyperParameters, "hparams") +args = parser.parse_args() +print(args.hparams) +expected = """ +WGANGP.HyperParameters(learning_rate=0.0001, optimizer='ADAM', n_disc_iters_per_g_iter=1, e_drift=0.0001, gp_coefficient=10.0) +""" diff --git a/docs/examples/inheritance/ml_inheritance_2.py b/docs/examples/inheritance/ml_inheritance_2.py new file mode 100644 index 00000000..b91faed7 --- /dev/null +++ b/docs/examples/inheritance/ml_inheritance_2.py @@ -0,0 +1,106 @@ +from dataclasses import dataclass, field + +from simple_parsing import ArgumentParser, choice +from simple_parsing.helpers import Serializable, list_field + +# import tensorflow as tf + + +@dataclass +class ConvBlock(Serializable): + """A Block of Conv Layers.""" + + n_layers: int = 4 # number of layers + n_filters: list[int] = list_field(16, 32, 64, 64) # filters per layer + + +@dataclass +class GeneratorHParams(ConvBlock): + """Settings of the Generator model.""" + + optimizer: str = choice("ADAM", "RMSPROP", "SGD", default="ADAM") + + +@dataclass +class DiscriminatorHParams(ConvBlock): + """Settings of the Discriminator model.""" + + optimizer: str = choice("ADAM", "RMSPROP", "SGD", default="ADAM") + + +@dataclass +class GanHParams(Serializable): + """Hyperparameters of the Generator and Discriminator networks.""" + + gen: GeneratorHParams + disc: DiscriminatorHParams + learning_rate: float = 1e-4 + n_disc_iters_per_g_iter: int = 1 # Number of Discriminator iterations per Generator iteration. + + +class GAN: + """Generative Adversarial Network Model.""" + + def __init__(self, hparams: GanHParams): + self.hparams = hparams + + +@dataclass +class WGanHParams(GanHParams): + """HParams of the WGAN model.""" + + e_drift: float = 1e-4 + """Coefficient from the progan authors which penalizes critic outputs for having a large + magnitude.""" + + +class WGAN(GAN): + """Wasserstein GAN.""" + + def __init__(self, hparams: WGanHParams): + self.hparams = hparams + + +@dataclass +class CriticHParams(DiscriminatorHParams): + """HyperParameters specific to a Critic.""" + + lambda_coefficient: float = 1e-5 + + +@dataclass +class WGanGPHParams(WGanHParams): + """Hyperparameters of the WGAN with Gradient Penalty.""" + + e_drift: float = 1e-4 + """Coefficient from the progan authors which penalizes critic outputs for having a large + magnitude.""" + gp_coefficient: float = 10.0 + """Multiplying coefficient for the gradient penalty term of the loss equation. + + (10.0 is the default value, and was used by the PROGAN authors.) + """ + disc: CriticHParams = field(default_factory=CriticHParams) + # overwrite the usual 'disc' field of the WGanHParams dataclass. + """ Parameters of the Critic. """ + + +class WGANGP(WGAN): + """Wasserstein GAN with Gradient Penalty.""" + + def __init__(self, hparams: WGanGPHParams): + self.hparams = hparams + + +parser = ArgumentParser() +parser.add_arguments(WGanGPHParams, "hparams") +args = parser.parse_args() + +print(args.hparams) + +expected = """ +WGanGPHParams(gen=GeneratorHParams(n_layers=4, n_filters=[16, 32, 64, 64], \ +optimizer='ADAM'), disc=CriticHParams(n_layers=4, n_filters=[16, 32, 64, 64], \ +optimizer='ADAM', lambda_coefficient=1e-05), learning_rate=0.0001, \ +n_disc_iters_per_g_iter=1, e_drift=0.0001, gp_coefficient=10.0) +""" diff --git a/docs/examples/merging/README.md b/docs/examples/merging/README.md new file mode 100644 index 00000000..896c3ef0 --- /dev/null +++ b/docs/examples/merging/README.md @@ -0,0 +1,15 @@ +# Parsing Multiple Dataclasses with Merging + +Here, we demonstrate parsing multiple classes each of which has a list attribute. +There are a few options for doing this. For example, if we want to let each instance have a distinct prefix for its arguments, we could use the ConflictResolution.AUTO option. + +In the following examples, we instead want to create a multiple instances of the argument dataclasses from the command line, but we don't want to have a different prefix for each instance. + +To do this, we pass the `ConflictResolution.ALWAYS_MERGE` option to the argument parser constructor. This creates a single argument for each attribute that will be set as multiple (i.e., if the attribute was of type `str`, the argument becomes a list of `str`, one for each class instance). + +For more info, check out the docstring of the `ConflictResolution` enum. + +## Examples: + +- [multiple_example.py](multiple_example.py) +- [multiple_lists_example.py](multiple_lists_example.py) diff --git a/docs/examples/merging/multiple_example.py b/docs/examples/merging/multiple_example.py new file mode 100644 index 00000000..047ee140 --- /dev/null +++ b/docs/examples/merging/multiple_example.py @@ -0,0 +1,42 @@ +"""Example of how to create multiple instances of a class from the command-line. + +# NOTE: If your dataclass has a list attribute, and you wish to parse multiple instances of that class from the command line, +# simply enclose each list with single or double quotes. +# For this example, something like: +>>> python examples/multiple_instances_example.py --num_instances 2 --foo 1 2 --list_of_ints "3 5 7" "4 6 10" +""" +from dataclasses import dataclass + +from simple_parsing import ArgumentParser, ConflictResolution + +parser = ArgumentParser(conflict_resolution=ConflictResolution.ALWAYS_MERGE) + + +@dataclass +class Config: + """A class which groups related parameters.""" + + run_name: str = "train" # Some parameter for the run name. + some_int: int = 10 # an optional int parameter. + log_dir: str = "logs" # an optional string parameter. + """The logging directory to use. + + (This is an attribute docstring for the log_dir attribute, and shows up when using the "--help" + argument!) + """ + + +parser.add_arguments(Config, "train_config") +parser.add_arguments(Config, "valid_config") + +args = parser.parse_args() + +train_config: Config = args.train_config +valid_config: Config = args.valid_config + +print(train_config, valid_config, sep="\n") + +expected = """ +Config(run_name='train', some_int=10, log_dir='logs') +Config(run_name='train', some_int=10, log_dir='logs') +""" diff --git a/docs/examples/merging/multiple_lists_example.py b/docs/examples/merging/multiple_lists_example.py new file mode 100644 index 00000000..788df5ec --- /dev/null +++ b/docs/examples/merging/multiple_lists_example.py @@ -0,0 +1,63 @@ +"""Here, we demonstrate parsing multiple classes each of which has a list attribute. There are a +few options for doing this. For example, if we want to let each instance have a distinct prefix for +its arguments, we could use the ConflictResolution.AUTO option. + +Here, we want to create a few instances of `CNNStack` from the command line, +but don't want to have a different prefix for each instance. +To do this, we pass the `ConflictResolution.ALWAYS_MERGE` option to the argument parser constructor. +This creates a single argument for each attribute, that will be set as multiple +(i.e., if the attribute is a `str`, the argument becomes a list of `str`, one for each class instance). + +For more info, check out the docstring of the `ConflictResolution` enum. +""" + +from dataclasses import dataclass, field + +from simple_parsing import ArgumentParser, ConflictResolution + + +@dataclass +class CNNStack: + name: str = "stack" + num_layers: int = 3 + kernel_sizes: tuple[int, int, int] = (7, 5, 5) + num_filters: list[int] = field(default_factory=[32, 64, 64].copy) + + +parser = ArgumentParser(conflict_resolution=ConflictResolution.ALWAYS_MERGE) + +num_stacks = 3 +for i in range(num_stacks): + parser.add_arguments(CNNStack, dest=f"stack_{i}", default=CNNStack()) + +args = parser.parse_args() +stack_0 = args.stack_0 +stack_1 = args.stack_1 +stack_2 = args.stack_2 + +# BUG: When the list length and the number of instances to parse is the same, +# AND there is no default value passed to `add_arguments`, it gets parsed as +# multiple lists each with only one element, rather than duplicating the field's +# default value correctly. + +print(stack_0, stack_1, stack_2, sep="\n") +expected = """\ +CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[32, 64, 64]) +CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[32, 64, 64]) +CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[32, 64, 64]) +""" + +# Example of how to pass different lists for each instance: + +args = parser.parse_args("--num_filters [1,2,3] [4,5,6] [7,8,9] ".split()) +stack_0 = args.stack_0 +stack_1 = args.stack_1 +stack_2 = args.stack_2 + +# BUG: TODO: Fix the multiple + list attributes bug. +print(stack_0, stack_1, stack_2, sep="\n") +expected += """\ +CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[1, 2, 3]) +CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[4, 5, 6]) +CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[7, 8, 9]) +""" diff --git a/docs/examples/nesting/README.md b/docs/examples/nesting/README.md new file mode 100644 index 00000000..21505001 --- /dev/null +++ b/docs/examples/nesting/README.md @@ -0,0 +1,282 @@ +# Nesting!! + +You can nest dataclasses within dataclasses. In the following example (taken from an actual Data Science project), we show how we can reuse the `TaskHyperParameters` class to define the parameters of three models: `gender`, `age_group`, and `personality`: + +```python +from simple_parsing import ArgumentParser +from dataclasses import dataclass +from typing import * + +@dataclass +class TaskHyperParameters(): + """ + HyperParameters for a task-specific model + """ + # name of the task + name: str + # number of dense layers + num_layers: int = 1 + # units per layer + num_units: int = 8 + # activation function + activation: str = "tanh" + # whether or not to use batch normalization after each dense layer + use_batchnorm: bool = False + # whether or not to use dropout after each dense layer + use_dropout: bool = True + # the dropout rate + dropout_rate: float = 0.1 + # whether or not image features should be used as input + use_image_features: bool = True + # whether or not 'likes' features should be used as input + use_likes: bool = True + # L1 regularization coefficient + l1_reg: float = 0.005 + # L2 regularization coefficient + l2_reg: float = 0.005 + # Whether or not a task-specific Embedding layer should be used on the 'likes' features. + # When set to 'True', it is expected that there no shared embedding is used. + embed_likes: bool = False + +@dataclass +class HyperParameters(): + """Hyperparameters of our model.""" + # the batch size + batch_size: int = 128 + # Which optimizer to use during training. + optimizer: str = "sgd" + # Learning Rate + learning_rate: float = 0.001 + + # number of individual 'pages' that were kept during preprocessing of the 'likes'. + # This corresponds to the number of entries in the multi-hot like vector. + num_like_pages: int = 10_000 + + gender_loss_weight: float = 1.0 + age_loss_weight: float = 1.0 + + num_text_features: ClassVar[int] = 91 + num_image_features: ClassVar[int] = 65 + + max_number_of_likes: int = 2000 + embedding_dim: int = 8 + + shared_likes_embedding: bool = True + + # Whether or not to use Rémi's better kept like pages + use_custom_likes: bool = True + + # Gender model settings: + gender: TaskHyperParameters = TaskHyperParameters( + "gender", + num_layers=1, + num_units=32, + use_batchnorm=False, + use_dropout=True, + dropout_rate=0.1, + use_image_features=True, + use_likes=True, + ) + + # Age Group Model settings: + age_group: TaskHyperParameters = TaskHyperParameters( + "age_group", + num_layers=2, + num_units=64, + use_batchnorm=False, + use_dropout=True, + dropout_rate=0.1, + use_image_features=True, + use_likes=True, + ) + + # Personality Model(s) settings: + personality: TaskHyperParameters = TaskHyperParameters( + "personality", + num_layers=1, + num_units=8, + use_batchnorm=False, + use_dropout=True, + dropout_rate=0.1, + use_image_features=False, + use_likes=False, + ) + + +parser = ArgumentParser() +parser.add_arguments(HyperParameters, dest="hparams") +args = parser.parse_args() +hparams: HyperParameters = args.hparams +print(hparams) +``` + +The help string we get would have been impossibly hard to create by hand: + +```console +$ python nesting_example.py --help +usage: nesting_example.py [-h] [--batch_size int] [--optimizer str] + [--learning_rate float] [--num_like_pages int] + [--gender_loss_weight float] + [--age_loss_weight float] + [--max_number_of_likes int] [--embedding_dim int] + [--shared_likes_embedding [bool]] + [--use_custom_likes [bool]] [--gender.name str] + [--gender.num_layers int] [--gender.num_units int] + [--gender.activation str] + [--gender.use_batchnorm [bool]] + [--gender.use_dropout [bool]] + [--gender.dropout_rate float] + [--gender.use_image_features [bool]] + [--gender.use_likes [bool]] + [--gender.l1_reg float] [--gender.l2_reg float] + [--gender.embed_likes [bool]] + [--age_group.name str] [--age_group.num_layers int] + [--age_group.num_units int] + [--age_group.activation str] + [--age_group.use_batchnorm [bool]] + [--age_group.use_dropout [bool]] + [--age_group.dropout_rate float] + [--age_group.use_image_features [bool]] + [--age_group.use_likes [bool]] + [--age_group.l1_reg float] + [--age_group.l2_reg float] + [--age_group.embed_likes [bool]] + [--personality.name str] + [--personality.num_layers int] + [--personality.num_units int] + [--personality.activation str] + [--personality.use_batchnorm [bool]] + [--personality.use_dropout [bool]] + [--personality.dropout_rate float] + [--personality.use_image_features [bool]] + [--personality.use_likes [bool]] + [--personality.l1_reg float] + [--personality.l2_reg float] + [--personality.embed_likes [bool]] + +optional arguments: + -h, --help show this help message and exit + +HyperParameters ['hparams']: + Hyperparameters of our model. + + --batch_size int the batch size (default: 128) + --optimizer str Which optimizer to use during training. (default: sgd) + --learning_rate float + Learning Rate (default: 0.001) + --num_like_pages int number of individual 'pages' that were kept during + preprocessing of the 'likes'. This corresponds to the + number of entries in the multi-hot like vector. + (default: 10000) + --gender_loss_weight float + --age_loss_weight float + --max_number_of_likes int + --embedding_dim int + --shared_likes_embedding [bool] + --use_custom_likes [bool] + Whether or not to use Rémi's better kept like pages + (default: True) + +TaskHyperParameters ['hparams.gender']: + Gender model settings: + + --gender.name str name of the task (default: gender) + --gender.num_layers int + number of dense layers (default: 1) + --gender.num_units int + units per layer (default: 32) + --gender.activation str + activation function (default: tanh) + --gender.use_batchnorm [bool] + whether or not to use batch normalization after each + dense layer (default: False) + --gender.use_dropout [bool] + whether or not to use dropout after each dense layer + (default: True) + --gender.dropout_rate float + the dropout rate (default: 0.1) + --gender.use_image_features [bool] + whether or not image features should be used as input + (default: True) + --gender.use_likes [bool] + whether or not 'likes' features should be used as input + (default: True) + --gender.l1_reg float + L1 regularization coefficient (default: 0.005) + --gender.l2_reg float + L2 regularization coefficient (default: 0.005) + --gender.embed_likes [bool] + Whether or not a task-specific Embedding layer should + be used on the 'likes' features. When set to 'True', + it is expected that there no shared embedding is used. + (default: False) + +TaskHyperParameters ['hparams.age_group']: + Age Group Model settings: + + --age_group.name str name of the task (default: age_group) + --age_group.num_layers int + number of dense layers (default: 2) + --age_group.num_units int + units per layer (default: 64) + --age_group.activation str + activation function (default: tanh) + --age_group.use_batchnorm [bool] + whether or not to use batch normalization after each + dense layer (default: False) + --age_group.use_dropout [bool] + whether or not to use dropout after each dense layer + (default: True) + --age_group.dropout_rate float + the dropout rate (default: 0.1) + --age_group.use_image_features [bool] + whether or not image features should be used as input + (default: True) + --age_group.use_likes [bool] + whether or not 'likes' features should be used as input + (default: True) + --age_group.l1_reg float + L1 regularization coefficient (default: 0.005) + --age_group.l2_reg float + L2 regularization coefficient (default: 0.005) + --age_group.embed_likes [bool] + Whether or not a task-specific Embedding layer should + be used on the 'likes' features. When set to 'True', + it is expected that there no shared embedding is used. + (default: False) + +TaskHyperParameters ['hparams.personality']: + Personality Model(s) settings: + + --personality.name str + name of the task (default: personality) + --personality.num_layers int + number of dense layers (default: 1) + --personality.num_units int + units per layer (default: 8) + --personality.activation str + activation function (default: tanh) + --personality.use_batchnorm [bool] + whether or not to use batch normalization after each + dense layer (default: False) + --personality.use_dropout [bool] + whether or not to use dropout after each dense layer + (default: True) + --personality.dropout_rate float + the dropout rate (default: 0.1) + --personality.use_image_features [bool] + whether or not image features should be used as input + (default: False) + --personality.use_likes [bool] + whether or not 'likes' features should be used as input + (default: False) + --personality.l1_reg float + L1 regularization coefficient (default: 0.005) + --personality.l2_reg float + L2 regularization coefficient (default: 0.005) + --personality.embed_likes [bool] + Whether or not a task-specific Embedding layer should + be used on the 'likes' features. When set to 'True', + it is expected that there no shared embedding is used. + (default: False) +``` diff --git a/docs/examples/nesting/nesting_example.py b/docs/examples/nesting/nesting_example.py new file mode 100644 index 00000000..55076c74 --- /dev/null +++ b/docs/examples/nesting/nesting_example.py @@ -0,0 +1,135 @@ +import functools +from dataclasses import dataclass, field +from typing import ClassVar + +from simple_parsing import ArgumentParser + + +@dataclass +class TaskHyperParameters: + """HyperParameters for a task-specific model.""" + + # name of the task + name: str + # number of dense layers + num_layers: int = 1 + # units per layer + num_units: int = 8 + # activation function + activation: str = "tanh" + # whether or not to use batch normalization after each dense layer + use_batchnorm: bool = False + # whether or not to use dropout after each dense layer + use_dropout: bool = True + # the dropout rate + dropout_rate: float = 0.1 + # whether or not image features should be used as input + use_image_features: bool = True + # whether or not 'likes' features should be used as input + use_likes: bool = True + # L1 regularization coefficient + l1_reg: float = 0.005 + # L2 regularization coefficient + l2_reg: float = 0.005 + # Whether or not a task-specific Embedding layer should be used on the 'likes' features. + # When set to 'True', it is expected that there no shared embedding is used. + embed_likes: bool = False + + +@dataclass +class HyperParameters: + """Hyperparameters of our model.""" + + # the batch size + batch_size: int = 128 + # Which optimizer to use during training. + optimizer: str = "sgd" + # Learning Rate + learning_rate: float = 0.001 + + # number of individual 'pages' that were kept during preprocessing of the 'likes'. + # This corresponds to the number of entries in the multi-hot like vector. + num_like_pages: int = 10_000 + + gender_loss_weight: float = 1.0 + age_loss_weight: float = 1.0 + + num_text_features: ClassVar[int] = 91 + num_image_features: ClassVar[int] = 65 + + max_number_of_likes: int = 2000 + embedding_dim: int = 8 + + shared_likes_embedding: bool = True + + # Whether or not to use Rémi's better kept like pages + use_custom_likes: bool = True + + # Gender model settings: + gender: TaskHyperParameters = field( + default_factory=functools.partial( + TaskHyperParameters, + "gender", + num_layers=1, + num_units=32, + use_batchnorm=False, + use_dropout=True, + dropout_rate=0.1, + use_image_features=True, + use_likes=True, + ) + ) + + # Age Group Model settings: + age_group: TaskHyperParameters = field( + default_factory=functools.partial( + TaskHyperParameters, + "age_group", + num_layers=2, + num_units=64, + use_batchnorm=False, + use_dropout=True, + dropout_rate=0.1, + use_image_features=True, + use_likes=True, + ) + ) + + # Personality Model(s) settings: + personality: TaskHyperParameters = field( + default_factory=functools.partial( + TaskHyperParameters, + "personality", + num_layers=1, + num_units=8, + use_batchnorm=False, + use_dropout=True, + dropout_rate=0.1, + use_image_features=False, + use_likes=False, + ) + ) + + +parser = ArgumentParser() +parser.add_arguments(HyperParameters, dest="hparams") +args = parser.parse_args() +hparams: HyperParameters = args.hparams +print(hparams) +expected = ( + "HyperParameters(batch_size=128, optimizer='sgd', " + "learning_rate=0.001, num_like_pages=10000, gender_loss_weight=1.0, " + "age_loss_weight=1.0, max_number_of_likes=2000, embedding_dim=8, " + "shared_likes_embedding=True, use_custom_likes=True, " + "gender=TaskHyperParameters(name='gender', num_layers=1, num_units=32, " + "activation='tanh', use_batchnorm=False, use_dropout=True, dropout_rate=0.1, " + "use_image_features=True, use_likes=True, l1_reg=0.005, l2_reg=0.005, " + "embed_likes=False), age_group=TaskHyperParameters(name='age_group', " + "num_layers=2, num_units=64, activation='tanh', use_batchnorm=False, " + "use_dropout=True, dropout_rate=0.1, use_image_features=True, use_likes=True, " + "l1_reg=0.005, l2_reg=0.005, embed_likes=False), " + "personality=TaskHyperParameters(name='personality', num_layers=1, num_units=8, " + "activation='tanh', use_batchnorm=False, use_dropout=True, dropout_rate=0.1, " + "use_image_features=False, use_likes=False, l1_reg=0.005, l2_reg=0.005, " + "embed_likes=False))" +) diff --git a/docs/examples/partials/README.md b/docs/examples/partials/README.md new file mode 100644 index 00000000..0b63f062 --- /dev/null +++ b/docs/examples/partials/README.md @@ -0,0 +1 @@ +# Partials - Configuring arbitrary classes / callables diff --git a/docs/examples/partials/partials_example.py b/docs/examples/partials/partials_example.py new file mode 100644 index 00000000..f3c2b7c6 --- /dev/null +++ b/docs/examples/partials/partials_example.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from simple_parsing import ArgumentParser +from simple_parsing.helpers import subgroups +from simple_parsing.helpers.partial import Partial, config_for + + +# Suppose we want to choose between the Adam and SGD optimizers from PyTorch: +# (NOTE: We don't import pytorch here, so we just create the types to illustrate) +class Optimizer: + def __init__(self, params): + ... + + +class Adam(Optimizer): + def __init__( + self, + params, + lr: float = 3e-4, + beta1: float = 0.9, + beta2: float = 0.999, + eps: float = 1e-08, + ): + self.params = params + self.lr = lr + self.beta1 = beta1 + self.beta2 = beta2 + self.eps = eps + + +class SGD(Optimizer): + def __init__( + self, + params, + lr: float = 3e-4, + weight_decay: float | None = None, + momentum: float = 0.9, + eps: float = 1e-08, + ): + self.params = params + self.lr = lr + self.weight_decay = weight_decay + self.momentum = momentum + self.eps = eps + + +# Dynamically create a dataclass that will be used for the above type: +# NOTE: We could use Partial[Adam] or Partial[Optimizer], however this would treat `params` as a +# required argument. +# AdamConfig = Partial[Adam] # would treat 'params' as a required argument. +# SGDConfig = Partial[SGD] # same here +AdamConfig: type[Partial[Adam]] = config_for(Adam, ignore_args="params") +SGDConfig: type[Partial[SGD]] = config_for(SGD, ignore_args="params") + + +@dataclass +class Config: + # Which optimizer to use. + optimizer: Partial[Optimizer] = subgroups( + { + "sgd": SGDConfig, + "adam": AdamConfig, + }, + default_factory=AdamConfig, + ) + + +parser = ArgumentParser() +parser.add_arguments(Config, "config") +args = parser.parse_args() + + +config: Config = args.config +print(config) +expected = "Config(optimizer=AdamConfig(lr=0.0003, beta1=0.9, beta2=0.999, eps=1e-08))" + +my_model_parameters = [123] # nn.Sequential(...).parameters() + +optimizer = config.optimizer(params=my_model_parameters) +print(vars(optimizer)) +expected += """ +{'params': [123], 'lr': 0.0003, 'beta1': 0.9, 'beta2': 0.999, 'eps': 1e-08} +""" diff --git a/docs/examples/prefixing/README.md b/docs/examples/prefixing/README.md new file mode 100644 index 00000000..bbe6fb7a --- /dev/null +++ b/docs/examples/prefixing/README.md @@ -0,0 +1,17 @@ +# Prefixing Mechanics + +Before starting to use multiple dataclasses or nesting them, it is good to first understand the prefixing mechanism used by `simple-parsing`. + +What's important to consider is that in `argparse`, arguments can only be provided as a "flat" list. + +In order to be able to "reuse" arguments and parse multiple instances of the same class from the command-line, we therefore have to choose between these options: + +1. Give each individual argument a differentiating prefix; (default) +2. Disallow the reuse of arguments; +3. Parse a List of values instead of a single value, and later redistribute the value to the instances. + +You can control which of these three behaviours the parser is to use by setting the `conflict_resolution` argument of `simple_parsing.ArgumentParser`'s `__init__` method. + +- For option 1, use either the `ConflictResolution.AUTO` or `ConflictResolution.EXPLICIT` options +- For option 2, use the `ConflictResolution.NONE` option. +- For option 3, use the `ConflictResolution.ALWAYS_MERGE` option. diff --git a/docs/examples/prefixing/manual_prefix_example.py b/docs/examples/prefixing/manual_prefix_example.py new file mode 100644 index 00000000..3c8053c7 --- /dev/null +++ b/docs/examples/prefixing/manual_prefix_example.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + + +@dataclass +class Config: + """Simple example of a class that can be reused.""" + + log_dir: str = "logs" + + +parser = ArgumentParser() +parser.add_arguments(Config, "train_config", prefix="train_") +parser.add_arguments(Config, "valid_config", prefix="valid_") +args = parser.parse_args() +print(vars(args)) diff --git a/docs/examples/serialization/README.md b/docs/examples/serialization/README.md new file mode 100644 index 00000000..91f3d9c5 --- /dev/null +++ b/docs/examples/serialization/README.md @@ -0,0 +1,119 @@ +# Serialization + +The `Serializable` class makes it easy to serialize any dataclass to and from json or yaml. +It is also very easy to add support for serializing/deserializing your own custom types! + +```python +>>> from simple_parsing.helpers import Serializable +>>> from dataclasses import dataclass +>>> +>>> @dataclass +... class Person(Serializable): +... name: str = "Bob" +... age: int = 20 +... +>>> @dataclass +... class Student(Person): +... domain: str = "Computer Science" +... average_grade: float = 0.80 +... +>>> # Serialization: +... # We can dump to yaml or json: +... charlie = Person(name="Charlie") +>>> print(charlie.dumps_yaml()) +age: 20 +name: Charlie + +>>> print(charlie.dumps_json()) +{"name": "Charlie", "age": 20} +>>> print(charlie.dumps()) # JSON by default +{"name": "Charlie", "age": 20} +>>> # Deserialization: +... bob = Student() +>>> print(bob) +Student(name='Bob', age=20, domain='Computer Science', average_grade=0.8) +>>> bob.save("bob.yaml") +>>> # Can load a Student from the base class: this will use the first subclass +... # that has all the required fields. +... _bob = Person.load("bob.yaml", drop_extra_fields=False) +>>> assert isinstance(_bob, Student) +>>> assert _bob == bob +``` + +## Adding custom types + +Register a new encoding function using `encode`, and a new decoding function using `register_decoding_fn` + +For example: Consider the same example as above, but we add a Tensor attribute from `pytorch`. + +```python +from dataclasses import dataclass +from typing import List + +import torch +from torch import Tensor + +from simple_parsing.helpers import Serializable +from simple_parsing.helpers.serialization import encode, register_decoding_fn + +expected: str = "" + +@dataclass +class Person(Serializable): + name: str = "Bob" + age: int = 20 + t: Tensor = torch.arange(4) + + +@dataclass +class Student(Person): + domain: str = "Computer Science" + average_grade: float = 0.80 + + +@encode.register +def encode_tensor(obj: Tensor) -> List: + """ We choose to encode a tensor as a list, for instance """ + return obj.tolist() + +# We will use `torch.as_tensor` as our decoding function +register_decoding_fn(Tensor, torch.as_tensor) + +# Serialization: +# We can dump to yaml or json: +charlie = Person(name="Charlie") +print(charlie.dumps_yaml()) +expected += """\ +age: 20 +name: Charlie +t: +- 0 +- 1 +- 2 +- 3 + +""" + + +print(charlie.dumps_json()) +expected += """\ +{"name": "Charlie", "age": 20, "t": [0, 1, 2, 3]} +""" + +# Deserialization: +bob = Student() +print(bob) +expected += """\ +Student(name='Bob', age=20, t=tensor([0, 1, 2, 3]), domain='Computer Science', average_grade=0.8) +""" + +# Can load a Student from the base class: this will use the first subclass +# that has all the required fields. +bob.save("bob.yaml") +_bob = Person.load("bob.yaml", drop_extra_fields=False) +assert isinstance(_bob, Student), _bob +# Note: using _bob == bob doesn't work here because of Tensor comparison, +# But this basically shows the same thing as the previous example. +assert str(_bob) == str(bob) + +``` diff --git a/docs/examples/serialization/bob.json b/docs/examples/serialization/bob.json new file mode 100644 index 00000000..525ee440 --- /dev/null +++ b/docs/examples/serialization/bob.json @@ -0,0 +1 @@ +{"name": "Bob", "age": 20, "domain": "Computer Science", "average_grade": 0.8} diff --git a/docs/examples/serialization/custom_types_example.py b/docs/examples/serialization/custom_types_example.py new file mode 100644 index 00000000..52d276d3 --- /dev/null +++ b/docs/examples/serialization/custom_types_example.py @@ -0,0 +1,73 @@ +# Cleaning up +import os +from dataclasses import dataclass + +import torch +from torch import Tensor + +from simple_parsing.helpers import Serializable +from simple_parsing.helpers.serialization import encode, register_decoding_fn + +expected: str = "" + + +@dataclass +class Person(Serializable): + name: str = "Bob" + age: int = 20 + t: Tensor = torch.arange(4) + + +@dataclass +class Student(Person): + domain: str = "Computer Science" + average_grade: float = 0.80 + + +@encode.register +def encode_tensor(obj: Tensor) -> list: + """We choose to encode a tensor as a list, for instance.""" + return obj.tolist() + + +# We will use `torch.as_tensor` as our decoding function +register_decoding_fn(Tensor, torch.as_tensor) + +# Serialization: +# We can dump to yaml or json: +charlie = Person(name="Charlie") +print(charlie.dumps_yaml()) +expected += """\ +age: 20 +name: Charlie +t: +- 0 +- 1 +- 2 +- 3 + +""" + + +print(charlie.dumps_json()) +expected += """\ +{"name": "Charlie", "age": 20, "t": [0, 1, 2, 3]} +""" + +# Deserialization: +bob = Student() +print(bob) +expected += """\ +Student(name='Bob', age=20, t=tensor([0, 1, 2, 3]), domain='Computer Science', average_grade=0.8) +""" + +# Can load a Student from the base class: this will use the first subclass +# that has all the required fields. +bob.save("bob.yaml") +_bob = Person.load("bob.yaml", drop_extra_fields=False) +assert isinstance(_bob, Student), _bob +# Note: using _bob == bob doesn't work here because of Tensor comparison, +# But this basically shows the same thing as the previous example. +assert str(_bob) == str(bob) + +os.remove("bob.yaml") diff --git a/docs/examples/serialization/serialization_example.ipynb b/docs/examples/serialization/serialization_example.ipynb new file mode 100644 index 00000000..cd56862b --- /dev/null +++ b/docs/examples/serialization/serialization_example.ipynb @@ -0,0 +1,87 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from simple_parsing.helpers import Serializable\n", + "from dataclasses import dataclass\n", + "\n", + "@dataclass\n", + "class Person(Serializable):\n", + " name: str = \"Bob\"\n", + " age: int = 20\n", + "\n", + "@dataclass\n", + "class Student(Person):\n", + " domain: str = \"Computer Science\"\n", + " average_grade: float = 0.80\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "\n", + "# Serialization:\n", + "# We can dump to yaml or json:\n", + "charlie = Person(name=\"Charlie\")\n", + "print(charlie.dumps_yaml())\n", + "print(charlie.dumps_json())\n", + "print(charlie.dumps()) # JSON by default" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Deserialization:\n", + "bob = Student()\n", + "bob.save(\"bob.yaml\")\n", + "bob.save(\"bob.json\")\n", + "# Can load a Student from the base class: this will use the first subclass\n", + "# that has all the required fields.\n", + "_bob = Person.load(\"bob.yaml\", drop_extra_fields=False)\n", + "assert isinstance(_bob, Student), _bob\n", + "assert _bob == bob" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.10-final" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/examples/serialization/serialization_example.py b/docs/examples/serialization/serialization_example.py new file mode 100644 index 00000000..fe5eb236 --- /dev/null +++ b/docs/examples/serialization/serialization_example.py @@ -0,0 +1,54 @@ +import os +from dataclasses import dataclass + +from simple_parsing.helpers import Serializable + + +@dataclass +class Person(Serializable): + name: str = "Bob" + age: int = 20 + + +@dataclass +class Student(Person): + domain: str = "Computer Science" + average_grade: float = 0.80 + + +expected: str = "" + +# Serialization: +# We can dump to yaml or json: +charlie = Person(name="Charlie") +print(charlie.dumps_yaml()) +expected += """\ +age: 20 +name: Charlie + +""" +print(charlie.dumps_json()) +expected += """\ +{"name": "Charlie", "age": 20} +""" +print(charlie.dumps()) # JSON by default +expected += """\ +{"name": "Charlie", "age": 20} +""" +# Deserialization: +bob = Student() +print(bob) +expected += """\ +Student(name='Bob', age=20, domain='Computer Science', average_grade=0.8) +""" + +bob.save("bob.yaml") +# Can load a Student from the base class: this will use the first subclass +# that has all the required fields. +_bob = Person.load("bob.yaml", drop_extra_fields=False) +assert isinstance(_bob, Student), _bob +assert _bob == bob + +# Cleaning up + +os.remove("bob.yaml") diff --git a/docs/examples/simple/_before.py b/docs/examples/simple/_before.py new file mode 100644 index 00000000..d86f775b --- /dev/null +++ b/docs/examples/simple/_before.py @@ -0,0 +1,44 @@ +from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) + +group = parser.add_argument_group( + title="Options", description="Set of options for the training of a Model." +) +group.add_argument("--num_layers", default=4, help="Number of layers to use") +group.add_argument("--num_units", default=64, help="Number of units per layer") +group.add_argument("--learning_rate", default=0.001, help="Learning rate to use") +group.add_argument( + "--optimizer", + default="ADAM", + choices=["ADAM", "SGD", "RMSPROP"], + help="Which optimizer to use", +) + +args = parser.parse_args() +print(args) +expected = """ +Namespace(learning_rate=0.001, num_layers=4, num_units=64, optimizer='ADAM') +""" + +parser.print_help() +expected += """ +usage: _before.py [-h] [--num_layers NUM_LAYERS] [--num_units NUM_UNITS] + [--learning_rate LEARNING_RATE] + [--optimizer {ADAM,SGD,RMSPROP}] + +optional arguments: + -h, --help show this help message and exit + +Options: + Set of options for the training of a Model. + + --num_layers NUM_LAYERS + Number of layers to use (default: 4) + --num_units NUM_UNITS + Number of units per layer (default: 64) + --learning_rate LEARNING_RATE + Learning rate to use (default: 0.001) + --optimizer {ADAM,SGD,RMSPROP} + Which optimizer to use (default: ADAM) +""" diff --git a/docs/examples/simple/basic.py b/docs/examples/simple/basic.py new file mode 100644 index 00000000..f66a6fee --- /dev/null +++ b/docs/examples/simple/basic.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + + +@dataclass +class HParams: + """Set of options for the training of a Model.""" + + num_layers: int = 4 + num_units: int = 64 + optimizer: str = "ADAM" + learning_rate: float = 0.001 + + +parser = ArgumentParser() +parser.add_arguments(HParams, dest="hparams") + + +args = parser.parse_args() + + +print(args.hparams) +expected = """ +HParams(num_layers=4, num_units=64, optimizer='ADAM', learning_rate=0.001) +""" + + +parser.print_help() +expected += """ +usage: basic.py [-h] [--num_layers int] [--num_units int] [--optimizer str] + [--learning_rate float] + +optional arguments: + -h, --help show this help message and exit + +HParams ['hparams']: + Set of options for the training of a Model. + + --num_layers int (default: 4) + --num_units int (default: 64) + --optimizer str (default: ADAM) + --learning_rate float + (default: 0.001) +""" + + +print(parser.equivalent_argparse_code()) +expected += """ +parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + +group = parser.add_argument_group(title="HParams ['hparams']", description="Set of options for the training of a Model.") +group.add_argument(*['--num_layers'], **{'type': int, 'required': False, 'dest': 'hparams.num_layers', 'default': 4, 'help': ' '}) +group.add_argument(*['--num_units'], **{'type': int, 'required': False, 'dest': 'hparams.num_units', 'default': 64, 'help': ' '}) +group.add_argument(*['--optimizer'], **{'type': str, 'required': False, 'dest': 'hparams.optimizer', 'default': 'ADAM', 'help': ' '}) +group.add_argument(*['--learning_rate'], **{'type': float, 'required': False, 'dest': 'hparams.learning_rate', 'default': 0.001, 'help': ' '}) + +args = parser.parse_args() +print(args) +""" diff --git a/docs/examples/simple/choice.py b/docs/examples/simple/choice.py new file mode 100644 index 00000000..773a7f54 --- /dev/null +++ b/docs/examples/simple/choice.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser, choice + + +@dataclass +class HParams: + """Set of options for the training of a Model.""" + + num_layers: int = 4 + num_units: int = 64 + optimizer: str = choice("ADAM", "SGD", "RMSPROP", default="ADAM") + learning_rate: float = 0.001 + + +parser = ArgumentParser() +parser.add_arguments(HParams, dest="hparams") +args = parser.parse_args() + +print(args.hparams) +expected = """ +HParams(num_layers=4, num_units=64, optimizer='ADAM', learning_rate=0.001) +""" + +parser.print_help() +expected += """ +usage: choice.py [-h] [--num_layers int] [--num_units int] + [--optimizer {ADAM,SGD,RMSPROP}] [--learning_rate float] + +optional arguments: + -h, --help show this help message and exit + +HParams ['hparams']: + Set of options for the training of a Model. + + --num_layers int (default: 4) + --num_units int (default: 64) + --optimizer {ADAM,SGD,RMSPROP} + (default: ADAM) + --learning_rate float + (default: 0.001) +""" diff --git a/docs/examples/simple/flag.py b/docs/examples/simple/flag.py new file mode 100644 index 00000000..f3f28f44 --- /dev/null +++ b/docs/examples/simple/flag.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser +from simple_parsing.helpers import flag + + +def parse(cls, args: str = ""): + """Removes some boilerplate code from the examples.""" + parser = ArgumentParser() # Create an argument parser + parser.add_arguments(cls, dest="hparams") # add arguments for the dataclass + ns = parser.parse_args(args.split()) # parse the given `args` + return ns.hparams + + +@dataclass +class HParams: + """Set of options for the training of a Model.""" + + num_layers: int = 4 + num_units: int = 64 + optimizer: str = "ADAM" + learning_rate: float = 0.001 + train: bool = flag(default=True, negative_prefix="--no-") + + +# Example 1 using default flag, i.e. train set to True +args = parse(HParams) + +print(args) +expected = """ +HParams(num_layers=4, num_units=64, optimizer='ADAM', learning_rate=0.001, train=True) +""" + +# Example 2 using the flags negative prefix +assert parse(HParams, "--no-train") == HParams(train=False) + + +# showing what --help outputs +parser = ArgumentParser() # Create an argument parser +parser.add_arguments(HParams, dest="hparams") # add arguments for the dataclass +parser.print_help() +expected += """ +usage: flag.py [-h] [--num_layers int] [--num_units int] [--optimizer str] + [--learning_rate float] [--train bool] + +optional arguments: + -h, --help show this help message and exit + +HParams ['hparams']: + Set of options for the training of a Model. + + --num_layers int (default: 4) + --num_units int (default: 64) + --optimizer str (default: ADAM) + --learning_rate float + (default: 0.001) + --train bool, --no-train bool + (default: True) +""" diff --git a/docs/examples/simple/help.py b/docs/examples/simple/help.py new file mode 100644 index 00000000..b0bbdbbb --- /dev/null +++ b/docs/examples/simple/help.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + + +@dataclass +class HParams: + """Set of options for the training of a ML Model. + + Some more detailed description can be placed here, and will show-up in + the auto-generated "--help" text. + + Some other **cool** uses for this space: + - Provide links to previous works: (easy to click on from the command-line) + - MAML: https://arxiv.org/abs/1703.03400 + - google: https://www.google.com + - This can also interact nicely with documentation tools like Sphinx! + For instance, you could add links to other parts of your documentation. + + The default HelpFormatter used by `simple_parsing` will keep the formatting + of this section intact, will add an indicator of the default values, and + will use the name of the attribute's type as the metavar in the help string. + For more info, check out the `SimpleFormatter` class found in + ./simple_parsing/utils.py + """ + + num_layers: int = 4 # Number of layers in the model. + num_units: int = 64 # Number of units (neurons) per layer. + optimizer: str = "ADAM" # Which optimizer to use. + learning_rate: float = 0.001 # Learning_rate used by the optimizer. + + alpha: float = 0.05 # TODO: Tune this. (This doesn't appear in '--help') + """A detailed description of this new 'alpha' parameter, which can potentially span multiple + lines.""" + + +parser = ArgumentParser() +parser.add_arguments(HParams, dest="hparams") +args = parser.parse_args() + +print(args.hparams) +expected = """ +HParams(num_layers=4, num_units=64, optimizer='ADAM', learning_rate=0.001, alpha=0.05) +""" + +parser.print_help() +expected += """ +usage: help.py [-h] [--num_layers int] [--num_units int] [--optimizer str] + [--learning_rate float] [--alpha float] + +optional arguments: + -h, --help show this help message and exit + +HParams ['hparams']: + Set of options for the training of a ML Model. + + Some more detailed description can be placed here, and will show-up in + the auto-generated "--help" text. + + Some other **cool** uses for this space: + - Provide links to previous works: (easy to click on from the command-line) + - MAML: https://arxiv.org/abs/1703.03400 + - google: https://www.google.com + - This can also interact nicely with documentation tools like Sphinx! + For instance, you could add links to other parts of your documentation. + + The default HelpFormatter used by `simple_parsing` will keep the formatting + of this section intact, will add an indicator of the default values, and + will use the name of the attribute's type as the metavar in the help string. + For more info, check out the `SimpleFormatter` class found in + ./simple_parsing/utils.py + + + --num_layers int Number of layers in the model. (default: 4) + --num_units int Number of units (neurons) per layer. (default: 64) + --optimizer str Which optimizer to use. (default: ADAM) + --learning_rate float + Learning_rate used by the optimizer. (default: 0.001) + --alpha float A detailed description of this new 'alpha' parameter, + which can potentially span multiple lines. (default: + 0.05) +""" diff --git a/docs/examples/simple/inheritance.py b/docs/examples/simple/inheritance.py new file mode 100644 index 00000000..b116135d --- /dev/null +++ b/docs/examples/simple/inheritance.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + + +@dataclass +class Method: + """Set of options for the training of a Model.""" + + num_layers: int = 4 + num_units: int = 64 + optimizer: str = "ADAM" + learning_rate: float = 0.001 + + +@dataclass +class MAML(Method): + """Overwrites some of the default values and adds new arguments/attributes.""" + + num_layers: int = 6 + num_units: int = 128 + + # method + name: str = "MAML" + + +parser = ArgumentParser() +parser.add_arguments(MAML, dest="hparams") +args = parser.parse_args() + + +print(args.hparams) +expected = """ +MAML(num_layers=6, num_units=128, optimizer='ADAM', learning_rate=0.001, name='MAML') +""" + +parser.print_help() +expected += """ +usage: inheritance.py [-h] [--num_layers int] [--num_units int] + [--optimizer str] [--learning_rate float] [--name str] + +optional arguments: + -h, --help show this help message and exit + +MAML ['hparams']: + Overwrites some of the default values and adds new arguments/attributes. + + --num_layers int (default: 6) + --num_units int (default: 128) + --optimizer str (default: ADAM) + --learning_rate float + (default: 0.001) + --name str method (default: MAML) +""" + + +print(parser.equivalent_argparse_code()) +expected += """ +parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + +group = parser.add_argument_group(title="MAML ['hparams']", description="Overwrites some of the default values and adds new arguments/attributes.") +group.add_argument(*['--num_layers'], **{'type': int, 'required': False, 'dest': 'hparams.num_layers', 'default': 6, 'help': ' '}) +group.add_argument(*['--num_units'], **{'type': int, 'required': False, 'dest': 'hparams.num_units', 'default': 128, 'help': ' '}) +group.add_argument(*['--optimizer'], **{'type': str, 'required': False, 'dest': 'hparams.optimizer', 'default': 'ADAM', 'help': ' '}) +group.add_argument(*['--learning_rate'], **{'type': float, 'required': False, 'dest': 'hparams.learning_rate', 'default': 0.001, 'help': ' '}) +group.add_argument(*['--name'], **{'type': str, 'required': False, 'dest': 'hparams.name', 'default': 'MAML', 'help': 'method'}) + +args = parser.parse_args() +print(args) +""" diff --git a/docs/examples/simple/option_strings.py b/docs/examples/simple/option_strings.py new file mode 100644 index 00000000..306ffde1 --- /dev/null +++ b/docs/examples/simple/option_strings.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass + +from simple_parsing import ArgumentParser, field +from simple_parsing.wrappers.field_wrapper import ArgumentGenerationMode + + +@dataclass +class HParams: + """Set of options for the training of a Model.""" + + num_layers: int = field(4, alias="-n") + num_units: int = field(64, alias="-u") + optimizer: str = field("ADAM", alias=["-o", "--opt"]) + learning_rate: float = field(0.001, alias="-lr") + + +parser = ArgumentParser() +parser.add_arguments(HParams, dest="hparams") +args = parser.parse_args() + +print(args.hparams) +expected = """ +HParams(num_layers=4, num_units=64, optimizer='ADAM', learning_rate=0.001) +""" + +parser.print_help() +expected += """ +usage: option_strings.py [-h] [-n int] [-u int] [-o str] [-lr float] + +optional arguments: + -h, --help show this help message and exit + +HParams ['hparams']: + Set of options for the training of a Model. + + -n int, --num_layers int + (default: 4) + -u int, --num_units int + (default: 64) + -o str, --opt str, --optimizer str + (default: ADAM) + -lr float, --learning_rate float + (default: 0.001) +""" + +# Now if we wanted to also be able to set the arguments using their full paths: +parser = ArgumentParser(argument_generation_mode=ArgumentGenerationMode.BOTH) +parser.add_arguments(HParams, dest="hparams") +parser.print_help() +expected += """ +usage: option_strings.py [-h] [-n int] [-u int] [-o str] [-lr float] + +optional arguments: + -h, --help show this help message and exit + +HParams ['hparams']: + Set of options for the training of a Model. + + -n int, --num_layers int, --hparams.num_layers int + (default: 4) + -u int, --num_units int, --hparams.num_units int + (default: 64) + -o str, --opt str, --optimizer str, --hparams.optimizer str + (default: ADAM) + -lr float, --learning_rate float, --hparams.learning_rate float + (default: 0.001) +""" diff --git a/docs/examples/simple/reuse.py b/docs/examples/simple/reuse.py new file mode 100644 index 00000000..bce48e62 --- /dev/null +++ b/docs/examples/simple/reuse.py @@ -0,0 +1,69 @@ +"""Modular and reusable! With SimpleParsing, you can easily add similar groups of command-line +arguments by simply reusing the dataclasses you define! There is no longer need for any copy- +pasting of blocks, or adding prefixes everywhere by hand. + +Instead, the ArgumentParser detects when more than one instance of the same `@dataclass` needs to +be parsed, and automatically adds the relevant prefixes to the arguments for you. +""" + +from dataclasses import dataclass + +from simple_parsing import ArgumentParser + + +@dataclass +class HParams: + """Set of options for the training of a Model.""" + + num_layers: int = 4 + num_units: int = 64 + optimizer: str = "ADAM" + learning_rate: float = 0.001 + + +parser = ArgumentParser() +parser.add_arguments(HParams, dest="train") +parser.add_arguments(HParams, dest="valid") +args = parser.parse_args() + +print(args.train) +print(args.valid) +expected = """ +HParams(num_layers=4, num_units=64, optimizer='ADAM', learning_rate=0.001) +HParams(num_layers=4, num_units=64, optimizer='ADAM', learning_rate=0.001) +""" + +parser.print_help() +expected += """ +usage: reuse.py [-h] [--train.num_layers int] [--train.num_units int] + [--train.optimizer str] [--train.learning_rate float] + [--valid.num_layers int] [--valid.num_units int] + [--valid.optimizer str] [--valid.learning_rate float] + +optional arguments: + -h, --help show this help message and exit + +HParams ['train']: + Set of options for the training of a Model. + + --train.num_layers int + (default: 4) + --train.num_units int + (default: 64) + --train.optimizer str + (default: ADAM) + --train.learning_rate float + (default: 0.001) + +HParams ['valid']: + Set of options for the training of a Model. + + --valid.num_layers int + (default: 4) + --valid.num_units int + (default: 64) + --valid.optimizer str + (default: ADAM) + --valid.learning_rate float + (default: 0.001) +""" diff --git a/docs/examples/simple/to_json.py b/docs/examples/simple/to_json.py new file mode 100644 index 00000000..2969962d --- /dev/null +++ b/docs/examples/simple/to_json.py @@ -0,0 +1,37 @@ +import os +from dataclasses import asdict, dataclass + +from simple_parsing import ArgumentParser +from simple_parsing.helpers import Serializable + + +@dataclass +class HParams(Serializable): + """Set of options for the training of a Model.""" + + num_layers: int = 4 + num_units: int = 64 + optimizer: str = "ADAM" + learning_rate: float = 0.001 + + +parser = ArgumentParser() +parser.add_arguments(HParams, dest="hparams") +args = parser.parse_args() + + +hparams: HParams = args.hparams + + +print(asdict(hparams)) +expected = """ +{'num_layers': 4, 'num_units': 64, 'optimizer': 'ADAM', 'learning_rate': 0.001} +""" + + +hparams.save_json("config.json") +hparams_ = HParams.load_json("config.json") +assert hparams == hparams_ + + +os.remove("config.json") diff --git a/docs/examples/subgroups/README.md b/docs/examples/subgroups/README.md new file mode 100644 index 00000000..0f500989 --- /dev/null +++ b/docs/examples/subgroups/README.md @@ -0,0 +1,151 @@ +# Subgroups + +Adding a choice between different subgroups of arguments can be very difficult using Argparse. +Subparsers are not exactly meant for this, and they introduce many errors + +This friction is one of the motivating factors for a plethora of argument parsing frameworks +such as Hydra, Click, and others. + +Simple-Parsing makes this easy, using the `subgroups` function! + +```python +from __future__ import annotations +from dataclasses import dataclass +from simple_parsing import ArgumentParser, choice, subgroups +from pathlib import Path + + +@dataclass +class ModelConfig: + ... + + +@dataclass +class DatasetConfig: + ... + + +@dataclass +class ModelAConfig(ModelConfig): + lr: float = 3e-4 + optimizer: str = "Adam" + betas: tuple[float, float] = 0.9, 0.999 + + +@dataclass +class ModelBConfig(ModelConfig): + lr: float = 1e-3 + optimizer: str = "SGD" + momentum: float = 1.234 + + +@dataclass +class Dataset1Config(DatasetConfig): + data_dir: str | Path = "data/foo" + foo: bool = False + + +@dataclass +class Dataset2Config(DatasetConfig): + data_dir: str | Path = "data/bar" + bar: float = 1.2 + + +@dataclass +class Config: + + # Which model to use + model: ModelConfig = subgroups( + {"model_a": ModelAConfig, "model_b": ModelBConfig}, + default=ModelAConfig(), + ) + + # Which dataset to use + dataset: DatasetConfig = subgroups( + {"dataset_1": Dataset1Config, "dataset_2": Dataset2Config}, default=Dataset2Config() + ) + + +parser = ArgumentParser() +parser.add_arguments(Config, dest="config") +args = parser.parse_args() + +config: Config = args.config + +print(config) +``` + +Calling this without arguments uses the defaults: + +```console +$ python examples/subgroups/subgroups_example.py +Config(model=ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999)), dataset=Dataset2Config(data_dir='data/bar', bar=1.2)) +``` + +The --help option is generated for the selected subgroups: + +```console +$ python examples/subgroups/subgroups_example.py --help +usage: subgroups_example.py [-h] [--model {model_a,model_b}] [--dataset {dataset_1,dataset_2}] [--model.lr float] [--model.optimizer str] [--model.betas float float] + [--dataset.data_dir str|Path] [--dataset.bar float] + +options: + -h, --help show this help message and exit + +Config ['config']: + Config(model: 'ModelConfig' = ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999)), dataset: 'DatasetConfig' = Dataset2Config(data_dir='data/bar', bar=1.2)) + + --model {model_a,model_b} + Which model to use (default: ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999))) + --dataset {dataset_1,dataset_2} + Which dataset to use (default: Dataset2Config(data_dir='data/bar', bar=1.2)) + +ModelAConfig ['config.model']: + ModelAConfig(lr: 'float' = 0.0003, optimizer: 'str' = 'Adam', betas: 'tuple[float, float]' = (0.9, 0.999)) + + --model.lr float (default: 0.0003) + --model.optimizer str + (default: Adam) + --model.betas float float + (default: (0.9, 0.999)) + +Dataset2Config ['config.dataset']: + Dataset2Config(data_dir: 'str | Path' = 'data/bar', bar: 'float' = 1.2) + + --dataset.data_dir str|Path + (default: data/bar) + --dataset.bar float (default: 1.2) +``` + +```console +$ python examples/subgroups/subgroups_example.py --model model_b --help +usage: subgroups_example.py [-h] [--model {model_a,model_b}] [--dataset {dataset_1,dataset_2}] [--model.lr float] [--model.optimizer str] [--model.momentum float] + [--dataset.data_dir str|Path] [--dataset.bar float] + +options: + -h, --help show this help message and exit + +Config ['config']: + Config(model: 'ModelConfig' = ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999)), dataset: 'DatasetConfig' = Dataset2Config(data_dir='data/bar', bar=1.2)) + + --model {model_a,model_b} + Which model to use (default: ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999))) + --dataset {dataset_1,dataset_2} + Which dataset to use (default: Dataset2Config(data_dir='data/bar', bar=1.2)) + +ModelBConfig ['config.model']: + ModelBConfig(lr: 'float' = 0.001, optimizer: 'str' = 'SGD', momentum: 'float' = 1.234) + + --model.lr float (default: 0.001) + --model.optimizer str + (default: SGD) + --model.momentum float + (default: 1.234) + +Dataset2Config ['config.dataset']: + Dataset2Config(data_dir: 'str | Path' = 'data/bar', bar: 'float' = 1.2) + + --dataset.data_dir str|Path + (default: data/bar) + --dataset.bar float (default: 1.2) +``` diff --git a/docs/examples/subgroups/subgroups_example.py b/docs/examples/subgroups/subgroups_example.py new file mode 100644 index 00000000..0b4610bc --- /dev/null +++ b/docs/examples/subgroups/subgroups_example.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +from simple_parsing import ArgumentParser, subgroups +from simple_parsing.wrappers.field_wrapper import ArgumentGenerationMode, NestedMode + + +@dataclass +class ModelConfig: + ... + + +@dataclass +class DatasetConfig: + ... + + +@dataclass +class ModelAConfig(ModelConfig): + lr: float = 3e-4 + optimizer: str = "Adam" + betas: tuple[float, float] = (0.9, 0.999) + + +@dataclass +class ModelBConfig(ModelConfig): + lr: float = 1e-3 + optimizer: str = "SGD" + momentum: float = 1.234 + + +@dataclass +class Dataset1Config(DatasetConfig): + data_dir: str | Path = "data/foo" + foo: bool = False + + +@dataclass +class Dataset2Config(DatasetConfig): + data_dir: str | Path = "data/bar" + bar: float = 1.2 + + +@dataclass +class Config: + # Which model to use + model: ModelConfig = subgroups( + {"model_a": ModelAConfig, "model_b": ModelBConfig}, + default_factory=ModelAConfig, + ) + + # Which dataset to use + dataset: DatasetConfig = subgroups( + {"dataset_1": Dataset1Config, "dataset_2": Dataset2Config}, + default_factory=Dataset2Config, + ) + + +parser = ArgumentParser( + argument_generation_mode=ArgumentGenerationMode.NESTED, nested_mode=NestedMode.WITHOUT_ROOT +) +parser.add_arguments(Config, dest="config") +args = parser.parse_args() + +config: Config = args.config + +print(config) +expected = """ +Config(model=ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999)), dataset=Dataset2Config(data_dir='data/bar', bar=1.2)) +""" + +parser.print_help() +expected += """ +usage: subgroups_example.py [-h] [--model {model_a,model_b}] [--dataset {dataset_1,dataset_2}] [--model.lr float] [--model.optimizer str] [--model.betas float float] + [--dataset.data_dir str|Path] [--dataset.bar float] + +options: + -h, --help show this help message and exit + +Config ['config']: + Config(model: 'ModelConfig' = ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999)), dataset: 'DatasetConfig' = Dataset2Config(data_dir='data/bar', bar=1.2)) + + --model {model_a,model_b} + Which model to use (default: ModelAConfig(lr=0.0003, optimizer='Adam', betas=(0.9, 0.999))) + --dataset {dataset_1,dataset_2} + Which dataset to use (default: Dataset2Config(data_dir='data/bar', bar=1.2)) + +ModelAConfig ['config.model']: + ModelAConfig(lr: 'float' = 0.0003, optimizer: 'str' = 'Adam', betas: 'tuple[float, float]' = (0.9, 0.999)) + + --model.lr float (default: 0.0003) + --model.optimizer str + (default: Adam) + --model.betas float float + (default: (0.9, 0.999)) + +Dataset2Config ['config.dataset']: + Dataset2Config(data_dir: 'str | Path' = 'data/bar', bar: 'float' = 1.2) + + --dataset.data_dir str|Path + (default: data/bar) + --dataset.bar float (default: 1.2) +""" diff --git a/docs/examples/subparsers/README.md b/docs/examples/subparsers/README.md new file mode 100644 index 00000000..3a29cfd5 --- /dev/null +++ b/docs/examples/subparsers/README.md @@ -0,0 +1,115 @@ +### [(Examples Home)](../README.md) + +# Creating Commands with Subparsers + +Subparsers are one of the more advanced features of `argparse`. They allow the creation of subcommands, each having their own set of arguments. The `git` command, for instance, takes different arguments than the `pull` subcommand in `git pull`. + +For some more info on subparsers, check out the [argparse documentation](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_subparsers). + +With `simple-parsing`, subparsers can easily be created by using a `Union` type annotation on a dataclass attribute. By annotating a variable with a Union type, for example `x: Union[T1, T2]`, we simply state that `x` can either be of type `T1` or `T2`. When the arguments to the `Union` type **are all dataclasses**, `simple-parsing` creates subparsers for each dataclass type, using the lowercased class name as the command name by default. + +If you want to extend or change this behaviour (to have "t" and "train" map to the same training subcommand, for example), use the `subparsers` function, passing in a dictionary mapping command names to the appropriate type. + + + +## Example: + +```python +from dataclasses import dataclass +from typing import * +from pathlib import Path +from simple_parsing import ArgumentParser, subparsers + +@dataclass +class Train: + """Example of a command to start a Training run.""" + # the training directory + train_dir: Path = Path("~/train") + + def execute(self): + print(f"Training in directory {self.train_dir}") + + +@dataclass +class Test: + """Example of a command to start a Test run.""" + # the testing directory + test_dir: Path = Path("~/train") + + def execute(self): + print(f"Testing in directory {self.test_dir}") + + +@dataclass +class Program: + """Some top-level command""" + command: Union[Train, Test] + verbose: bool = False # log additional messages in the console. + + def execute(self): + print(f"Executing Program (verbose: {self.verbose})") + return self.command.execute() + + +parser = ArgumentParser() +parser.add_arguments(Program, dest="prog") +args = parser.parse_args() +prog: Program = args.prog + +print("prog:", prog) +prog.execute() +``` + +Here are some usage examples: + +- Executing the training command: + + ```console + $ python examples/subparsers/subparsers_example.py train + prog: Program(command=Train(train_dir=PosixPath('~/train')), verbose=False) + Executing Program (verbose: False) + Training in directory ~/train + ``` + +- Passing a custom training directory: + + ```console + $ python examples/subparsers/subparsers_example.py train --train_dir ~/train + prog: Program(command=Train(train_dir=PosixPath('/home/fabrice/train')), verbose=False) + Executing Program (verbose: False) + Training in directory /home/fabrice/train + ``` + +- Getting help for a subcommand: + + ```console + $ python examples/subparsers/subparsers_example.py train --help + usage: subparsers_example.py train [-h] [--train_dir Path] + + optional arguments: + -h, --help show this help message and exit + + Train ['prog.command']: + Example of a command to start a Training run. + + --train_dir Path the training directory (default: ~/train) + ``` + +- Getting Help for the parent command: + + ```console + $ python examples/subparsers/subparsers_example.py --help + usage: subparsers_example.py [-h] [--verbose [str2bool]] {train,test} ... + + optional arguments: + -h, --help show this help message and exit + + Program ['prog']: + Some top-level command + + --verbose [str2bool] log additional messages in the console. (default: + False) + + command: + {train,test} + ``` diff --git a/docs/examples/subparsers/optional_subparsers.py b/docs/examples/subparsers/optional_subparsers.py new file mode 100644 index 00000000..39d76f82 --- /dev/null +++ b/docs/examples/subparsers/optional_subparsers.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass +from typing import Union + +from simple_parsing import ArgumentParser +from simple_parsing.helpers.fields import subparsers + + +@dataclass +class AConfig: + foo: int = 123 + + +@dataclass +class BConfig: + bar: float = 4.56 + + +@dataclass +class Options: + config: Union[AConfig, BConfig] = subparsers( + {"a": AConfig, "b": BConfig}, default_factory=AConfig + ) + + +def main(): + parser = ArgumentParser() + + parser.add_arguments(Options, dest="options") + + # Equivalent to: + # subparsers = parser.add_subparsers(title="config", required=False) + # parser.set_defaults(config=AConfig()) + # a_parser = subparsers.add_parser("a", help="A help.") + # a_parser.add_arguments(AConfig, dest="config") + # b_parser = subparsers.add_parser("b", help="B help.") + # b_parser.add_arguments(BConfig, dest="config") + + args = parser.parse_args() + + print(args) + options: Options = args.options + print(options) + + +main() +expected = """ +Namespace(options=Options(config=AConfig(foo=123))) +Options(config=AConfig(foo=123)) +""" diff --git a/docs/examples/subparsers/subparsers_example.py b/docs/examples/subparsers/subparsers_example.py new file mode 100644 index 00000000..7b30c7aa --- /dev/null +++ b/docs/examples/subparsers/subparsers_example.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Union + +from simple_parsing import ArgumentParser + + +@dataclass +class Train: + """Example of a command to start a Training run.""" + + # the training directory + train_dir: Path = Path("~/train") + + def execute(self): + print(f"Training in directory {self.train_dir}") + + +@dataclass +class Test: + """Example of a command to start a Test run.""" + + # the testing directory + test_dir: Path = Path("~/train") + + def execute(self): + print(f"Testing in directory {self.test_dir}") + + +@dataclass +class Program: + """Some top-level command.""" + + command: Union[Train, Test] + verbose: bool = False # log additional messages in the console. + + def execute(self): + print(f"Program (verbose: {self.verbose})") + return self.command.execute() + + +parser = ArgumentParser() +parser.add_arguments(Program, dest="prog") +args = parser.parse_args() +prog: Program = args.prog + +print("prog:", prog) +prog.execute() diff --git a/docs/examples/ugly/ugly_example_after.py b/docs/examples/ugly/ugly_example_after.py new file mode 100644 index 00000000..5275c171 --- /dev/null +++ b/docs/examples/ugly/ugly_example_after.py @@ -0,0 +1,251 @@ +"""Parameters module.""" + +# import getpass +# import torch +# import torch.nn.parallel +# import torch.backends.cudnn as cudnn +# import torch.utils.data +from dataclasses import dataclass, field +from typing import ClassVar, Optional + +import simple_parsing +from simple_parsing import choice + + +@dataclass +class DatasetParams: + """Dataset Parameters.""" + + default_root: ClassVar[str] = "/dataset" # the default root directory to use. + dataset: str = "objects_folder_multi" # laptop,pistol + """dataset name: [shapenet, objects_folder, objects_folder]')""" + + root_dir: str = default_root # dataset root directory + root_dir1: str = default_root # dataset root directory + root_dir2: str = default_root # dataset root directory + root_dir3: str = default_root # dataset root directory + root_dir4: str = default_root # dataset root directory + + synsets: str = "" # Synsets from the shapenet dataset to use + classes: str = "bowl" # Classes from the shapenet dataset to use #,cap,can,laptop + workers: int = 0 # number of data loading workers + light_change: int = 2000 # number of data loading workers + + toy_example: bool = False # Use toy example + use_old_sign: bool = True # Use toy example + use_quartic: bool = False # Use toy example + rescaled: bool = False # Use toy example + full_sphere_sampling: bool = False # Use toy example + full_sphere_sampling_light: bool = True # Use toy example + random_rotation: bool = True # Use toy example + stoch_enc: bool = False # Use toy example + only_background: bool = False # Use toy example + only_foreground: bool = False # Use toy example + rotate_foreground: bool = False # Use toy example + use_penality: bool = True # Use toy example + use_mesh: bool = True # Render dataset with meshes + + gen_model_path: Optional[str] = None # 'dataset root directory + gen_model_path2: Optional[str] = None # dataset root directory + dis_model_path: Optional[str] = None # dataset root directory + dis_model_path2: Optional[str] = None # dataset root directory + bg_model: str = "../../../data/halfbox.obj" # Background model path + + gz_gi_loss: float = 0.0 # grad z and grad img consistency. + pixel_samples: int = 1 # Samples per pixel. + + +@dataclass +class NetworkParams: + # Network parameters + gen_type: str = choice( + "dcgan", "mlp", "cnn", "resnet", default="dcgan" + ) # One of: mlp, cnn, dcgan, resnet # try resnet :) + gen_norm: str = choice( + "batchnorm", "instancenorm", default="batchnorm" + ) # One of: None, batchnorm, instancenorm + ngf: int = 75 # number of features in the generator network + nef: int = 65 # number of features in the generator network + gen_nextra_layers: int = 0 # number of extra layers in the generator network + gen_bias_type: Optional[str] = choice(None, "plane", default=None) # One of: None, plane + netG: str = "" # path to netG (to continue training) + netG2: str = "" # path to netG2 (normal generator to continue training) + fix_splat_pos: bool = True # X and Y coordinates are fix + zloss: float = 0.0 # use Z loss + unit_normalloss: float = 0.0 # use unit_normal loss + norm_sph_coord: bool = True # Use spherical coordinates for the normal + max_gnorm: float = 500.0 # max grad norm to which it will be clipped (if exceeded) + disc_type: str = choice("cnn", "dcgan", default="cnn") # One of: cnn, dcgan + disc_norm: str = choice( + "None", "batchnorm", "instancenorm", default="None" + ) # One of: None, batchnorm, instancenorm + ndf: int = 75 # number of features in the discriminator network + disc_nextra_layers: int = 0 # number of extra layers in the discriminator network + nz: int = 100 # size of the latent z vector + netD: str = "" # path to netD (to continue training) + netE: str = "" # path to netD (to continue training) + + +@dataclass +class OptimizerParams: + """Optimization parameters.""" + + optimizer: str = "adam" # Optimizer (adam, rmsprop) + lr: float = 0.0001 # learning rate, default=0.0002 + lr_sched_type: str = "step" # Learning rate scheduler type. + z_lr_sched_step: int = 100000 # Learning rate schedule for z. + lr_iter: int = 10000 # Learning rate operation iterations + normal_lr_sched_step: int = 100000 # Learning rate schedule for normal. + z_lr_sched_gamma: float = 1.0 # Learning rate gamma for z. + normal_lr_sched_gamma: float = 1.0 # Learning rate gamma for normal. + normal_consistency_loss_weight: float = 1e-3 # Normal consistency loss weight. + z_norm_weight_init: float = 1e-2 # Normal consistency loss weight. + z_norm_activate_iter: float = 1000 # Normal consistency loss weight. + spatial_var_loss_weight: float = 1e-2 # Spatial variance loss weight. + grad_img_depth_loss: float = 2.0 # Spatial variance loss weight. + spatial_loss_weight: float = 0.5 # Spatial smoothness loss weight. + beta1: float = 0.0 # beta1 for adam. default=0.5 + n_iter: int = 76201 # number of iterations to train + batchSize: int = 4 # input batch size + alt_opt_zn_interval: Optional[int] = None + """Alternating optimization interval. + + - None: joint optimization + - 20: every 20 iterations, etc. + """ + alt_opt_zn_start: int = 100000 + """Alternating optimization start interaction. + + - -1: starts immediately, + - '100: starts alternating after the first 100 iterations. + """ + + +@dataclass +class GanParams: + """Gan parameters.""" + + criterion: str = choice("GAN", "WGAN", default="WGAN") # GAN Training criterion + gp: str = choice("None", "original", default="original") # Add gradient penalty + gp_lambda: float = 10.0 # GP lambda + critic_iters: int = 5 # Number of critic iterations + clamp: float = 0.01 # clamp the weights for WGAN + + +@dataclass +class OtherParams: + """Other parameters.""" + + manualSeed: int = 1 # manual seed + no_cuda: bool = False # enables cuda + ngpu: int = 1 # number of GPUs to use + out_dir: str = "default_output" + name: str = "" + + +@dataclass +class CameraParams: + """Camera Parameters.""" + + cam_pos: tuple[float, float, float] = (0.0, 0.0, 0.0) # Camera position. + width: int = 128 + height: int = 128 + cam_dist: float = 3.0 # Camera distance from the center of the object + nv: int = 10 # Number of views to generate + angle: int = 30 # cam angle + fovy: float = 30 # Field of view in the vertical direction. + focal_length: float = 0.1 # focal length + theta: tuple[float, float] = (20, 80) # Angle in degrees from the z-axis. + phi: tuple[float, float] = (20, 70) # Angle in degrees from the x-axis. + axis: tuple[float, float, float] = ( + 0.0, + 1.0, + 0.0, + ) # Axis for random camera position. + at: tuple[float, float, float] = (0.05, 0.0, 0.0) # Camera lookat position. + sphere_halfbox: bool = False # Renders demo sphere-halfbox + norm_depth_image_only: bool = False # Render on the normalized depth image. + mesh: bool = False # Render as mesh if enabled. + test_cam_dist: bool = ( + False # Check if the images are consistent with a camera at a fixed distance. + ) + + +@dataclass +class RenderingParams: + splats_img_size: int = 128 # the height / width of the number of generator splats + render_type: str = "img" # render the image or the depth map [img, depth] + render_img_size: int = 128 # Width/height of the rendering image + splats_radius: float = 0.05 # radius of the splats (fix) + est_normals: bool = False # Estimate normals from splat positions. + n_splats: Optional[int] = None + same_view: bool = False # before we add conditioning on cam pose, this is necessary + """Data with view fixed.""" + + print_interval: int = 10 # Print loss interval. + save_image_interval: int = 100 # Save image interval. + save_interval: int = 5000 # Save state interval. + + +@dataclass +class Parameters: + """Base options.""" + + # Dataset parameters. + dataset: DatasetParams = field(default_factory=DatasetParams) + # Set of parameters related to the optimizer. + optimizer: OptimizerParams = field(default_factory=OptimizerParams) + # GAN Settings + gan: GanParams = field(default_factory=GanParams) + # Camera settings + camera: CameraParams = field(default_factory=CameraParams) + # Rendering-related settings + rendering: RenderingParams = field(default_factory=RenderingParams) + # other (misc) settings + other: OtherParams = field(default_factory=OtherParams) + + def __post_init__(self): + """Post-initialization code.""" + # Make output folder + # try: + # os.makedirs(self.other.out_dir) + # except OSError: + # pass + + # Set render number of channels + if self.rendering.render_type == "img": + self.rendering.render_img_nc = 3 + elif self.rendering.render_type == "depth": + self.rendering.render_img_nc = 1 + else: + raise ValueError("Unknown rendering type") + + # # Set random seed + # if self.other.manualSeed is None: + # self.other.manualSeed = random.randint(1, 10000) + # print("Random Seed: ", self.other.manualSeed) + # random.seed(self.other.manualSeed) + # torch.manual_seed(self.other.manualSeed) + # if not self.other.no_cuda: + # torch.cuda.manual_seed_all(self.other.manualSeed) + + # # Set number of splats param + # self.rendering.n_splats = self.rendering.splats_img_size ** 2 + + # # Check CUDA is selected + # cudnn.benchmark = True + # if torch.cuda.is_available() and self.other.no_cuda: + # print("WARNING: You have a CUDA device, so you should " + # "probably run with --cuda") + + @classmethod + def parse(cls): + parser = simple_parsing.ArgumentParser() + parser.add_arguments(cls, dest="parameters") + args = parser.parse_args() + instance: Parameters = args.parameters + return instance + + +params = Parameters.parse() +print(params) diff --git a/docs/examples/ugly/ugly_example_before.py b/docs/examples/ugly/ugly_example_before.py new file mode 100644 index 00000000..505a6679 --- /dev/null +++ b/docs/examples/ugly/ugly_example_before.py @@ -0,0 +1,552 @@ +"""Parameters module.""" +import argparse +import textwrap + +# import torch +# import torch.nn.parallel +# import torch.backends.cudnn as cudnn +# import torch.utils.data + + +class Parameters: + """Base options.""" + + def __init__(self): + """Constructor.""" + self.parser = argparse.ArgumentParser() + self.initialized = False + + def initialize(self): + """Initialize.""" + # Define training set depending on the user name + default_root = "default" + default_out = "out" + # Dataset parameters + self.parser.add_argument( + "--dataset", + type=str, + default="objects_folder_multi", + help="dataset name: [shapenet, objects_folder, objects_folder]", + ) # laptop,pistol + # self.parser.add_argument('--dataset', type=str, default='objects_folder', help='dataset name: [shapenet, objects_folder]') + self.parser.add_argument( + "--root_dir", type=str, default=default_root, help="dataset root directory" + ) + self.parser.add_argument( + "--root_dir1", type=str, default=default_root, help="dataset root directory" + ) + self.parser.add_argument( + "--root_dir2", type=str, default=default_root, help="dataset root directory" + ) + self.parser.add_argument( + "--root_dir3", type=str, default=default_root, help="dataset root directory" + ) + self.parser.add_argument( + "--root_dir4", type=str, default=default_root, help="dataset root directory" + ) + self.parser.add_argument( + "--synsets", + type=str, + default="", + help="Synsets from the shapenet dataset to use", + ) + self.parser.add_argument( + "--classes", + type=str, + default="bowl", + help="Classes from the shapenet dataset to use", + ) # ,cap,can,laptop + self.parser.add_argument( + "--workers", type=int, default=0, help="number of data loading workers" + ) + self.parser.add_argument( + "--light_change", + type=int, + default=2000, + help="number of data loading workers", + ) + self.parser.add_argument( + "--toy_example", action="store_true", default=False, help="Use toy example" + ) + self.parser.add_argument( + "--use_old_sign", action="store_true", default=True, help="Use toy example" + ) + self.parser.add_argument( + "--use_quartic", action="store_true", default=False, help="Use toy example" + ) + self.parser.add_argument( + "--rescaled", action="store_true", default=False, help="Use toy example" + ) + self.parser.add_argument( + "--full_sphere_sampling", + action="store_true", + default=False, + help="Use toy example", + ) + self.parser.add_argument( + "--full_sphere_sampling_light", + action="store_true", + default=True, + help="Use toy example", + ) + self.parser.add_argument( + "--random_rotation", + action="store_true", + default=True, + help="Use toy example", + ) + self.parser.add_argument( + "--stoch_enc", action="store_true", default=False, help="Use toy example" + ) + self.parser.add_argument( + "--only_background", + action="store_true", + default=False, + help="Use toy example", + ) + self.parser.add_argument( + "--only_foreground", + action="store_true", + default=False, + help="Use toy example", + ) + self.parser.add_argument( + "--rotate_foreground", + action="store_true", + default=False, + help="Use toy example", + ) + self.parser.add_argument( + "--use_penality", action="store_true", default=True, help="Use toy example" + ) + self.parser.add_argument( + "--use_mesh", + action="store_true", + default=True, + help="Render dataset with meshes", + ) + self.parser.add_argument( + "--gen_model_path", type=str, default=None, help="dataset root directory" + ) + self.parser.add_argument( + "--gen_model_path2", type=str, default=None, help="dataset root directory" + ) + self.parser.add_argument( + "--dis_model_path", type=str, default=None, help="dataset root directory" + ) + self.parser.add_argument( + "--dis_model_path2", type=str, default=None, help="dataset root directory" + ) + self.parser.add_argument( + "--bg_model", + type=str, + default="../../../data/halfbox.obj", + help="Background model path", + ) + self.parser.add_argument( + "--gz_gi_loss", + type=float, + default=0.0, + help="grad z and grad img consistency.", + ) + self.parser.add_argument("--pixel_samples", type=int, default=1, help="Samples per pixel.") + + # Network parameters + self.parser.add_argument( + "--gen_type", + type=str, + default="dcgan", + help="One of: mlp, cnn, dcgan, resnet", + ) # try resnet :) + self.parser.add_argument( + "--gen_norm", + type=str, + default="batchnorm", + help="One of: None, batchnorm, instancenorm", + ) + self.parser.add_argument( + "--ngf", + type=int, + default=75, + help="number of features in the generator network", + ) + self.parser.add_argument( + "--nef", + type=int, + default=65, + help="number of features in the generator network", + ) + self.parser.add_argument( + "--gen_nextra_layers", + type=int, + default=0, + help="number of extra layers in the generator network", + ) + self.parser.add_argument( + "--gen_bias_type", type=str, default=None, help="One of: None, plane" + ) + self.parser.add_argument("--netG", default="", help="path to netG (to continue training)") + self.parser.add_argument( + "--netG2", + default="", + help="path to netG2 (normal generator to continue training)", + ) + self.parser.add_argument( + "--fix_splat_pos", + action="store_true", + default=True, + help="X and Y coordinates are fix", + ) + self.parser.add_argument("--zloss", type=float, default=0.0, help="use Z loss") + self.parser.add_argument( + "--unit_normalloss", type=float, default=0.0, help="use unit_normal loss" + ) + self.parser.add_argument( + "--norm_sph_coord", + action="store_true", + default=True, + help="Use spherical coordinates for the normal", + ) + self.parser.add_argument( + "--max_gnorm", + type=float, + default=500.0, + help="max grad norm to which it will be clipped (if exceeded)", + ) + self.parser.add_argument("--disc_type", type=str, default="cnn", help="One of: cnn, dcgan") + self.parser.add_argument( + "--disc_norm", + type=str, + default="None", + help="One of: None, batchnorm, instancenorm", + ) + self.parser.add_argument( + "--ndf", + type=int, + default=75, + help="number of features in the discriminator network", + ) + self.parser.add_argument( + "--disc_nextra_layers", + type=int, + default=0, + help="number of extra layers in the discriminator network", + ) + self.parser.add_argument("--nz", type=int, default=100, help="size of the latent z vector") + self.parser.add_argument("--netD", default="", help="path to netD (to continue training)") + self.parser.add_argument("--netE", default="", help="path to netD (to continue training)") + + # Optimization parameters + self.parser.add_argument( + "--optimizer", type=str, default="adam", help="Optimizer (adam, rmsprop)" + ) + self.parser.add_argument( + "--lr", type=float, default=0.0001, help="learning rate, default=0.0002" + ) + self.parser.add_argument( + "--lr_sched_type", + type=str, + default="step", + help="Learning rate scheduler type.", + ) + self.parser.add_argument( + "--z_lr_sched_step", + type=int, + default=100000, + help="Learning rate schedule for z.", + ) + self.parser.add_argument( + "--lr_iter", + type=int, + default=10000, + help="Learning rate operation iterations", + ) + self.parser.add_argument( + "--normal_lr_sched_step", + type=int, + default=100000, + help="Learning rate schedule for " "normal.", + ) + self.parser.add_argument( + "--z_lr_sched_gamma", + type=float, + default=1.0, + help="Learning rate gamma for z.", + ) + self.parser.add_argument( + "--normal_lr_sched_gamma", + type=int, + default=1.0, + help="Learning rate gamma for " "normal.", + ) + self.parser.add_argument( + "--alt_opt_zn_interval", + type=int, + default=None, + help="Alternating optimization interval. " + "[None: joint optimization, 20: every 20 iterations, etc.]", + ) + self.parser.add_argument( + "--alt_opt_zn_start", + type=int, + default=100000, + help="Alternating optimization start interaction. [-1: starts immediately," + "100: starts alternating after the first 100 iterations.", + ) + self.parser.add_argument( + "--normal_consistency_loss_weight", + type=float, + default=1e-3, + help="Normal consistency loss weight.", + ) + self.parser.add_argument( + "--z_norm_weight_init", + type=float, + default=1e-2, + help="Normal consistency loss weight.", + ) + self.parser.add_argument( + "--z_norm_activate_iter", + type=float, + default=1000, + help="Normal consistency loss weight.", + ) + self.parser.add_argument( + "--spatial_var_loss_weight", + type=float, + default=1e-2, + help="Spatial variance loss weight.", + ) + self.parser.add_argument( + "--grad_img_depth_loss", + type=float, + default=2.0, + help="Spatial variance loss weight.", + ) + self.parser.add_argument( + "--spatial_loss_weight", + type=float, + default=0.5, + help="Spatial smoothness loss weight.", + ) + self.parser.add_argument( + "--beta1", type=float, default=0.0, help="beta1 for adam. default=0.5" + ) + self.parser.add_argument( + "--n_iter", type=int, default=76201, help="number of iterations to train" + ) + self.parser.add_argument("--batchSize", type=int, default=4, help="input batch size") + + # GAN parameters + self.parser.add_argument( + "--criterion", + help="GAN Training criterion", + choices=["GAN", "WGAN"], + default="WGAN", + ) + self.parser.add_argument( + "--gp", + help="Add gradient penalty", + choices=["None", "original"], + default="original", + ) + self.parser.add_argument("--gp_lambda", help="GP lambda", type=float, default=10.0) + self.parser.add_argument( + "--critic_iters", type=int, default=5, help="Number of critic iterations" + ) + self.parser.add_argument( + "--clamp", type=float, default=0.01, help="clamp the weights for WGAN" + ) + + # Other parameters + self.parser.add_argument( + "--no_cuda", action="store_true", default=False, help="enables cuda" + ) + self.parser.add_argument("--ngpu", type=int, default=1, help="number of GPUs to use") + self.parser.add_argument("--manualSeed", type=int, help="manual seed") + self.parser.add_argument("--out_dir", type=str, default=default_out) + self.parser.add_argument("--name", type=str, default="", required=False) + + # Camera parameters + self.parser.add_argument("--width", type=int, default=128) + self.parser.add_argument("--height", type=int, default=128) + self.parser.add_argument( + "--cam_dist", + type=float, + default=3.0, + help="Camera distance from the center of the object", + ) + self.parser.add_argument("--nv", type=int, default=10, help="Number of views to generate") + self.parser.add_argument("--angle", type=int, default=30, help="cam angle") + self.parser.add_argument( + "--fovy", + type=float, + default=30, + help="Field of view in the vertical direction. Default: 15.0", + ) + self.parser.add_argument("--focal_length", type=float, default=0.1, help="focal length") + self.parser.add_argument( + "--theta", + nargs=2, + type=float, + default=[20, 80], + help="Angle in degrees from the z-axis.", + ) + self.parser.add_argument( + "--phi", + nargs=2, + type=float, + default=[20, 70], + help="Angle in degrees from the x-axis.", + ) + self.parser.add_argument( + "--axis", + nargs=3, + default=[0.0, 1.0, 0.0], + type=float, + help="Axis for random camera position.", + ) + self.parser.add_argument("--cam_pos", nargs=3, type=float, help="Camera position.") + self.parser.add_argument( + "--at", + nargs=3, + default=[0.05, 0.0, 0], + type=float, + help="Camera lookat position.", + ) + # self.parser.add_argument('--at', nargs=3, default=[ 0, 1, 0], type=float, help='Camera lookat position.') + self.parser.add_argument( + "--sphere-halfbox", action="store_true", help="Renders demo sphere-halfbox" + ) + self.parser.add_argument( + "--norm_depth_image_only", + action="store_true", + default=False, + help="Render on the normalized" " depth image.", + ) + self.parser.add_argument("--mesh", action="store_true", help="Render as mesh if enabled.") + self.parser.add_argument( + "--test_cam_dist", + action="store_true", + help="Check if the images are consistent with a" "camera at a fixed distance.", + ) + + # Rendering parameters + self.parser.add_argument( + "--splats_img_size", + type=int, + default=128, + help="the height / width of the number of generator splats", + ) + self.parser.add_argument( + "--render_type", + type=str, + default="img", + help="render the image or the depth map [img, depth]", + ) + self.parser.add_argument( + "--render_img_size", + type=int, + default=128, + help="Width/height of the rendering image", + ) + self.parser.add_argument( + "--splats_radius", + type=float, + default=0.05, + help="radius of the splats (fix)", + ) + self.parser.add_argument( + "--est_normals", + action="store_true", + help="Estimate normals from splat positions.", + ) + self.parser.add_argument( + "--same_view", action="store_true", help="data with view fixed" + ) # before we add conditioning on cam pose, this is necessary + self.parser.add_argument( + "--print_interval", type=int, default=10, help="Print loss interval." + ) + self.parser.add_argument( + "--save_image_interval", type=int, default=100, help="Save image interval." + ) + self.parser.add_argument( + "--save_interval", type=int, default=5000, help="Save state interval." + ) + + def parse(self): + """Parse.""" + if not self.initialized: + self.initialize() + self.opt = self.parser.parse_args() + + # Make output folder + # try: + # os.makedirs(self.opt.out_dir) + # except OSError: + # pass + + # Set render number of channels + if self.opt.render_type == "img": + self.opt.render_img_nc = 3 + elif self.opt.render_type == "depth": + self.opt.render_img_nc = 1 + else: + raise ValueError("Unknown rendering type") + + # # Set random seed + # if self.opt.manualSeed is None: + # self.opt.manualSeed = random.randint(1, 10000) + # print("Random Seed: ", self.opt.manualSeed) + # random.seed(self.opt.manualSeed) + # torch.manual_seed(self.opt.manualSeed) + # if not self.opt.no_cuda: + # torch.cuda.manual_seed_all(self.opt.manualSeed) + + # # Set number of splats param + # self.opt.n_splats = self.opt.splats_img_size*self.opt.splats_img_size + + # # Check CUDA is selected + # cudnn.benchmark = True + # if torch.cuda.is_available() and self.opt.no_cuda: + # print("WARNING: You have a CUDA device, so you should " + # "probably run with --cuda") + + return self.opt + + +param = Parameters() +args = param.parse() + +print("\n".join(textwrap.wrap(str(args), width=80))) +# print(args) +expected = """ +Namespace(alt_opt_zn_interval=None, alt_opt_zn_start=100000, angle=30, at=[0.05, +0.0, 0], axis=[0.0, 1.0, 0.0], batchSize=4, beta1=0.0, +bg_model='../../../data/halfbox.obj', cam_dist=3.0, cam_pos=None, clamp=0.01, +classes='bowl', criterion='WGAN', critic_iters=5, +dataset='objects_folder_multi', dis_model_path=None, dis_model_path2=None, +disc_nextra_layers=0, disc_norm='None', disc_type='cnn', est_normals=False, +fix_splat_pos=True, focal_length=0.1, fovy=30, full_sphere_sampling=False, +full_sphere_sampling_light=True, gen_bias_type=None, gen_model_path=None, +gen_model_path2=None, gen_nextra_layers=0, gen_norm='batchnorm', +gen_type='dcgan', gp='original', gp_lambda=10.0, grad_img_depth_loss=2.0, +gz_gi_loss=0.0, height=128, light_change=2000, lr=0.0001, lr_iter=10000, +lr_sched_type='step', manualSeed=None, max_gnorm=500.0, mesh=False, +n_iter=76201, name='', ndf=75, nef=65, netD='', netE='', netG='', netG2='', +ngf=75, ngpu=1, no_cuda=False, norm_depth_image_only=False, norm_sph_coord=True, +normal_consistency_loss_weight=0.001, normal_lr_sched_gamma=1.0, +normal_lr_sched_step=100000, nv=10, nz=100, only_background=False, +only_foreground=False, optimizer='adam', out_dir='out', phi=[20, 70], +pixel_samples=1, print_interval=10, random_rotation=True, render_img_nc=3, +render_img_size=128, render_type='img', rescaled=False, root_dir='default', +root_dir1='default', root_dir2='default', root_dir3='default', +root_dir4='default', rotate_foreground=False, same_view=False, +save_image_interval=100, save_interval=5000, spatial_loss_weight=0.5, +spatial_var_loss_weight=0.01, sphere_halfbox=False, splats_img_size=128, +splats_radius=0.05, stoch_enc=False, synsets='', test_cam_dist=False, theta=[20, +80], toy_example=False, unit_normalloss=0.0, use_mesh=True, use_old_sign=True, +use_penality=True, use_quartic=False, width=128, workers=0, +z_lr_sched_gamma=1.0, z_lr_sched_step=100000, z_norm_activate_iter=1000, +z_norm_weight_init=0.01, zloss=0.0) +""" diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..3eca923e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,5 @@ +# Welcome to simple_parsing Documentation + +This documentation provides an overview of the `simple_parsing` library and its features, along with usage examples. + +Navigate through the sections to explore different aspects of the library. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..6b359251 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,95 @@ +site_name: simple_parsing Documentation +theme: + name: material +nav: + - Home: index.md + - Examples: + - Overview: examples/README.md + - Demo: examples/demo.py + - Demo Simple: examples/demo_simple.py + - ML: + - Overview: examples/ML/README.md + - Ml Example After: examples/ML/ml_example_after.py + - Ml Example Before: examples/ML/ml_example_before.py + - Other Ml Example: examples/ML/other_ml_example.py + - Aliases: + - Overview: examples/aliases/README.md + - Aliases Example: examples/aliases/aliases_example.py + - Config Files: + - Overview: examples/config_files/README.md + - Composition: examples/config_files/composition.py + - Many Configs: examples/config_files/many_configs.py + - One Config: examples/config_files/one_config.py + - Container Types: + - Overview: examples/container_types/README.md + - Lists Example: examples/container_types/lists_example.py + - Custom Args: + - Overview: examples/custom_args/README.md + - Custom Args Example: examples/custom_args/custom_args_example.py + - Dataclasses: + - Overview: examples/dataclasses/README.md + - Dataclass Example: examples/dataclasses/dataclass_example.py + - Hyperparameters Example: examples/dataclasses/hyperparameters_example.py + - Docstrings: + - Overview: examples/docstrings/README.md + - Docstrings Example: examples/docstrings/docstrings_example.py + - Enums: + - Overview: examples/enums/README.md + - Enums Example: examples/enums/enums_example.py + - Inheritance: + - Overview: examples/inheritance/README.md + - Inheritance Example: examples/inheritance/inheritance_example.py + - Ml Inheritance: examples/inheritance/ml_inheritance.py + - Ml Inheritance 2: examples/inheritance/ml_inheritance_2.py + - Merging: + - Overview: examples/merging/README.md + - Multiple Example: examples/merging/multiple_example.py + - Multiple Lists Example: examples/merging/multiple_lists_example.py + - Nesting: + - Overview: examples/nesting/README.md + - Nesting Example: examples/nesting/nesting_example.py + - Partials: + - Overview: examples/partials/README.md + - Partials Example: examples/partials/partials_example.py + - Prefixing: + - Overview: examples/prefixing/README.md + - Manual Prefix Example: examples/prefixing/manual_prefix_example.py + - Serialization: + - Overview: examples/serialization/README.md + - Custom Types Example: examples/serialization/custom_types_example.py + - Serialization Example: examples/serialization/serialization_example.py + - Simple: + - Basic: examples/simple/basic.py + - Choice: examples/simple/choice.py + - Flag: examples/simple/flag.py + - Help: examples/simple/help.py + - Inheritance: examples/simple/inheritance.py + - Option Strings: examples/simple/option_strings.py + - Reuse: examples/simple/reuse.py + - To Json: examples/simple/to_json.py + - Subgroups: + - Overview: examples/subgroups/README.md + - Subgroups Example: examples/subgroups/subgroups_example.py + - Subparsers: + - Overview: examples/subparsers/README.md + - Optional Subparsers: examples/subparsers/optional_subparsers.py + - Subparsers Example: examples/subparsers/subparsers_example.py + - Ugly: + - Ugly Example After: examples/ugly/ugly_example_after.py + - Ugly Example Before: examples/ugly/ugly_example_before.py + - API Reference: api_reference.md +markdown_extensions: + - pymdownx.superfences + - toc: + permalink: true + - codehilite: + guess_lang: false + noclasses: true +plugins: + - search # Add search if not already there, common plugin + - mkdocstrings: + handlers: + python: + options: + show_root_heading: True + show_source: True diff --git a/pyproject.toml b/pyproject.toml index aecc7640..0a0bab52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,4 +49,7 @@ dev = [ "pytest-cov>=6.0.0", "pytest-regressions>=2.7.0", "pytest-xdist>=3.6.1", + "mkdocs>=1.0", + "mkdocs-material>=7.0", + "mkdocstrings[python]>=0.18", ] diff --git a/uv.lock b/uv.lock index 12e56d28..64f3a0d2 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,145 @@ version = 1 +revision = 1 requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, +] + +[[package]] +name = "backrefs" +version = "5.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382 }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536 }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349 }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365 }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499 }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735 }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786 }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436 }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] [[package]] name = "colorama" @@ -244,6 +384,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/e6/efdcd5d6858b951c29d56de31a19355579d826712bf390d964a21b076ddb/fonttools-4.55.8-py3-none-any.whl", hash = "sha256:07636dae94f7fe88561f9da7a46b13d8e3f529f87fdb221b11d85f91eabceeb7", size = 1089900 }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "griffe" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, +] + [[package]] name = "importlib-resources" version = "6.5.2" @@ -265,6 +450,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + [[package]] name = "kiwisolver" version = "1.4.7" @@ -365,6 +562,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811 }, ] +[[package]] +name = "markdown" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, +] + [[package]] name = "matplotlib" version = "3.9.4" @@ -425,6 +702,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624 }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fa/0101de32af88f87cf5cc23ad5f2e2030d00995f74e616306513431b8ab4b/mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754", size = 3951707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/a1/7fdb959ad592e013c01558822fd3c22931a95a0f08cf0a7c36da13a5b2b5/mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b", size = 8703767 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "mkdocstrings" +version = "0.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/a3/0c7559a355fa21127a174a5aa2d3dca2de6e479ddd9c63ca4082d5f9980c/mkdocstrings_python-1.16.11.tar.gz", hash = "sha256:935f95efa887f99178e4a7becaaa1286fb35adafffd669b04fd611d97c00e5ce", size = 205392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/c4/ffa32f2c7cdb1728026c7a34aab87796b895767893aaa54611a79b4eef45/mkdocstrings_python-1.16.11-py3-none-any.whl", hash = "sha256:25d96cc9c1f9c272ea1bd8222c900b5f852bf46c984003e9c7c56eaa4696190f", size = 124282 }, +] + [[package]] name = "numpy" version = "2.0.2" @@ -486,6 +896,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + [[package]] name = "pillow" version = "11.1.0" @@ -564,6 +992,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 }, ] +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -582,6 +1019,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, ] +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845 }, +] + [[package]] name = "pyparsing" version = "3.2.1" @@ -738,6 +1197,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + [[package]] name = "simple-parsing" source = { editable = "." } @@ -758,6 +1244,9 @@ yaml = [ [package.dev-dependencies] dev = [ { name = "matplotlib" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, { name = "numpy" }, { name = "pytest" }, { name = "pytest-benchmark" }, @@ -774,10 +1263,14 @@ requires-dist = [ { name = "tomli-w", marker = "extra == 'toml'", specifier = ">=1.0.0" }, { name = "typing-extensions", specifier = ">=4.5.0" }, ] +provides-extras = ["toml", "yaml"] [package.metadata.requires-dev] dev = [ { name = "matplotlib", specifier = ">=3.9.4" }, + { name = "mkdocs", specifier = ">=1.0" }, + { name = "mkdocs-material", specifier = ">=7.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.18" }, { name = "numpy", specifier = ">=2.0.2" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, @@ -852,6 +1345,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390 }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386 }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017 }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903 }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + [[package]] name = "zipp" version = "3.21.0"