Repository: omni-us/jsonargparse

Issue:
`format_usage()` doesn't work on captured parser
## 🐛 Bug report

<!-- A clear and concise description of the bug. -->
`jsonargparse.capture_parser(main).format_usage()` fails with 
`LookupError: <ContextVar name='parent_parser' at 0x7f0859cb54e0>`

### To reproduce

```
import jsonargparse

def fun(a: int):
    pass

def main():
    jsonargparse.CLI(fun)

# THIS IS OK
jsonargparse.capture_parser(main).print_usage()

# THIS FAILS
print(jsonargparse.capture_parser(main).format_usage())
```

Output: 
```
$ python python-jsonargparse-format-usage.py 
usage: python-jsonargparse-format-usage.py [-h] [--config CONFIG] [--print_config[=flags]] a
Traceback (most recent call last):
  File "/home/mati/tests/python-jsonargparse-format-usage.py", line 12, in <module>
    print(jsonargparse.capture_parser(main).format_usage())
  File "/usr/lib/python3.10/argparse.py", line 2540, in format_usage
    return formatter.format_help()
  File "/usr/lib/python3.10/argparse.py", line 295, in format_help
    help = self._root_section.format_help()
  File "/usr/lib/python3.10/argparse.py", line 226, in format_help
    item_help = join([func(*args) for func, args in self.items])
  File "/usr/lib/python3.10/argparse.py", line 226, in <listcomp>
    item_help = join([func(*args) for func, args in self.items])
  File "/home/mati/.local/lib/python3.10/site-packages/jsonargparse/_formatters.py", line 90, in _format_usage
    parser = parent_parser.get()
LookupError: <ContextVar name='parent_parser' at 0x7f2efed7c130>
```
### Expected behavior

Should print same thing in both cases.

### Environment

- jsonargparse version: 4.28.0
- Python version: 3.10.12
- How jsonargparse was installed: `pip install jsonargparse`
- OS (e.g., Linux): Ubuntu Jammy 


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=502) **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=502&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=502&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=502&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=502&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=502)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502?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 100.00%. Comparing base [(`6d4f87b`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/6d4f87bb9b2f9f148d5c2215938d213a9e959f08?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`fbe3afa`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502?dropdown=coverage&src=pr&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      #502   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         6121      6126    +5     
=========================================
+ Hits          6121      6126    +5     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/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/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.25% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.64% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `49.13% <78.57%> (+0.02%)` | :arrow_up: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.90% <78.57%> (+0.02%)` | :arrow_up: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.66% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.23% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.64% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.66% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.38% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.64% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.66% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.79% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.25% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.25% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.96% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/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.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/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/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.80% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/502/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: |

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/502?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).


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 429e533..a88cb63 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -23,9 +23,11 @@ Added
 Fixed
 ^^^^^
 - Parameter resolving falling back to assumptions resolver for optional
-  ``Union`` types.
+  ``Union`` types (`#498 <https://github.com/omni-us/jsonargparse/pull/498>`__).
 - Nested parameters failing to parse from command line when value includes
-  space.
+  space (`#499 <https://github.com/omni-us/jsonargparse/pull/499>`__).
+- ``format_usage()`` not working (`#501
+  <https://github.com/omni-us/jsonargparse/issues/501>`__).
 
 
 v4.28.0 (2024-04-17)
diff --git a/jsonargparse/_common.py b/jsonargparse/_common.py
index 9a5b2b0..36d558e 100644
--- a/jsonargparse/_common.py
+++ b/jsonargparse/_common.py
@@ -51,7 +51,7 @@ else:
 InstantiatorsDictType = Dict[Tuple[type, bool], InstantiatorCallable]
 
 
-parent_parser: ContextVar["ArgumentParser"] = ContextVar("parent_parser")
+parent_parser: ContextVar[Optional["ArgumentParser"]] = ContextVar("parent_parser", default=None)
 parser_capture: ContextVar[bool] = ContextVar("parser_capture", default=False)
 defaults_cache: ContextVar[Optional[Namespace]] = ContextVar("defaults_cache", default=None)
 lenient_check: ContextVar[Union[bool, str]] = ContextVar("lenient_check", default=False)
diff --git a/jsonargparse/_formatters.py b/jsonargparse/_formatters.py
index 408e074..6b86aa5 100644
--- a/jsonargparse/_formatters.py
+++ b/jsonargparse/_formatters.py
@@ -88,17 +88,19 @@ class DefaultHelpFormatter(HelpFormatter):
     def _format_usage(self, *args, **kwargs) -> str:
         usage = super()._format_usage(*args, **kwargs)
         parser = parent_parser.get()
-        for key in parser.required_args:
-            try:
-                default = parser.get_default(key)
-            except KeyError:
-                default = None
-            if default is None and f"[--{key} " in usage:
-                usage = re.sub(f"\\[(--{key} [^\\]]+)]", r"\1", usage, count=1)
+        if parser:
+            for key in parser.required_args:
+                try:
+                    default = parser.get_default(key)
+                except KeyError:
+                    default = None
+                if default is None and f"[--{key} " in usage:
+                    usage = re.sub(f"\\[(--{key} [^\\]]+)]", r"\1", usage, count=1)
         return usage
 
     def _format_action_invocation(self, action: Action) -> str:
         parser = parent_parser.get()
+        assert parser is not None
         if isinstance(action, _ActionSubCommands):
             value = "Available subcommands:"
             if parser.default_env:
@@ -263,6 +265,7 @@ def get_env_var(
         parser = parent_parser.get()
     else:
         parser = parser_or_formatter
+    assert parser is not None
     env_var = ""
     if isinstance(parser.env_prefix, str):
         env_var = parser.env_prefix.replace("-", "_") + "_"
diff --git a/jsonargparse/_loaders_dumpers.py b/jsonargparse/_loaders_dumpers.py
index c22e550..8d7ff05 100644
--- a/jsonargparse/_loaders_dumpers.py
+++ b/jsonargparse/_loaders_dumpers.py
@@ -96,7 +96,9 @@ loader_exceptions: Dict[str, Tuple[Type[Exception], ...]] = {
 def get_load_value_mode() -> str:
     mode = load_value_mode.get()
     if mode is None:
-        mode = parent_parser.get().parser_mode
+        parser = parent_parser.get()
+        assert parser is not None
+        mode = parser.parser_mode
     return mode
 
 


Test patch:
diff --git a/jsonargparse_tests/test_formatters.py b/jsonargparse_tests/test_formatters.py
index 26960d8..c6f175e 100644
--- a/jsonargparse_tests/test_formatters.py
+++ b/jsonargparse_tests/test_formatters.py
@@ -1,7 +1,9 @@
 from __future__ import annotations
 
+import os
 from pathlib import Path
 from typing import Tuple
+from unittest.mock import patch
 
 import pytest
 
@@ -129,3 +131,9 @@ def test_help_default_config_files_with_required(tmp_path, parser):
     help_str = get_parser_help(parser)
     assert "req description" in help_str
     assert "from yaml" in help_str
+
+
+def test_format_usage(parser):
+    parser.add_argument("--v1")
+    with patch.dict(os.environ, {"COLUMNS": "200"}):
+        assert parser.format_usage() == "usage: app [-h] [--v1 V1]\n"