Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: omni-us/jsonargparse
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v3.5.1
Choose a base ref
...
head repository: omni-us/jsonargparse
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v3.6.0
Choose a head ref
  • 3 commits
  • 14 files changed
  • 1 contributor

Commits on Mar 8, 2021

  1. - Added function to register additional types for use in parsers.

    - Added type hint support for complex and UUID classes.
    - PositiveInt and NonNegativeInt now gives error instead of silently truncating when given float.
    - Types created with restricted_number_type and restricted_string_type now share a common TypeCore base class.
    - Fixed ActionOperators not give error if type already registered.
    - Fixed List[Tuple] types not working correctly.
    mauvilsa committed Mar 8, 2021
    Copy the full SHA
    da82a62 View commit details
  2. - Fixed bug in dump where some nested dicts were kept as Namespace.

    - Addressed sonarcloud comment on repeated message in _flat_namespace_to_dict.
    - Preparing for release 3.6.0.
    mauvilsa committed Mar 8, 2021
    Copy the full SHA
    7c3cd08 View commit details
  3. Copy the full SHA
    2856ff2 View commit details
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.5.1
current_version = 3.6.0
commit = True
tag = True
tag_name = v{new_version}
2 changes: 1 addition & 1 deletion .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
sonar.sources=jsonargparse
sonar.projectVersion=3.5.1
sonar.projectVersion=3.6.0
22 changes: 22 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -10,6 +10,28 @@ only be introduced in major versions with advance notice in the **Deprecated**
section of releases.


v3.6.0 (2021-03-08)
-------------------

Added
^^^^^
- Function to register additional types for use in parsers.
- Type hint support for complex and UUID classes.

Changed
^^^^^^^
- PositiveInt and NonNegativeInt now gives error instead of silently truncating
when given float.
- Types created with restricted_number_type and restricted_string_type now share
a common TypeCore base class.

Fixed
^^^^^
- ActionOperators not give error if type already registered.
- List[Tuple] types not working correctly.
- Some nested dicts kept as Namespace by dump.


v3.5.1 (2021-02-26)
-------------------

49 changes: 44 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
@@ -560,11 +560,11 @@ Some notes about this support are:
- Nested types are supported as long as at least one child type is supported.

- Fully supported types are: :code:`str`, :code:`bool`, :code:`int`,
:code:`float`, :code:`List`, :code:`Iterable`, :code:`Sequence`, :code:`Any`,
:code:`Union`, :code:`Optional`, :code:`Enum`, restricted types as explained
in sections :ref:`restricted-numbers` and :ref:`restricted-strings` and paths
and URLs as explained in sections :ref:`parsing-paths` and
:ref:`parsing-urls`.
:code:`float`, :code:`complex`, :code:`List`, :code:`Iterable`,
:code:`Sequence`, :code:`Any`, :code:`Union`, :code:`Optional`, :code:`Enum`,
:code:`UUID`, restricted types as explained in sections
:ref:`restricted-numbers` and :ref:`restricted-strings` and paths and URLs as
explained in sections :ref:`parsing-paths` and :ref:`parsing-urls`.

- :code:`Dict` is supported but only with :code:`str` or :code:`int` keys.

@@ -582,6 +582,45 @@ Some notes about this support are:
:code:`type: Union[str, null], default: null`.


.. _registering-types:

Registering types
=================

With the :func:`.register_type` function it is possible to register additional
types for use in jsonargparse parsers. If the type class can be instantiated
with a string representation and by casting the instance to :code:`str` gives
back the string representation, then only the type class is given to
:func:`.register_type`. For example in the :code:`jsonargparse.typing` package
this is how complex numbers are registered: :code:`register_type(complex)`. For
other type classes that don't have these properties, to register it might be
necessary to provide a serializer and/or deserializer function. Including the
serializer and deserializer functions, the registration of the complex numbers
example is equivalent to :code:`register_type(complex, serializer=str,
deserializer=complex)`.

A more useful example could be registering the :code:`datetime` class. This case
requires to give both a serializer and a deserializer as seen below.

.. code-block:: python
from datetime import datetime
from jsonargparse import ArgumentParser
from jsonargparse.typing import register_type
def serializer(v):
return v.isoformat()
def deserializer(v):
return datetime.strptime(v, '%Y-%m-%dT%H:%M:%S')
register_type(datetime, serializer, deserializer)
parser = ArgumentParser()
parser.add_argument('--datetime', type=datetime)
parser.parse_args(['--datetime=2008-09-03T20:56:35'])
.. _sub-classes:

Class type and sub-classes
2 changes: 1 addition & 1 deletion jsonargparse/__init__.py
Original file line number Diff line number Diff line change
@@ -19,4 +19,4 @@
)


__version__ = '3.5.1'
__version__ = '3.6.0'
9 changes: 7 additions & 2 deletions jsonargparse/actions.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
from argparse import Namespace, Action, SUPPRESS, _StoreAction, _HelpAction, _SubParsersAction

