Repository: ipython/traitlets
Pull request #919 (2025-04-11 08:11:38)

accept integer-valued numbers for Integer
allows config to cast e.g. the float 4e9 to int without error if the value is unchanged


Patch:
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index a9d02c7..0f487e0 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -44,6 +44,7 @@ from __future__ import annotations
 import contextlib
 import enum
 import inspect
+import numbers
 import os
 import re
 import sys
@@ -1290,11 +1291,7 @@ class DefaultHandler(EventHandler):
 class HasDescriptors(metaclass=MetaHasDescriptors):
     """The base class for all classes that have descriptors."""
 
-    def __new__(*args: t.Any, **kwargs: t.Any) -> t.Any:
-        # Pass cls as args[0] to allow "cls" as keyword argument
-        cls = args[0]
-        args = args[1:]
-
+    def __new__(cls, /, *args: t.Any, **kwargs: t.Any) -> Self:
         # This is needed because object.__new__ only accepts
         # the cls argument.
         new_meth = super(HasDescriptors, cls).__new__
@@ -1305,13 +1302,10 @@ class HasDescriptors(metaclass=MetaHasDescriptors):
         inst.setup_instance(*args, **kwargs)
         return inst
 
-    def setup_instance(*args: t.Any, **kwargs: t.Any) -> None:
+    def setup_instance(self, /, *args: t.Any, **kwargs: t.Any) -> None:
         """
         This is called **before** self.__init__ is called.
         """
-        # Pass self as args[0] to allow "self" as keyword argument
-        self = args[0]
-        args = args[1:]
 
         self._cross_validation_lock = False
         cls = self.__class__
@@ -1333,11 +1327,7 @@ class HasTraits(HasDescriptors, metaclass=MetaHasTraits):
     _traits: dict[str, t.Any]
     _all_trait_default_generators: dict[str, t.Any]
 
-    def setup_instance(*args: t.Any, **kwargs: t.Any) -> None:
-        # Pass self as args[0] to allow "self" as keyword argument
-        self = args[0]
-        args = args[1:]
-
+    def setup_instance(self, /, *args: t.Any, **kwargs: t.Any) -> None:
         # although we'd prefer to set only the initial values not present
         # in kwargs, we will overwrite them in `__init__`, and simply making
         # a copy of a dict is faster than checking for each key.
@@ -2646,6 +2636,15 @@ class Int(TraitType[G, S]):
         )
 
     def validate(self, obj: t.Any, value: t.Any) -> G:
+        if not isinstance(value, int) and isinstance(value, numbers.Number):
+            # allow casting integer-valued numbers to int
+            # allows for more concise assignment like `4e9` which is a float
+            try:
+                int_value = int(value)
+                if int_value == value:
+                    value = int_value
+            except Exception:
+                pass
         if not isinstance(value, int):
             self.error(obj, value)
         return _validate_bounds(self, obj, value)  # type:ignore[no-any-return]


Test patch:
diff --git a/tests/test_traitlets.py b/tests/test_traitlets.py
index f9f623b..665422c 100644
--- a/tests/test_traitlets.py
+++ b/tests/test_traitlets.py
@@ -7,6 +7,7 @@
 # also under the terms of the Modified BSD License.
 from __future__ import annotations
 
+import decimal
 import pickle
 import re
 import typing as t
@@ -1391,7 +1392,7 @@ class TestLong(TraitTestBase):
     obj = LongTrait()
 
     _default_value = 99
-    _good_values = [10, -10]
+    _good_values = [10, -10, 10.0, decimal.Decimal("10.0")]
     _bad_values = [
         "ten",
         [10],
@@ -1401,6 +1402,7 @@ class TestLong(TraitTestBase):
         1j,
         10.1,
         -10.1,
+        decimal.Decimal("10.1"),
         "10",
         "-10",
         "10L",