Skip to content

Commit 89f88de

Browse files
committed
- ArgumentParser.error_handler is now deprecated and will be removed in v5.0.0.
- When parsing fails now argparse.ArgumentError is raised instead of ParserError.
1 parent 2051bc6 commit 89f88de

21 files changed

+508
-452
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ paths are considered internals and can change in minor and patch releases.
1515
v4.20.0 (2023-01-??)
1616
--------------------
1717

18+
Changed
19+
^^^^^^^
20+
- When parsing fails now ``argparse.ArgumentError`` is raised instead of
21+
``ParserError``.
22+
1823
Deprecated
1924
^^^^^^^^^^
2025
- Path ``skip_check`` parameter is deprecated and will be removed in v5.0.0.
@@ -24,6 +29,8 @@ Deprecated
2429
absolute``.
2530
- ``ActionPathList`` is deprecated and will be removed in v5.0.0. Instead use as
2631
type ``List[<path_type>]`` with ``enable_path=True``.
32+
- ``ArgumentParser.error_handler`` is deprecated and will be removed in v5.0.0.
33+
Instead use the new exit_on_error parameter from argparse.
2734

2835

2936
v4.19.0 (2022-12-27)

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ shown above you would observe:
318318

319319
If the parsing fails the standard behavior is that the usage is printed and the
320320
program is terminated. Alternatively you can initialize the parser with
321-
``error_handler=None`` in which case a :class:`.ParserError` is raised.
321+
``exit_on_error=False`` in which case an :class:`.ArgumentError` is raised.
322322

323323

324324
Override order

jsonargparse/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
from argparse import ONE_OR_MORE, OPTIONAL, PARSER, REMAINDER, SUPPRESS, ZERO_OR_MORE
1+
from argparse import (
2+
ONE_OR_MORE,
3+
OPTIONAL,
4+
PARSER,
5+
REMAINDER,
6+
SUPPRESS,
7+
ZERO_OR_MORE,
8+
ArgumentError,
9+
)
210

311
from .actions import *
412
from .cli import *
@@ -16,6 +24,7 @@
1624
from .util import *
1725

1826
__all__ = [
27+
'ArgumentError',
1928
'OPTIONAL',
2029
'REMAINDER',
2130
'SUPPRESS',

jsonargparse/actions.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
from .util import (
1919
LoggerProperty,
2020
NoneType,
21-
ParserError,
2221
Path,
22+
argument_error,
2323
change_to_path_dir,
2424
default_config_option_help,
2525
get_typehint_origin,
@@ -246,7 +246,7 @@ def __call__(self, parser, namespace, value, option_string=None):
246246
flags = value[0].split(',')
247247
invalid_flags = [f for f in flags if f not in valid_flags]
248248
if len(invalid_flags) > 0:
249-
raise ParserError(f'Invalid option "{invalid_flags[0]}" for {option_string}')
249+
raise argument_error(f'Invalid option "{invalid_flags[0]}" for {option_string}')
250250
for flag in [f for f in flags if f != '']:
251251
kwargs[valid_flags[flag]] = True
252252
while hasattr(parser, 'parent_parser'):
@@ -383,7 +383,7 @@ def print_help(self, call_args, baseclass, dest):
383383
args = self.get_args_after_opt(parser.args)
384384
if args:
385385
subparser.parse_args(args)
386-
raise ParserError(f'Expected a nested --*.help option, got: {args}.')
386+
raise argument_error(f'Expected a nested --*.help option, got: {args}.')
387387
else:
388388
subparser.print_help()
389389
parser.exit()
@@ -598,7 +598,8 @@ def add_subcommand(self, name, parser, **kwargs):
598598
parser.default_env = self.parent_parser.default_env
599599
parser.parent_parser = self.parent_parser
600600
parser.parser_mode = self.parent_parser.parser_mode
601-
parser.error_handler = self.parent_parser.error_handler
601+
parser._error_handler = self.parent_parser._error_handler
602+
parser.exit_on_error = self.parent_parser.exit_on_error
602603
parser.logger = self.parent_parser.logger
603604
parser.subcommand = name
604605

jsonargparse/core.py

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
parent_parsers,
4040
previous_config,
4141
)
42+
from .deprecated import ParserDeprecations
4243
from .formatters import DefaultHelpFormatter, empty_help, get_env_var
4344
from .jsonnet import ActionJsonnet, ActionJsonnetExtVars
4445
from .jsonschema import ActionJsonSchema
@@ -74,14 +75,13 @@
7475
from .typehints import ActionTypeHint, is_subclass_spec
7576
from .typing import is_final_class
7677
from .util import (
77-
ParserError,
7878
Path,
79+
argument_error,
7980
change_to_path_dir,
8081
get_private_kwargs,
8182
identity,
8283
is_subclass,
8384
return_parser_if_captured,
84-
usage_and_exit_error_handler,
8585
)
8686

8787
__all__ = ['ActionsContainer', 'ArgumentParser']
@@ -183,7 +183,7 @@ class _ArgumentGroup(ActionsContainer, argparse._ArgumentGroup):
183183
parser: Optional['ArgumentParser'] = None
184184

185185

186-
class ArgumentParser(ActionsContainer, ArgumentLinking, argparse.ArgumentParser):
186+
class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argparse.ArgumentParser):
187187
"""Parser for command line, yaml/jsonnet files and environment variables."""
188188

