Skip to content

Commit

Permalink
staticx: Add type info to staticx.utils
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JonathonReinhart committed Jan 14, 2024
1 parent b251e16 commit dfbd098
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 23 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ modules = [
"staticx", # __init__.py
"staticx.constants",
"staticx.errors",
"staticx.utils",
]

[[tool.mypy.overrides]]
Expand Down
57 changes: 37 additions & 20 deletions staticx/utils.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,78 @@
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)

shutil.copystat(srcpath, fdst.name)
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()
fdst.seek(0)
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:
Expand All @@ -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")
3 changes: 0 additions & 3 deletions unittest/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down

0 comments on commit dfbd098

Please sign in to comment.