This is an automated email from the ASF dual-hosted git repository.

dianfu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 75892ba1214 [FLINK-40529][python] Warn on use of deprecated APIs not 
at import time (#29061)
75892ba1214 is described below

commit 75892ba1214137a3dd18fefe719244616d813a6c
Author: Deepyaman Datta <[email protected]>
AuthorDate: Wed Sep 9 23:52:21 2026 -0600

    [FLINK-40529][python] Warn on use of deprecated APIs not at import time 
(#29061)
    
    Generated-by: Claude Code 2.1.252 (Claude Opus 5)
---
 flink-python/dev/integration_test.sh               |   3 +
 flink-python/pyflink/table/table.py                |  14 +-
 .../pyflink/util/api_stability_decorators.py       |  78 +++-
 flink-python/pyflink/util/tests/__init__.py        |  17 +
 .../util/tests/test_api_stability_decorators.py    | 443 +++++++++++++++++++++
 flink-python/pyproject.toml                        |   2 +
 flink-python/setup.py                              |   3 +
 7 files changed, 543 insertions(+), 17 deletions(-)

diff --git a/flink-python/dev/integration_test.sh 
b/flink-python/dev/integration_test.sh
index ba7ceb47aea..7bd9f34340c 100755
--- a/flink-python/dev/integration_test.sh
+++ b/flink-python/dev/integration_test.sh
@@ -42,6 +42,9 @@ function test_all_modules() {
 
     # test table module
     test_module "table"
+
+    # test util module
+    test_module "util"
 }
 
 # CURRENT_DIR is "flink/flink-python/dev/"
diff --git a/flink-python/pyflink/table/table.py 
b/flink-python/pyflink/table/table.py
index e7898e8752d..820600efd63 100644
--- a/flink-python/pyflink/table/table.py
+++ b/flink-python/pyflink/table/table.py
@@ -116,10 +116,11 @@ class Table(object):
 
             >>> tab.select(tab.a)
         """
-        if name not in self.get_schema().get_field_names():
+        column_names = self.get_resolved_schema().get_column_names()
+        if name not in column_names:
             raise AttributeError(
                 "The current table has no column named '%s', available 
columns: [%s]"
-                % (name, ', '.join(self.get_schema().get_field_names())))
+                % (name, ', '.join(column_names)))
         return col(name)
 
     def select(self, *fields: Expression) -> 'Table':
@@ -943,27 +944,26 @@ class Table(object):
             
.get(gateway.jvm.org.apache.flink.python.PythonOptions.MAX_ARROW_BATCH_SIZE)
         batches_iterator = 
gateway.jvm.org.apache.flink.table.runtime.arrow.ArrowUtils\
             .collectAsPandasDataFrame(self._j_table, max_arrow_batch_size)
+        schema = self.get_schema()
         if batches_iterator.hasNext():
             import pytz
             timezone = pytz.timezone(
                 
self._j_table.getTableEnvironment().getConfig().getLocalTimeZone().getId())
             serializer = ArrowSerializer(
-                create_arrow_schema(self.get_schema().get_field_names(),
-                                    self.get_schema().get_field_data_types()),
-                self.get_schema().to_row_data_type(),
+                create_arrow_schema(schema.get_field_names(), 
schema.get_field_data_types()),
+                schema.to_row_data_type(),
                 timezone)
             import pyarrow as pa
             table = 
pa.Table.from_batches(serializer.load_from_iterator(batches_iterator))
             pdf = table.to_pandas()
 
-            schema = self.get_schema()
             for field_name in schema.get_field_names():
                 pdf[field_name] = tz_convert_from_internal(
                     pdf[field_name], schema.get_field_data_type(field_name), 
timezone)
             return pdf
         else:
             import pandas as pd
-            return pd.DataFrame.from_records([], 
columns=self.get_schema().get_field_names())
+            return pd.DataFrame.from_records([], 
columns=schema.get_field_names())
 
     @Deprecated(since="2.1.0", detail="Use :func:`Table.get_resolved_schema` 
instead.")
     def get_schema(self) -> TableSchema:
diff --git a/flink-python/pyflink/util/api_stability_decorators.py 
b/flink-python/pyflink/util/api_stability_decorators.py
index abfa508cd4a..ba5b568b43f 100644
--- a/flink-python/pyflink/util/api_stability_decorators.py
+++ b/flink-python/pyflink/util/api_stability_decorators.py
@@ -16,11 +16,12 @@
 # limitations under the License.
 
################################################################################
 
+import functools
 from inspect import getmembers, isfunction, isclass
-from typing import TypeVar, Callable, Any, Union, Type, Optional
+from typing import TypeVar, Callable, Any, Union, Type, Optional, cast
 from abc import ABCMeta, abstractmethod
 import warnings
-from typing_extensions import override
+from typing_extensions import deprecated, override
 from textwrap import dedent, indent
 
 __all__ = ["Deprecated", "Experimental", "Internal", "PublicEvolving", 
"Public"]
@@ -84,7 +85,12 @@ class BaseAPIStabilityDecorator(metaclass=ABCMeta):
             stability_decorators = getattr(func_or_cls, 
'__stability_decorators')
             stability_decorators.add(self.__class__)
         else:
-            setattr(func_or_cls, '__stability_decorators', {self.__class__})
+            # A property rejects attribute assignment, so it goes unrecorded 
rather
+            # than failing the import.
+            try:
+                setattr(func_or_cls, '__stability_decorators', 
{self.__class__})
+            except (AttributeError, TypeError):
+                pass
 
         if isclass(func_or_cls):
             for name, method in getmembers(
@@ -126,20 +132,72 @@ class Deprecated(BaseAPIStabilityDecorator):
         self.detail = detail
 
     def get_directive(self, func_or_cls: T) -> str:
-        return f".. deprecated:: {self.since}\n{indent(dedent(self.detail), '  
 ')}"
+        directive = f".. deprecated:: {self.since}"
+        if self.detail is not None:
+            directive = f"{directive}\n{indent(dedent(self.detail), '   ')}"
+        return directive
 
-    @override
-    def __call__(self, func_or_cls: T) -> T:
+    def _get_message(self, func_or_cls: T) -> str:
         """
-        Emit a warning on the deprecation of the given function/class. Then 
call the base class
-        for docstring modification.
+        Returns the warning message emitted when the deprecated API element is 
used.
         """
         msg = f"{func_or_cls.__qualname__} has been deprecated since version 
{self.since}."
         if self.detail is not None:
             msg = f"{msg} {self.detail}"
+        return msg
+
+    @override
+    def __call__(self, func_or_cls: T) -> T:
+        """
+        Arranges for a :class:`DeprecationWarning` to be emitted when the 
decorated API
+        element is *used*, and calls the base class for docstring modification.
+
+        The warning cannot be emitted here: this runs while the module 
defining the API is
+        being imported, so it would warn every user who imports PyFlink and 
never the ones
+        who use the deprecated API.
+        """
+        # typing_extensions.deprecated rejects a classmethod, and turns a 
staticmethod into
+        # a plain function that breaks when called on an instance.
+        if isinstance(func_or_cls, (staticmethod, classmethod)):
+            return cast(T, type(func_or_cls)(self(func_or_cls.__func__)))
+
+        func_or_cls = super().__call__(func_or_cls)
 
-        warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
-        return super().__call__(func_or_cls)
+        if isclass(func_or_cls):
+            self._deprecate_class(func_or_cls)
+        elif isfunction(func_or_cls):
+            return cast(T, 
deprecated(self._get_message(func_or_cls))(func_or_cls))
+
+        # A property is neither, and typing_extensions.deprecated rejects it, 
so it keeps
+        # the docstring directive alone.
+        return func_or_cls
+
+    def _deprecate_class(self, cls: Type[Any]) -> None:
+        """
+        Wraps the __init__ of the given class so that instantiating it warns, 
leaving the
+        class object itself in place so that isinstance checks and subclassing 
keep working.
+
+        typing_extensions.deprecated is not used here: following PEP 702 it 
also warns when
+        a deprecated class is subclassed, and PyFlink subclasses its own 
deprecated classes
+        at module level, so that would warn on import.
+        """
+        msg = self._get_message(cls)
+        original_init = cls.__init__
+
+        @functools.wraps(original_init)
+        def __init__(self: Any, *args: Any, **kwargs: Any) -> None:
+            # As in PEP 702, only the deprecated class itself warns, never a 
subclass,
+            # which may not be deprecated and would otherwise warn twice.
+            if type(self) is cls:
+                warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
+            if original_init is object.__init__ and (args or kwargs) \
+                    and type(self).__new__ is object.__new__:
+                # object.__new__ raises this itself for a class that defines 
neither
+                # __new__ nor __init__; the __init__ installed here would mask 
it.
+                raise TypeError(f"{type(self).__name__}() takes no arguments")
+            original_init(self, *args, **kwargs)
+
+        cls.__init__ = __init__  # type: ignore[misc]
 
 
 class Experimental(BaseAPIStabilityDecorator):
diff --git a/flink-python/pyflink/util/tests/__init__.py 
b/flink-python/pyflink/util/tests/__init__.py
new file mode 100644
index 00000000000..65b48d4d79b
--- /dev/null
+++ b/flink-python/pyflink/util/tests/__init__.py
@@ -0,0 +1,17 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
diff --git a/flink-python/pyflink/util/tests/test_api_stability_decorators.py 
b/flink-python/pyflink/util/tests/test_api_stability_decorators.py
new file mode 100644
index 00000000000..23cf1669d06
--- /dev/null
+++ b/flink-python/pyflink/util/tests/test_api_stability_decorators.py
@@ -0,0 +1,443 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+import abc
+import enum
+import inspect
+import os
+import subprocess
+import sys
+import textwrap
+import unittest
+import warnings
+
+from pyflink.util.api_stability_decorators import (
+    Deprecated,
+    Experimental,
+    Internal,
+    Public,
+    PublicEvolving,
+)
+
+
+class DeprecatedTests(unittest.TestCase):
+    """
+    Tests for the :class:`Deprecated` decorator, which must warn when a 
deprecated API is
+    used, and not when it is defined.
+
+    Blocks that must not warn turn warnings into errors, so that one fails 
where it is
+    raised rather than in a comparison afterwards.
+    """
+
+    def test_decoration_does_not_warn(self):
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+
+            @Deprecated(since="1.0.0", detail="Use :func:`new_func` instead.")
+            def func():
+                pass
+
+            @Deprecated(since="1.0.0")
+            class Cls:
+                def __init__(self):
+                    pass
+
+    def test_importing_pyflink_table_does_not_warn(self):
+        # A fresh interpreter is the only way to observe an import: 
pyflink.table is
+        # already in sys.modules here, so importing it again is a no-op. Only 
this
+        # decorator's own warnings are inspected, so third-party noise cannot 
fail it.
+        script = textwrap.dedent(
+            """
+            import warnings
+
+            with warnings.catch_warnings(record=True) as caught:
+                warnings.simplefilter("always")
+                import pyflink.table
+                import pyflink.table.descriptors
+
+            print([str(warning.message) for warning in caught
+                   if "has been deprecated since version" in 
str(warning.message)])
+            """
+        )
+        result = subprocess.run(
+            [sys.executable, "-c", script], capture_output=True, text=True
+        )
+
+        self.assertEqual(0, result.returncode, result.stderr)
+        self.assertEqual("[]", result.stdout.strip())
+
+    def test_function_warns_when_called(self):
+        @Deprecated(since="1.0.0", detail="Use :func:`new_func` instead.")
+        def func(a, b=2):
+            return a + b
+
+        with self.assertWarns(DeprecationWarning) as caught:
+            self.assertEqual(3, func(1))
+
+        self.assertEqual(
+            f"{func.__qualname__} has been deprecated since version 1.0.0. "
+            f"Use :func:`new_func` instead.",
+            str(caught.warning),
+        )
+
+    def test_function_without_detail_warns_when_called(self):
+        @Deprecated(since="1.0.0")
+        def func():
+            pass
+
+        with self.assertWarns(DeprecationWarning) as caught:
+            func()
+
+        self.assertEqual(
+            f"{func.__qualname__} has been deprecated since version 1.0.0.",
+            str(caught.warning),
+        )
+
+    def test_function_warns_once_per_call(self):
+        @Deprecated(since="1.0.0")
+        def func():
+            pass
+
+        with warnings.catch_warnings(record=True) as caught:
+            warnings.simplefilter("always")
+            func()
+
+        self.assertEqual(1, len(caught))
+
+    def test_function_wrapper_preserves_metadata(self):
+        @Deprecated(since="1.0.0")
+        def func():
+            """Some documentation."""
+
+        self.assertEqual("func", func.__name__)
+        self.assertEqual(
+            
"DeprecatedTests.test_function_wrapper_preserves_metadata.<locals>.func",
+            func.__qualname__,
+        )
+        self.assertIn("Some documentation.", func.__doc__)
+
+    def test_class_warns_when_instantiated(self):
+        @Deprecated(since="1.0.0", detail="Use :class:`NewClass` instead.")
+        class Cls:
+            def __init__(self, x):
+                self.x = x
+
+        with self.assertWarns(DeprecationWarning) as caught:
+            instance = Cls(1)
+
+        self.assertEqual(1, instance.x)
+        self.assertEqual(
+            f"{Cls.__qualname__} has been deprecated since version 1.0.0. "
+            f"Use :class:`NewClass` instead.",
+            str(caught.warning),
+        )
+
+    def test_class_warns_once_per_instantiation(self):
+        @Deprecated(since="1.0.0")
+        class Cls:
+            def __init__(self):
+                pass
+
+        with warnings.catch_warnings(record=True) as caught:
+            warnings.simplefilter("always")
+            Cls()
+
+        self.assertEqual(1, len(caught))
+
+    def test_class_without_own_init_warns_when_instantiated(self):
+        @Deprecated(since="1.0.0")
+        class Cls:
+            pass
+
+        with self.assertWarns(DeprecationWarning) as caught:
+            Cls()
+
+        self.assertEqual(
+            f"{Cls.__qualname__} has been deprecated since version 1.0.0.",
+            str(caught.warning),
+        )
+
+        # Warning about the class must not swallow the error an argument would 
have raised.
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            with self.assertRaises(TypeError):
+                Cls(1)
+
+    def test_class_is_returned_unchanged(self):
+        @Deprecated(since="1.0.0")
+        class Cls:
+            def __init__(self):
+                self.x = 1
+
+        class Subclass(Cls):
+            pass
+
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+            instance = Subclass()
+
+        self.assertIsInstance(instance, Cls)
+        self.assertTrue(issubclass(Subclass, Cls))
+        self.assertEqual("Cls", Cls.__name__)
+
+    def test_subclass_of_deprecated_class_does_not_warn(self):
+        # As in PEP 702: deprecating a class says nothing about its 
subclasses, which
+        # are the ones users are typically pointed at.
+        @Deprecated(since="1.0.0")
+        class Cls:
+            def __init__(self):
+                pass
+
+        class Subclass(Cls):
+            pass
+
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+            Subclass()
+
+    def test_defining_a_subclass_does_not_warn(self):
+        # PEP 702 also warns when a deprecated class is subclassed. PyFlink 
subclasses
+        # its own deprecated classes at module level, so that would warn on 
import.
+        @Deprecated(since="1.0.0")
+        class Cls:
+            def __init__(self):
+                pass
+
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+
+            class Subclass(Cls):
+                pass
+
+    def test_deprecated_subclass_inheriting_init_warns_once(self):
+        @Deprecated(since="1.0.0")
+        class Cls:
+            def __init__(self):
+                pass
+
+        @Deprecated(since="2.0.0")
+        class Subclass(Cls):
+            pass
+
+        with warnings.catch_warnings(record=True) as caught:
+            warnings.simplefilter("always")
+            Subclass()
+
+        self.assertEqual(
+            [f"{Subclass.__qualname__} has been deprecated since version 
2.0.0."],
+            [str(warning.message) for warning in caught],
+        )
+
+    def test_function_warning_points_at_the_caller(self):
+        @Deprecated(since="1.0.0")
+        def func():
+            pass
+
+        with self.assertWarns(DeprecationWarning) as caught:
+            lineno = inspect.currentframe().f_lineno + 1
+            func()
+
+        self.assertEqual(os.path.abspath(__file__), 
os.path.abspath(caught.filename))
+        self.assertEqual(lineno, caught.lineno)
+
+    def test_class_warning_points_at_the_caller(self):
+        @Deprecated(since="1.0.0")
+        class Cls:
+            def __init__(self):
+                pass
+
+        with self.assertWarns(DeprecationWarning) as caught:
+            lineno = inspect.currentframe().f_lineno + 1
+            Cls()
+
+        self.assertEqual(os.path.abspath(__file__), 
os.path.abspath(caught.filename))
+        self.assertEqual(lineno, caught.lineno)
+
+    def test_docstring_directives_are_still_applied(self):
+        @Deprecated(since="1.0.0", detail="Use :func:`new_func` instead.")
+        def func():
+            """Function documentation."""
+
+        @Deprecated(since="1.0.0")
+        class Cls:
+            """Class documentation."""
+
+            def method(self):
+                """Method documentation."""
+
+        self.assertEqual(
+            "Function documentation.\n.. deprecated:: 1.0.0\n   Use 
:func:`new_func` instead.",
+            func.__doc__,
+        )
+        self.assertEqual("Class documentation.\n.. deprecated:: 1.0.0", 
Cls.__doc__)
+        self.assertEqual("Method documentation.\n.. deprecated:: 1.0.0", 
Cls.method.__doc__)
+
+    def test_stability_decorators_attribute_is_still_populated(self):
+        @Deprecated(since="1.0.0")
+        def func():
+            pass
+
+        @Deprecated(since="1.0.0")
+        @PublicEvolving()
+        class Cls:
+            def __init__(self):
+                pass
+
+        self.assertEqual({Deprecated}, getattr(func, "__stability_decorators"))
+        self.assertEqual({Deprecated, PublicEvolving}, getattr(Cls, 
"__stability_decorators"))
+
+    def test_static_and_class_methods(self):
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+
+            class Cls:
+                @Deprecated(since="1.0.0")
+                @staticmethod
+                def static_method(x):
+                    """Static method documentation."""
+                    return x
+
+                @Deprecated(since="1.0.0")
+                @classmethod
+                def class_method(cls, x):
+                    return x
+
+        # The descriptors must survive: a plain function wrapper around a 
staticmethod
+        # breaks when called on an instance.
+        self.assertIsInstance(Cls.__dict__["static_method"], staticmethod)
+        self.assertIsInstance(Cls.__dict__["class_method"], classmethod)
+        self.assertIn(".. deprecated:: 1.0.0", Cls.static_method.__doc__)
+
+        with warnings.catch_warnings(record=True) as caught:
+            warnings.simplefilter("always")
+            self.assertEqual(1, Cls.static_method(1))
+            self.assertEqual(2, Cls.class_method(2))
+            self.assertEqual(3, Cls().static_method(3))
+            self.assertEqual(4, Cls().class_method(4))
+
+        expected = [
+            f"{Cls.__qualname__}.static_method has been deprecated since 
version 1.0.0.",
+            f"{Cls.__qualname__}.class_method has been deprecated since 
version 1.0.0.",
+        ]
+        # Once through the class, once through an instance.
+        self.assertEqual(expected * 2, [str(warning.message) for warning in 
caught])
+
+    def test_property(self):
+        # A property only gets the docstring directive, but must not raise.
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+
+            class Cls:
+                @Deprecated(since="1.0.0")
+                @property
+                def value(self):
+                    """Property documentation."""
+                    return 1
+
+            self.assertEqual(1, Cls().value)
+
+        self.assertIn(".. deprecated:: 1.0.0", Cls.__dict__["value"].__doc__)
+
+    def test_abstract_class(self):
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+
+            @Deprecated(since="1.0.0")
+            class Abstract(abc.ABC):
+                """Abstract class documentation."""
+
+                @abc.abstractmethod
+                def method(self):
+                    pass
+
+            with self.assertRaises(TypeError):
+                Abstract()
+
+        self.assertIn(".. deprecated:: 1.0.0", Abstract.__doc__)
+
+    def test_enum_class(self):
+        # Members are created before the decorator runs, so they are 
unaffected;
+        # decorating an Enum must not raise and must leave lookup working.
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+
+            @Deprecated(since="1.0.0")
+            class Colour(enum.Enum):
+                """Enum documentation."""
+
+                RED = 1
+
+            self.assertIs(Colour.RED, Colour(1))
+            self.assertEqual(1, Colour.RED.value)
+            self.assertEqual([Colour.RED], list(Colour))
+
+        self.assertIn(".. deprecated:: 1.0.0", Colour.__doc__)
+
+
+class OtherStabilityDecoratorTests(unittest.TestCase):
+    """
+    Tests that the decorators other than :class:`Deprecated` document without 
warning.
+    """
+
+    def test_decorators_never_warn(self):
+        for decorator in (Experimental, Internal, Public, PublicEvolving):
+            with self.subTest(decorator=decorator.__name__):
+                with warnings.catch_warnings():
+                    warnings.simplefilter("error")
+
+                    @decorator()
+                    def func():
+                        """Function documentation."""
+
+                    @decorator()
+                    class Cls:
+                        """Class documentation."""
+
+                        def method(self):
+                            """Method documentation."""
+
+                    func()
+                    Cls().method()
+
+    def test_decorated_elements_are_returned_unchanged(self):
+        for decorator in (Experimental, Internal, Public, PublicEvolving):
+            with self.subTest(decorator=decorator.__name__):
+
+                def func():
+                    pass
+
+                class Cls:
+                    pass
+
+                self.assertIs(func, decorator()(func))
+                self.assertIs(Cls, decorator()(Cls))
+
+    def test_docstring_directives_and_attribute(self):
+        @Public()
+        class Cls:
+            """Class documentation."""
+
+            def method(self):
+                """Method documentation."""
+
+        self.assertIn("is marked as **public**", Cls.__doc__)
+        self.assertIn("is marked as **public**", Cls.method.__doc__)
+        self.assertEqual({Public}, getattr(Cls, "__stability_decorators"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/flink-python/pyproject.toml b/flink-python/pyproject.toml
index 9b334e2cbb9..f8f9c867c79 100644
--- a/flink-python/pyproject.toml
+++ b/flink-python/pyproject.toml
@@ -51,6 +51,8 @@ dev = [
   "protobuf>=6.31.1,<7.0.0.dev0",
   "pytest~=8.0",
   "ruamel.yaml>=0.18.4",
+  "typing-extensions>=4.5.0; python_version < '3.12'",
+  "typing-extensions>=4.7.0; python_version >= '3.12'",
 ]
 tox = [
     "tox==3.14.0"
diff --git a/flink-python/setup.py b/flink-python/setup.py
index 92394e5e8ff..d224f6c05f2 100644
--- a/flink-python/setup.py
+++ b/flink-python/setup.py
@@ -330,6 +330,9 @@ try:
                         'pemja>=0.5.7,<0.5.8;platform_system != "Windows"',
                         'httplib2>=0.19.0',
                         'ruamel.yaml>=0.18.4',
+                        # deprecated() landed in 4.5.0; 4.7.0 declares 3.12 
support.
+                        'typing-extensions>=4.5.0;python_version < "3.12"',
+                        'typing-extensions>=4.7.0;python_version >= "3.12"',
                         apache_flink_libraries_dependency]
 
     setup(

Reply via email to