189189
formatter_class: Type[DefaultHelpFormatter]
@@ -195,8 +195,8 @@ def __init__(
195195
self,
196196
*args,
197197
env_prefix: Union[bool, str] = True,
198-
error_handler: Optional[Callable[['ArgumentParser', str], None]] = usage_and_exit_error_handler,
199198
formatter_class: Type[DefaultHelpFormatter] = DefaultHelpFormatter,
199+
exit_on_error: bool = True,
200200
logger: Union[bool, str, dict, logging.Logger] = False,
201201
version: Optional[str] = None,
202202
print_config: Optional[str] = '--print_config',
@@ -215,7 +215,6 @@ def __init__(
215215
216216
Args:
217217
env_prefix: Prefix for environment variables. ``True`` to derive from ``prog``.
218-
error_handler: Handler for parsing errors, if None raises :class:`.ParserError` exception.
219218
formatter_class: Class for printing help messages.
220219
logger: Configures the logger, see :class:`.LoggerProperty`.
221220
version: Program version which will be printed by the --version argument.
@@ -229,6 +228,7 @@ def __init__(
229228
super().__init__(*args, formatter_class=formatter_class, logger=logger, **kwargs)
230229
if self.groups is None:
231230
self.groups = {}
231+
self.exit_on_error = exit_on_error
232232
self.required_args: Set[str] = set()
233233
self.save_path_content: Set[str] = set()
234234
self.default_config_files = default_config_files # type: ignore
@@ -237,7 +237,6 @@ def __init__(
237237
self.env_prefix = env_prefix
238238
self.parser_mode = parser_mode
239239
self.dump_header = dump_header
240-
self.error_handler = error_handler
241240
self._print_config = print_config
242241
if version is not None:
243242
self.add_argument('--version', action='version', version='%(prog)s '+version, help='Print version and exit.')
@@ -257,7 +256,7 @@ def parse_known_args(self, args=None, namespace=None):
257256
try:
258257
with patch_namespace(), parser_context(parent_parser=self, lenient_check=True), ActionTypeHint.subclass_arg_context(self):
259258
namespace, args = self._parse_known_args(args, namespace)
260-
except (argparse.ArgumentError, ParserError) as ex:
259+
except argparse.ArgumentError as ex:
261260
self.error(str(ex), ex)
262261

263262
return namespace, args
@@ -367,7 +366,7 @@ def parse_args( # type: ignore[override]
367366
A config object with all parsed values.
368367
369368
Raises:
370-
ParserError: If there is a parsing error and error_handler=None.
369+
ArgumentError: If the parsing fails error and exit_on_error=True.
371370
"""
372371
skip_check = get_private_kwargs(kwargs, _skip_check=False)
373372
return_parser_if_captured(self)
@@ -427,7 +426,7 @@ def parse_object(
427426
A config object with all parsed values.
428427
429428
Raises:
430-
ParserError: If there is a parsing error and error_handler=None.
429+
ArgumentError: If the parsing fails error and exit_on_error=True.
431430
"""
432431
skip_check, skip_required = get_private_kwargs(kwargs, _skip_check=False, _skip_required=False)
433432

@@ -506,7 +505,7 @@ def parse_env(
506505
A config object with all parsed values.
507506
508507
Raises:
509-
ParserError: If there is a parsing error and error_handler=None.
508+
ArgumentError: If the parsing fails error and exit_on_error=True.
510509
"""
511510
skip_check, skip_subcommands = get_private_kwargs(kwargs, _skip_check=False, _skip_subcommands=False)
512511

@@ -551,7 +550,7 @@ def parse_path(
551550
A config object with all parsed values.
552551
553552
Raises:
554-
ParserError: If there is a parsing error and error_handler=None.
553+
ArgumentError: If the parsing fails error and exit_on_error=True.
555554
"""
556555
fpath = Path(cfg_path, mode=get_config_read_mode())
557556
with change_to_path_dir(fpath):
@@ -594,7 +593,7 @@ def parse_string(
594593
A config object with all parsed values.
595594
596595
Raises:
597-
ParserError: If there is a parsing error and error_handler=None.
596+
ArgumentError: If the parsing fails error and exit_on_error=True.
598597
"""
599598
skip_check, fail_no_subcommand = get_private_kwargs(kwargs, _skip_check=False, _fail_no_subcommand=True)
600599

@@ -981,8 +980,8 @@ def get_defaults(self, skip_check: bool = False) -> Namespace:
981980
skip_check=skip_check,
982981
skip_required=True,
983982
)
984-
except (TypeError, KeyError, ParserError) as ex:
985-
raise ParserError(f'Problem in default config file "{default_config_file}" :: {ex.args[0]}') from ex
983+
except (TypeError, KeyError, argparse.ArgumentError) as ex:
984+
raise argument_error(f'Problem in default config file "{default_config_file}": {ex.args[0]}') from ex
986985
meta = cfg.get('__default_config__')
987986
if isinstance(meta, list):
988987
meta.append(default_config_file)
@@ -1000,14 +999,19 @@ def get_defaults(self, skip_check: bool = False) -> Namespace:
1000999
## Other methods ##
10011000

10021001
def error(self, message: str, ex: Optional[Exception] = None) -> NoReturn:
1003-
"""Logs error message if a logger is set, calls the error handler and raises a ParserError."""
1002+
"""Logs error message if a logger is set and exits or raises an ArgumentError."""
10041003
self._logger.error(message)
1005-
if self._error_handler is not None:
1004+
if callable(self._error_handler):
10061005
self._error_handler(self, message)
1007-
if ex is None:
1008-
raise ParserError(message)
1009-
else:
1010-
raise ParserError(message) from ex
1006+
if not self.exit_on_error:
1007+
raise argument_error(message) from ex
1008+
elif 'JSONARGPARSE_DEBUG' in os.environ:
1009+
self._logger.debug('Debug enabled, thus raising exception instead of exit.')
1010+
raise argument_error(message) from ex
1011+
self.print_usage(sys.stderr)
1012+
args = {'prog': self.prog, 'message': message}
1013+
sys.stderr.write('%(prog)s: error: %(message)s\n' % args)
1014+
self.exit(2)
10111015

10121016

10131017
def check_config(
@@ -1193,7 +1197,7 @@ def format_help(self) -> str:
11931197
if isinstance(config_files, list):
11941198
config_files = [str(x) for x in config_files]
11951199
note = f'default values below are the ones overridden by the contents of: {config_files}'
1196-
except ParserError as ex:
1200+
except argparse.ArgumentError as ex:
11971201
note = f'tried getting defaults considering default_config_files but failed due to: {ex}'
11981202
group = self._default_config_files_group
11991203
group.description = f'{self._default_config_files}, Note: {note}'
@@ -1327,27 +1331,6 @@ def _check_value_key(self, action: argparse.Action, value: Any, key: str, cfg: O
13271331

13281332
## Properties ##
13291333

1330-
@property
1331-
def error_handler(self) -> Optional[Callable[['ArgumentParser', str], None]]:
1332-
"""Property for the error_handler function that is called when there are parsing errors.
1333-
1334-
:getter: Returns the current error_handler function.
1335-
:setter: Sets a new error_handler function (Callable[self, message:str] or None).
1336-
1337-
Raises:
1338-
ValueError: If an invalid value is given.
1339-
"""
1340-
return self._error_handler
1341-
1342-
1343-
@error_handler.setter
1344-
def error_handler(self, error_handler: Optional[Callable[['ArgumentParser', str], None]]):
1345-
if callable(error_handler) or error_handler is None:
1346-
self._error_handler = error_handler
1347-
else:
1348-
raise ValueError('error_handler can be either a Callable or None.')
1349-
1350-
13511334
@property
13521335
def default_config_files(self) -> List[str]:
13531336
"""Default config file locations.

jsonargparse/deprecated.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44
import inspect
55
import os
66
import sys
7-
from argparse import Action
7+
from argparse import Action, ArgumentError
88
from enum import Enum
9-
from typing import Any, Dict, Optional, Set
9+
from typing import Any, Callable, Dict, Optional, Set
1010

1111
from .namespace import Namespace
1212
from .optionals import FilesCompleterMethod
13+
from .type_checking import ArgumentParser
1314

1415
__all__ = [
1516
'ActionEnum',
1617
'ActionOperators',
1718
'ActionPath',
1819
'ActionPathList',
20+
'ParserError',
1921
'set_url_support',
22+
'usage_and_exit_error_handler',
2023
]
2124

2225

@@ -380,7 +383,6 @@ def path_skip_check_deprecation():
380383
Please update your code to use the new property names and don't modify path
381384
attributes. The changes are: ``rel_path`` -> ``relative`` and ``abs_path``
382385
-> ``absolute``, ``cwd`` no name change, ``skip_check`` will be removed.
383-
384386
"""
385387

386388
class PathDeprecations:
@@ -435,6 +437,69 @@ def skip_check(self, skip_check):
435437
self._skip_check = skip_check
436438

437439

440+
class DebugException(Exception):
441+
pass
442+
443+
444+
@deprecated("""
445+
usage_and_exit_error_handler was deprecated in v4.20.0 and will be removed
446+
in v5.0.0. With the removal of error_handler, there is no longer a need for
447+
this function.
448+
""")
449+
def usage_and_exit_error_handler(parser: 'ArgumentParser', message: str) -> None:
450+
"""Prints the usage and exits with error code 2 (same behavior as argparse).
451+
452+
Args:
453+
parser: The parser object.
454+
message: The message describing the error being handled.
455+
"""
456+
parser.print_usage(sys.stderr)
457+
args = {'prog': parser.prog, 'message': message}
458+
sys.stderr.write('%(prog)s: error: %(message)s\n' % args)
459+
parser.exit(2)
460+
461+
462+
error_handler_message = """
463+
ArgumentParser's error_handler was deprecated in v4.20.0 and will be removed
464+
in v5.0.0. Instead use the new exit_on_error parameter from argparse.
465+
"""
466+
467+
468+
def deprecation_warning_error_handler():
469+
deprecation_warning('ArgumentParser.error_handler', error_handler_message)
470+
471+
472+
class ParserDeprecations:
473+
474+
def __init__(self, *args, error_handler=False, **kwargs):
475+
super().__init__(*args, **kwargs)
476+
self.error_handler = error_handler
477+
478+
@property
479+
def error_handler(self) -> Optional[Callable[['ArgumentParser', str], None]]:
480+
"""Property for the error_handler function that is called when there are parsing errors.
481+
482+
:getter: Returns the current error_handler function.
483+
:setter: Sets a new error_handler function (Callable[self, message:str] or None).
484+
485+
Raises:
486+
ValueError: If an invalid value is given.
487+
"""
488+
return self._error_handler
489+
490+
@error_handler.setter
491+
def error_handler(self, error_handler):
492+
if error_handler is not False:
493+
deprecation_warning_error_handler()
494+
if callable(error_handler) or error_handler in {None, False}:
495+
self._error_handler = error_handler
496+
else:
497+
raise ValueError('error_handler can be either a Callable or None.')
498+
499+
500+
ParserError = ArgumentError
501+
502+
438503
import jsonargparse.optionals
439504

440505
jsonargparse.optionals.import_docstring_parse = import_docstring_parse # type: ignore

0 commit comments

Comments
 (0)