Skip to content

Commit 590b2e4

Browse files
committed
types: add Python type hints to public API + low-risk internals
Adds annotations via PEP 563 (from __future__ import annotations) so all annotations are strings at runtime — zero overhead, zero introspection surprises for PySpark or any other consumer. Annotated: * java_gateway.py public API: GatewayParameters / CallbackServerParameters / JavaGateway __init__ signatures; launch_gateway; java_import; get_field / set_field; is_instance_of; exception class constructors (Py4JError, Py4JNetworkError, Py4JAuthenticationError, Py4JJavaError). * protocol.py: encode/decode function signatures (escape_new_line, unescape_new_line, smart_decode, encode_float, encode_bytearray, decode_bytearray, is_python_proxy, get_command_part, get_return_value) plus is_error, is_fatal_error. * signals.py: Signal.connect / disconnect / send. * finalizer.py: ThreadSafeFinalizer.add_finalizer / remove_finalizer / clear_finalizers. Deliberately NOT annotated: * JavaObject / JavaMember / JavaClass / JavaPackage / JVMView — attribute access resolves dynamically via __getattr__ + Java reflection; annotating would either lie about specific types or be pure Any. Honest stance: leave dynamic dispatch unannotated. * java_collections.py (JavaList/JavaMap/JavaSet) — same dynamic dispatch pattern. * GatewayClient / CallbackServer internal methods that touch JavaObject — same reason. mypy is not gated in CI for this PR (deferred per spec); annotations are added for IDE / static-analysis benefit. A future discussion on style / lint tooling (likely covering ruff + black + mypy together) will revisit CI gating. Co-authored-by: Isaac
1 parent 3099007 commit 590b2e4

4 files changed

Lines changed: 89 additions & 47 deletions

File tree

‎py4j-python/src/py4j/finalizer.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
77
:author: Barthelemy Dagenais
88
"""
9+
from __future__ import annotations
10+
911
from threading import RLock
12+
from typing import Any
1013

1114

1215
class ThreadSafeFinalizer(object):
@@ -28,7 +31,7 @@ class ThreadSafeFinalizer(object):
2831
lock = RLock()
2932

3033
@classmethod
31-
def add_finalizer(cls, id, weak_ref):
34+
def add_finalizer(cls, id: Any, weak_ref: Any) -> None:
3235
"""Registers a finalizer with an id.
3336
3437
:param id: The id of the object referenced by the weak reference.
@@ -38,7 +41,7 @@ def add_finalizer(cls, id, weak_ref):
3841
cls.finalizers[id] = weak_ref
3942

4043
@classmethod
41-
def remove_finalizer(cls, id):
44+
def remove_finalizer(cls, id: Any) -> None:
4245
"""Removes a finalizer associated with this id.
4346
4447
:param id: The id of the object for which the finalizer will be
@@ -48,7 +51,7 @@ def remove_finalizer(cls, id):
4851
cls.finalizers.pop(id, None)
4952

5053
@classmethod
51-
def clear_finalizers(cls, clear_all=False):
54+
def clear_finalizers(cls, clear_all: bool = False) -> None:
5255
"""Removes all registered finalizers.
5356
5457
:param clear_all: If `True`, all finalizers are deleted. Otherwise,

‎py4j-python/src/py4j/java_gateway.py‎

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@
99
1010
:author: Barthelemy Dagenais
1111
"""
12+
from __future__ import annotations
13+
1214
from collections import deque
1315
import logging
1416
import os
1517
from pydoc import pager
1618
from queue import Queue
1719
import select
1820
import socket
21+
import ssl
1922
import struct
2023
from subprocess import Popen, PIPE
2124
import subprocess
2225
import sys
2326
import traceback
2427
from threading import Thread, RLock
28+
from typing import Any
2529
import weakref
2630

2731
from py4j.compat import hasattr2
@@ -190,7 +194,7 @@ def deprecated(name, last_version, use_instead="", level=logging.DEBUG,
190194
raise DeprecationWarning(msg)
191195

192196

193-
def java_import(jvm_view, import_str):
197+
def java_import(jvm_view: Any, import_str: str) -> None:
194198
"""Imports the package or class specified by `import_str` in the
195199
jvm view namespace.
196200
@@ -242,12 +246,21 @@ def find_jar_path():
242246
return ""
243247

244248

