Repository: omni-us/jsonargparse
Pull request #177 (2022-10-18 10:25:05)

Fix some issues identified together related to type hints
## What does this PR do?

As the title says.

- Order of types in `Union` not being considered.
- `str` type fails to parse values of the form `^\w+: *`.
- `parse_object` does not consider given namespace for previous `class_path` values.

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/master/CONTRIBUTING.rst)?
- [n/a] 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=177)

[![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=177&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=177&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=177&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=177&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=177&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=177&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=177&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=177&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=177&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=177&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=177&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=177&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=177) 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=177&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=177&metric=new_duplicated_lines_density&view=list)


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=177)

[![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=177&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=177&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=177&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=177&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=177&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=177&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=177&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=177&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=177&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=177&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=177&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=177&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=177) 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=177&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=177&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 871cd2e..4558eb4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -27,6 +27,10 @@ Fixed
 - Cannot override Callable ``init_args`` without passing the ``class_path``
   `#174 <https://github.com/omni-us/jsonargparse/issues/174>`__.
 - Positional subclass type incorrectly adds subclass help as positional.
+- Order of types in ``Union`` not being considered.
+- ``str`` type fails to parse values of the form ``^\w+: *``.
+- ``parse_object`` does not consider given namespace for previous ``class_path``
+  values.
 
 
 v4.15.1 (2022-10-07)
diff --git a/jsonargparse/core.py b/jsonargparse/core.py
index d8d57d2..c1cb5b7 100644
--- a/jsonargparse/core.py
+++ b/jsonargparse/core.py
@@ -419,7 +419,7 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
             if cfg_base:
                 cfg = self.merge_config(cfg_base, cfg)
 
-            cfg_obj = self._apply_actions(cfg_obj)
+            cfg_obj = self._apply_actions(cfg_obj, prev_cfg=cfg)
             cfg = self.merge_config(cfg_obj, cfg)
 
             parsed_cfg = self._parse_common(
diff --git a/jsonargparse/loaders_dumpers.py b/jsonargparse/loaders_dumpers.py
index 5e2c3ac..ae3b953 100644
--- a/jsonargparse/loaders_dumpers.py
+++ b/jsonargparse/loaders_dumpers.py
@@ -106,12 +106,15 @@ def get_loader_exceptions():
     return loader_exceptions[load_value_mode.get()]
 
 
-def load_value(value: str, **kwargs):
+def load_value(value: str, simple_types: bool = False, **kwargs):
     loader = loaders[load_value_mode.get()]
     if kwargs:
         params = set(list(inspect.signature(loader).parameters)[1:])
         kwargs = {k: v for k, v in kwargs.items() if k in params}
-    return loader(value, **kwargs)
+    loaded_value = loader(value, **kwargs)
+    if not simple_types and isinstance(loaded_value, (int, float, bool)):
+        loaded_value = value
+    return loaded_value
 
 
 dump_yaml_kwargs = {
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
index 844accd..f9f8d03 100644
--- a/jsonargparse/typehints.py
+++ b/jsonargparse/typehints.py
@@ -32,7 +32,7 @@ from typing import (
 )
 
 from .actions import _ActionHelpClassPath, _find_action, _find_parent_action, _is_action_value_list
-from .loaders_dumpers import get_loader_exceptions, load_value, load_value_context
+from .loaders_dumpers import get_loader_exceptions, load_value, load_value_context, pyyaml_exceptions, yaml_load
 from .namespace import Namespace
 from .typing import get_registered_type, is_final_class
 from .optionals import (
@@ -48,7 +48,6 @@ from .util import (
     indent_text,
     is_subclass,
     iter_to_set_str,
-    lenient_check,
     lenient_check_context,
     NestedArg,
     NoneType,
@@ -395,16 +394,16 @@ class ActionTypeHint(Action):
                     with change_to_path_dir(config_path):
                         val = adapt_typehints(val, self._typehint, **kwargs)
                 except ValueError as ex:
-                    val_is_int_float_or_none = isinstance(val, (int, float)) or val is None
-                    if lenient_check.get():
-                        value[num] = orig_val if val_is_int_float_or_none else val
-                        continue
-                    if val_is_int_float_or_none and config_path is None:
-                        val = adapt_typehints(orig_val, self._typehint, **kwargs)
-                    else:
+                    try:
+                        if isinstance(orig_val, str):
+                            with change_to_path_dir(config_path):
+                                val = adapt_typehints(orig_val, self._typehint, **kwargs)
+                            ex = None
+                    except ValueError:
                         if self._enable_path and config_path is None and isinstance(orig_val, str):
                             msg = f'\n- Expected a config path but "{orig_val}" either not accessible or invalid.\n- '
                             raise type(ex)(msg+str(ex)) from ex
+                    if ex:
                         raise ex
 
                 if not append and self._supports_append:
@@ -491,7 +490,7 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
         'append': append,
         'sub_add_kwargs': sub_add_kwargs or {},
     }
-    subtypehints = get_typehint_subtypes(typehint, append=append)
+    subtypehints = getattr(typehint, '__args__', None)
     typehint_origin = get_typehint_origin(typehint) or typehint
 
     # Any
@@ -501,22 +500,29 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
             val = adapt_typehints(val, type_val, **adapt_kwargs)
         elif isinstance(val, str):
             try:
-                val, _ = parse_value_or_config(val, enable_path=False)
+                val, _ = parse_value_or_config(val, enable_path=False, simple_types=True)
             except get_loader_exceptions():
                 pass
 
     # Literal
     elif typehint_origin == Literal:
+        if val not in subtypehints and isinstance(val, str):
+            subtypes = Union[tuple(set(type(v) for v in subtypehints))]
+            val = adapt_typehints(val, subtypes, **adapt_kwargs)
         if val not in subtypehints:
             raise ValueError(f'Expected a {typehint} but got "{val}"')
 
     # Basic types
     elif typehint in leaf_types:
-        if not isinstance(val, typehint):
+        if not isinstance(val, typehint) and isinstance(val, str) and typehint is not str:
+            try:
+                val = yaml_load(val)
+            except pyyaml_exceptions:
+                pass
             if typehint is float and isinstance(val, int):
                 val = float(val)
-            else:
-                raise ValueError(f'Expected a {typehint} but got "{val}"')
+        if not isinstance(val, typehint):
+            raise ValueError(f'Expected a {typehint} but got "{val}"')
 
     # Registered types
     elif get_registered_type(typehint):
@@ -554,6 +560,7 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
     # Union
     elif typehint_origin == Union:
         vals = []
+        subtypehints = sort_subtypes_for_append(subtypehints) if append else subtypehints
         for subtypehint in subtypehints:
             try:
                 vals.append(adapt_typehints(val, subtypehint, **adapt_kwargs))
@@ -593,7 +600,7 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
                     prev_val = [adapt_typehints(prev_val, subtypehints[0], **adapt_kwargs)]
                 except Exception:
                     prev_val = []
-            val = prev_val + (val if isinstance(val, list) else [val])
+            val = prev_val + (val if isinstance(val, list) else [adapt_typehints(val, subtypehints[0], **adapt_kwargs)])
             prev_val = prev_val + [None] * (len(val) if isinstance(val, list) else 1)
         if isinstance(val, NestedArg) and subtypehints is not None:
             val = (prev_val[:-1] if isinstance(prev_val, list) else []) + [val]
@@ -699,7 +706,7 @@ def is_subclass_spec(val):
 
 
 def subclass_spec_as_namespace(val, prev_val=None):
-    if val is None:
+    if not isinstance(val, (str, dict, Namespace, NestedArg)):
         return None
     if isinstance(val, str):
         return Namespace(class_path=val)
@@ -854,7 +861,7 @@ def adapt_class_type(value, serialize, instantiate_classes, sub_add_kwargs, prev
             return value
         return val_class(**{**init_args, **dict_kwargs})
 
-    prev_init_args = prev_val.get('init_args') if prev_val else None
+    prev_init_args = prev_val.get('init_args') if isinstance(prev_val, Namespace) else None
 
     if isinstance(init_args, NestedArg):
         value['init_args'] = parser.parse_args(
@@ -881,7 +888,10 @@ def adapt_class_type(value, serialize, instantiate_classes, sub_add_kwargs, prev
     if dict_kwargs:
         if prev_val and prev_val.get('class_path') == value['class_path'] and prev_val.get('dict_kwargs'):
             dict_kwargs = {**prev_val.get('dict_kwargs'), **dict_kwargs}
-        value['dict_kwargs'] = dict_kwargs
+        value['dict_kwargs'] = {
+            k: load_value(v, simple_types=True) if isinstance(v, str) else v
+            for k, v in dict_kwargs.items()
+        }
     return value
 
 
@@ -892,9 +902,8 @@ def not_append_diff(val1, val2):
     return val1 != val2
 
 
-def get_typehint_subtypes(typehint, append):
-    subtypes = getattr(typehint, '__args__', None)
-    if append and subtypes:
+def sort_subtypes_for_append(subtypes):
+    if subtypes and len(subtypes) > 1:
         subtypes = sorted(subtypes, key=lambda x: get_typehint_origin(x) not in sequence_origin_types)
     return subtypes
 
@@ -1002,7 +1011,6 @@ class LazyInitBaseClass:
     def __init__(self, class_type: Type, lazy_kwargs: dict):
         check_lazy_kwargs(class_type, lazy_kwargs)
         self._lazy_class_type = class_type
-        self._lazy_class_path = get_import_path(class_type)
         self._lazy_kwargs = lazy_kwargs
         self._lazy_methods = {}
         seen_methods: Dict = {}
@@ -1032,7 +1040,7 @@ class LazyInitBaseClass:
         init_args = Namespace(self._lazy_kwargs)
         if is_final_class(self._lazy_class_type):
             return init_args
-        init = Namespace(class_path=self._lazy_class_path)
+        init = Namespace(class_path=get_import_path(self._lazy_class_type))
         if len(self._lazy_kwargs) > 0:
             init['init_args'] = init_args
         return init
diff --git a/jsonargparse/util.py b/jsonargparse/util.py
index 9d38208..0df0b81 100644
--- a/jsonargparse/util.py
+++ b/jsonargparse/util.py
@@ -124,7 +124,7 @@ def identity(value):
 NestedArg = namedtuple('NestedArg', 'key val')
 
 
-def parse_value_or_config(value: Any, enable_path: bool = True) -> Tuple[Any, Optional['Path']]:
+def parse_value_or_config(value: Any, enable_path: bool = True, simple_types: bool = False) -> Tuple[Any, Optional['Path']]:
     """Parses yaml/json config in a string or a path"""
     nested_arg: Union[bool, NestedArg] = False
     if isinstance(value, NestedArg):
@@ -138,9 +138,9 @@ def parse_value_or_config(value: Any, enable_path: bool = True) -> Tuple[Any, Op
             pass
         else:
             with change_to_path_dir(cfg_path):
-                value = load_value(cfg_path.get_content())
+                value = load_value(cfg_path.get_content(), simple_types=simple_types)
     if type(value) is str and value.strip() != '':
-        parsed_val = load_value(value)
+        parsed_val = load_value(value, simple_types=simple_types)
         if type(parsed_val) is not str:
             value = parsed_val
     if isinstance(value, dict) and cfg_path is not None:


Test patch:
diff --git a/jsonargparse_tests/test_cli.py b/jsonargparse_tests/test_cli.py
index 9186eff..c988fb1 100755
--- a/jsonargparse_tests/test_cli.py
+++ b/jsonargparse_tests/test_cli.py
@@ -230,7 +230,6 @@ class CLITempDirTests(TempDirTestCase):
             def cmd_b(self):
                 print(self.b.a.p1)
 
-
         with mock_module(A, B) as module:
             a_yaml = {
                 'class_path': f'{module}.A',
diff --git a/jsonargparse_tests/test_loaders_dumpers.py b/jsonargparse_tests/test_loaders_dumpers.py
index ffc7c2b..66a7639 100755
--- a/jsonargparse_tests/test_loaders_dumpers.py
+++ b/jsonargparse_tests/test_loaders_dumpers.py
@@ -2,10 +2,11 @@
 
 import os
 import unittest
+import unittest.mock
 import yaml
 from importlib.util import find_spec
 from typing import List
-from jsonargparse import ActionConfigFile, ArgumentParser, set_dumper, set_loader, ParserError
+from jsonargparse import ActionConfigFile, ArgumentParser, set_dumper, set_loader
 from jsonargparse.loaders_dumpers import loaders, load_value, load_value_context, yaml_dump
 
 
@@ -25,17 +26,6 @@ class LoadersTests(unittest.TestCase):
             self.assertEqual(dump, '{list: [1, 2, 3]}\n')
 
 
-    def test_set_loader_safe_load_invalid_scientific_notation(self):
-        parser = ArgumentParser(error_handler=None)
-        parser.add_argument('--num', type=float)
-
-        with unittest.mock.patch.dict('jsonargparse.loaders_dumpers.loaders'):
-            set_loader('yaml', yaml.safe_load)
-            self.assertRaises(ParserError, lambda: parser.parse_args(['--num=1e-3']))
-
-        self.assertEqual(1e-3, parser.parse_args(['--num=1e-3']).num)
-
-
     def test_disable_implicit_mapping_values(self):
         parser = ArgumentParser(error_handler=None)
         parser.add_argument('--val', type=str)
diff --git a/jsonargparse_tests/test_signatures.py b/jsonargparse_tests/test_signatures.py
index b9f142c..9ee7ba1 100755
--- a/jsonargparse_tests/test_signatures.py
+++ b/jsonargparse_tests/test_signatures.py
@@ -121,7 +121,6 @@ class SignaturesTests(unittest.TestCase):
         self.assertEqual('a', Class3(**cfg.as_dict())())
 
         self.assertRaises(ParserError, lambda: parser.parse_args([]))  # c3_a0 is required
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--c3_a0=0', '--c3_a7=["3", "3", 3.0]']))  # tuple[1] is int
 
         if docstring_parser_support:
             self.assertEqual('Class3 short description', parser.groups['Class3'].title)
@@ -448,10 +447,9 @@ class SignaturesTests(unittest.TestCase):
             def __init__(self, cal: Union[Calendar, bool] = lazy_instance(MyCalendar, param=1)):
                 self.cal = cal
 
-        parser = ArgumentParser(error_handler=None)
-        parser.add_class_arguments(Main, 'main')
-
-        with warnings.catch_warnings(record=True) as w:
+        with mock_module(MyCalendar), warnings.catch_warnings(record=True) as w:
+            parser = ArgumentParser(error_handler=None)
+            parser.add_class_arguments(Main, 'main')
             parser.parse_args(['--main.cal=Calendar'])
             self.assertIn("discarding init_args: {'param': 1}", str(w[0].message))
 
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index 1a69853..36a7ff8 100755
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -22,6 +22,7 @@ from jsonargparse import ActionConfigFile, ArgumentParser, CLI, lazy_instance, N
 from jsonargparse.typehints import ActionTypeHint, is_optional, Literal
 from jsonargparse.typing import (
     Email,
+    final,
     NotEmptyStr,
     OpenUnitInterval,
     Path_drw,
@@ -87,6 +88,7 @@ class TypeHintsTests(unittest.TestCase):
         self.assertEqual('  ', parser.parse_args(['--op', '  ']).op)
         self.assertEqual('', parser.parse_args(['--op', '']).op)
         self.assertEqual(' abc ', parser.parse_args(['--op= abc ']).op)
+        self.assertEqual('xyz: ', parser.parse_args(['--op=xyz: ']).op)
         self.assertEqual(' ', parser.parse_args(['--cfg={"op":" "}']).op)
         self.assertIsNone(parser.parse_args(['--op=null']).op)
 
@@ -98,6 +100,28 @@ class TypeHintsTests(unittest.TestCase):
         self.assertEqual('2022-04-32', parser.parse_args(['2022-04-32']).foo)
 
 
+    def test_float_scientific_notation(self):
+        parser = ArgumentParser()
+        parser.add_argument('--num', type=float)
+        self.assertEqual(1e-3, parser.parse_args(['--num=1e-3']).num)
+
+
+    def test_str_with_number_value(self):
+        class Class:
+            def __init__(self, val: str = '-'):
+                pass
+
+        with mock_module(Class):
+            parser = ArgumentParser()
+            parser.add_argument('--val', type=str)
+            parser.add_argument('--cls', type=Class, default=lazy_instance(Class))
+
+            for value in ['1', '02', '3.40', '5.7e-8']:
+                with self.subTest(value):
+                    self.assertEqual(value, parser.parse_args([f'--val={value}']).val)
+                    self.assertEqual(value, parser.parse_args([f'--cls.val={value}']).cls.init_args.val)
+
+
     def test_list(self):
         for list_type in [Iterable, List, Sequence]:
             with self.subTest(str(list_type)):
@@ -195,7 +219,6 @@ class TypeHintsTests(unittest.TestCase):
         self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple=[]']))
         self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple=[2, "a", "b", 5]']))
         self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple=[2, "a"]']))
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple=["2", "a", "b"]']))
         self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple={"a":1, "b":"2"}']))
         out = StringIO()
         parser.print_help(out)
@@ -293,6 +316,25 @@ class TypeHintsTests(unittest.TestCase):
             self.assertEqual(cfg.val, [Namespace(class_path=f'{module}.Class', init_args=Namespace(p1=1, p2=2))])
 
 
+    def test_list_append_subclass_nonclass_default(self):
+        @final
+        class Class:
+            def __init__(self, cal: Union[Calendar, Iterable[Calendar], bool] = True):
+                self.cal = cal
+
+        parser = ArgumentParser(error_handler=None)
+        parser.add_argument('cls', type=Class)
+
+        cfg = parser.parse_args(['--cls.cal=calendar.TextCalendar', '--cls.cal.firstweekday=2'])
+        self.assertNotIsInstance(cfg.cls.cal, list)
+        self.assertEqual('calendar.TextCalendar', cfg.cls.cal.class_path)
+        self.assertEqual(2, cfg.cls.cal.init_args.firstweekday)
+
+        cfg = parser.parse_args(['--cls.cal=calendar.TextCalendar', '--cls.cal+=calendar.HTMLCalendar'])
+        self.assertIsInstance(cfg.cls.cal, list)
+        self.assertEqual(['TextCalendar', 'HTMLCalendar'], [c.class_path.split('.')[1] for c in cfg.cls.cal])
+
+
     def test_list_append_subcommand_subclass(self):
         class A:
             def __init__(self, cals: Union[Calendar, List[Calendar]] = None):
@@ -347,6 +389,27 @@ class TypeHintsTests(unittest.TestCase):
         self.assertEqual('[[[', parser.parse_args(['--any=[[['])['any'])
 
 
+    def test_union_subtypes_order(self):
+        for subtypes, arg, expected in [
+            ((bool, str), '=true', True),
+            ((str, bool), '=true', 'true'),
+            ((int, str), '=1', 1),
+            ((str, int), '=2', '2'),
+            ((float, int), '=3', 3.0),
+            ((int, float), '=4', 4),
+            ((int, List[int]), '=5', 5),
+            ((List[int], int), '=6', 6),
+            ((int, List[int]), '+=7', [7]),
+            ((List[int], int), '+=8', [8]),
+        ]:
+            with self.subTest(f'{subtypes}, {arg}, {expected}'):
+                parser = ArgumentParser()
+                parser.add_argument('--val', type=Union[subtypes])
+                val = parser.parse_args([f'--val{arg}']).val
+                self.assertIsInstance(val, type(expected))
+                self.assertEqual(val, expected)
+
+
     @unittest.skipIf(not Literal, 'Literal introduced in python 3.8 or backported in typing_extensions')
     def test_Literal(self):
         parser = ArgumentParser(error_handler=None)