Repository: omni-us/jsonargparse
Pull request #191 (2022-11-10 18:39:01)

Fix subclass help for when type uses new union type syntax
## What does this PR do?

Reported by @Andry-Bal in pytorch-lightning slack.

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/master/CONTRIBUTING.rst)?
- [x] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=191)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=191&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=191) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=191&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=191&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 859f507..b38814f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -28,6 +28,8 @@ Fixed
   `#189 <https://github.com/omni-us/jsonargparse/issues/189>`__.
 - ``Namespace`` clone should not deepcopy leaf values `#187
   <https://github.com/omni-us/jsonargparse/issues/187>`__.
+- ``_ActionHelpClassPath`` actions fail to instantiate when base class uses new
+  union type syntax.
 
 Changed
 ^^^^^^^
diff --git a/jsonargparse/actions.py b/jsonargparse/actions.py
index 5a9a48f..3c95800 100644
--- a/jsonargparse/actions.py
+++ b/jsonargparse/actions.py
@@ -17,6 +17,7 @@ from .typing import path_type
 from .util import (
     change_to_path_dir,
     default_config_option_help,
+    get_typehint_origin,
     import_object,
     indent_text,
     is_subclass,
@@ -322,7 +323,7 @@ class _ActionHelpClassPath(Action):
 
     def __init__(self, baseclass=None, **kwargs):
         if baseclass is not None:
-            if getattr(baseclass, '__origin__', None) == Union:
+            if get_typehint_origin(baseclass) == Union:
                 baseclasses = [c for c in baseclass.__args__ if c is not NoneType]
                 if len(baseclasses) == 1:
                     baseclass = baseclasses[0]
@@ -333,7 +334,7 @@ class _ActionHelpClassPath(Action):
             super().__init__(**kwargs)
 
     def update_init_kwargs(self, kwargs):
-        if getattr(self._baseclass, '__origin__', None) == Union:
+        if get_typehint_origin(self._baseclass) == Union:
             from .typehints import ActionTypeHint
             self._basename = iter_to_set_str(
                 c.__name__ for c in self._baseclass.__args__
@@ -361,7 +362,7 @@ class _ActionHelpClassPath(Action):
             val_class = import_object(resolve_class_path_by_name(baseclass, value))
         except Exception as ex:
             raise TypeError(f'{option_string}: {ex}')
-        if getattr(self._baseclass, '__origin__', None) == Union:
+        if get_typehint_origin(self._baseclass) == Union:
             baseclasses = self._baseclass.__args__
         else:
             baseclasses = [baseclass]
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
index 871643b..709cb7f 100644
--- a/jsonargparse/typehints.py
+++ b/jsonargparse/typehints.py
@@ -45,6 +45,7 @@ from .util import (
     change_to_path_dir,
     ClassType,
     get_import_path,
+    get_typehint_origin,
     import_object,
     indent_text,
     is_subclass,
@@ -965,12 +966,6 @@ def sort_subtypes_for_append(subtypes):
     return subtypes
 
 
-def get_typehint_origin(typehint):
-    if not hasattr(typehint, '__origin__') and get_import_path(typehint.__class__) == 'types.UnionType':
-        return Union
-    return getattr(typehint, '__origin__', None)
-
-
 def is_ellipsis_tuple(typehint):
     return typehint.__origin__ in {Tuple, tuple} and len(typehint.__args__) > 1 and typehint.__args__[1] == Ellipsis
 
diff --git a/jsonargparse/util.py b/jsonargparse/util.py
index 3c2b623..e71042e 100644
--- a/jsonargparse/util.py
+++ b/jsonargparse/util.py
@@ -259,6 +259,12 @@ def object_path_serializer(value):
         raise ValueError(f'Only possible to serialize an importable object, given {value}: {ex}') from ex
 
 
+def get_typehint_origin(typehint):
+    if not hasattr(typehint, '__origin__') and get_import_path(typehint.__class__) == 'types.UnionType':
+        return Union
+    return getattr(typehint, '__origin__', None)
+
+
 lenient_check: ContextVar = ContextVar('lenient_check', default=False)
 
 


Test patch:
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index 9b6b60e..8bbec31 100755
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -600,10 +600,20 @@ class TypeHintsTests(unittest.TestCase):
     @unittest.skipIf(sys.version_info[:2] < (3, 10), 'new union syntax introduced in python 3.10')
     def test_union_new_syntax(self):
         parser = ArgumentParser(error_handler=None)
-        parser.add_argument('--str', type=eval('int | None'))
-        self.assertEqual(123, parser.parse_args(['--str=123']).str)
-        self.assertIsNone(parser.parse_args(['--str=null']).str)
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--str=abc']))
+        parser.add_argument('--val', type=eval('int | None'))
+        self.assertEqual(123, parser.parse_args(['--val=123']).val)
+        self.assertIsNone(parser.parse_args(['--val=null']).val)
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--val=abc']))
+
+
+    @unittest.skipIf(sys.version_info[:2] < (3, 10), 'new union syntax introduced in python 3.10')
+    def test_union_new_syntax_subclasses(self):
+        parser = ArgumentParser(error_handler=None)
+        parser.add_argument('--op', type=eval('Calendar | bool'))
+        out = StringIO()
+        with redirect_stdout(out), self.assertRaises(SystemExit):
+            parser.parse_args(['--op.help=TextCalendar'])
+        self.assertIn('--op.init_args.firstweekday', out.getvalue())
 
 
     def test_Callable_with_function_path(self):