Repository: omni-us/jsonargparse

Issue:
Support for conversion to `torch.Tensor`
## 🐛 Bug report

I am encountering a problem using the package with Lightning.

When trying to specify an `init_args` which expects a `torch.Tensor` (for example when specifying the weights of a loss function), an error occurs because torch module expects a tensor, not a list.

It should be possible to [register](https://jsonargparse.readthedocs.io/en/stable/index.html#registering-types) pytorch tensors using

```python
def serializer(x):
    return x.tolist()

def deserializer(x):
    return torch.tensor(x)

register_type(torch.Tensor, serializer, deserializer)
```

However, doing so results in an error:

```bash
main: error: Parser key "model": Problem with given class_path "MyModel":
  - Configuration check failed :: Parser key "my_model": Value "Namespace(class_path='MySubModel', init_args=Namespace(..., loss=Namespace(class_path='torch.nn.CrossEntropyLoss', init_args=Namespace(weight=tensor([  7.6200, 104.9100,   1.5600,  20.6500,  14.0500,  14.0600,   1.0000,  36.0700]), size_average=None, ignore_index=-100, reduce=None, reduction='mean', label_smoothing=0.0))))" does not validate against any of the types in typing.Optional[MySubModel]:
    - Boolean value of Tensor with more than one value is ambiguous
...
```

Since a list is specified in the yaml and the namespace shows a `tensor([...])`, it looks like the deserializer is working correctly, but something is going wrong.

### Expected behavior

The list should be converted to a tensor without error.

### Environment

- jsonargparse 4.15.2
- Python 3.10.6



Comments:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 1d800e2..005aa34 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -21,6 +21,8 @@ Fixed
 - ``parse_string`` raises ``AttributeError`` when given a simple string.
 - Added missing ``return_parser`` deprecation warning when ``CLI`` has
   subcommands.
+- Parsing fails for registered types that can't be cast to boolean `#196
+  <https://github.com/omni-us/jsonargparse/issues/196>`__.
 
 Changed
 ^^^^^^^
diff --git a/jsonargparse/actions.py b/jsonargparse/actions.py
index c93b2df..e9ee140 100644
--- a/jsonargparse/actions.py
+++ b/jsonargparse/actions.py
@@ -480,7 +480,7 @@ class ActionParser:
         """Initializer for ActionParser instance.
 
         Args:
-            parser: A parser to parse the option with.
+            parser (Optional[ArgumentParser]): A parser to parse the option with.
 
         Raises:
             ValueError: If the parser parameter is invalid.
diff --git a/jsonargparse/core.py b/jsonargparse/core.py
index 57bf6d7..f700fdc 100644
--- a/jsonargparse/core.py
+++ b/jsonargparse/core.py
@@ -212,7 +212,7 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
             self.groups = {}
         self.required_args: Set[str] = set()
         self.save_path_content: Set[str] = set()
-        self.default_config_files = default_config_files
+        self.default_config_files = default_config_files  # type: ignore
         self.default_meta = default_meta
         self.default_env = default_env
         self.env_prefix = env_prefix
@@ -861,11 +861,11 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
 
     ## Methods related to defaults ##
 
-    def set_defaults(self, *args, **kwargs) -> None:
+    def set_defaults(self, *args: Dict[str, Any], **kwargs: Any) -> None:
         """Sets default values from dictionary or keyword arguments.
 
         Args:
-            *args (dict): Dictionary defining the default values to set.
+            *args: Dictionary defining the default values to set.
             **kwargs: Sets default values based on keyword arguments.
 
         Raises:
@@ -938,7 +938,6 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
         """Returns a namespace with all default values.
 
         Args:
-            nested: Whether the namespace should be nested.
             skip_check: Whether to skip check if configuration is valid.
 
         Returns:
@@ -1314,7 +1313,7 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
     ## Properties ##
 
     @property
-    def error_handler(self):
+    def error_handler(self) -> Optional[Callable[['ArgumentParser', str], None]]:
         """Property for the error_handler function that is called when there are parsing errors.
 
         :getter: Returns the current error_handler function.
@@ -1327,7 +1326,7 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
 
 
     @error_handler.setter
-    def error_handler(self, error_handler):
+    def error_handler(self, error_handler: Optional[Callable[['ArgumentParser', str], None]]):
         if callable(error_handler) or error_handler is None:
             self._error_handler = error_handler
         else:
@@ -1335,7 +1334,7 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
 
 
     @property
-    def default_config_files(self):
+    def default_config_files(self) -> List[str]:
         """Default config file locations.
 
         :getter: Returns the current default config file locations.
@@ -1348,7 +1347,7 @@ class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser)
 
 
     @default_config_files.setter
-    def default_config_files(self, default_config_files:Optional[List[str]]):
+    def default_config_files(self, default_config_files: Optional[List[str]]):
         if default_config_files is None:
             self._default_config_files = []
         elif isinstance(default_config_files, list) and all(isinstance(x, str) for x in default_config_files):
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
index 5c7a680..f3344ce 100644
--- a/jsonargparse/typehints.py
+++ b/jsonargparse/typehints.py
@@ -384,7 +384,7 @@ class ActionTypeHint(Action):
                 path_meta = val.pop('__path__', None) if isinstance(val, dict) else None
 
                 prev_val = cfg.get(self.dest) if cfg else None
-                if not prev_val and not sub_defaults.get() and is_subclass_spec(self.default):
+                if prev_val is None and not sub_defaults.get() and is_subclass_spec(self.default):
                     prev_val = Namespace(class_path=self.default.class_path)
 
                 kwargs = {
diff --git a/jsonargparse/util.py b/jsonargparse/util.py
index e71042e..0f86ba8 100644
--- a/jsonargparse/util.py
+++ b/jsonargparse/util.py
@@ -586,12 +586,12 @@ class LoggerProperty:
 
     def __init__(self, *args, logger: Union[bool, str, dict, logging.Logger] = False, **kwargs):
         """Initializer for LoggerProperty class."""
-        self.logger = logger
+        self.logger = logger  # type: ignore
         super().__init__(*args, **kwargs)
 
 
     @property
-    def logger(self):
+    def logger(self) -> logging.Logger:
         """The logger property for the class.
 
         :getter: Returns the current logger.


Test patch:
diff --git a/jsonargparse_tests/test_typing.py b/jsonargparse_tests/test_typing.py
index bbaf0dd..a800071 100755
--- a/jsonargparse_tests/test_typing.py
+++ b/jsonargparse_tests/test_typing.py
@@ -17,6 +17,7 @@ from jsonargparse.typing import (
     path_type,
     PositiveFloat,
     PositiveInt,
+    register_type,
     RegisteredType,
     registered_types,
     restricted_number_type,
@@ -254,5 +255,26 @@ class OtherTests(unittest.TestCase):
                 self.assertRaises(ValueError, lambda: timedelta_type.deserializer(delta_in))
 
 
+    def test_register_non_bool_cast_type(self):
+        class Elems:
+            def __init__(self, *elems):
+                self.elems = list(elems)
+            def __bool__(self):
+                raise RuntimeError('bool not supported')
+
+        self.assertRaises(RuntimeError, lambda: not Elems(1, 2))
+        register_type(Elems, lambda x: x.elems, lambda x: Elems(*x))
+
+        parser = ArgumentParser(error_handler=None)
+        parser.add_argument('--elems', type=Elems)
+        cfg = parser.parse_args(['--elems=[1, 2, 3]'])
+        self.assertIsInstance(cfg.elems, Elems)
+        self.assertEqual(cfg.elems.elems, [1, 2, 3])
+        dump = parser.dump(cfg, format='json')
+        self.assertEqual(dump, '{"elems":[1,2,3]}')
+
+        del registered_types[Elems]
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)