Repository: omni-us/jsonargparse

Issue:
Add support for async objects in `CLI`
<!-- If you like this project, please ⭐ star it https://github.com/omni-us/jsonargparse/ -->

## 🚀 Feature request

Add support for objects that require asynchronous context for their creation.

### Motivation

Currently I cannot instantiate objects like `aiokafka.AIOKafkaProducer`. For example:

In `main.py`:

```Python
import aiokafka
from jsonargparse import CLI


async def main(producer: aiokafka.AIOKafkaProducer) -> None:
    print(producer)


if __name__ == "__main__":
    CLI(main)
```

In `config.yaml`:

```Yaml
producer:
  class_path: aiokafka.AIOKafkaProducer
  init_args:
    bootstrap_servers: ["localhost:9092"]
```

In terminal:

```Bash
python -m main --config config.yaml
```

Gives error:

```
RuntimeError: The object should be created within an async function or provide loop directly.
```

### Pitch

The above code should runs without errors.

### Alternatives

<!-- Any alternative solutions or features you've considered, if any. -->


Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=531) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=531&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=531&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=531&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=531&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=531)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.96%. Comparing base [(`6c89858`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/6c898583f260499a559b55532a30ecba14d43d68?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`0f609ea`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/0f609ea03f7ea566dd9a358dcc8444c759cb8384?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).


<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              main     #531      +/-   ##
===========================================
- Coverage   100.00%   99.96%   -0.04%     
===========================================
  Files           22       22              
  Lines         6375     6376       +1     
===========================================
- Hits          6375     6374       -1     
- Misses           0        2       +2     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [py3.10](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.64% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.68% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.57% <0.00%> (-0.01%)` | :arrow_down: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.68% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.77% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.68% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.12% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.24% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.27% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.31% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.40% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.41% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.17% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.29% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.30% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/531?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=531) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=531&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=531&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=531&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=531&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=531&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=531)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=531) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=531&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=531&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=531&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=531&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=531&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=531)



Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index ecd0090..015027e 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -6,7 +6,7 @@ on:
   pull_request:
     branches: [ "main" ]
   schedule:
-    - cron: "57 5 * * 3"
+    - cron: "57 5 * * 3"  # M H d m w (Every Wednesday at 5:57 AM)
 
 jobs:
   analyze:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f23c37a..c77c510 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -17,6 +17,8 @@ v4.31.0 (2024-06-??)
 
 Added
 ^^^^^
+- Support async functions and methods in ``CLI`` (`#531
+  <https://github.com/omni-us/jsonargparse/pull/531>`__).
 - Support for ``Protocol`` types only accepting exact matching signature of
   public methods (`#526
   <https://github.com/omni-us/jsonargparse/pull/526>`__).
diff --git a/jsonargparse/_cli.py b/jsonargparse/_cli.py
index 43a08ba..1805347 100644
--- a/jsonargparse/_cli.py
+++ b/jsonargparse/_cli.py
@@ -92,8 +92,8 @@ def CLI(
             deprecation_warning_cli_return_parser()
             return parser
         cfg = parser.parse_args(args)
-        cfg_init = parser.instantiate_classes(cfg)
-        return _run_component(components, cfg_init)
+        init = parser.instantiate_classes(cfg)
+        return _run_component(components, init)
 
     elif isinstance(components, list):
         components = {c.__name__: c for c in components}
@@ -192,12 +192,13 @@ def _add_component_to_parser(
 
 def _run_component(component, cfg):
     cfg.pop("config", None)
-    if not inspect.isclass(component):
-        return component(**cfg)
     subcommand = cfg.pop("subcommand")
-    if not subcommand:
-        return component(**cfg)
-    subcommand_cfg = cfg.pop(subcommand, {})
-    subcommand_cfg.pop("config", None)
-    component_obj = component(**cfg)
-    return getattr(component_obj, subcommand)(**subcommand_cfg)
+    if inspect.isclass(component) and subcommand:
+        subcommand_cfg = cfg.pop(subcommand, {})
+        subcommand_cfg.pop("config", None)
+        component_obj = component(**cfg)
+        component = getattr(component_obj, subcommand)
+        cfg = subcommand_cfg
+    if inspect.iscoroutinefunction(component):
+        return __import__("asyncio").run(component(**cfg))
+    return component(**cfg)
diff --git a/pyproject.toml b/pyproject.toml
index 21a2259..1eb018a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -105,7 +105,6 @@ dev = [
     "jsonargparse[test]",
     "jsonargparse[coverage]",
     "jsonargparse[doc]",
-    "jsonargparse[mypy]",
     "pre-commit>=2.19.0",
     "tox>=3.25.0",
     "build>=0.10.0",


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 656a789..db6793f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -28,7 +28,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python: [3.8, "3.10"]
+        python: [3.8, "3.10", 3.12]
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
@@ -39,7 +39,7 @@ jobs:
       - run: tox -e py-all-extras
 
   unittest-omegaconf:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
@@ -50,7 +50,7 @@ jobs:
       - run: tox -e omegaconf
 
   doctest:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
diff --git a/jsonargparse_tests/test_cli.py b/jsonargparse_tests/test_cli.py
index 06d4879..ae51830 100644
--- a/jsonargparse_tests/test_cli.py
+++ b/jsonargparse_tests/test_cli.py
@@ -1,12 +1,13 @@
 from __future__ import annotations
 
+import asyncio
 import os
 import sys
 from contextlib import redirect_stderr, redirect_stdout, suppress
 from dataclasses import asdict, dataclass
 from io import StringIO
 from pathlib import Path
-from typing import Optional
+from typing import Callable, Optional
 from unittest.mock import patch
 
 import pytest
@@ -535,3 +536,44 @@ def test_final_and_subclass_type_config_file(tmp_cwd):
 
     out = CLI(run_bf, args=["--config=config.yaml"])
     assert "a yaml" == out
+
+
+# async tests
+
+
+async def run_async(time: float = 0.1):
+    await asyncio.sleep(time)
+    return "done"
+
+
+def test_async_function():
+    assert "done" == CLI(run_async, args=["--time=0.0"])
+
+
+class AsyncMethod:
+    def __init__(self, time: float = 0.1, require_async: bool = False):
+        self.time = time
+        if require_async:
+            self.loop = asyncio.get_event_loop()
+
+    async def run(self):
+        await asyncio.sleep(self.time)
+        return "done"
+
+
+def test_async_method():
+    assert "done" == CLI(AsyncMethod, args=["--time=0.0", "run"])
+
+
+async def run_async_instance(cls: Callable[[], AsyncMethod]):
+    return await cls().run()
+
+
+def test_async_instance():
+    config = {
+        "cls": {
+            "class_path": f"{__name__}.AsyncMethod",
+            "init_args": {"time": 0.0, "require_async": True},
+        }
+    }
+    assert "done" == CLI(run_async_instance, args=[f"--config={config}"])