Skip to content

Commit c30f244

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

File tree

5 files changed

+58
-29
lines changed

5 files changed

+58
-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: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,28 @@
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, TypeVar
3740

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

43+
if sys.version_info >= (3, 10):
44+
from typing import ParamSpec
45+
46+
P = ParamSpec("P")
47+
48+
T = TypeVar("T")
49+
4050
_logger = logging.getLogger("find_libpython")
4151

4252
_is_apple = sys.platform == "darwin"
@@ -57,7 +67,7 @@
5767
_SHLIB_SUFFIX = ".dylib"
5868

5969

60-
def _linked_libpython_unix(libpython):
70+
def _linked_libpython_unix(libpython: Any) -> str | None:
6171
if not hasattr(libpython, "Py_GetVersion"):
6272
return None
6373

@@ -83,7 +93,9 @@ class Dl_info(ctypes.Structure):
8393
return os.path.realpath(dlinfo.dli_fname.decode())
8494

8595

86-
def _library_name(name, suffix=_SHLIB_SUFFIX, _is_windows=_is_windows):
96+
def _library_name(
97+
name: str, suffix: str = _SHLIB_SUFFIX, _is_windows: bool = _is_windows
98+
) -> str:
8799
"""
88100
Convert a file basename `name` to a library name (no "lib" and ".so" etc.)
89101
@@ -103,12 +115,12 @@ def _library_name(name, suffix=_SHLIB_SUFFIX, _is_windows=_is_windows):
103115
return name
104116

105117

106-
def _append_truthy(list, item):
118+
def _append_truthy(list: list[T], item: T) -> None:
107119
if item:
108120
list.append(item)
109121

110122

111-
def _uniquifying(items):
123+
def _uniquifying(items: Iterable[str]) -> Iterable[str]:
112124
"""
113125
Yield items while excluding the duplicates and preserving the order.
114126
@@ -122,17 +134,17 @@ def _uniquifying(items):
122134
seen.add(x)
123135

124136

125-
def _uniquified(func):
137+
def _uniquified(func: Callable[P, Iterable[str]]) -> Callable[P, Iterable[str]]:
126138
"""Wrap iterator returned from `func` by `_uniquifying`."""
127139

128140
@wraps(func)
129-
def wrapper(*args, **kwds):
141+
def wrapper(*args: P.args, **kwds: P.kwargs) -> Iterable[str]:
130142
return _uniquifying(func(*args, **kwds))
131143

132144
return wrapper
133145

134146

135-
def _get_proc_library():
147+
def _get_proc_library() -> Iterable[str]:
136148
pid = os.getpid()
137149
path = f"/proc/{pid}/maps"
138150
lines = open(path).readlines()
@@ -146,7 +158,7 @@ def _get_proc_library():
146158

147159

