Skip to content

Commit 96e7f0a

Browse files
authored
Define @deprecated and @experimental decorators
Differential Revision: D61367967 Pull Request resolved: #4744
1 parent a54d62c commit 96e7f0a

File tree

6 files changed

+233
-0
lines changed

6 files changed

+233
-0
lines changed

exir/TARGETS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,11 @@ python_library(
268268
"//caffe2:torch",
269269
],
270270
)
271+
272+
python_library(
273+
name = "_warnings",
274+
srcs = ["_warnings.py"],
275+
deps = [
276+
"fbsource//third-party/pypi/typing-extensions:typing-extensions",
277+
],
278+
)

exir/_warnings.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Decorators used to warn about non-stable APIs."""
8+
9+
# pyre-strict
10+
11+
from typing import Any, Dict, Optional, Sequence, Type
12+
13+
from typing_extensions import deprecated
14+
15+
__all__ = ["deprecated", "experimental"]
16+
17+
18+
class ExperimentalWarning(DeprecationWarning):
19+
"""Emitted when calling an experimental API.
20+
21+
Derives from DeprecationWarning so that it is similarly filtered out by
22+
default.
23+
"""
24+
25+
def __init__(self, /, *args: Sequence[Any], **kwargs: Dict[str, Any]) -> None:
26+
super().__init__(*args, **kwargs)
27+
28+
29+
class experimental(deprecated):
30+
"""Indicates that a class, function or overload is experimental.
31+
32+
When this decorator is applied to an object, the type checker
33+
will generate a diagnostic on usage of the experimental object.
34+
"""
35+
36+
def __init__(
37+
self,
38+
message: str,
39+
/,
40+
*,
41+
category: Optional[Type[Warning]] = ExperimentalWarning,
42+
stacklevel: int = 1,
43+
) -> None:
44+
super().__init__(message, category=category, stacklevel=stacklevel)

exir/tests/TARGETS

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,13 @@ python_unittest(
433433
"//executorch/exir/passes:lib",
434434
],
435435
)
436+
437+
python_unittest(
438+
name = "warnings",
439+
srcs = [
440+
"test_warnings.py",
441+
],
442+
deps = [
443+
"//executorch/exir:_warnings",
444+
],
445+
)

exir/tests/test_warnings.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-strict
8+
import unittest
9+
import warnings
10+
from typing import Any, Callable, Optional
11+
12+
from executorch.exir._warnings import deprecated, experimental, ExperimentalWarning
13+
14+
#
15+
# Classes
16+
#
17+
18+
19+
class UndecoratedClass:
20+
pass
21+
22+
23+
@deprecated("DeprecatedClass message")
24+
class DeprecatedClass:
25+
pass
26+
27+
28+
@experimental("ExperimentalClass message")
29+
class ExperimentalClass:
30+
pass
31+
32+
33+
#
34+
# Functions
35+
#
36+
37+
38+
def undecorated_function() -> None:
39+
pass
40+
41+
42+
@deprecated("deprecated_function message")
43+
def deprecated_function() -> None:
44+
pass
45+
46+
47+
@experimental("experimental_function message")
48+
def experimental_function() -> None:
49+
pass
50+
51+
52+
#
53+
# Methods
54+
#
55+
56+
57+
class TestClass:
58+
def undecorated_method(self) -> None:
59+
pass
60+
61+
@deprecated("deprecated_method message")
62+
def deprecated_method(self) -> None:
63+
pass
64+
65+
@experimental("experimental_method message")
66+
def experimental_method(self) -> None:
67+
pass
68+
69+
70+
# NOTE: Variables and fields cannot be decorated.
71+
72+
73+
class TestApiLifecycle(unittest.TestCase):
74+
75+
def is_deprecated(
76+
self,
77+
callable: Callable[[], Any], # pyre-ignore[2]: Any type
78+
message: Optional[str] = None,
79+
) -> bool:
80+
with warnings.catch_warnings(record=True) as w:
81+
# Cause all warnings to always be triggered.
82+
warnings.simplefilter("always")
83+
84+
# Try to trigger a warning.
85+
callable()
86+
87+
if not w:
88+
# No warnings were triggered.
89+
return False
90+
if not issubclass(w[-1].category, DeprecationWarning):
91+
# There was a warning, but it wasn't a DeprecationWarning.
92+
return False
93+
if issubclass(w[-1].category, ExperimentalWarning):
94+
# ExperimentalWarning is a subclass of DeprecationWarning.
95+
return False
96+
if message:
97+
return message in str(w[-1].message)
98+
return True
99+
100+
def is_experimental(
101+
self,
102+
callable: Callable[[], Any], # pyre-ignore[2]: Any type
103+
message: Optional[str] = None,
104+
) -> bool:
105+
with warnings.catch_warnings(record=True) as w:
106+
# Cause all warnings to always be triggered.
107+
warnings.simplefilter("always")
108+
109+
# Try to trigger a warning.
110+
callable()
111+
112+
if not w:
113+
# No warnings were triggered.
114+
return False
115+
if not issubclass(w[-1].category, ExperimentalWarning):
116+
# There was a warning, but it wasn't an ExperimentalWarning.
117+
return False
118+
if message:
119+
return message in str(w[-1].message)
120+
return True
121+
122+
def test_undecorated_class(self) -> None:
123+
self.assertFalse(self.is_deprecated(UndecoratedClass))
124+
self.assertFalse(self.is_experimental(UndecoratedClass))
125+
126+
def test_deprecated_class(self) -> None:
127+
self.assertTrue(self.is_deprecated(DeprecatedClass, "DeprecatedClass message"))
128+
self.assertFalse(self.is_experimental(DeprecatedClass))
129+
130+
def test_experimental_class(self) -> None:
131+
self.assertFalse(self.is_deprecated(ExperimentalClass))
132+
self.assertTrue(
133+
self.is_experimental(ExperimentalClass, "ExperimentalClass message")
134+
)
135+
136+
def test_undecorated_function(self) -> None:
137+
self.assertFalse(self.is_deprecated(undecorated_function))
138+
self.assertFalse(self.is_experimental(undecorated_function))
139+
140+
def test_deprecated_function(self) -> None:
141+
self.assertTrue(
142+
self.is_deprecated(deprecated_function, "deprecated_function message")
143+
)
144+
self.assertFalse(self.is_experimental(deprecated_function))
145+
146+
def test_experimental_function(self) -> None:
147+
self.assertFalse(self.is_deprecated(experimental_function))
148+
self.assertTrue(
149+
self.is_experimental(experimental_function, "experimental_function message")
150+
)
151+
152+
def test_undecorated_method(self) -> None:
153+
tc = TestClass()
154+
self.assertFalse(self.is_deprecated(tc.undecorated_method))
155+
self.assertFalse(self.is_experimental(tc.undecorated_method))
156+
157+
def test_deprecated_method(self) -> None:
158+
tc = TestClass()
159+
self.assertTrue(
160+
self.is_deprecated(tc.deprecated_method, "deprecated_method message")
161+
)
162+
self.assertFalse(self.is_experimental(tc.deprecated_method))
163+
164+
def test_experimental_method(self) -> None:
165+
tc = TestClass()
166+
self.assertFalse(self.is_deprecated(tc.experimental_method))
167+
self.assertTrue(
168+
self.is_experimental(tc.experimental_method, "experimental_method message")
169+
)

install_requirements.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ TORCH_NIGHTLY_URL="https://download.pytorch.org/whl/nightly/cpu"
125125
EXIR_REQUIREMENTS=(
126126
torch=="2.5.0.${NIGHTLY_VERSION}"
127127
torchvision=="0.20.0.${NIGHTLY_VERSION}" # For testing.
128+
typing-extensions
128129
)
129130

130131
# pip packages needed for development.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ dependencies=[
6868
"ruamel.yaml",
6969
"sympy",
7070
"tabulate",
71+
"typing-extensions",
7172
]
7273

7374
[project.urls]

0 commit comments

Comments
 (0)