Skip to content

Commit 79ca9a9

Browse files
peace-makerCopilot
andauthored
Add type hints to timeout, term.text, hashes, and more (#2723)
* Add typehints to timeout module Co-authored-by: Copilot <copilot@github.com> * Add typehints to device class * Add typehints to PwnlibException * Add general typehints for term.text module Since the available module attributes are handled dynamically like `text.green()` or `text.green_on_white()` or `text.underline_red()`, it's not feasible to type out all possible combinations. Just tell the type checker that all module attibutes have the same type and thus avoid the `error: Module has no attribute "bold_blue" [attr-defined]` errors. * Add typehints for config module * Add typehints for hashes module The guaranteed algorithms in hashlib are pretty stable and we can generate the list out proper type checking and auto complete. Still add a check in CI that notifies us if there are changes in the future. * Add atexception and atexit types atexception.unregister was not documented correctly since the function expects an identifier returned by `atexception.register` instead of the handler function. * Fix toplevel type hints in shellcraft.internal * Fix toplevel type hints in term.key --------- Co-authored-by: Copilot <copilot@github.com>
1 parent 9871b5a commit 79ca9a9

12 files changed

Lines changed: 191 additions & 114 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ jobs:
133133
pip install 'unicorn==2.1.2'
134134
135135
- name: Sanity checks
136-
run: PWNLIB_NOTERM=1 python -bb -c 'from pwn import *; print(pwnlib.term.term_mode)'
136+
run: |
137+
PWNLIB_NOTERM=1 python -bb -c 'from pwn import *; print(pwnlib.term.term_mode)'
138+
python -c "import hashlib; assert hashlib.algorithms_guaranteed == {'sha256', 'blake2s', 'sha3_384', 'shake_256', 'sha1', 'md5', 'shake_128', 'sha224', 'sha512', 'blake2b', 'sha3_512', 'sha3_224', 'sha3_256', 'sha384'}"
137139
138140
- name: Install documentation dependencies
139141
run: pip install -r docs/requirements.txt

mypy-baseline.txt

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ pwn/toplevel.py:0: error: Incompatible import of "size" (imported name has type
44
pwn/toplevel.py:0: error: Incompatible import of "size" (imported name has type "Callable[[Any, Any, Any], Any]", local name has type "str") [assignment]
55
pwn/toplevel.py:0: error: Incompatible import of "size" (imported name has type "int", local name has type "str") [assignment]
66
pwnlib/abi.py:0: error: Need type annotation for "register_arguments" (hint: "register_arguments: list[<type>] = ...") [var-annotated]
7-
pwnlib/asm.py:0: error: Module "pwnlib.util.hashes" has no attribute "sha1sumhex" [attr-defined]
87
pwnlib/asm.py:0: error: Need type annotation for "util_versions" [var-annotated]
9-
pwnlib/atexception.py:0: error: Function "_run_handlers" could always be true in boolean context [truthy-function]
108
pwnlib/commandline/cyclic.py:0: error: "Callable[[Any, Any, Any, Any], Any]" has no attribute "add_argument" [attr-defined]
119
pwnlib/commandline/cyclic.py:0: error: "Callable[[Any, Any, Any, Any], Any]" has no attribute "add_argument" [attr-defined]
1210
pwnlib/commandline/cyclic.py:0: error: Incompatible types in assignment (expression has type "_MutuallyExclusiveGroup", variable has type "Callable[[Any, Any, Any, Any], Any]") [assignment]
@@ -37,21 +35,6 @@ pwnlib/filesystem/path.py:0: error: "classmethod" used with a non-method [misc]
3735
pwnlib/filesystem/path.py:0: error: "type[Path]" has no attribute "mkdtemp" [attr-defined]
3836
pwnlib/filesystem/path.py:0: error: "type[Path]" has no attribute "mktemp" [attr-defined]
3937
pwnlib/gdb_api_bridge.py:0: error: Name "socket_path" is not defined [name-defined]
40-
pwnlib/libcdb.py:0: error: Module "pwnlib.util.hashes" has no attribute "md5filehex" [attr-defined]
41-
pwnlib/libcdb.py:0: error: Module "pwnlib.util.hashes" has no attribute "sha1filehex" [attr-defined]
42-
pwnlib/libcdb.py:0: error: Module "pwnlib.util.hashes" has no attribute "sha256filehex" [attr-defined]
43-
pwnlib/log.py:0: error: Module has no attribute "bold_blue" [attr-defined]
44-
pwnlib/log.py:0: error: Module has no attribute "bold_blue" [attr-defined]
45-
pwnlib/log.py:0: error: Module has no attribute "bold_blue" [attr-defined]
46-
pwnlib/log.py:0: error: Module has no attribute "bold_green" [attr-defined]
47-
pwnlib/log.py:0: error: Module has no attribute "bold_red" [attr-defined]
48-
pwnlib/log.py:0: error: Module has no attribute "bold_red" [attr-defined]
49-
pwnlib/log.py:0: error: Module has no attribute "bold_yellow" [attr-defined]
50-
pwnlib/log.py:0: error: Module has no attribute "bold_yellow" [attr-defined]
51-
pwnlib/log.py:0: error: Module has no attribute "magenta" [attr-defined]
52-
pwnlib/log.py:0: error: Module has no attribute "on_red" [attr-defined]
53-
pwnlib/log.py:0: error: Module has no attribute "on_red" [attr-defined]
54-
pwnlib/log.py:0: error: Module has no attribute "on_red" [attr-defined]
5538
pwnlib/log.py:0: error: Need type annotation for "_one_time_infos" (hint: "_one_time_infos: set[<type>] = ...") [var-annotated]
5639
pwnlib/log.py:0: error: Need type annotation for "_one_time_warnings" (hint: "_one_time_warnings: set[<type>] = ...") [var-annotated]
5740
pwnlib/memleak.py:0: error: Module "pwnlib.util.packing" has no attribute "_p8lu" [attr-defined]
@@ -86,22 +69,11 @@ pwnlib/shellcraft/__init__.py:0: error: Argument 1 to "append" of "list" has inc
8669
pwnlib/shellcraft/__init__.py:0: error: Need type annotation for "_templates" (hint: "_templates: list[<type>] = ...") [var-annotated]
8770
pwnlib/term/__init__.py:0: error: Module has no attribute "height" [attr-defined]
8871
pwnlib/term/__init__.py:0: error: Module has no attribute "width" [attr-defined]
89-
pwnlib/term/key.py:0: error: Need type annotation for "_kbuf" (hint: "_kbuf: list[<type>] = ...") [var-annotated]
90-
pwnlib/term/readline.py:0: error: Module has no attribute "reverse" [attr-defined]
9172
pwnlib/term/readline.py:0: error: Need type annotation for "history" (hint: "history: list[<type>] = ...") [var-annotated]
9273
pwnlib/term/readline.py:0: error: Need type annotation for "search_results" (hint: "search_results: list[<type>] = ...") [var-annotated]
9374
pwnlib/term/term.py:0: error: Need type annotation for "on_winch" (hint: "on_winch: list[<type>] = ...") [var-annotated]
75+
pwnlib/timeout.py:0: error: Incompatible return value type (got "float | TimeoutDefault", expected "float") [return-value]
9476
pwnlib/tubes/process.py:0: error: Cannot assign to a type [misc]
9577
pwnlib/tubes/process.py:0: error: Incompatible types in assignment (expression has type "PTY", variable has type "type[PTY]") [assignment]
96-
pwnlib/tubes/process.py:0: error: Module "pwnlib.util.hashes" has no attribute "sha256file" [attr-defined]
9778
pwnlib/tubes/tube.py:0: error: Argument 1 to "make_wrapper" has incompatible type "function"; expected "tube" [arg-type]
98-
pwnlib/util/fiddling.py:0: error: Module has no attribute "blue" [attr-defined]
99-
pwnlib/util/fiddling.py:0: error: Module has no attribute "blue" [attr-defined]
100-
pwnlib/util/fiddling.py:0: error: Module has no attribute "gray" [attr-defined]
101-
pwnlib/util/fiddling.py:0: error: Module has no attribute "gray" [attr-defined]
102-
pwnlib/util/fiddling.py:0: error: Module has no attribute "green" [attr-defined]
103-
pwnlib/util/fiddling.py:0: error: Module has no attribute "has_gray" [attr-defined]
104-
pwnlib/util/fiddling.py:0: error: Module has no attribute "has_gray" [attr-defined]
105-
pwnlib/util/fiddling.py:0: error: Module has no attribute "red" [attr-defined]
106-
pwnlib/util/fiddling.py:0: error: Module has no attribute "red" [attr-defined]
10779
pwnlib/util/iters.py:0: error: Name "pairwise" already defined (possibly by an import) [no-redef]

pwnlib/atexception.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@
55
import sys
66
import threading
77
import traceback
8+
from types import TracebackType
9+
from typing import Any, Callable, ParamSpec, TypeVar
810

911
from pwnlib.context import context
1012

1113
__all__ = ['register', 'unregister']
1214

1315
_lock = threading.Lock()
1416
_ident = 0
15-
_handlers = {}
17+
_handlers: dict[int, tuple[Callable[..., Any], Any, Any, dict[str, Any]]] = {}
1618

17-
def register(func, *args, **kwargs):
19+
_P = ParamSpec('_P')
20+
_R = TypeVar('_R')
21+
22+
def register(func: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs) -> int:
1823
"""register(func, *args, **kwargs)
1924
2025
Registers a function to be called when an unhandled exception occurs. The
@@ -40,7 +45,8 @@ def handler():
4045
actual exception-handler. The exception-handler can then be unregistered
4146
with::
4247
43-
atexception.unregister(handler)
48+
ident = atexception.register(handler)
49+
atexception.unregister(ident)
4450
4551
This function is thread safe.
4652
@@ -52,16 +58,16 @@ def handler():
5258
_handlers[ident] = (func, args, kwargs, vars(context))
5359
return ident
5460

55-
def unregister(func):
56-
"""unregister(func)
61+
def unregister(ident: int) -> None:
62+
"""unregister(ident)
5763
58-
Remove `func` from the collection of registered functions. If `func` isn't
59-
registered this is a no-op.
64+
Remove the exception-handler identified by `ident` from the list of registered
65+
handlers. If `ident` isn't registered this is a no-op.
6066
"""
61-
if func in _handlers:
62-
del _handlers[func]
67+
if ident in _handlers:
68+
del _handlers[ident]
6369

64-
def _run_handlers():
70+
def _run_handlers() -> None:
6571
"""_run_handlers()
6672
6773
Run registered handlers. They run in the reverse order of which they were
@@ -83,20 +89,20 @@ def _run_handlers():
8389
# extract the current exception and rewind the traceback to where it
8490
# originated
8591
typ, val, tb = sys.exc_info()
86-
traceback.print_exception(typ, val, tb.tb_next)
92+
traceback.print_exception(typ, value=val, tb=tb.tb_next if tb else None)
8793

8894
# we rely on the existing excepthook to print exceptions
8995
_oldhook = getattr(sys, 'excepthook', None)
9096

91-
def _newhook(typ, val, tb):
97+
def _newhook(typ: type[BaseException], val: BaseException, tb: TracebackType | None) -> None:
9298
"""_newhook(typ, val, tb)
9399
94100
Our excepthook replacement. First the original hook is called to print the
95101
exception, then each handler is called.
96102
"""
97103
if _oldhook:
98104
_oldhook(typ, val, tb)
99-
if _run_handlers:
105+
if _handlers:
100106
_run_handlers()
101107

102108
sys.excepthook = _newhook

pwnlib/atexit.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@
1111
import threading
1212
import traceback
1313
import atexit as std_atexit
14+
from typing import Any, Callable, ParamSpec, TypeVar
1415

1516
from pwnlib.context import context
1617

1718
__all__ = ['register', 'unregister']
1819

1920
_lock = threading.Lock()
2021
_ident = 0
21-
_handlers = {}
22+
_handlers: dict[int, tuple[Callable[..., Any], Any, Any, dict[str, Any]]] = {}
2223

23-
def register(func, *args, **kwargs):
24+
_P = ParamSpec('_P')
25+
_R = TypeVar('_R')
26+
27+
def register(func: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs) -> int:
2428
"""register(func, *args, **kwargs)
2529
2630
Registers a function to be called on program termination. The function will
@@ -44,7 +48,8 @@ def handler():
4448
Notice however that this will bind ``handler`` to the identifier and not the
4549
actual exit-handler. The exit-handler can then be unregistered with::
4650
47-
atexit.unregister(handler)
51+
ident = atexit.register(handler)
52+
atexit.unregister(ident)
4853
4954
This function is thread safe.
5055
@@ -56,7 +61,7 @@ def handler():
5661
_handlers[ident] = (func, args, kwargs, vars(context))
5762
return ident
5863

59-
def unregister(ident):
64+
def unregister(ident: int) -> None:
6065
"""unregister(ident)
6166
6267
Remove the exit-handler identified by `ident` from the list of registered
@@ -65,7 +70,7 @@ def unregister(ident):
6570
if ident in _handlers:
6671
del _handlers[ident]
6772

68-
def _run_handlers():
73+
def _run_handlers() -> None:
6974
"""_run_handlers()
7075
7176
Run registered exit-handlers. They run in the reverse order of which they
@@ -87,13 +92,6 @@ def _run_handlers():
8792
# extract the current exception and rewind the traceback to where it
8893
# originated
8994
typ, val, tb = sys.exc_info()
90-
traceback.print_exception(typ, val, tb.tb_next)
91-
92-
# if there's already an exitfunc registered be sure to run that too
93-
if hasattr(sys, "exitfunc"):
94-
register(sys.exitfunc)
95+
traceback.print_exception(typ, value=val, tb=tb.tb_next if tb else None)
9596

96-
if sys.version_info[0] < 3:
97-
sys.exitfunc = _run_handlers
98-
else:
99-
std_atexit.register(_run_handlers)
97+
std_atexit.register(_run_handlers)

pwnlib/config.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
file in library mode, invoke :func:`.config.initialize`.
1414
1515
The ``context`` section supports complex types, at least as far as is
16-
supported by ``pwnlib.util.safeeval.expr``.
16+
supported by :func:`pwnlib.util.safeeval.expr`.
1717
1818
::
1919
@@ -33,10 +33,13 @@
3333
"""
3434
import configparser
3535
import os
36+
from typing import Any, Callable
3637

37-
registered_configs = {}
38+
ConfigHandler = Callable[[dict[str, Any]], None]
3839

39-
def register_config(section, function):
40+
registered_configs: dict[str, ConfigHandler] = {}
41+
42+
def register_config(section: str, function: ConfigHandler) -> None:
4043
"""Registers a configuration section.
4144
4245
Arguments:
@@ -46,7 +49,7 @@ def register_config(section, function):
4649
"""
4750
registered_configs[section] = function
4851

49-
def initialize():
52+
def initialize() -> None:
5053
"""Read the configuration files."""
5154
from pwnlib.log import getLogger
5255
log = getLogger(__name__)

pwnlib/device.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
class Device:
2-
arch = None
3-
bits = None
4-
endian = None
5-
serial = None
6-
os = None
2+
arch: str | None = None
3+
bits: int | None = None
4+
endian: str | None = None
5+
serial: str | None = None
6+
os: str | None = None
77

8-
def __init__(self, serial=None):
8+
def __init__(self, serial: str | None = None):
99
self.serial = serial
1010

11-
def __str__(self):
12-
return self.serial
11+
def __str__(self) -> str:
12+
return self.serial or "<no serial>"
1313

14-
def __eq__(self, other):
14+
def __eq__(self, other: object) -> bool:
1515
return self.serial == other or self.serial == str(other)

pwnlib/exception.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
__all__ = ['PwnlibException']
22
import sys
33
import traceback
4+
from types import TracebackType
45

6+
OptExcInfo = tuple[
7+
type[BaseException],
8+
BaseException,
9+
TracebackType,
10+
] | tuple[None, None, None]
511

612
class PwnlibException(Exception):
713
'''Exception thrown by :func:`pwnlib.log.error`.
814
915
Pwnlib functions that encounters unrecoverable errors should call the
1016
:func:`pwnlib.log.error` function instead of throwing this exception directly.'''
11-
def __init__(self, msg, reason = None, exit_code = None):
17+
def __init__(self, msg: str, reason: OptExcInfo | None = None, exit_code: int | None = None):
1218
'''bar'''
1319
Exception.__init__(self, msg)
1420
self.reason = reason
1521
self.exit_code = exit_code
1622
self.message = msg
1723

18-
def __repr__(self):
24+
def __repr__(self) -> str:
1925
s = 'PwnlibException: %s' % self.message
2026
if self.reason:
2127
s += '\nReason:\n'

pwnlib/shellcraft/internal.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
from __future__ import annotations
12
import os
23
import sys
4+
from typing import TYPE_CHECKING
35

46
from pwnlib.context import context
57

68
__all__ = ['make_function']
79

8-
loaded = {}
10+
if TYPE_CHECKING:
11+
from mako.template import Template
12+
13+
loaded: dict[str, Template] = {}
914
lookup = None
1015
def init_mako():
1116
global lookup, render_global

0 commit comments

Comments
 (0)