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.

Note that we need to use both collections.abc.Iterable (for isinstance()) and
typing.Iterable (for type annotation) until Python 3.9 (#265). If we
attempt to use the former for type annotation in Python < 3.9, we get
the following error:

    TypeError: 'ABCMeta' object is not subscriptable

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 a40833c commit 3a4679d
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 24 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ modules = [
"staticx", # __init__.py
"staticx.constants",
"staticx.errors",
"staticx.utils",
]

[[tool.mypy.overrides]]
Expand Down
60 changes: 39 additions & 21 deletions staticx/utils.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,79 @@
import os
import errno
import shutil
from collections.abc import Iterable
from collections.abc import Iterable as ABCIterable
from pathlib import Path
from tempfile import NamedTemporaryFile
from .errors import *
from tempfile import _TemporaryFileWrapper
# TODO(#265): Use collections.abc.Callable and collections.abc.Iterable when Python 3.8 is dropped.
from typing import cast, Any, BinaryIO, Callable, List, Iterable, Optional, TypeVar, Union
from .errors import DirectoryExistsError

def make_mode_executable(mode):

Pathlike = Union[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: str, dst: str) -> 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"""
return isinstance(x, Iterable) and not isinstance(x, str)
# TODO: Return typing.TypeGuard
return isinstance(x, ABCIterable) and not isinstance(x, str)

def coerce_sequence(x: Union[T, Iterable[T]]) -> List[T]:
if is_iterable(x):
return list(cast(Iterable, x))
return [cast(T, x)]

def coerce_sequence(x, t=list):
if not is_iterable(x):
x = [x]
return 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: Optional[Callable[[T], bool]] = None,
default: Union[T, None, _Sentinel] = _NO_DEFAULT,
) -> Optional[T]:
"""Returns a single item from iterable
Arguments:
Expand All @@ -76,18 +91,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 3a4679d

Please sign in to comment.