Skip to content

Commit 03aa8ca

Browse files
committed
Add Python type annotations
Check in pre-commit with mypy
1 parent 1769226 commit 03aa8ca

File tree

5 files changed

+56
-29
lines changed

5 files changed

+56
-29
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,10 @@ repos:
3434
# Run the formatter.
3535
- id: ruff-format
3636

37+
- repo: https://github.com/pre-commit/mirrors-mypy
38+
rev: v1.18.2
39+
hooks:
40+
- id: mypy
41+
3742
ci:
3843
autofix_prs: false

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,6 @@ ignore = [
4848
"E501", # Line too long
4949
"PLR0912", # Too many branches
5050
]
51+
52+
[tool.mypy]
53+
packages = ["find_libpython"]

src/find_libpython/__init__.py

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,23 @@
2525
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
2626
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2727
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28+
from __future__ import annotations
2829

2930
import argparse
3031
import ctypes
3132
import logging
3233
import os
3334
import sys
35+
from collections.abc import Iterable
3436
from ctypes.util import find_library as _find_library
3537
from functools import wraps
3638
from sysconfig import get_config_var as _get_config_var
39+
from typing import Any, Callable, ParamSpec, TypeVar
3740

3841
from find_libpython._version import __version__ # noqa: F401
3942

43+
T = TypeVar("T")
44+
4045
_logger = logging.getLogger("find_libpython")
4146

4247
_is_apple = sys.platform == "darwin"
@@ -57,7 +62,7 @@
5762
_SHLIB_SUFFIX = ".dylib"
5863

5964

60-
def _linked_libpython_unix(libpython):
65+
def _linked_libpython_unix(libpython: Any) -> str | None:
6166
if not hasattr(libpython, "Py_GetVersion"):
6267
return None
6368

@@ -83,7 +88,9 @@ class Dl_info(ctypes.Structure):
8388
return os.path.realpath(dlinfo.dli_fname.decode())
8489

8590

86-
def _library_name(name, suffix=_SHLIB_SUFFIX, _is_windows=_is_windows):
91+
def _library_name(
92+
name: str, suffix: str = _SHLIB_SUFFIX, _is_windows: bool = _is_windows
93+
) -> str:
8794
"""
8895
Convert a file basename `name` to a library name (no "lib" and ".so" etc.)
8996
@@ -103,12 +110,12 @@ def _library_name(name, suffix=_SHLIB_SUFFIX, _is_windows=_is_windows):
103110
return name
104111

105112

106-
def _append_truthy(list, item):
113+
def _append_truthy(list: list[T], item: T) -> None:
107114
if item:
108115
list.append(item)
109116

110117

111-
def _uniquifying(items):
118+
def _uniquifying(items: Iterable[str]) -> Iterable[str]:
112119
"""
113120
Yield items while excluding the duplicates and preserving the order.
114121
@@ -122,17 +129,20 @@ def _uniquifying(items):
122129
seen.add(x)
123130

124131

125-
def _uniquified(func):
132+
P = ParamSpec("P")
133+
134+
135+
def _uniquified(func: Callable[P, Iterable[str]]) -> Callable[P, Iterable[str]]:
126136
"""Wrap iterator returned from `func` by `_uniquifying`."""
127137

128138
@wraps(func)
129-
def wrapper(*args, **kwds):
139+
def wrapper(*args: P.args, **kwds: P.kwargs) -> Iterable[str]:
130140
return _uniquifying(func(*args, **kwds))
131141

132142
return wrapper
133143

134144

135-
def _get_proc_library():
145+
def _get_proc_library() -> Iterable[str]:
136146
pid = os.getpid()
137147
path = f"/proc/{pid}/maps"
138148
lines = open(path).readlines()
@@ -146,7 +156,7 @@ def _get_proc_library():
146156

147157