245-
def launch_gateway(port=0, jarpath="", classpath="", javaopts=[],
246-
die_on_exit=False, redirect_stdout=None,
247-
redirect_stderr=None, daemonize_redirect=True,
248-
java_path="java", create_new_process_group=False,
249-
enable_auth=False, cwd=None, return_proc=False,
250-
use_shell=False):
249+
def launch_gateway(
250+
port: int = 0,
251+
jarpath: str = "",
252+
classpath: str = "",
253+
javaopts: list[str] = [],
254+
die_on_exit: bool = False,
255+
redirect_stdout: Any = None,
256+
redirect_stderr: Any = None,
257+
daemonize_redirect: bool = True,
258+
java_path: str = "java",
259+
create_new_process_group: bool = False,
260+
enable_auth: bool = False,
261+
cwd: str | None = None,
262+
return_proc: bool = False,
263+
use_shell: bool = False) -> int | tuple[int, Popen] | tuple[int, str] | tuple[int, str, Popen]:
251264
"""Launch a `Gateway` in a new Java process.
252265
253266
The redirect parameters accept file-like objects, Queue, or deque. When
@@ -397,7 +410,7 @@ def launch_gateway(port=0, jarpath="", classpath="", javaopts=[],
397410
return output
398411

399412

400-
def get_field(java_object, field_name):
413+
def get_field(java_object: Any, field_name: str) -> Any:
401414
"""Retrieves the field named `field_name` from the `java_object`.
402415
403416
This function is useful when `auto_field=false` in a gateway or
@@ -422,7 +435,7 @@ def get_field(java_object, field_name):
422435
field_name)
423436

424437

425-
def set_field(java_object, field_name, value):
438+
def set_field(java_object: Any, field_name: str, value: Any) -> None:
426439
"""Sets the field named `field_name` of `java_object` to `value`.
427440
428441
This function is the only way to set a field because the assignment
@@ -468,7 +481,7 @@ def get_method(java_object, method_name):
468481
java_object._gateway_client)
469482

470483

471-
def is_instance_of(gateway, java_object, java_class):
484+
def is_instance_of(gateway: JavaGateway, java_object: Any, java_class: Any) -> bool:
472485
"""Indicates whether a java object is an instance of the provided
473486
java_class.
474487
@@ -779,10 +792,17 @@ class GatewayParameters(object):
779792
"""
780793

781794
def __init__(
782-
self, address=DEFAULT_ADDRESS, port=DEFAULT_PORT, auto_field=False,
783-
auto_close=True, auto_convert=False, eager_load=False,
784-
ssl_context=None, enable_memory_management=True,
785-
read_timeout=None, auth_token=None):
795+
self,
796+
address: str = DEFAULT_ADDRESS,
797+
port: int = DEFAULT_PORT,
798+
auto_field: bool = False,
799+
auto_close: bool = True,
800+
auto_convert: bool = False,
801+
eager_load: bool = False,
802+
ssl_context: ssl.SSLContext | None = None,
803+
enable_memory_management: bool = True,
804+
read_timeout: float | None = None,
805+
auth_token: str | None = None) -> None:
786806
"""
787807
:param address: the address to which the client will request a
788808
connection. If you're assing a `SSLContext` with
@@ -842,12 +862,17 @@ class CallbackServerParameters(object):
842862
"""
843863

844864
def __init__(
845-
self, address=DEFAULT_ADDRESS, port=DEFAULT_PYTHON_PROXY_PORT,
846-
daemonize=False, daemonize_connections=False, eager_load=True,
847-
ssl_context=None,
848-
accept_timeout=DEFAULT_ACCEPT_TIMEOUT_PLACEHOLDER,
849-
read_timeout=None, propagate_java_exceptions=False,
850-
auth_token=None):
865+
self,
866+
address: str = DEFAULT_ADDRESS,
867+
port: int = DEFAULT_PYTHON_PROXY_PORT,
868+
daemonize: bool = False,
869+
daemonize_connections: bool = False,
870+
eager_load: bool = True,
871+
ssl_context: ssl.SSLContext | None = None,
872+
accept_timeout: float | str = DEFAULT_ACCEPT_TIMEOUT_PLACEHOLDER,
873+
read_timeout: float | None = None,
874+
propagate_java_exceptions: bool = False,
875+
auth_token: str | None = None) -> None:
851876
"""
852877
:param address: the address to which the client will request a
853878
connection
@@ -1843,12 +1868,17 @@ class JavaGateway(object):
18431868
"""
18441869

18451870
def __init__(
1846-
self, gateway_client=None, auto_field=False,
1847-
python_proxy_port=DEFAULT_PYTHON_PROXY_PORT,
1848-
start_callback_server=False, auto_convert=False, eager_load=False,
1849-
gateway_parameters=None, callback_server_parameters=None,
1850-
python_server_entry_point=None,
1851-
java_process=None):
1871+
self,
1872+
gateway_client: GatewayClient | None = None,
1873+
auto_field: bool = False,
1874+
python_proxy_port: int = DEFAULT_PYTHON_PROXY_PORT,
1875+
start_callback_server: bool = False,
1876+
auto_convert: bool = False,
1877+
eager_load: bool = False,
1878+
gateway_parameters: GatewayParameters | None = None,
1879+
callback_server_parameters: CallbackServerParameters | None = None,
1880+
python_server_entry_point: Any = None,
1881+
java_process: Popen | None = None) -> None:
18521882
"""
18531883
:param gateway_parameters: An instance of `GatewayParameters` used to
18541884
configure the various options of the gateway.