from .optionals import get_config_read_mode, FilesCompleterMethod
from .typing import restricted_number_type
from .typing import restricted_number_type, registered_types
from .util import (
yamlParserError,
yamlScannerError,
@@ -340,7 +340,12 @@ class ActionOperators:

def __init__(self, **kwargs):
if 'expr' in kwargs:
self._type = restricted_number_type(None, kwargs.get('type', int), kwargs['expr'], kwargs.get('join', 'and'))
restrictions = [kwargs['expr']] if isinstance(kwargs['expr'], tuple) else kwargs['expr']
register_key = (tuple(sorted(restrictions)), kwargs.get('type', int), kwargs.get('join', 'and'))
if register_key in registered_types:
self._type = registered_types[register_key]
else:
self._type = restricted_number_type(None, kwargs.get('type', int), kwargs['expr'], kwargs.get('join', 'and'))
else:
raise ValueError('Expected expr keyword argument.')

4 changes: 3 additions & 1 deletion jsonargparse/core.py
Original file line number Diff line number Diff line change
@@ -710,6 +710,8 @@ def cleanup_actions(cfg, actions):
cfg[action.dest] = str(cfg[action.dest])
elif isinstance(action, ActionParser):
cleanup_actions(cfg, action._parser._actions)
elif isinstance(action, ActionJsonSchema) and action._annotation is not None and cfg.get(action.dest) is not None:
cfg[action.dest] = ActionJsonSchema._adapt_types(cfg[action.dest], action._annotation, action._subschemas, True)

def cleanup_types(cfg):
for dest, val in cfg.items():
@@ -728,7 +730,7 @@ def cleanup_types(cfg):
cfg = namespace_to_dict(_dict_to_flat_namespace(cfg))
cleanup_actions(cfg, self._actions)
cleanup_types(cfg)
cfg = _flat_namespace_to_dict(dict_to_namespace(cfg))
cfg = _flat_namespace_to_dict(_dict_to_flat_namespace(cfg))

if format == 'parser_mode':
format = 'yaml' if self.parser_mode == 'yaml' else 'json_indented'
79 changes: 43 additions & 36 deletions jsonargparse/jsonschema.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
from typing import Any, Union, Tuple, List, Iterable, Sequence, Set, Dict, Type

from .actions import _is_action_value_list
from .typing import is_optional, annotation_to_schema, type_to_str
from .typing import is_optional, type_to_str, registered_types
from .util import (
namespace_to_dict,
Path,
@@ -149,7 +149,7 @@ def _check_type(self, value, cfg=None):
if isinstance(val, dict) and fpath is not None:
val['__path__'] = fpath
value[num] = val
except (TypeError, yamlParserError, yamlScannerError, jsonschemaValidationError) as ex:
except (TypeError, ValueError, yamlParserError, yamlScannerError, jsonschemaValidationError) as ex:
elem = '' if not islist else ' element '+str(num+1)
raise TypeError('Parser key "'+self.dest+'"'+elem+': '+str(ex)) from ex
return value if islist else value[0]
@@ -181,8 +181,24 @@ def _instantiate_classes(self, val):
@staticmethod
def _adapt_types(val, annotation, subschemas, reverse=False, instantiate_classes=False):

def validate_adapt(v, subschema):
def validate_adapt(v, subschema, annotation=None):
if subschema is not None:
if isinstance(subschema, list):
vals = []
for s in subschema:
try:
vv = validate_adapt(v, s)
if reverse and s[0] in typesmap and not isinstance(vv, s[0]):
raise ValueError('Value "'+str(vv)+'" not instance of '+str(s[0]))
vals.append(vv)
break
except Exception as ex:
vals.append(ex)
if all(isinstance(v, Exception) for v in vals):
e = ' :: '.join(str(v) for v in vals)
raise ParserError('Value "'+str(val)+'" does not validate against any of the types in '+str(annotation)+' :: '+e)
return [v for v in vals if not isinstance(v, Exception)][0]

subannotation, subvalidator, subsubschemas = subschema
if reverse:
v = ActionJsonSchema._adapt_types(v, subannotation, subsubschemas, reverse, instantiate_classes)
@@ -191,25 +207,26 @@ def validate_adapt(v, subschema):
if subvalidator is not None and not instantiate_classes:
subvalidator.validate(v)
v = ActionJsonSchema._adapt_types(v, subannotation, subsubschemas, reverse, instantiate_classes)
except jsonschemaValidationError:
pass
except jsonschemaValidationError as ex:
raise ValueError(str(ex)) from ex
return v

if subschemas is None:
subschemas = []

if _issubclass(annotation, Enum):
if annotation in registered_types:
registered_type = registered_types[annotation]
if reverse and val.__class__ == registered_type.type_class:
val = registered_type.serializer(val)
elif not reverse:
val = registered_type.deserializer(val)

elif _issubclass(annotation, Enum):
if reverse and isinstance(val, annotation):
val = val.name
elif not reverse and val in annotation.__members__:
val = annotation[val]

elif _issubclass(annotation, Path):
if reverse and isinstance(val, annotation):
val = str(val)
elif not reverse:
val = annotation(val)

elif not hasattr(annotation, '__origin__'):
if not reverse and \
not _issubclass(annotation, (str, int, float)) and \
@@ -233,34 +250,29 @@ def validate_adapt(v, subschema):
return val

elif annotation.__origin__ == Union:
for subschema in subschemas:
val = validate_adapt(val, subschema)
val = validate_adapt(val, subschemas, annotation)

elif annotation.__origin__ in {Tuple, tuple, Set, set} and isinstance(val, (list, tuple, set)):
if reverse:
val = list(val)
for n, v in enumerate(val):
if len(subschemas) == 0:
break
subschema = subschemas[n if n < len(subschemas) else -1]
if subschema is not None:
val[n] = validate_adapt(v, subschema)
val[n] = validate_adapt(v, subschema, annotation)
if not reverse:
val = tuple(val) if annotation.__origin__ in {Tuple, tuple} else set(val)

elif annotation.__origin__ in {List, list, Set, set, Iterable, Sequence} and isinstance(val, list):
for n, v in enumerate(val):
for subschema in subschemas:
val[n] = validate_adapt(v, subschema)
val[n] = validate_adapt(v, subschemas, annotation)

elif annotation.__origin__ in {Dict, dict} and isinstance(val, dict):
if annotation.__args__[0] == int:
cast = str if reverse else int
val = {cast(k): v for k, v in val.items()}
if annotation.__args__[1] not in typesmap:
for k, v in val.items():
for subschema in subschemas:
val[k] = validate_adapt(v, subschema)
val[k] = validate_adapt(v, subschemas, annotation)

return val

@@ -269,7 +281,9 @@ def validate_adapt(v, subschema):
def _typing_schema(annotation):
"""Generates a schema based on a type annotation."""

if annotation == Any:
jsonvalidator = import_jsonschema('ActionJsonSchema')[1]

if annotation == Any or annotation in registered_types:
return {}, None

elif annotation in typesmap:
@@ -278,12 +292,6 @@ def _typing_schema(annotation):
elif _issubclass(annotation, Enum):
return {'type': 'string', 'enum': list(annotation.__members__.keys())}, [(annotation, None, None)]

elif _issubclass(annotation, Path):
return {'type': 'string'}, [(annotation, None, None)]

elif _issubclass(annotation, (str, int, float)):
return annotation_to_schema(annotation), None

elif annotation == dict:
return {'type': 'object'}, None

@@ -298,7 +306,6 @@ def _typing_schema(annotation):
'required': ['class_path'],
'additionalProperties': False,
}
jsonvalidator = import_jsonschema('ActionJsonSchema')[1]
return schema, [(annotation, jsonvalidator(schema), None)]
return None, None

@@ -309,9 +316,7 @@ def _typing_schema(annotation):
schema, subschemas = ActionJsonSchema._typing_schema(arg)
if schema is not None:
members.append(schema)
if arg not in typesmap:
jsonvalidator = import_jsonschema('ActionJsonSchema')[1]
union_subschemas.append((arg, jsonvalidator(schema), subschemas))
union_subschemas.append((arg, jsonvalidator(schema), subschemas))
if len(members) == 1:
return members[0], union_subschemas
elif len(members) > 1:
@@ -327,19 +332,21 @@ def _typing_schema(annotation):
break
item, subschemas = ActionJsonSchema._typing_schema(arg)
items.append(item)
if arg not in typesmap:
jsonvalidator = import_jsonschema('ActionJsonSchema')[1]
tuple_subschemas.append((arg, jsonvalidator(item), subschemas))
tuple_subschemas.append((arg, jsonvalidator(item), subschemas))
schema = {'type': 'array', 'items': items, 'minItems': len(items)}
if has_ellipsis:
schema['additionalItems'] = items[-1]
else:
schema['maxItems'] = len(items)
return schema, tuple_subschemas
return schema, tuple_subschemas if tuple_subschemas != [] else None

elif annotation.__origin__ in {List, list, Iterable, Sequence, Set, set}:
items, subschemas = ActionJsonSchema._typing_schema(annotation.__args__[0])
if items is not None:
if subschemas is None:
subschemas = [(annotation.__args__[0], None, None)]
else:
subschemas = [(annotation.__args__[0], jsonvalidator(items), subschemas)]
return {'type': 'array', 'items': items}, subschemas

elif annotation.__origin__ in {Dict, dict} and annotation.__args__[0] in {str, int}:
Loading