We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Upgrade from Python 3.10.6 to Python 3.10.8
<!-- What issue does this PR close? -->
Closes #

<!-- What does this PR achieve? [feature|hotfix|fix|refactor] -->
Repeat the process discussed in https://github.com/internetarchive/openlibrary/pull/6340#issuecomment-1086270358 to upgrade CPython.
[This update](https://docs.python.org/release/3.10.8/whatsnew/changelog.html) includes a mitigation for [CVE-2020-10735](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10735) and several other security fixes.

### Technical
<!-- What should be noted about the implementation? -->

### Testing
<!-- Steps for reviewer to reproduce/verify what this PR does/fixes. -->
For testing, please do (from https://github.com/internetarchive/openlibrary/pull/6340#issuecomment-1086270358):
* [x] On `ol-dev1` (in `ol-cclauss`), check out branch and build a new olbase image tagged `-t openlibrary/olbase:py3.10.8`
    * [x] `git fetch origin && git checkout python3.10.8`
    * [x] `docker build --no-cache -t openlibrary/olbase:py3.10.8 -f docker/Dockerfile.olbase .`
    * [x] `hostname ; docker image ls | grep 3.10.8 ` # -> `ol-dev1.us.archive.org`, `repo: openlibrary/olbase, tag: py3.10.8`
* [x] Ask **@cdrini** to push this up to docker hub once it's done
* [ ] Pull it down locally and test the site with the `OLIMAGE` param
    * [ ] Also test solr-updater starts up ok ; it's using FnToCLI which has caused issues with types/python handling of types
* [ ] Restart testing.openlibrary.org with the OLIMAGE param ; confirm things work
* [ ] Merge!

### During the `docker build` process -- DISK WARNING - free space: / 8905 MB (15% inode=38%):

### npm deprication warnings --> #7084

### During the `docker build` process -- Warning: apt-key is deprecated.
```
Step 9/27 : RUN wget -O - https://openresty.org/package/pubkey.gpg | apt-key add -
 ---> Running in d075e06118ad
--2022-10-16 13:25:19--  https://openresty.org/package/pubkey.gpg
Resolving openresty.org (openresty.org)... Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
3.131.85.84, 2600:1f1c:9b2:8000:f183:c67e:2c64:855f
Connecting to openresty.org (openresty.org)|3.131.85.84|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1688 (1.6K) [text/plain]
Saving to: 'STDOUT'

     0K .                                                     100% 65.4M=0s

2022-10-16 13:25:20 (65.4 MB/s) - written to stdout [1688/1688]
```
### Screenshot
<!-- If this PR touches UI, please post evidence (screenshots) of it behaving correctly. -->

### Stakeholders
<!-- @ tag stakeholders of this bug -->


<!-- Attribution Disclaimer: By proposing this pull request, I affirm to have made a best-effort and exercised my discretion to make sure relevant sections of this code that substantially leverage code suggestions, code generation, or code snippets from sources (e.g. Stack Overflow, GitHub) have been annotated with basic attribution so reviewers & contributors may have confidence and access to the correct context to evaluate and use this code. -->


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `scripts/oldump.py`
Content:
```
1 #!/usr/bin/env python
2 
3 import logging
4 import os
5 import sys
6 from datetime import datetime
7 
8 import _init_path  # noqa: F401  Imported for its side effect of setting PYTHONPATH
9 
10 logger = logging.getLogger(__file__)
11 logger.setLevel(logging.DEBUG)
12 
13 
14 def log(*args) -> None:
15     args_str = " ".join(str(a) for a in args)
16     msg = f"{datetime.now():%Y-%m-%d %H:%M:%S} [openlibrary.dump] {args_str}"
17     logger.info(msg)
18     print(msg, file=sys.stderr)
19 
20 
21 if __name__ == "__main__":
22     from contextlib import redirect_stdout
23     from infogami import config
24     from openlibrary.config import load_config
25     from openlibrary.data import dump
26     from openlibrary.utils.sentry import Sentry
27 
28     log("{} on Python {}.{}.{}".format(sys.argv, *sys.version_info))  # Python 3.10.4
29 
30     ol_config = os.getenv("OL_CONFIG")
31     if ol_config:
32         logger.info(f"loading config from {ol_config}")
33         # Squelch output from infobase (needed for sentry setup)
34         # So it doesn't end up in our data dumps body
35         with open(os.devnull, 'w') as devnull:
36             with redirect_stdout(devnull):
37                 load_config(ol_config)
38         sentry = Sentry(getattr(config, "sentry_cron_jobs", {}))
39         if sentry.enabled:
40             sentry.init()
41     log(f"sentry.enabled = {bool(ol_config and sentry.enabled)}")
42 
43     dump.main(sys.argv[1], sys.argv[2:])
44
```

Path: `scripts/solr_builder/solr_builder/fn_to_cli.py`
Content:
```
1 import asyncio
2 import typing
3 from argparse import (
4     ArgumentParser,
5     ArgumentDefaultsHelpFormatter,
6     BooleanOptionalAction,
7     Namespace,
8 )
9 
10 
11 class FnToCLI:
12     """
13     A utility class which automatically infers and generates ArgParse command
14     line options from a function based on defaults/type annotations
15 
16     This is _very_ basic; supports:
17     * Args of int, str types (same logic as default argparse)
18     * Args of bool type (Uses argparse BooleanOptionalAction)
19         * eg `do_blah=False` becomes `--do-blah, --no-do-blah`
20     * Args of typing.Optional (or anything with a default)
21     * Args of typing.Literal (uses argparse choices)
22         * eg `color: Literal['red, 'black']` becomes `--color red|black` (with docs)
23     * Type deduction of default values
24     * Supports async functions automatically
25     * Includes docstring if it's in `:param my_arg: Description of my arg` format
26 
27     Anything else will likely error :)
28 
29     Example:
30     if __name__ == '__main__':
31         FnToCLI(my_func).run()
32     """
33 
34     def __init__(self, fn: typing.Callable):
35         self.fn = fn
36         arg_names = fn.__code__.co_varnames[: fn.__code__.co_argcount]
37         annotations = typing.get_type_hints(fn)
38         defaults: list = fn.__defaults__ or []  # type: ignore[assignment]
39         num_required = len(arg_names) - len(defaults)
40         default_args = arg_names[num_required:]
41         defaults: dict = {  # type: ignore[no-redef]
42             arg: default for [arg, default] in zip(default_args, defaults)
43         }
44 
45         docs = fn.__doc__ or ''
46         arg_docs = self.parse_docs(docs)
47         self.parser = ArgumentParser(
48             description=docs.split(':param', 1)[0],
49             formatter_class=ArgumentDefaultsHelpFormatter,
50         )
51         self.args: typing.Optional[Namespace] = None
52         for arg in arg_names:
53             optional = arg in defaults
54             cli_name = arg.replace('_', '-')
55 
56             if arg in annotations:
57                 arg_opts = self.type_to_argparse(annotations[arg])
58             elif arg in defaults:
59                 arg_opts = self.type_to_argparse(type(defaults[arg]))  # type: ignore[call-overload]
60             else:
61                 raise ValueError(f'{arg} has no type information')
62 
63             # Help needs to always be defined, or it won't show the default :/
64             arg_opts['help'] = arg_docs.get(arg) or '-'
65 
66             if optional:
67                 opt_name = f'--{cli_name}' if len(cli_name) > 1 else f'-{cli_name}'
68                 self.parser.add_argument(opt_name, default=defaults[arg], **arg_opts)  # type: ignore[call-overload]
69             else:
70                 self.parser.add_argument(cli_name, **arg_opts)
71 
72     def parse_args(self):
73         self.args = self.parser.parse_args()
74         return self.args
75 
76     def args_dict(self):
77         if not self.args:
78             self.parse_args()
79 
80         return {k.replace('-', '_'): v for k, v in self.args.__dict__.items()}
81 
82     def run(self):
83         args_dicts = self.args_dict()
84         if asyncio.iscoroutinefunction(self.fn):
85             asyncio.run(self.fn(**args_dicts))
86         else:
87             self.fn(**args_dicts)
88 
89     @staticmethod
90     def parse_docs(docs):
91         params = docs.strip().split(':param ')[1:]
92         params = [p.strip() for p in params]
93         params = [p.split(':', 1) for p in params if p]
94         return {name: docs.strip() for [name, docs] in params}
95 
96     @staticmethod
97     def type_to_argparse(typ: type) -> dict:
98         if FnToCLI.is_optional(typ):
99             return FnToCLI.type_to_argparse(
100                 next(t for t in typing.get_args(typ) if not isinstance(t, type(None)))
101             )
102         if typ == bool:
103             return {'type': typ, 'action': BooleanOptionalAction}
104         if typ in (int, str, float):
105             return {'type': typ}
106         if typ == list[str]:
107             return {'nargs': '*'}
108         if typing.get_origin(typ) == typing.Literal:
109             return {'choices': typing.get_args(typ)}
110         raise ValueError(f'Unsupported type: {typ}')
111 
112     @staticmethod
113     def is_optional(typ: type) -> bool:
114         return (
115             typing.get_origin(typ) is typing.Union
116             and type(None) in typing.get_args(typ)
117             and len(typing.get_args(typ)) == 2
118         )
119
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/scripts/oldump.py b/scripts/oldump.py
--- a/scripts/oldump.py
+++ b/scripts/oldump.py
@@ -25,7 +25,7 @@
     from openlibrary.data import dump
     from openlibrary.utils.sentry import Sentry
 
-    log("{} on Python {}.{}.{}".format(sys.argv, *sys.version_info))  # Python 3.10.4
+    log("{} on Python {}.{}.{}".format(sys.argv, *sys.version_info))  # Python 3.11.1
 
     ol_config = os.getenv("OL_CONFIG")
     if ol_config:
diff --git a/scripts/solr_builder/solr_builder/fn_to_cli.py b/scripts/solr_builder/solr_builder/fn_to_cli.py
--- a/scripts/solr_builder/solr_builder/fn_to_cli.py
+++ b/scripts/solr_builder/solr_builder/fn_to_cli.py
@@ -1,4 +1,5 @@
 import asyncio
+import types
 import typing
 from argparse import (
     ArgumentParser,
@@ -112,7 +113,7 @@
     @staticmethod
     def is_optional(typ: type) -> bool:
         return (
-            typing.get_origin(typ) is typing.Union
+            (typing.get_origin(typ) is typing.Union or isinstance(typ, types.UnionType))
             and type(None) in typing.get_args(typ)
             and len(typing.get_args(typ)) == 2
         )

{"golden_diff": "diff --git a/scripts/oldump.py b/scripts/oldump.py\n--- a/scripts/oldump.py\n+++ b/scripts/oldump.py\n@@ -25,7 +25,7 @@\n     from openlibrary.data import dump\n     from openlibrary.utils.sentry import Sentry\n \n-    log(\"{} on Python {}.{}.{}\".format(sys.argv, *sys.version_info))  # Python 3.10.4\n+    log(\"{} on Python {}.{}.{}\".format(sys.argv, *sys.version_info))  # Python 3.11.1\n \n     ol_config = os.getenv(\"OL_CONFIG\")\n     if ol_config:\ndiff --git a/scripts/solr_builder/solr_builder/fn_to_cli.py b/scripts/solr_builder/solr_builder/fn_to_cli.py\n--- a/scripts/solr_builder/solr_builder/fn_to_cli.py\n+++ b/scripts/solr_builder/solr_builder/fn_to_cli.py\n@@ -1,4 +1,5 @@\n import asyncio\n+import types\n import typing\n from argparse import (\n     ArgumentParser,\n@@ -112,7 +113,7 @@\n     @staticmethod\n     def is_optional(typ: type) -> bool:\n         return (\n-            typing.get_origin(typ) is typing.Union\n+            (typing.get_origin(typ) is typing.Union or isinstance(typ, types.UnionType))\n             and type(None) in typing.get_args(typ)\n             and len(typing.get_args(typ)) == 2\n         )\n", "issue": "Upgrade from Python 3.10.6 to Python 3.10.8\n<!-- What issue does this PR close? -->\r\nCloses #\r\n\r\n<!-- What does this PR achieve? [feature|hotfix|fix|refactor] -->\r\nRepeat the process discussed in https://github.com/internetarchive/openlibrary/pull/6340#issuecomment-1086270358 to upgrade CPython.\r\n[This update](https://docs.python.org/release/3.10.8/whatsnew/changelog.html) includes a mitigation for [CVE-2020-10735](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10735) and several other security fixes.\r\n\r\n### Technical\r\n<!-- What should be noted about the implementation? -->\r\n\r\n### Testing\r\n<!-- Steps for reviewer to reproduce/verify what this PR does/fixes. -->\r\nFor testing, please do (from https://github.com/internetarchive/openlibrary/pull/6340#issuecomment-1086270358):\r\n* [x] On `ol-dev1` (in `ol-cclauss`), check out branch and build a new olbase image tagged `-t openlibrary/olbase:py3.10.8`\r\n    * [x] `git fetch origin && git checkout python3.10.8`\r\n    * [x] `docker build --no-cache -t openlibrary/olbase:py3.10.8 -f docker/Dockerfile.olbase .`\r\n    * [x] `hostname ; docker image ls | grep 3.10.8 ` # -> `ol-dev1.us.archive.org`, `repo: openlibrary/olbase, tag: py3.10.8`\r\n* [x] Ask **@cdrini** to push this up to docker hub once it's done\r\n* [ ] Pull it down locally and test the site with the `OLIMAGE` param\r\n    * [ ] Also test solr-updater starts up ok ; it's using FnToCLI which has caused issues with types/python handling of types\r\n* [ ] Restart testing.openlibrary.org with the OLIMAGE param ; confirm things work\r\n* [ ] Merge!\r\n\r\n### During the `docker build` process -- DISK WARNING - free space: / 8905 MB (15% inode=38%):\r\n\r\n### npm deprication warnings --> #7084\r\n\r\n### During the `docker build` process -- Warning: apt-key is deprecated.\r\n```\r\nStep 9/27 : RUN wget -O - https://openresty.org/package/pubkey.gpg | apt-key add -\r\n ---> Running in d075e06118ad\r\n--2022-10-16 13:25:19--  https://openresty.org/package/pubkey.gpg\r\nResolving openresty.org (openresty.org)... Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).\r\n3.131.85.84, 2600:1f1c:9b2:8000:f183:c67e:2c64:855f\r\nConnecting to openresty.org (openresty.org)|3.131.85.84|:443... connected.\r\nHTTP request sent, awaiting response... 200 OK\r\nLength: 1688 (1.6K) [text/plain]\r\nSaving to: 'STDOUT'\r\n\r\n     0K .                                                     100% 65.4M=0s\r\n\r\n2022-10-16 13:25:20 (65.4 MB/s) - written to stdout [1688/1688]\r\n```\r\n### Screenshot\r\n<!-- If this PR touches UI, please post evidence (screenshots) of it behaving correctly. -->\r\n\r\n### Stakeholders\r\n<!-- @ tag stakeholders of this bug -->\r\n\r\n\r\n<!-- Attribution Disclaimer: By proposing this pull request, I affirm to have made a best-effort and exercised my discretion to make sure relevant sections of this code that substantially leverage code suggestions, code generation, or code snippets from sources (e.g. Stack Overflow, GitHub) have been annotated with basic attribution so reviewers & contributors may have confidence and access to the correct context to evaluate and use this code. -->\r\n\n", "before_files": [{"content": "#!/usr/bin/env python\n\nimport logging\nimport os\nimport sys\nfrom datetime import datetime\n\nimport _init_path  # noqa: F401  Imported for its side effect of setting PYTHONPATH\n\nlogger = logging.getLogger(__file__)\nlogger.setLevel(logging.DEBUG)\n\n\ndef log(*args) -> None:\n    args_str = \" \".join(str(a) for a in args)\n    msg = f\"{datetime.now():%Y-%m-%d %H:%M:%S} [openlibrary.dump] {args_str}\"\n    logger.info(msg)\n    print(msg, file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n    from contextlib import redirect_stdout\n    from infogami import config\n    from openlibrary.config import load_config\n    from openlibrary.data import dump\n    from openlibrary.utils.sentry import Sentry\n\n    log(\"{} on Python {}.{}.{}\".format(sys.argv, *sys.version_info))  # Python 3.10.4\n\n    ol_config = os.getenv(\"OL_CONFIG\")\n    if ol_config:\n        logger.info(f\"loading config from {ol_config}\")\n        # Squelch output from infobase (needed for sentry setup)\n        # So it doesn't end up in our data dumps body\n        with open(os.devnull, 'w') as devnull:\n            with redirect_stdout(devnull):\n                load_config(ol_config)\n        sentry = Sentry(getattr(config, \"sentry_cron_jobs\", {}))\n        if sentry.enabled:\n            sentry.init()\n    log(f\"sentry.enabled = {bool(ol_config and sentry.enabled)}\")\n\n    dump.main(sys.argv[1], sys.argv[2:])\n", "path": "scripts/oldump.py"}, {"content": "import asyncio\nimport typing\nfrom argparse import (\n    ArgumentParser,\n    ArgumentDefaultsHelpFormatter,\n    BooleanOptionalAction,\n    Namespace,\n)\n\n\nclass FnToCLI:\n    \"\"\"\n    A utility class which automatically infers and generates ArgParse command\n    line options from a function based on defaults/type annotations\n\n    This is _very_ basic; supports:\n    * Args of int, str types (same logic as default argparse)\n    * Args of bool type (Uses argparse BooleanOptionalAction)\n        * eg `do_blah=False` becomes `--do-blah, --no-do-blah`\n    * Args of typing.Optional (or anything with a default)\n    * Args of typing.Literal (uses argparse choices)\n        * eg `color: Literal['red, 'black']` becomes `--color red|black` (with docs)\n    * Type deduction of default values\n    * Supports async functions automatically\n    * Includes docstring if it's in `:param my_arg: Description of my arg` format\n\n    Anything else will likely error :)\n\n    Example:\n    if __name__ == '__main__':\n        FnToCLI(my_func).run()\n    \"\"\"\n\n    def __init__(self, fn: typing.Callable):\n        self.fn = fn\n        arg_names = fn.__code__.co_varnames[: fn.__code__.co_argcount]\n        annotations = typing.get_type_hints(fn)\n        defaults: list = fn.__defaults__ or []  # type: ignore[assignment]\n        num_required = len(arg_names) - len(defaults)\n        default_args = arg_names[num_required:]\n        defaults: dict = {  # type: ignore[no-redef]\n            arg: default for [arg, default] in zip(default_args, defaults)\n        }\n\n        docs = fn.__doc__ or ''\n        arg_docs = self.parse_docs(docs)\n        self.parser = ArgumentParser(\n            description=docs.split(':param', 1)[0],\n            formatter_class=ArgumentDefaultsHelpFormatter,\n        )\n        self.args: typing.Optional[Namespace] = None\n        for arg in arg_names:\n            optional = arg in defaults\n            cli_name = arg.replace('_', '-')\n\n            if arg in annotations:\n                arg_opts = self.type_to_argparse(annotations[arg])\n            elif arg in defaults:\n                arg_opts = self.type_to_argparse(type(defaults[arg]))  # type: ignore[call-overload]\n            else:\n                raise ValueError(f'{arg} has no type information')\n\n            # Help needs to always be defined, or it won't show the default :/\n            arg_opts['help'] = arg_docs.get(arg) or '-'\n\n            if optional:\n                opt_name = f'--{cli_name}' if len(cli_name) > 1 else f'-{cli_name}'\n                self.parser.add_argument(opt_name, default=defaults[arg], **arg_opts)  # type: ignore[call-overload]\n            else:\n                self.parser.add_argument(cli_name, **arg_opts)\n\n    def parse_args(self):\n        self.args = self.parser.parse_args()\n        return self.args\n\n    def args_dict(self):\n        if not self.args:\n            self.parse_args()\n\n        return {k.replace('-', '_'): v for k, v in self.args.__dict__.items()}\n\n    def run(self):\n        args_dicts = self.args_dict()\n        if asyncio.iscoroutinefunction(self.fn):\n            asyncio.run(self.fn(**args_dicts))\n        else:\n            self.fn(**args_dicts)\n\n    @staticmethod\n    def parse_docs(docs):\n        params = docs.strip().split(':param ')[1:]\n        params = [p.strip() for p in params]\n        params = [p.split(':', 1) for p in params if p]\n        return {name: docs.strip() for [name, docs] in params}\n\n    @staticmethod\n    def type_to_argparse(typ: type) -> dict:\n        if FnToCLI.is_optional(typ):\n            return FnToCLI.type_to_argparse(\n                next(t for t in typing.get_args(typ) if not isinstance(t, type(None)))\n            )\n        if typ == bool:\n            return {'type': typ, 'action': BooleanOptionalAction}\n        if typ in (int, str, float):\n            return {'type': typ}\n        if typ == list[str]:\n            return {'nargs': '*'}\n        if typing.get_origin(typ) == typing.Literal:\n            return {'choices': typing.get_args(typ)}\n        raise ValueError(f'Unsupported type: {typ}')\n\n    @staticmethod\n    def is_optional(typ: type) -> bool:\n        return (\n            typing.get_origin(typ) is typing.Union\n            and type(None) in typing.get_args(typ)\n            and len(typing.get_args(typ)) == 2\n        )\n", "path": "scripts/solr_builder/solr_builder/fn_to_cli.py"}], "after_files": [{"content": "#!/usr/bin/env python\n\nimport logging\nimport os\nimport sys\nfrom datetime import datetime\n\nimport _init_path  # noqa: F401  Imported for its side effect of setting PYTHONPATH\n\nlogger = logging.getLogger(__file__)\nlogger.setLevel(logging.DEBUG)\n\n\ndef log(*args) -> None:\n    args_str = \" \".join(str(a) for a in args)\n    msg = f\"{datetime.now():%Y-%m-%d %H:%M:%S} [openlibrary.dump] {args_str}\"\n    logger.info(msg)\n    print(msg, file=sys.stderr)\n\n\nif __name__ == \"__main__\":\n    from contextlib import redirect_stdout\n    from infogami import config\n    from openlibrary.config import load_config\n    from openlibrary.data import dump\n    from openlibrary.utils.sentry import Sentry\n\n    log(\"{} on Python {}.{}.{}\".format(sys.argv, *sys.version_info))  # Python 3.11.1\n\n    ol_config = os.getenv(\"OL_CONFIG\")\n    if ol_config:\n        logger.info(f\"loading config from {ol_config}\")\n        # Squelch output from infobase (needed for sentry setup)\n        # So it doesn't end up in our data dumps body\n        with open(os.devnull, 'w') as devnull:\n            with redirect_stdout(devnull):\n                load_config(ol_config)\n        sentry = Sentry(getattr(config, \"sentry_cron_jobs\", {}))\n        if sentry.enabled:\n            sentry.init()\n    log(f\"sentry.enabled = {bool(ol_config and sentry.enabled)}\")\n\n    dump.main(sys.argv[1], sys.argv[2:])\n", "path": "scripts/oldump.py"}, {"content": "import asyncio\nimport types\nimport typing\nfrom argparse import (\n    ArgumentParser,\n    ArgumentDefaultsHelpFormatter,\n    BooleanOptionalAction,\n    Namespace,\n)\n\n\nclass FnToCLI:\n    \"\"\"\n    A utility class which automatically infers and generates ArgParse command\n    line options from a function based on defaults/type annotations\n\n    This is _very_ basic; supports:\n    * Args of int, str types (same logic as default argparse)\n    * Args of bool type (Uses argparse BooleanOptionalAction)\n        * eg `do_blah=False` becomes `--do-blah, --no-do-blah`\n    * Args of typing.Optional (or anything with a default)\n    * Args of typing.Literal (uses argparse choices)\n        * eg `color: Literal['red, 'black']` becomes `--color red|black` (with docs)\n    * Type deduction of default values\n    * Supports async functions automatically\n    * Includes docstring if it's in `:param my_arg: Description of my arg` format\n\n    Anything else will likely error :)\n\n    Example:\n    if __name__ == '__main__':\n        FnToCLI(my_func).run()\n    \"\"\"\n\n    def __init__(self, fn: typing.Callable):\n        self.fn = fn\n        arg_names = fn.__code__.co_varnames[: fn.__code__.co_argcount]\n        annotations = typing.get_type_hints(fn)\n        defaults: list = fn.__defaults__ or []  # type: ignore[assignment]\n        num_required = len(arg_names) - len(defaults)\n        default_args = arg_names[num_required:]\n        defaults: dict = {  # type: ignore[no-redef]\n            arg: default for [arg, default] in zip(default_args, defaults)\n        }\n\n        docs = fn.__doc__ or ''\n        arg_docs = self.parse_docs(docs)\n        self.parser = ArgumentParser(\n            description=docs.split(':param', 1)[0],\n            formatter_class=ArgumentDefaultsHelpFormatter,\n        )\n        self.args: typing.Optional[Namespace] = None\n        for arg in arg_names:\n            optional = arg in defaults\n            cli_name = arg.replace('_', '-')\n\n            if arg in annotations:\n                arg_opts = self.type_to_argparse(annotations[arg])\n            elif arg in defaults:\n                arg_opts = self.type_to_argparse(type(defaults[arg]))  # type: ignore[call-overload]\n            else:\n                raise ValueError(f'{arg} has no type information')\n\n            # Help needs to always be defined, or it won't show the default :/\n            arg_opts['help'] = arg_docs.get(arg) or '-'\n\n            if optional:\n                opt_name = f'--{cli_name}' if len(cli_name) > 1 else f'-{cli_name}'\n                self.parser.add_argument(opt_name, default=defaults[arg], **arg_opts)  # type: ignore[call-overload]\n            else:\n                self.parser.add_argument(cli_name, **arg_opts)\n\n    def parse_args(self):\n        self.args = self.parser.parse_args()\n        return self.args\n\n    def args_dict(self):\n        if not self.args:\n            self.parse_args()\n\n        return {k.replace('-', '_'): v for k, v in self.args.__dict__.items()}\n\n    def run(self):\n        args_dicts = self.args_dict()\n        if asyncio.iscoroutinefunction(self.fn):\n            asyncio.run(self.fn(**args_dicts))\n        else:\n            self.fn(**args_dicts)\n\n    @staticmethod\n    def parse_docs(docs):\n        params = docs.strip().split(':param ')[1:]\n        params = [p.strip() for p in params]\n        params = [p.split(':', 1) for p in params if p]\n        return {name: docs.strip() for [name, docs] in params}\n\n    @staticmethod\n    def type_to_argparse(typ: type) -> dict:\n        if FnToCLI.is_optional(typ):\n            return FnToCLI.type_to_argparse(\n                next(t for t in typing.get_args(typ) if not isinstance(t, type(None)))\n            )\n        if typ == bool:\n            return {'type': typ, 'action': BooleanOptionalAction}\n        if typ in (int, str, float):\n            return {'type': typ}\n        if typ == list[str]:\n            return {'nargs': '*'}\n        if typing.get_origin(typ) == typing.Literal:\n            return {'choices': typing.get_args(typ)}\n        raise ValueError(f'Unsupported type: {typ}')\n\n    @staticmethod\n    def is_optional(typ: type) -> bool:\n        return (\n            (typing.get_origin(typ) is typing.Union or isinstance(typ, types.UnionType))\n            and type(None) in typing.get_args(typ)\n            and len(typing.get_args(typ)) == 2\n        )\n", "path": "scripts/solr_builder/solr_builder/fn_to_cli.py"}]}