‎py4j-python/src/py4j/protocol.py‎

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616
1717
:author: Barthelemy Dagenais
1818
"""
19+
from __future__ import annotations
20+
1921
from base64 import standard_b64encode, standard_b64decode
2022

2123
from decimal import Decimal
24+
from typing import Any, TYPE_CHECKING
25+
26+
if TYPE_CHECKING:
27+
from py4j.java_gateway import PythonProxyPool
2228

2329

2430
JAVA_MAX_INT = 2147483647
@@ -168,7 +174,7 @@
168174
EMPTY_RESPONSE = "empty_response"
169175

170176

171-
def escape_new_line(original):
177+
def escape_new_line(original: str | bytes) -> str:
172178
"""Replaces new line characters by a backslash followed by a n.
173179
174180
Backslashes are also escaped by another backslash.
@@ -197,7 +203,7 @@ def escape_new_line(original):
197203
return original
198204

199205

200-
def unescape_new_line(escaped):
206+
def unescape_new_line(escaped: str) -> str:
201207
"""Replaces escaped characters by unescaped characters.
202208
203209
For example, double backslashes are replaced by a single backslash.
@@ -218,7 +224,7 @@ def unescape_new_line(escaped):
218224
return escaped
219225

220226

221-
def smart_decode(s):
227+
def smart_decode(s: bytes | str) -> str:
222228
if isinstance(s, str):
223229
return s
224230
elif isinstance(s, bytes):
@@ -227,7 +233,7 @@ def smart_decode(s):
227233
return str(s)
228234

229235

230-
def encode_float(float_value):
236+
def encode_float(float_value: float) -> str:
231237
# str(float) on Python 3 already returns the same shortest-
232238
# roundtrip repr that smart_decode(repr(...)) was producing on
233239
# py2; smart_decode here was a no-op dispatcher.
@@ -241,15 +247,15 @@ def encode_float(float_value):
241247
return float_str
242248

243249

244-
def encode_bytearray(barray):
250+
def encode_bytearray(barray: bytes | bytearray) -> str:
245251
if isinstance(barray, bytes):
246252
return str(standard_b64encode(barray), encoding="ascii")
247253
else:
248254
newbytestr = bytes(barray)
249255
return str(standard_b64encode(newbytestr), encoding="ascii")
250256

251257

252-
def decode_bytearray(encoded):
258+
def decode_bytearray(encoded: str) -> bytes:
253259
# Per @PaperTsar's analysis in issue #570: the prior
254260
# implementation built a Python list of ints (one PyObject per
255261
# byte) then reconstructed bytes from that list — pure overhead
@@ -260,7 +266,7 @@ def decode_bytearray(encoded):
260266
return bytes(standard_b64decode(encoded.encode("ascii")))
261267

262268

263-
def is_python_proxy(parameter):
269+
def is_python_proxy(parameter: object) -> bool:
264270
"""Determines whether parameter is a Python Proxy, i.e., it has a Java
265271
internal class with an `implements` member.
266272
@@ -275,7 +281,7 @@ def is_python_proxy(parameter):
275281
return is_proxy
276282

277283

