diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c7260a940..c1ee46e44 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,8 +6,7 @@ jobs: strategy: fail-fast: false matrix: - # Only care about the oldest and newest versions - os: [ubuntu-22.04, ubuntu-18.04] + os: [ubuntu-22.04, ubuntu-20.04, ubuntu-18.04] runs-on: ${{ matrix.os }} timeout-minutes: 10 steps: diff --git a/dev-requirements.txt b/dev-requirements.txt index 98db74a16..292f3784d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,10 +6,14 @@ flake8==5.0.4; python_version < '3.9' flake8==6.0.0; python_version >= '3.9' flake8-builtins==2.0.0; python_version < '3.7' flake8-builtins==2.1.0; python_version >= '3.7' +mypy==0.991; python_version >= '3.7' testresources==2.0.1 isort==5.10.1; python_version < '3.7' isort==5.11.4; python_version >= '3.7' pytest==7.0.1; python_version < '3.7' pytest==7.2.0; python_version >= '3.7' types-gdb==12.1.4.1 +types-psutil==5.9.5 +types-Pygments==2.12.1 +types-tabulate==0.9.0.0 vermin==1.5.1 diff --git a/lint.sh b/lint.sh index e991670bc..04e677907 100755 --- a/lint.sh +++ b/lint.sh @@ -59,3 +59,9 @@ fi vermin -q -t=3.6 --violations ./pwndbg/ flake8 --show-source ${LINT_FILES} + +if [ -x "$(command -v mypy)" ]; then + mypy pwndbg +else + echo "mypy not installed, skipping" +fi diff --git a/pwndbg/lib/memoize.py b/pwndbg/lib/memoize.py index 728880374..a3a814076 100644 --- a/pwndbg/lib/memoize.py +++ b/pwndbg/lib/memoize.py @@ -6,6 +6,7 @@ new library/objfile are loaded, etc. import functools import sys +import typing as t from collections.abc import Hashable from typing import Any from typing import Callable @@ -14,179 +15,180 @@ from typing import List # noqa: F401 debug = False +# https://stackoverflow.com/a/75013308/803801 +if t.TYPE_CHECKING: + F = t.TypeVar("F") + reset_on_stop: Callable[[F], F] + reset_on_prompt: Callable[[F], F] + reset_on_exit: Callable[[F], F] + reset_on_objfile: Callable[[F], F] + reset_on_start: Callable[[F], F] + reset_on_cont: Callable[[F], F] + reset_on_thread: Callable[[F], F] +else: + + class memoize: + """ + Base memoization class. Do not use directly. Instead use one of classes defined below. + """ + + caching = True + + def __init__(self, func: Callable) -> None: + self.func = func + self.cache: Dict[Any, Any] = {} + self.caches.append(self) # must be provided by base class + functools.update_wrapper(self, func) + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + how = None + + if not isinstance(args, Hashable): + print("Cannot memoize %r!", file=sys.stderr) + how = "Not memoizeable!" + value = self.func(*args) + + if self.caching and args in self.cache: + how = "Cached" + value = self.cache[args] + + else: + how = "Executed" + value = self.func(*args, **kwargs) + self.cache[args] = value -class memoize: - """ - Base memoization class. Do not use directly. Instead use one of classes defined below. - """ + if isinstance(value, list): + print("Should not cache mutable types! %r" % self.func.__name__) - caching = True + if debug: + print("%s: %s(%r)" % (how, self, args)) + print(".... %r" % (value,)) + return value - def __init__(self, func: Callable) -> None: - self.func = func - self.cache: Dict[Any, Any] = {} - self.caches.append(self) # must be provided by base class - functools.update_wrapper(self, func) + def __repr__(self) -> str: + funcname = self.func.__module__ + "." + self.func.__name__ + return "<%s-memoized function %s>" % (self.kind, funcname) - def __call__(self, *args: Any, **kwargs: Any) -> Any: - how = None + def __get__(self, obj, objtype: type) -> Callable: + return functools.partial(self.__call__, obj) - if not isinstance(args, Hashable): - print("Cannot memoize %r!", file=sys.stderr) - how = "Not memoizeable!" - value = self.func(*args) + def clear(self) -> None: + if debug: + print("Clearing %s %r" % (self, self.cache)) + self.cache.clear() - if self.caching and args in self.cache: - how = "Cached" - value = self.cache[args] + class forever(memoize): + """ + Memoizes forever - for a pwndbg session or until `_reset` is called explicitly. + """ + + caches = [] # type: List[forever] + kind = "forever" - else: - how = "Executed" - value = self.func(*args, **kwargs) - self.cache[args] = value + @staticmethod + def _reset() -> None: + for obj in forever.caches: + obj.cache.clear() + + class reset_on_stop(memoize): + caches = [] # type: List[reset_on_stop] + kind = "stop" + + @staticmethod + def __reset_on_stop() -> None: + for obj in reset_on_stop.caches: + obj.cache.clear() + + _reset = __reset_on_stop + + class reset_on_prompt(memoize): + caches = [] # type: List[reset_on_prompt] + kind = "prompt" + + @staticmethod + def __reset_on_prompt() -> None: + for obj in reset_on_prompt.caches: + obj.cache.clear() + + _reset = __reset_on_prompt + + class reset_on_exit(memoize): + caches = [] # type: List[reset_on_exit] + kind = "exit" + + @staticmethod + def __reset_on_exit() -> None: + for obj in reset_on_exit.caches: + obj.clear() + + _reset = __reset_on_exit + + class reset_on_objfile(memoize): + caches = [] # type: List[reset_on_objfile] + kind = "objfile" - if isinstance(value, list): - print("Should not cache mutable types! %r" % self.func.__name__) + @staticmethod + def __reset_on_objfile() -> None: + for obj in reset_on_objfile.caches: + obj.clear() - if debug: - print("%s: %s(%r)" % (how, self, args)) - print(".... %r" % (value,)) - return value + _reset = __reset_on_objfile - def __repr__(self) -> str: - funcname = self.func.__module__ + "." + self.func.__name__ - return "<%s-memoized function %s>" % (self.kind, funcname) + class reset_on_start(memoize): + caches = [] # type: List[reset_on_start] + kind = "start" - def __get__(self, obj, objtype: type) -> Callable: - return functools.partial(self.__call__, obj) + @staticmethod + def __reset_on_start() -> None: + for obj in reset_on_start.caches: + obj.clear() - def clear(self) -> None: - if debug: - print("Clearing %s %r" % (self, self.cache)) - self.cache.clear() + _reset = __reset_on_start + + class reset_on_cont(memoize): + caches = [] # type: List[reset_on_cont] + kind = "cont" + + @staticmethod + def __reset_on_cont() -> None: + for obj in reset_on_cont.caches: + obj.clear() + _reset = __reset_on_cont -class forever(memoize): - """ - Memoizes forever - for a pwndbg session or until `_reset` is called explicitly. - """ + class reset_on_thread(memoize): + caches = [] # type: List[reset_on_thread] + kind = "thread" - caches = [] # type: List[forever] - kind = "forever" + @staticmethod + def __reset_on_thread() -> None: + for obj in reset_on_thread.caches: + obj.clear() - @staticmethod - def _reset() -> None: - for obj in forever.caches: - obj.cache.clear() + _reset = __reset_on_thread + class while_running(memoize): + caches = [] # type: List[while_running] + kind = "running" + caching = False -class reset_on_stop(memoize): - caches = [] # type: List[reset_on_stop] - kind = "stop" + @staticmethod + def _start_caching() -> None: + while_running.caching = True - @staticmethod - def __reset_on_stop() -> None: - for obj in reset_on_stop.caches: - obj.cache.clear() + @staticmethod + def __reset_while_running() -> None: + for obj in while_running.caches: + obj.clear() + while_running.caching = False - _reset = __reset_on_stop + _reset = __reset_while_running - -class reset_on_prompt(memoize): - caches = [] # type: List[reset_on_prompt] - kind = "prompt" - - @staticmethod - def __reset_on_prompt() -> None: - for obj in reset_on_prompt.caches: - obj.cache.clear() - - _reset = __reset_on_prompt - - -class reset_on_exit(memoize): - caches = [] # type: List[reset_on_exit] - kind = "exit" - - @staticmethod - def __reset_on_exit() -> None: - for obj in reset_on_exit.caches: - obj.clear() - - _reset = __reset_on_exit - - -class reset_on_objfile(memoize): - caches = [] # type: List[reset_on_objfile] - kind = "objfile" - - @staticmethod - def __reset_on_objfile() -> None: - for obj in reset_on_objfile.caches: - obj.clear() - - _reset = __reset_on_objfile - - -class reset_on_start(memoize): - caches = [] # type: List[reset_on_start] - kind = "start" - - @staticmethod - def __reset_on_start() -> None: - for obj in reset_on_start.caches: - obj.clear() - - _reset = __reset_on_start - - -class reset_on_cont(memoize): - caches = [] # type: List[reset_on_cont] - kind = "cont" - - @staticmethod - def __reset_on_cont() -> None: - for obj in reset_on_cont.caches: - obj.clear() - - _reset = __reset_on_cont - - -class reset_on_thread(memoize): - caches = [] # type: List[reset_on_thread] - kind = "thread" - - @staticmethod - def __reset_on_thread() -> None: - for obj in reset_on_thread.caches: - obj.clear() - - _reset = __reset_on_thread - - -class while_running(memoize): - caches = [] # type: List[while_running] - kind = "running" - caching = False - - @staticmethod - def _start_caching() -> None: - while_running.caching = True - - @staticmethod - def __reset_while_running() -> None: - for obj in while_running.caches: - obj.clear() - while_running.caching = False - - _reset = __reset_while_running - - -def reset() -> None: - forever._reset() - reset_on_stop._reset() - reset_on_exit._reset() - reset_on_objfile._reset() - reset_on_start._reset() - reset_on_cont._reset() - while_running._reset() + def reset() -> None: + forever._reset() + reset_on_stop._reset() + reset_on_exit._reset() + reset_on_objfile._reset() + reset_on_start._reset() + reset_on_cont._reset() + while_running._reset() diff --git a/pwndbg/lib/stdio.py b/pwndbg/lib/stdio.py index ade68da67..b05663230 100644 --- a/pwndbg/lib/stdio.py +++ b/pwndbg/lib/stdio.py @@ -4,11 +4,13 @@ which prevent output from appearing on-screen inside of certain event handlers. """ import sys -from typing import * # noqa note: TextIO is not abaliable in low python version +from typing import List +from typing import TextIO +from typing import Tuple class Stdio: - queue = [] # type: List[Tuple[TextIO, TextIO, TextIO]] + queue: List[Tuple[TextIO, TextIO, TextIO]] = [] def __enter__(self, *a, **kw) -> None: self.queue.append((sys.stdin, sys.stdout, sys.stderr)) diff --git a/pyproject.toml b/pyproject.toml index a6288e7ca..681fcad88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,7 @@ disable_error_code = [ # Lots of dynamic attribute access "attr-defined", # https://github.com/python/mypy/issues/6232 - "assignment", - # Issues with @property - "operator", + "assignment" ] [[tool.mypy.overrides]]