diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f9b654..614880d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -89,60 +89,6 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true - rhel8-system-python: - name: rhel8-system-python - runs-on: ubuntu-latest - container: "almalinux:8" - steps: - - name: Install System Python and Git - run: yum install -y python3-devel python3-pip python3 git - - uses: actions/checkout@v4 - - name: Install Testing Requirements - run: python3 -m pip install nox - - name: Run Tests - run: python3 -m nox -e tests - # - name: Upload to codecov - # uses: codecov/codecov-action@v5 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} - # fail_ci_if_error: true - - rhel8-appstream-py38: - name: rhel8-appstream-py38 - runs-on: ubuntu-latest - container: "almalinux:8" - steps: - - name: Install Python 3.8 and Git from AppStream - run: yum install -y python38-devel python38-pip python38-pip-wheel python38 git - - uses: actions/checkout@v4 - - name: Install Testing Requirements - run: python3.8 -m pip install nox - - name: Run Tests - run: python3.8 -m nox -e tests - # - name: Upload to codecov - # uses: codecov/codecov-action@v5 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} - # fail_ci_if_error: true - - rhel8-appstream-py39: - name: rhel8-appstream-py39 - runs-on: ubuntu-latest - container: "almalinux:8" - steps: - - name: Install Python 3.9 and Git from AppStream - run: yum install -y python39-devel python39-pip python39-pip-wheel python39 git - - uses: actions/checkout@v4 - - name: Install Testing Requirements - run: python3.9 -m pip install nox - - name: Run Tests - run: python3.9 -m nox -e tests - # - name: Upload to codecov - # uses: codecov/codecov-action@v5 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} - # fail_ci_if_error: true - rhel9-system-python: name: rhel9-system-python runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index c1e64c1..c7e2fea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ +*.egg-info/ .coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82f9d91..73fb3fc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,5 +34,10 @@ repos: # Run the formatter. - id: ruff-format +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.2 + hooks: + - id: mypy + ci: autofix_prs: false diff --git a/noxfile.py b/noxfile.py index 1017e60..afef089 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,6 +6,8 @@ pytest_cov_args = ["--cov=find_libpython", "--cov-branch"] coverage_file = "coverage.xml" +nox.options.default_venv_backend = "uv|virtualenv" + @nox.session def tests(session): diff --git a/pyproject.toml b/pyproject.toml index 4906da6..f696075 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,40 @@ [build-system] -requires = ["setuptools>=43", "wheel"] +requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" +[project] +name = "find_libpython" +authors = [{name = "Takafumi Arakaki"}] +maintainers = [{name = "Kaleb Barrett", email = "dev.ktbarrett@gmail.com"}] +license = {text = "MIT"} +description = "Finds the libpython associated with your environment, wherever it may be hiding" +classifiers = [ + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries", +] +requires-python = ">=3.9" +dynamic = ["version"] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.urls] +Homepage = "https://github.com/ktbarrett/find_libpython" + +[project.scripts] +find_libpython = "find_libpython:main" + +[tool.setuptools] +packages = ["find_libpython"] +package-dir = {"" = "src"} +include-package-data = false + +[tool.setuptools.dynamic] +version = {attr = "find_libpython.__version__"} + [tool.ruff] -target-version = "py37" +target-version = "py39" [tool.ruff.lint] select = [ @@ -17,3 +48,13 @@ ignore = [ "E501", # Line too long "PLR0912", # Too many branches ] + +[tool.mypy] +packages = ["find_libpython"] + +[dependency-groups] +dev = [ + "mypy", + "nox[uv]", + "ruff", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b519197..0000000 --- a/setup.cfg +++ /dev/null @@ -1,26 +0,0 @@ -[metadata] -name = find_libpython -version = attr: find_libpython.__version__ -url = https://github.com/ktbarrett/find_libpython -author = Takafumi Arakaki -maintainer = Kaleb Barrett -maintainer_email = dev.ktbarrett@gmail.com -license = MIT -description = Finds the libpython associated with your environment, wherever it may be hiding -long_description = file: README.md -long_description_content_type = text/markdown -keywords = - libpython -classifiers = - Programming Language :: Python :: 3 - Topic :: Software Development :: Libraries - -[options] -packages = - find_libpython -package_dir = - = src - -[options.entry_points] -console_scripts = - find_libpython = find_libpython:main diff --git a/setup.py b/setup.py deleted file mode 100644 index 6068493..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/src/find_libpython/__init__.py b/src/find_libpython/__init__.py index 62663d8..eded7af 100644 --- a/src/find_libpython/__init__.py +++ b/src/find_libpython/__init__.py @@ -25,18 +25,28 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from __future__ import annotations import argparse import ctypes import logging import os import sys +from collections.abc import Iterable from ctypes.util import find_library as _find_library from functools import wraps from sysconfig import get_config_var as _get_config_var +from typing import Any, Callable, TypeVar from find_libpython._version import __version__ # noqa: F401 +if sys.version_info >= (3, 10): + from typing import ParamSpec + + P = ParamSpec("P") + +T = TypeVar("T") + _logger = logging.getLogger("find_libpython") _is_apple = sys.platform == "darwin" @@ -57,7 +67,7 @@ _SHLIB_SUFFIX = ".dylib" -def _linked_libpython_unix(libpython): +def _linked_libpython_unix(libpython: Any) -> str | None: if not hasattr(libpython, "Py_GetVersion"): return None @@ -83,7 +93,9 @@ class Dl_info(ctypes.Structure): return os.path.realpath(dlinfo.dli_fname.decode()) -def _library_name(name, suffix=_SHLIB_SUFFIX, _is_windows=_is_windows): +def _library_name( + name: str, suffix: str = _SHLIB_SUFFIX, _is_windows: bool = _is_windows +) -> str: """ Convert a file basename `name` to a library name (no "lib" and ".so" etc.) @@ -103,12 +115,12 @@ def _library_name(name, suffix=_SHLIB_SUFFIX, _is_windows=_is_windows): return name -def _append_truthy(list, item): +def _append_truthy(list: list[T], item: T) -> None: if item: list.append(item) -def _uniquifying(items): +def _uniquifying(items: Iterable[str]) -> Iterable[str]: """ Yield items while excluding the duplicates and preserving the order. @@ -122,17 +134,17 @@ def _uniquifying(items): seen.add(x) -def _uniquified(func): +def _uniquified(func: Callable[P, Iterable[str]]) -> Callable[P, Iterable[str]]: """Wrap iterator returned from `func` by `_uniquifying`.""" @wraps(func) - def wrapper(*args, **kwds): + def wrapper(*args: P.args, **kwds: P.kwargs) -> Iterable[str]: return _uniquifying(func(*args, **kwds)) return wrapper -def _get_proc_library(): +def _get_proc_library() -> Iterable[str]: pid = os.getpid() path = f"/proc/{pid}/maps" lines = open(path).readlines() @@ -146,7 +158,7 @@ def _get_proc_library(): @_uniquified -def candidate_names(suffix=_SHLIB_SUFFIX): +def candidate_names(suffix: str = _SHLIB_SUFFIX) -> Iterable[str]: """ Iterate over candidate file names of libpython. @@ -203,7 +215,7 @@ def candidate_names(suffix=_SHLIB_SUFFIX): yield dlprefix + stem + suffix -def _linked_pythondll() -> str: +def _linked_pythondll() -> str | None: # On Windows there is the `sys.dllhandle` attribute which is the # DLL Handle ID for the associated python.dll for the installation. # We can use the GetModuleFileName function to get the path to the @@ -211,7 +223,7 @@ def _linked_pythondll() -> str: # sys.dllhandle is an module ID, which is just a void* cast to an integer, # we turn it back into a pointer for the ctypes call - dll_hmodule = ctypes.cast(sys.dllhandle, ctypes.c_void_p) + dll_hmodule = ctypes.cast(sys.dllhandle, ctypes.c_void_p) # type: ignore[attr-defined] # create a buffer for the return path of the maximum length of filepaths in Windows unicode interfaces # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation @@ -219,7 +231,7 @@ def _linked_pythondll() -> str: # GetModuleFileName sets the return buffer to the value of the path used to load the module. # We expect it to always be a normalized absolute path to python.dll. - r = ctypes.windll.kernel32.GetModuleFileNameW( + r = ctypes.windll.kernel32.GetModuleFileNameW( # type: ignore[attr-defined] dll_hmodule, path_return_buffer, len(path_return_buffer) ) @@ -233,7 +245,7 @@ def _linked_pythondll() -> str: @_uniquified -def candidate_paths(suffix=_SHLIB_SUFFIX): +def candidate_paths(suffix: str = _SHLIB_SUFFIX) -> Iterable[str]: """ Iterate over candidate paths of libpython. @@ -245,10 +257,11 @@ def candidate_paths(suffix=_SHLIB_SUFFIX): """ if _is_windows: - yield _linked_pythondll() + if (res := _linked_pythondll()) is not None: + yield res # List candidates for directories in which libpython may exist - lib_dirs = [] + lib_dirs: list[str] = [] _append_truthy(lib_dirs, _get_config_var("LIBPL")) _append_truthy(lib_dirs, _get_config_var("srcdir")) _append_truthy(lib_dirs, _get_config_var("LIBDIR")) @@ -284,7 +297,8 @@ def candidate_paths(suffix=_SHLIB_SUFFIX): except OSError: pass else: - yield _linked_libpython_unix(libpython) + if (res := _linked_libpython_unix(libpython)) is not None: + yield res try: yield from _get_proc_library() @@ -297,7 +311,8 @@ def candidate_paths(suffix=_SHLIB_SUFFIX): # In macOS and Windows, ctypes.util.find_library returns a full path: for basename in lib_basenames: - yield _find_library(_library_name(basename)) + if (res := _find_library(_library_name(basename))) is not None: + yield res # Possibly useful links: @@ -306,7 +321,9 @@ def candidate_paths(suffix=_SHLIB_SUFFIX): # * https://github.com/Valloric/ycmd/pull/519 -def _normalize_path(path, suffix=_SHLIB_SUFFIX, _is_apple=_is_apple): +def _normalize_path( + path: str, suffix: str = _SHLIB_SUFFIX, _is_apple: bool = _is_apple +) -> str | None: """ Normalize shared library `path` to a real path. @@ -334,7 +351,7 @@ def _normalize_path(path, suffix=_SHLIB_SUFFIX, _is_apple=_is_apple): return None -def _remove_suffix_apple(path): +def _remove_suffix_apple(path: str) -> str: """ Strip off .so or .dylib. @@ -353,7 +370,7 @@ def _remove_suffix_apple(path): @_uniquified -def _finding_libpython(): +def _finding_libpython() -> Iterable[str]: """ Iterate over existing libpython paths. @@ -374,7 +391,7 @@ def _finding_libpython(): _logger.debug("Not found.") -def find_libpython(): +def find_libpython() -> str | None: """ Return a path (`str`) to libpython or `None` if not found. @@ -385,18 +402,18 @@ def find_libpython(): """ for path in _finding_libpython(): return os.path.realpath(path) + return None -def _print_all(items): - for x in items: - print(x) - - -def _cli_find_libpython(cli_op, verbose): +def _cli_find_libpython(cli_op: str, verbose: bool) -> int: # Importing `logging` module here so that using `logging.debug` # instead of `_logger.debug` outside of this function becomes an # error. + def _print_all(items: Iterable[str]) -> None: + for x in items: + print(x) + if verbose: logging.basicConfig(format="%(levelname)s %(message)s", level=logging.DEBUG) @@ -414,6 +431,8 @@ def _cli_find_libpython(cli_op, verbose): return 1 print(path, end="") + return 0 + def _log_platform_info(): print(f"is_windows = {_is_windows}") @@ -423,7 +442,7 @@ def _log_platform_info(): print(f"is_posix = {_is_posix}") -def main(args=None): +def main(args: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-v", "--verbose", action="store_true", help="Print debugging information." @@ -463,4 +482,4 @@ def main(args=None): ) ns = parser.parse_args(args) - parser.exit(_cli_find_libpython(**vars(ns))) + return _cli_find_libpython(**vars(ns)) diff --git a/src/find_libpython/__main__.py b/src/find_libpython/__main__.py index aedc5ac..951e2a7 100644 --- a/src/find_libpython/__main__.py +++ b/src/find_libpython/__main__.py @@ -1,4 +1,6 @@ +import sys + from find_libpython import main if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/src/find_libpython/py.typed b/src/find_libpython/py.typed new file mode 100644 index 0000000..e69de29