278-
def get_command_part(parameter, python_proxy_pool=None):
284+
def get_command_part(parameter: object, python_proxy_pool: PythonProxyPool | None = None) -> str:
279285
"""Converts a Python object into a string representation respecting the
280286
Py4J protocol.
281287
@@ -317,7 +323,7 @@ def get_command_part(parameter, python_proxy_pool=None):
317323
return command_part
318324

319325

320-
def get_return_value(answer, gateway_client, target_id=None, name=None):
326+
def get_return_value(answer: str, gateway_client: Any, target_id: str | None = None, name: str | None = None) -> Any:
321327
"""Converts an answer received from the Java gateway into a Python object.
322328
323329
For example, string representation of integers are converted to Python
@@ -399,14 +405,14 @@ def compute_exception_message(default_message, extra_message=None):
399405
return message
400406

401407

402-
def is_error(answer):
408+
def is_error(answer: str) -> tuple[bool, None]:
403409
if len(answer) == 0 or answer[0] != SUCCESS:
404410
return (True, None)
405411
else:
406412
return (False, None)
407413

408414

409-
def is_fatal_error(answer):
415+
def is_fatal_error(answer: str) -> bool:
410416
return answer and len(answer) > 0 and answer[0] == FATAL_ERROR
411417

412418

@@ -455,21 +461,21 @@ def register_input_converter(converter, prepend=False):
455461
class Py4JError(Exception):
456462
"""Exception raised when a problem occurs with Py4J."""
457463

458-
def __init__(self, args=None, cause=None):
464+
def __init__(self, args: str | None = None, cause: BaseException | None = None) -> None:
459465
super().__init__(args)
460466
self.cause = cause
461467

462468

463469
class Py4JAuthenticationError(Py4JError):
464470
"""Exception raised when Py4J cannot authenticate a connection."""
465-
def __init__(self, args=None, cause=None):
471+
def __init__(self, args: str | None = None, cause: BaseException | None = None) -> None:
466472
super().__init__(args)
467473
self.cause = cause
468474

469475

470476
class Py4JNetworkError(Py4JError):
471477
"""Exception raised when a network error occurs with Py4J."""
472-
def __init__(self, args=None, cause=None, when=None):
478+
def __init__(self, args: str | None = None, cause: BaseException | None = None, when: str | None = None) -> None:
473479
super().__init__(args)
474480
self.cause = cause
475481
self.when = when
@@ -486,7 +492,7 @@ class Py4JJavaError(Py4JError):
486492
487493
"""
488494

489-
def __init__(self, msg, java_exception):
495+
def __init__(self, msg: str, java_exception: Any) -> None:
490496
self.args = (msg, java_exception)
491497
self.errmsg = msg
492498
self.java_exception = java_exception

‎py4j-python/src/py4j/signals.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
The signals pattern is very similar to the listener/observer pattern.
44
55
"""
6+
from __future__ import annotations
7+
68
from inspect import ismethod
79
from threading import Lock
10+
from typing import Any, Callable
811

912

1013
def make_id(func):
@@ -33,7 +36,7 @@ def __init__(self):
3336
# number of receivers to be very small.
3437
self.receivers = []
3538

36-
def connect(self, receiver, sender=None, unique_id=None):
39+
def connect(self, receiver: Callable[..., Any], sender: object = None, unique_id: Any = None) -> None:
3740
"""Registers a receiver for this signal.
3841
3942
The receiver must be a callable (e.g., function or instance method)
@@ -58,7 +61,7 @@ def connect(self, receiver, sender=None, unique_id=None):
5861
else:
5962
self.receivers.append((full_id, receiver))
6063

61-
def disconnect(self, receiver, sender=None, unique_id=None):
64+
def disconnect(self, receiver: Callable[..., Any], sender: object = None, unique_id: Any = None) -> bool:
6265
"""Unregisters a receiver for this signal.
6366
6467
:param receiver: The callable that was registered to receive the
@@ -82,7 +85,7 @@ def disconnect(self, receiver, sender=None, unique_id=None):
8285

8386
return disconnected
8487

85-
def send(self, sender, **params):
88+
def send(self, sender: object, **params: Any) -> list[tuple[Callable[..., Any], Any]]:
8689
"""Sends the signal to all connected receivers.
8790
8891
If a receiver raises an error, the error is propagated back and

0 commit comments

Comments
 (0)