From dfbd098886996eb86108c6ec17420acae2079b59 Mon Sep 17 00:00:00 2001 From: Jonathon Reinhart Date: Sun, 14 Jan 2024 10:47:59 -0500 Subject: [PATCH] staticx: Add type info to staticx.utils This also updates the single() implementation, replacing the use of _SENTINEL as a nothing-to-return marker with a separate bool variable. This also simplifies coerce_sequence() to always return a list, removing the return-type argument. --- pyproject.toml | 1 + staticx/utils.py | 57 +++++++++++++++++++++++++++--------------- unittest/test_utils.py | 3 --- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 68dddc6..10e4a21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ modules = [ "staticx", # __init__.py "staticx.constants", "staticx.errors", + "staticx.utils", ] [[tool.mypy.overrides]] diff --git a/staticx/utils.py b/staticx/utils.py index 7a1ee4d..8f71e23 100644 --- a/staticx/utils.py +++ b/staticx/utils.py @@ -1,36 +1,43 @@ import os import errno import shutil -from collections.abc import Iterable +from collections.abc import Callable, Iterable +from pathlib import Path from tempfile import NamedTemporaryFile -from .errors import * +from tempfile import _TemporaryFileWrapper +from typing import cast, Any, BinaryIO, TypeVar +from .errors import DirectoryExistsError -def make_mode_executable(mode): + +Pathlike = Path | str +T = TypeVar("T") + +def make_mode_executable(mode: int) -> int: mode |= (mode & 0o444) >> 2 # copy R bits to X return mode -def make_executable(path): +def make_executable(path: Pathlike) -> None: mode = os.stat(path).st_mode mode = make_mode_executable(mode) os.chmod(path, mode) -def get_symlink_target(path): +def get_symlink_target(path: Pathlike) -> str: dirpath = os.path.dirname(os.path.abspath(path)) return os.path.join(dirpath, os.readlink(path)) -def move_file(src, dst): +def move_file(src: Pathlike, dst: Pathlike) -> None: if os.path.isdir(dst): raise DirectoryExistsError(dst) shutil.move(src, dst) -def mkdirs_for(filename): +def mkdirs_for(filename: Pathlike) -> None: dirname = os.path.dirname(filename) os.makedirs(dirname, exist_ok=True) -def copy_to_tempfile(srcpath, **kwargs): +def copy_to_tempfile(srcpath: Pathlike, **kwargs: Any) -> _TemporaryFileWrapper: with open(srcpath, 'rb') as fsrc: fdst = copy_fileobj_to_tempfile(fsrc, **kwargs) @@ -38,7 +45,7 @@ def copy_to_tempfile(srcpath, **kwargs): return fdst -def copy_fileobj_to_tempfile(fsrc, **kwargs): +def copy_fileobj_to_tempfile(fsrc: BinaryIO, **kwargs: Any) -> _TemporaryFileWrapper: fdst = NamedTemporaryFile(**kwargs) shutil.copyfileobj(fsrc, fdst) fdst.flush() @@ -46,19 +53,26 @@ def copy_fileobj_to_tempfile(fsrc, **kwargs): return fdst -def is_iterable(x): +def is_iterable(x: object) -> bool: """Returns true if x is iterable but not a string""" + # TODO: Return typing.TypeGuard return isinstance(x, Iterable) and not isinstance(x, str) -def coerce_sequence(x, t=list): - if not is_iterable(x): - x = [x] - return t(x) +def coerce_sequence(x: T | Iterable[T]) -> list[T]: + if is_iterable(x): + return list(cast(Iterable, x)) + return [cast(T, x)] + +class _Sentinel: + pass -_SENTINEL = object() -_NO_DEFAULT = object() +_NO_DEFAULT = _Sentinel() -def single(iterable, key=None, default=_NO_DEFAULT): +def single( + iterable: Iterable[T], + key: Callable[[T], bool] | None = None, + default: T | None | _Sentinel = _NO_DEFAULT, +) -> T | None: """Returns a single item from iterable Arguments: @@ -76,18 +90,21 @@ def single(iterable, key=None, default=_NO_DEFAULT): if key is None: key = lambda _: True - result = _SENTINEL + result: T + have_result = False for i in iterable: if key(i): - if result is not _SENTINEL: + if have_result: raise KeyError("Multiple items match key") result = i + have_result = True - if result is not _SENTINEL: + if have_result: return result if default is not _NO_DEFAULT: + assert not isinstance(default, _Sentinel) return default raise KeyError("No items match key") diff --git a/unittest/test_utils.py b/unittest/test_utils.py index 8a3c8a6..4ca3dd0 100644 --- a/unittest/test_utils.py +++ b/unittest/test_utils.py @@ -35,9 +35,6 @@ def test_coerce_sequence_tuple_input(): assert utils.coerce_sequence((69, 420)) == [69, 420] assert utils.coerce_sequence(("foo", "bar")) == ["foo", "bar"] -def test_coerce_sequence_tuple_output(): - assert utils.coerce_sequence([69, 420], tuple) == (69, 420) - # single def test_single_success():