148158
@_uniquified
149-
def candidate_names(suffix=_SHLIB_SUFFIX):
159+
def candidate_names(suffix: str = _SHLIB_SUFFIX) -> Iterable[str]:
150160
"""
151161
Iterate over candidate file names of libpython.
152162
@@ -203,23 +213,23 @@ def candidate_names(suffix=_SHLIB_SUFFIX):
203213
yield dlprefix + stem + suffix
204214

205215

206-
def _linked_pythondll() -> str:
216+
def _linked_pythondll() -> str | None:
207217
# On Windows there is the `sys.dllhandle` attribute which is the
208218
# DLL Handle ID for the associated python.dll for the installation.
209219
# We can use the GetModuleFileName function to get the path to the
210220
# python.dll this way.
211221

212222
# sys.dllhandle is an module ID, which is just a void* cast to an integer,
213223
# we turn it back into a pointer for the ctypes call
214-
dll_hmodule = ctypes.cast(sys.dllhandle, ctypes.c_void_p)
224+
dll_hmodule = ctypes.cast(sys.dllhandle, ctypes.c_void_p) # type: ignore[attr-defined]
215225

216226
# create a buffer for the return path of the maximum length of filepaths in Windows unicode interfaces
217227
# https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
218228
path_return_buffer = ctypes.create_unicode_buffer(32768)
219229

220230
# GetModuleFileName sets the return buffer to the value of the path used to load the module.
221231
# We expect it to always be a normalized absolute path to python.dll.
222-
r = ctypes.windll.kernel32.GetModuleFileNameW(
232+
r = ctypes.windll.kernel32.GetModuleFileNameW( # type: ignore[attr-defined]
223233
dll_hmodule, path_return_buffer, len(path_return_buffer)
224234
)
225235

@@ -233,7 +243,7 @@ def _linked_pythondll() -> str:
233243

234244

235245
@_uniquified
236-
def candidate_paths(suffix=_SHLIB_SUFFIX):
246+
def candidate_paths(suffix: str = _SHLIB_SUFFIX) -> Iterable[str]:
237247
"""
238248
Iterate over candidate paths of libpython.
239249
@@ -245,10 +255,11 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
245255
"""
246256

247257
if _is_windows:
248-
yield _linked_pythondll()
258+
if (res := _linked_pythondll()) is not None:
259+
yield res
249260

250261
# List candidates for directories in which libpython may exist
251-
lib_dirs = []
262+
lib_dirs: list[str] = []
252263
_append_truthy(lib_dirs, _get_config_var("LIBPL"))
253264
_append_truthy(lib_dirs, _get_config_var("srcdir"))
254265
_append_truthy(lib_dirs, _get_config_var("LIBDIR"))
@@ -284,7 +295,8 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
284295
except OSError:
285296
pass
286297
else:
287-
yield _linked_libpython_unix(libpython)
298+
if (res := _linked_libpython_unix(libpython)) is not None:
299+
yield res
288300

289301
try:
290302
yield from _get_proc_library()
@@ -297,7 +309,8 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
297309

298310
# In macOS and Windows, ctypes.util.find_library returns a full path:
299311
for basename in lib_basenames:
300-
yield _find_library(_library_name(basename))
312+
if (res := _find_library(_library_name(basename))) is not None:
313+
yield res
301314

302315

303316
# Possibly useful links:
@@ -306,7 +319,9 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
306319
# * https://github.com/Valloric/ycmd/pull/519
307320

308321

309-
def _normalize_path(path, suffix=_SHLIB_SUFFIX, _is_apple=_is_apple):
322+
def _normalize_path(
323+
path: str, suffix: str = _SHLIB_SUFFIX, _is_apple: bool = _is_apple
324+
) -> str | None:
310325
"""
311326
Normalize shared library `path` to a real path.
312327
@@ -334,7 +349,7 @@ def _normalize_path(path, suffix=_SHLIB_SUFFIX, _is_apple=_is_apple):
334349
return None
335350

336351

337-
def _remove_suffix_apple(path):
352+
def _remove_suffix_apple(path: str) -> str:
338353
"""
339354
Strip off .so or .dylib.
340355
@@ -353,7 +368,7 @@ def _remove_suffix_apple(path):
353368

354369

355370
@_uniquified
356-
def _finding_libpython():
371+
def _finding_libpython() -> Iterable[str]:
357372
"""
358373
Iterate over existing libpython paths.
359374
@@ -374,7 +389,7 @@ def _finding_libpython():
374389
_logger.debug("Not found.")
375390

376391

377-
def find_libpython():
392+
def find_libpython() -> str | None:
378393
"""
379394
Return a path (`str`) to libpython or `None` if not found.
380395
@@ -385,18 +400,18 @@ def find_libpython():
385400
"""
386401
for path in _finding_libpython():
387402
return os.path.realpath(path)
403+
return None
388404

389405

390-
def _print_all(items):
391-
for x in items:
392-
print(x)
393-
394-
395-
def _cli_find_libpython(cli_op, verbose):
406+
def _cli_find_libpython(cli_op: str, verbose: bool) -> int:
396407
# Importing `logging` module here so that using `logging.debug`
397408
# instead of `_logger.debug` outside of this function becomes an
398409
# error.
399410

411+
def _print_all(items: Iterable[str]) -> None:
412+
for x in items:
413+
print(x)
414+
400415
if verbose:
401416
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.DEBUG)
402417

@@ -414,6 +429,8 @@ def _cli_find_libpython(cli_op, verbose):
414429
return 1
415430
print(path, end="")
416431

432+
return 0
433+
417434

418435
def _log_platform_info():
419436
print(f"is_windows = {_is_windows}")
@@ -423,7 +440,7 @@ def _log_platform_info():
423440
print(f"is_posix = {_is_posix}")
424441

425442

426-
def main(args=None):
443+
def main(args: list[str] | None = None) -> int:
427444
parser = argparse.ArgumentParser(description=__doc__)
428445
parser.add_argument(
429446
"-v", "--verbose", action="store_true", help="Print debugging information."
@@ -463,4 +480,4 @@ def main(args=None):
463480
)
464481

465482
ns = parser.parse_args(args)
466-
parser.exit(_cli_find_libpython(**vars(ns)))
483+
return _cli_find_libpython(**vars(ns))

src/find_libpython/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import sys
2+
13
from find_libpython import main
24

35
if __name__ == "__main__":
4-
main()
6+
sys.exit(main())

src/find_libpython/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)