Skip to content

Commit 9e1b382

Browse files
committed
Calculate correct stack level for deprecation warnings to show them to the user.
Generating deprecation warnings with the correct stacklevel is tricky to get right. Up to now, users have not been seeing these deprecation warnigns because they had incorrect stacklevels. See python/cpython#68493 and python/cpython#67998 for good discussions, as well as ipython/ipython#8478 (comment) and ipython/ipython#8480 (comment) for more context. We see these issues directly. Normally stacklevel=2 might be enough to target the caller of a function. However, in our deprecation warning in the __init__ method, each level of subclasses adds one stack frame, so depending on the subclass, the appropriate stacklevel is different. Thus this PR implements a trick from a discussion in Python to calculate the first stack frame that is external to the ipywidgets library, and use that as the target stack frame for the deprecation warning.
1 parent e722a0d commit 9e1b382

File tree

4 files changed

+46
-9
lines changed

4 files changed

+46
-9
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
import sys
5+
import warnings
6+
7+
# This function is from https://github.com/python/cpython/issues/67998
8+
# (https://bugs.python.org/file39550/deprecated_module_stacklevel.diff) and
9+
# calculates the appropriate stacklevel for deprecations to target the
10+
# deprecation for the caller, no matter how many internal stack frames we have
11+
# added in the process. For example, with the deprecation warning in the
12+
# __init__ below, the appropriate stacklevel will change depending on how deep
13+
# the inheritance hierarchy is.
14+
def external_stacklevel(internal):
15+
"""Find the first frame that doesn't any of the given internal strings
16+
17+
The depth will be 2 at minimum in order to start checking at the caller of
18+
the function that called this utility method.
19+
"""
20+
level = 2
21+
frame = sys._getframe(level)
22+
while frame and any(s in frame.f_code.co_filename for s in internal):
23+
level +=1
24+
frame = frame.f_back
25+
return level
26+
27+
def deprecation(message, internal=None):
28+
"""Generate a deprecation warning targeting the first external frame
29+
30+
internal is a list of strings, which if they appear in filenames in the
31+
frames, the frames will also be considered internal.
32+
"""
33+
if internal is None:
34+
internal = []
35+
warnings.warn(message, DeprecationWarning, stacklevel=external_stacklevel(internal+['ipywidgets/widgets/']))

python/ipywidgets/ipywidgets/widgets/widget_button.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
click events on the button and trigger backend code when the clicks are fired.
88
"""
99

10+
from .utils import deprecation
1011
from .domwidget import DOMWidget
1112
from .widget import CallbackDispatcher, register, widget_serialization
1213
from .widget_core import CoreWidget
1314
from .widget_style import Style
1415
from .trait_types import Color, InstanceDict
1516

1617
from traitlets import Unicode, Bool, CaselessStrEnum, Instance, validate, default
17-
import warnings
1818

1919

2020
@register
@@ -70,8 +70,9 @@ def _validate_icon(self, proposal):
7070
"""Strip 'fa-' if necessary'"""
7171
value = proposal['value']
7272
if 'fa-' in value:
73-
warnings.warn("icons names no longer need 'fa-', "
74-
"just use the class names themselves (for example, 'gear spin' instead of 'fa-gear fa-spin')", DeprecationWarning)
73+
deprecation("icons names no longer need 'fa-', "
74+
"just use the class names themselves (for example, 'gear spin' instead of 'fa-gear fa-spin')",
75+
internal=['traitlets/traitlets.py', '/contextlib.py'])
7576
value = value.replace('fa-', '')
7677
return value
7778

python/ipywidgets/ipywidgets/widgets/widget_description.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from .widget_style import Style
1010
from .widget_core import CoreWidget
1111
from .domwidget import DOMWidget
12+
from .utils import deprecation
13+
1214
import warnings
1315

1416
@register
@@ -27,7 +29,7 @@ class DescriptionWidget(DOMWidget, CoreWidget):
2729

2830
def __init__(self, *args, **kwargs):
2931
if 'description_tooltip' in kwargs:
30-
warnings.warn("the description_tooltip argument is deprecated, use tooltip instead", DeprecationWarning)
32+
deprecation("the description_tooltip argument is deprecated, use tooltip instead")
3133
kwargs.setdefault('tooltip', kwargs['description_tooltip'])
3234
del kwargs['description_tooltip']
3335
super().__init__(*args, **kwargs)
@@ -47,10 +49,10 @@ def description_tooltip(self):
4749
.. deprecated :: 8.0.0
4850
Use tooltip attribute instead.
4951
"""
50-
warnings.warn(".description_tooltip is deprecated, use .tooltip instead", DeprecationWarning)
52+
deprecation(".description_tooltip is deprecated, use .tooltip instead")
5153
return self.tooltip
5254

5355
@description_tooltip.setter
5456
def description_tooltip(self, tooltip):
55-
warnings.warn(".description_tooltip is deprecated, use .tooltip instead", DeprecationWarning)
57+
deprecation(".description_tooltip is deprecated, use .tooltip instead")
5658
self.tooltip = tooltip

python/ipywidgets/ipywidgets/widgets/widget_string.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from .widget import CallbackDispatcher, register, widget_serialization
1212
from .widget_core import CoreWidget
1313
from .trait_types import Color, InstanceDict, TypedTuple
14+
from .utils import deprecation
1415
from traitlets import Unicode, Bool, Int
15-
from warnings import warn
1616

1717

1818
class _StringStyle(DescriptionStyle, CoreWidget):
@@ -142,8 +142,7 @@ def on_submit(self, callback, remove=False):
142142
remove: bool (optional)
143143
Whether to unregister the callback
144144
"""
145-
import warnings
146-
warnings.warn("on_submit is deprecated. Instead, set the .continuous_update attribute to False and observe the value changing with: mywidget.observe(callback, 'value').", DeprecationWarning)
145+
deprecation("on_submit is deprecated. Instead, set the .continuous_update attribute to False and observe the value changing with: mywidget.observe(callback, 'value').")
147146
self._submission_callbacks.register_callback(callback, remove=remove)
148147

149148

0 commit comments

Comments
 (0)