Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type checking #276

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ jobs:
echo -e "\npatchelf:"
patchelf --version

- name: Static checks: mypy
run: |
mypy

- name: Build and install
run: |
python -m build
Expand Down
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,14 @@ requires = [
# TODO(#264): Remove when Python 3.7 support is removed.
"importlib_metadata; python_version<'3.8'",
]

# mypy
[tool.mypy]
warn_unused_configs = true
# TODO: Use staticx package when all modules have been updated.
#packages = ["staticx"]
modules = [
"staticx", # __init__.py
"staticx.errors",
"staticx.utils",
]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ pyinstaller
scuba
cffi # Used for test/pyinstall-cffi/
pytest
mypy
-r docs/requirements.txt
5 changes: 3 additions & 2 deletions staticx/elf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import logging
import errno
import os
import shutil

import elftools
from elftools.elf.elffile import ELFFile
from elftools.elf.dynamic import DynamicSegment
from elftools.common.exceptions import ELFError

from .errors import *
from .utils import coerce_sequence, single, which_exec
from .utils import coerce_sequence, single


def verify_tools():
Expand Down Expand Up @@ -78,7 +79,7 @@ def get_version(self):
return f"??? (exited {rc})"

def which(self):
return which_exec(self.cmd)
return shutil.which(self.cmd)



Expand Down
3 changes: 1 addition & 2 deletions staticx/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

from .constants import *
from .elf import *
from .errors import ArchiveError
from .utils import *

class ArchiveError(Exception):
pass

def open_archive(archive):
f = NamedTemporaryFile(prefix='staticx-archive-', suffix='.tar')
Expand Down
4 changes: 2 additions & 2 deletions staticx/hooks/pyinstaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ def _audit_libs(self, libs):
msg = "Unsupported PyInstaller input\n\n"
msg += "One or more libraries included in the PyInstaller"
msg += " archive uses unsupported RPATH/RUNPATH tags:\n\n"
for e in errors:
msg += f" {e.libpath}: {e.tag}={e.value!r}\n"
for err in errors:
msg += f" {err.libpath}: {err.tag}={err.value!r}\n"
msg += "\nSee https://github.com/JonathonReinhart/staticx/issues/188"
raise Error(msg)

Expand Down
62 changes: 35 additions & 27 deletions staticx/utils.py
Original file line number Diff line number Diff line change
@@ -1,72 +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 tempfile import _TemporaryFileWrapper
from typing import cast, BinaryIO, TypeVar
from .errors import *

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) -> _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) -> _TemporaryFileWrapper:
fdst = NamedTemporaryFile(**kwargs)
shutil.copyfileobj(fsrc, fdst)
fdst.flush()
fdst.seek(0)
return fdst


def which_exec(name, env=None):
for path in os.get_exec_path(env=env):
xp = os.path.join(path, name)
if os.access(xp, os.X_OK):
return xp
return None


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 @@ -84,18 +90,20 @@ 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

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")
11 changes: 0 additions & 11 deletions unittest/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,3 @@ def test_single_empty_default():

def test_single_key_none_default():
assert utils.single([1, 2, 3], key=lambda x: x<0, default='ok') == 'ok'

# which_exec
def test_which_exec_common():
def ext_which(name):
return subprocess.check_output(['which', name]).decode().strip()

for name in ('true', 'date', 'bash', 'python3'):
assert ext_which(name) == utils.which_exec(name)

def test_which_exec_bogus():
assert utils.which_exec('zZzZzZzZzZzZzZz') == None