148160
@_uniquified
149-
def candidate_names(suffix=_SHLIB_SUFFIX):
161+
def candidate_names(suffix: str = _SHLIB_SUFFIX) -> Iterable[str]:
150162
"""
151163
Iterate over candidate file names of libpython.
152164
@@ -203,23 +215,23 @@ def candidate_names(suffix=_SHLIB_SUFFIX):
203215
yield dlprefix + stem + suffix
204216

205217

206-
def _linked_pythondll() -> str:
218+
def _linked_pythondll() -> str | None:
207219
# On Windows there is the `sys.dllhandle` attribute which is the
208220
# DLL Handle ID for the associated python.dll for the installation.
209221
# We can use the GetModuleFileName function to get the path to the
210222
# python.dll this way.
211223

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

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

220232
# GetModuleFileName sets the return buffer to the value of the path used to load the module.
221233
# We expect it to always be a normalized absolute path to python.dll.
222-
r = ctypes.windll.kernel32.GetModuleFileNameW(
234+
r = ctypes.windll.kernel32.GetModuleFileNameW( # type: ignore[attr-defined]
223235
dll_hmodule, path_return_buffer, len(path_return_buffer)
224236
)
225237

@@ -233,7 +245,7 @@ def _linked_pythondll() -> str:
233245

234246

235247
@_uniquified
236-
def candidate_paths(suffix=_SHLIB_SUFFIX):
248+
def candidate_paths(suffix: str = _SHLIB_SUFFIX) -> Iterable[str]:
237249
"""
238250
Iterate over candidate paths of libpython.
239251
@@ -245,10 +257,11 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
245257
"""
246258

247259
if _is_windows:
248-
yield _linked_pythondll()
260+
if (res := _linked_pythondll()) is not None:
261+
yield res
249262

250263
# List candidates for directories in which libpython may exist
251-
lib_dirs = []
264+
lib_dirs: list[str] = []
252265
_append_truthy(lib_dirs, _get_config_var("LIBPL"))
253266
_append_truthy(lib_dirs, _get_config_var("srcdir"))
254267
_append_truthy(lib_dirs, _get_config_var("LIBDIR"))
@@ -284,7 +297,8 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
284297
except OSError:
285298
pass
286299
else:
287-
yield _linked_libpython_unix(libpython)
300+
if (res := _linked_libpython_unix(libpython)) is not None:
301+
yield res
288302

289303
try:
290304
yield from _get_proc_library()
@@ -297,7 +311,8 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
297311

298312
# In macOS and Windows, ctypes.util.find_library returns a full path:
299313
for basename in lib_basenames:
300-
yield _find_library(_library_name(basename))
314+
if (res := _find_library(_library_name(basename))) is not None:
315+
yield res
301316

302317

303318
# Possibly useful links:
@@ -306,7 +321,9 @@ def candidate_paths(suffix=_SHLIB_SUFFIX):
306321
# * https://github.com/Valloric/ycmd/pull/519
307322

308323

309-
def _normalize_path(path, suffix=_SHLIB_SUFFIX, _is_apple=_is_apple):
324+
def _normalize_path(
325+
path: str, suffix: str = _SHLIB_SUFFIX, _is_apple: bool = _is_apple
326+
) -> str | None:
310327
"""
311328
Normalize shared library `path` to a real path.
312329
@@ -334,7 +351,7 @@ def _normalize_path(path, suffix=_SHLIB_SUFFIX, _is_apple=_is_apple):
334351
return None
335352

336353

337-
def _remove_suffix_apple(path):
354+
def _remove_suffix_apple(path: str) -> str:
338355
"""
339356
Strip off .so or .dylib.
340357
@@ -353,7 +370,7 @@ def _remove_suffix_apple(path):
353370

354371

355372
@_uniquified
356-
def _finding_libpython():
373+
def _finding_libpython() -> Iterable[str]:
357374
"""
358375
Iterate over existing libpython paths.
359376
@@ -374,7 +391,7 @@ def _finding_libpython():
374391
_logger.debug("Not found.")
375392

376393

377-
def find_libpython():
394+
def find_libpython() -> str | None:
378395
"""
379396
Return a path (`str`) to libpython or `None` if not found.
380397
@@ -385,18 +402,18 @@ def find_libpython():
385402
"""
386403
for path in _finding_libpython():
387404
return os.path.realpath(path)
405+
return None
388406

389407

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

413+
def _print_all(items: Iterable[str]) -> None:
414+
for x in items:
415+
print(x)
416+
400417
if verbose:
401418
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.DEBUG)
402419

@@ -414,6 +431,8 @@ def _cli_find_libpython(cli_op, verbose):
414431
return 1
415432
print(path, end="")
416433

434+
return 0
435+
417436

418437
def _log_platform_info():
419438
print(f"is_windows = {_is_windows}")
@@ -423,7 +442,7 @@ def _log_platform_info():
423442
print(f"is_posix = {_is_posix}")
424443

425444

426-
def main(args=None):
445+
def main(args: list[str] | None = None) -> int:
427446
parser = argparse.ArgumentParser(description=__doc__)
428447
parser.add_argument(
429448
"-v", "--verbose", action="store_true", help="Print debugging information."
@@ -463,4 +482,4 @@ def main(args=None):
463482
)
464483

465484
ns = parser.parse_args(args)
466-
parser.exit(_cli_find_libpython(**vars(ns)))
485+
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)