diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f169513c..e741d69d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ v4.26.0 ======= * Decrease import time by delaying importing of ``urllib.request`` (#1416). +* Report durations whose amounts overflow ``Decimal`` (e.g. ``P1E1000000D``) as invalid instead of raising an uncaught ``decimal.Overflow`` from the ``duration`` format checker (#1511). v4.25.1 ======= diff --git a/jsonschema/_format.py b/jsonschema/_format.py index 62c0e4ee..f372bf53 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -518,7 +518,7 @@ def is_uri_template(instance: object) -> bool: @_checks_drafts( draft201909="duration", draft202012="duration", - raises=isoduration.DurationParsingException, + raises=(isoduration.DurationParsingException, ArithmeticError), ) def is_duration(instance: object) -> bool: if not isinstance(instance, str): diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py index d829f984..b5dd3ca2 100644 --- a/jsonschema/tests/test_format.py +++ b/jsonschema/tests/test_format.py @@ -2,7 +2,8 @@ Tests for the parts of jsonschema related to the :kw:`format` keyword. """ -from unittest import TestCase +from importlib.util import find_spec +from unittest import TestCase, skipIf from jsonschema import FormatChecker, ValidationError from jsonschema.exceptions import FormatError @@ -80,6 +81,23 @@ def test_format_checkers_come_with_defaults(self): with self.assertRaises(FormatError): checker.check(instance="not-an-ipv4", format="ipv4") + @skipIf( + find_spec("isoduration") is None, + "The isoduration dependency is not installed", + ) + def test_it_rejects_durations_that_overflow_decimal(self): + # Decimal parsing in isoduration raises decimal.Overflow (an + # ArithmeticError, not a DurationParsingException) for amounts + # past the context's Emax, e.g. an exponent of 1E1000000 or a + # plain digit run longer than 999999. Those must be reported as + # invalid durations rather than escaping the checker. + checker = FormatChecker() + for instance in ("P1E1000000D", "P" + "9" * 1000000 + "D"): + with self.assertRaises(FormatError): + checker.check(instance=instance, format="duration") + # A well-formed duration is still accepted. + self.assertTrue(checker.conforms("P1Y2M3DT4H5M6S", "duration")) + def test_repr(self): checker = FormatChecker(formats=()) checker.checks("foo")(lambda thing: True) # pragma: no cover