From 24efa58af2caeaf448eef08d710f98e1242bc710 Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Mon, 9 Oct 2023 11:20:10 -0400 Subject: [PATCH 1/4] add option to include stack trace in debug() output --- devtools/debug.py | 139 ++++++++++++++++++++++++++------------- requirements/testing.in | 2 + requirements/testing.txt | 38 ++++++++++- tests/test_main.nt | 111 +++++++++++++++++++++++++++++++ tests/test_main.py | 58 +++++++--------- 5 files changed, 268 insertions(+), 80 deletions(-) create mode 100644 tests/test_main.nt diff --git a/devtools/debug.py b/devtools/debug.py index 89cda24..c83a453 100644 --- a/devtools/debug.py +++ b/devtools/debug.py @@ -22,6 +22,47 @@ StrType = str +class DebugFrame: + __slots__ = 'function', 'path', 'lineno' + + @staticmethod + def from_call_frame(call_frame: 'FrameType') -> 'DebugFrame': + from pathlib import Path + + function = call_frame.f_code.co_name + + path = Path(call_frame.f_code.co_filename) + if path.is_absolute(): + # make the path relative + cwd = Path('.').resolve() + try: + path = path.relative_to(cwd) + except ValueError: + # happens if filename path is not within CWD + pass + + lineno = call_frame.f_lineno + + return DebugFrame(function, str(path), lineno) + + def __init__(self, function: str, path: str, lineno: int): + self.function = function + self.path = path + self.lineno = lineno + + def __str__(self) -> StrType: + return self.str() + + def str(self, highlight: bool = False) -> StrType: + if highlight: + return ( + f'{sformat(self.path, sformat.magenta)}:{sformat(self.lineno, sformat.green)} ' + f'{sformat(self.function, sformat.green, sformat.italic)}' + ) + else: + return f'{self.path}:{self.lineno} {self.function}' + + class DebugArgument: __slots__ = 'value', 'name', 'extra' @@ -66,43 +107,37 @@ class DebugOutput: """ arg_class = DebugArgument - __slots__ = 'filename', 'lineno', 'frame', 'arguments', 'warning' + __slots__ = 'call_context', 'arguments', 'warning' def __init__( self, *, - filename: str, - lineno: int, - frame: str, + call_context: 'List[DebugFrame]', arguments: 'List[DebugArgument]', warning: 'Union[None, str, bool]' = None, ) -> None: - self.filename = filename - self.lineno = lineno - self.frame = frame + self.call_context = call_context self.arguments = arguments self.warning = warning def str(self, highlight: bool = False) -> StrType: - if highlight: - prefix = ( - f'{sformat(self.filename, sformat.magenta)}:{sformat(self.lineno, sformat.green)} ' - f'{sformat(self.frame, sformat.green, sformat.italic)}' - ) - if self.warning: + prefix = '\n'.join(x.str(highlight) for x in self.call_context) + + if self.warning: + if highlight: prefix += sformat(f' ({self.warning})', sformat.dim) - else: - prefix = f'{self.filename}:{self.lineno} {self.frame}' - if self.warning: + else: prefix += f' ({self.warning})' - return f'{prefix}\n ' + '\n '.join(a.str(highlight) for a in self.arguments) + + return prefix + '\n ' + '\n '.join(a.str(highlight) for a in self.arguments) def __str__(self) -> StrType: return self.str() def __repr__(self) -> StrType: + context = self.call_context[-1] arguments = ' '.join(str(a) for a in self.arguments) - return f'' + return f'' class Debug: @@ -118,9 +153,10 @@ def __call__( file_: 'Any' = None, flush_: bool = True, frame_depth_: int = 2, + trace_: bool = False, **kwargs: 'Any', ) -> 'Any': - d_out = self._process(args, kwargs, frame_depth_) + d_out = self._process(args, kwargs, frame_depth_, trace_) s = d_out.str(use_highlight(self._highlight, file_)) print(s, file=file_, flush=flush_) if kwargs: @@ -130,8 +166,25 @@ def __call__( else: return args - def format(self, *args: 'Any', frame_depth_: int = 2, **kwargs: 'Any') -> DebugOutput: - return self._process(args, kwargs, frame_depth_) + def trace( + self, + *args: 'Any', + file_: 'Any' = None, + flush_: bool = True, + frame_depth_: int = 2, + **kwargs: 'Any', + ) -> 'Any': + return self.__call__( + *args, + file_=file_, + flush_=flush_, + frame_depth_=frame_depth_ + 1, + trace_=True, + **kwargs, + ) + + def format(self, *args: 'Any', frame_depth_: int = 2, trace_: bool = False, **kwargs: 'Any') -> DebugOutput: + return self._process(args, kwargs, frame_depth_, trace_) def breakpoint(self) -> None: import pdb @@ -141,7 +194,7 @@ def breakpoint(self) -> None: def timer(self, name: 'Optional[str]' = None, *, verbose: bool = True, file: 'Any' = None, dp: int = 3) -> Timer: return Timer(name=name, verbose=verbose, file=file, dp=dp) - def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int) -> DebugOutput: + def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int, trace: bool) -> DebugOutput: """ BEWARE: this must be called from a function exactly `frame_depth` levels below the top of the stack. """ @@ -149,30 +202,16 @@ def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int) -> DebugOutput: try: call_frame: 'FrameType' = sys._getframe(frame_depth) except ValueError: - # "If [ValueError] is deeper than the call stack, ValueError is raised" + # "If [the given frame depth] is deeper than the call stack, + # ValueError is raised" return self.output_class( - filename='', - lineno=0, - frame='', + call_context=[DebugFrame(function='', path='', lineno=0)], arguments=list(self._args_inspection_failed(args, kwargs)), warning=self._show_warnings and 'error parsing code, call stack too shallow', ) - function = call_frame.f_code.co_name - - from pathlib import Path - - path = Path(call_frame.f_code.co_filename) - if path.is_absolute(): - # make the path relative - cwd = Path('.').resolve() - try: - path = path.relative_to(cwd) - except ValueError: - # happens if filename path is not within CWD - pass + call_context = _make_call_context(call_frame, trace) - lineno = call_frame.f_lineno warning = None import executing @@ -183,7 +222,7 @@ def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int) -> DebugOutput: arguments = list(self._args_inspection_failed(args, kwargs)) else: ex = source.executing(call_frame) - function = ex.code_qualname() + call_context[-1].function = ex.code_qualname() if not ex.node: warning = 'executing failed to find the calling node' arguments = list(self._args_inspection_failed(args, kwargs)) @@ -191,9 +230,7 @@ def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int) -> DebugOutput: arguments = list(self._process_args(ex, args, kwargs)) return self.output_class( - filename=str(path), - lineno=lineno, - frame=function, + call_context=call_context, arguments=arguments, warning=self._show_warnings and warning, ) @@ -225,4 +262,18 @@ def _process_args(self, ex: 'Any', args: 'Any', kwargs: 'Any') -> 'Generator[Deb yield self.output_class.arg_class(value, name=name, variable=kw_arg_names.get(name)) +def _make_call_context(call_frame: 'Optional[FrameType]', trace: bool) -> 'List[DebugFrame]': + call_context: 'List[DebugFrame]' = [] + + while call_frame: + frame_info = DebugFrame.from_call_frame(call_frame) + call_context.insert(0, frame_info) + call_frame = call_frame.f_back + + if not trace: + break + + return call_context + + debug = Debug() diff --git a/requirements/testing.in b/requirements/testing.in index 6a3232a..42c09e9 100644 --- a/requirements/testing.in +++ b/requirements/testing.in @@ -2,6 +2,8 @@ coverage[toml] pytest pytest-mock pytest-pretty +pytest-tmp-files +parametrize-from-file # these packages are used in tests so install the latest version # no binaries for 3.7 asyncpg; python_version>='3.8' diff --git a/requirements/testing.txt b/requirements/testing.txt index a3b4099..5148b73 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -2,8 +2,10 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --output-file=requirements/testing.txt --resolver=backtracking requirements/testing.in +# pip-compile --output-file=requirements/testing.txt requirements/testing.in # +arrow==1.3.0 + # via inform asyncpg==0.27.0 ; python_version >= "3.8" # via -r requirements/testing.in attrs==22.2.0 @@ -14,24 +16,38 @@ click==8.1.3 # via black coverage[toml]==7.2.2 # via -r requirements/testing.in +decopatch==1.4.10 + # via parametrize-from-file exceptiongroup==1.1.3 # via pytest +greenlet==3.0.0 + # via sqlalchemy +inform==1.28 + # via nestedtext iniconfig==2.0.0 # via pytest +makefun==1.15.1 + # via decopatch markdown-it-py==2.2.0 # via rich mdurl==0.1.2 # via markdown-it-py +more-itertools==8.14.0 + # via parametrize-from-file multidict==6.0.4 ; python_version >= "3.8" # via -r requirements/testing.in mypy-extensions==1.0.0 # via black +nestedtext==3.6 + # via parametrize-from-file numpy==1.24.2 ; python_version >= "3.8" # via -r requirements/testing.in packaging==23.0 # via # black # pytest +parametrize-from-file==0.18.0 + # via -r requirements/testing.in pathspec==0.11.1 # via black platformdirs==3.2.0 @@ -45,21 +61,41 @@ pygments==2.15.0 pytest==7.2.2 # via # -r requirements/testing.in + # parametrize-from-file # pytest-mock # pytest-pretty + # pytest-tmp-files pytest-mock==3.10.0 # via -r requirements/testing.in pytest-pretty==1.2.0 # via -r requirements/testing.in +pytest-tmp-files==0.0.1 + # via -r requirements/testing.in +python-dateutil==2.8.2 + # via + # arrow + # pytest-tmp-files +pyyaml==6.0.1 + # via parametrize-from-file rich==13.3.3 # via pytest-pretty +six==1.16.0 + # via + # inform + # python-dateutil sqlalchemy==2.0.8 # via -r requirements/testing.in +tidyexc==0.10.0 + # via parametrize-from-file +toml==0.10.2 + # via parametrize-from-file tomli==2.0.1 # via # black # coverage # pytest +types-python-dateutil==2.8.19.14 + # via arrow typing-extensions==4.5.0 # via # pydantic diff --git a/tests/test_main.nt b/tests/test_main.nt new file mode 100644 index 0000000..61bba7c --- /dev/null +++ b/tests/test_main.nt @@ -0,0 +1,111 @@ +test_print_subprocess: + - + tmp_files: + test.py: + > from devtools import debug + > + > def test_func(v): + > debug('in test func', v) + > + > foobar = 'hello world' + > print('running debug...') + > debug(foobar) + > test_func(42) + > print('debug run.') + stdout: + > running debug... + > /path/to/test.py:8 + > foobar: 'hello world' (str) len=11 + > /path/to/test.py:4 test_func + > 'in test func' (str) len=12 + > v: 42 (int) + > debug run. + > + - + # Need to test the trace feature in a subprocess, because otherwise the + # stack trace would include a bunch of pytest frames. These frames would + # make the tests unreliable, because they could easily change between + # different versions of python. + + id: trace-call + tmp_files: + test.py: + > from devtools import debug + > + > def f(x): + > debug(x, trace_=True) + > g(x) + > + > def g(x): + > debug(x, trace_=True) + > + > x = 42 + > debug(x, trace_=True) + > f(x) + stdout: + > /path/to/test.py:11 + > x: 42 (int) + > /path/to/test.py:12 + > /path/to/test.py:4 f + > x: 42 (int) + > /path/to/test.py:12 + > /path/to/test.py:5 f + > /path/to/test.py:8 g + > x: 42 (int) + > + - + id: trace-format + tmp_files: + test.py: + > from devtools import debug + > + > def f(x): + > print(debug.format(x, trace_=True)) + > g(x) + > + > def g(x): + > print(debug.format(x, trace_=True)) + > + > x = 42 + > print(debug.format(x, trace_=True)) + > f(x) + stdout: + > /path/to/test.py:11 + > x: 42 (int) + > /path/to/test.py:12 + > /path/to/test.py:4 f + > x: 42 (int) + > /path/to/test.py:12 + > /path/to/test.py:5 f + > /path/to/test.py:8 g + > x: 42 (int) + > + - + id: trace-method + tmp_files: + test.py: + > from devtools import debug + > + > def f(x): + > debug.trace(x) + > g(x) + > + > def g(x): + > debug.trace(x) + > + > x = 42 + > debug.trace(x) + > f(x) + stdout: + > /path/to/test.py:11 + > x: 42 (int) + > /path/to/test.py:12 + > /path/to/test.py:4 f + > x: 42 (int) + > /path/to/test.py:12 + > /path/to/test.py:5 f + > /path/to/test.py:8 g + > x: 42 (int) + > + + diff --git a/tests/test_main.py b/tests/test_main.py index 1057313..ed4774e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,6 +5,7 @@ from subprocess import run import pytest +import parametrize_from_file as pff from devtools import Debug, debug from devtools.ansi import strip_ansi @@ -60,6 +61,28 @@ def test_print_generator(capsys): assert list(result) == [] +@pff.parametrize( + indirect=['tmp_files'], +) +@pytest.mark.xfail( + sys.platform == 'win32', + reason='Fatal Python error: _Py_HashRandomization_Init: failed to get random numbers to initialize Python', +) +def test_print_subprocess(tmp_files, stdout): + f = tmp_files / 'test.py' + p = run( + [sys.executable, str(f)], + capture_output=True, + text=True, + env={ + 'PYTHONPATH': str(Path(__file__).parents[1].resolve()), + }, + ) + assert p.stderr == '' + assert p.returncode == 0, (p.stderr, p.stdout) + assert p.stdout.replace(str(f), '/path/to/test.py') == stdout + + def test_format(): a = b'i might bite' b = 'hello this is a test' @@ -73,41 +96,6 @@ def test_format(): ) -@pytest.mark.xfail( - sys.platform == 'win32', - reason='Fatal Python error: _Py_HashRandomization_Init: failed to get random numbers to initialize Python', -) -def test_print_subprocess(tmpdir): - f = tmpdir.join('test.py') - f.write( - """\ -from devtools import debug - -def test_func(v): - debug('in test func', v) - -foobar = 'hello world' -print('running debug...') -debug(foobar) -test_func(42) -print('debug run.') - """ - ) - env = {'PYTHONPATH': str(Path(__file__).parent.parent.resolve())} - p = run([sys.executable, str(f)], capture_output=True, text=True, env=env) - assert p.stderr == '' - assert p.returncode == 0, (p.stderr, p.stdout) - assert p.stdout.replace(str(f), '/path/to/test.py') == ( - "running debug...\n" - "/path/to/test.py:8 \n" - " foobar: 'hello world' (str) len=11\n" - "/path/to/test.py:4 test_func\n" - " 'in test func' (str) len=12\n" - " v: 42 (int)\n" - "debug run.\n" - ) - - def test_odd_path(mocker): # all valid calls mocked_relative_to = mocker.patch('pathlib.Path.relative_to') From 3edf2e9a376ab5fcbe7a8069cae7f32300a3d43e Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Mon, 9 Oct 2023 15:03:03 -0400 Subject: [PATCH 2/4] try to satisfy linter --- tests/test_main.nt | 8 +++----- tests/test_main.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_main.nt b/tests/test_main.nt index 61bba7c..6ec61ee 100644 --- a/tests/test_main.nt +++ b/tests/test_main.nt @@ -22,9 +22,9 @@ test_print_subprocess: > debug run. > - - # Need to test the trace feature in a subprocess, because otherwise the - # stack trace would include a bunch of pytest frames. These frames would - # make the tests unreliable, because they could easily change between + # Need to test the trace feature in a subprocess, because otherwise the + # stack trace would include a bunch of pytest frames. These frames would + # make the tests unreliable, because they could easily change between # different versions of python. id: trace-call @@ -107,5 +107,3 @@ test_print_subprocess: > /path/to/test.py:8 g > x: 42 (int) > - - diff --git a/tests/test_main.py b/tests/test_main.py index ed4774e..5f46dd0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,8 +4,8 @@ from pathlib import Path from subprocess import run -import pytest import parametrize_from_file as pff +import pytest from devtools import Debug, debug from devtools.ansi import strip_ansi From 18e981eec03f725a8823b1163ea00c22fb7f9d5b Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Mon, 9 Oct 2023 15:53:46 -0400 Subject: [PATCH 3/4] address review comments --- devtools/debug.py | 87 +++++++++++++++--------------- requirements/testing.in | 2 - requirements/testing.txt | 38 +------------ tests/test_main.nt | 109 ------------------------------------- tests/test_main.py | 112 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 152 insertions(+), 196 deletions(-) delete mode 100644 tests/test_main.nt diff --git a/devtools/debug.py b/devtools/debug.py index c83a453..f041333 100644 --- a/devtools/debug.py +++ b/devtools/debug.py @@ -22,47 +22,6 @@ StrType = str -class DebugFrame: - __slots__ = 'function', 'path', 'lineno' - - @staticmethod - def from_call_frame(call_frame: 'FrameType') -> 'DebugFrame': - from pathlib import Path - - function = call_frame.f_code.co_name - - path = Path(call_frame.f_code.co_filename) - if path.is_absolute(): - # make the path relative - cwd = Path('.').resolve() - try: - path = path.relative_to(cwd) - except ValueError: - # happens if filename path is not within CWD - pass - - lineno = call_frame.f_lineno - - return DebugFrame(function, str(path), lineno) - - def __init__(self, function: str, path: str, lineno: int): - self.function = function - self.path = path - self.lineno = lineno - - def __str__(self) -> StrType: - return self.str() - - def str(self, highlight: bool = False) -> StrType: - if highlight: - return ( - f'{sformat(self.path, sformat.magenta)}:{sformat(self.lineno, sformat.green)} ' - f'{sformat(self.function, sformat.green, sformat.italic)}' - ) - else: - return f'{self.path}:{self.lineno} {self.function}' - - class DebugArgument: __slots__ = 'value', 'name', 'extra' @@ -129,7 +88,7 @@ def str(self, highlight: bool = False) -> StrType: else: prefix += f' ({self.warning})' - return prefix + '\n ' + '\n '.join(a.str(highlight) for a in self.arguments) + return f'{prefix}\n ' + '\n '.join(a.str(highlight) for a in self.arguments) def __str__(self) -> StrType: return self.str() @@ -202,8 +161,7 @@ def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int, trace: bool) -> try: call_frame: 'FrameType' = sys._getframe(frame_depth) except ValueError: - # "If [the given frame depth] is deeper than the call stack, - # ValueError is raised" + # "If [ValueError] is deeper than the call stack, ValueError is raised" return self.output_class( call_context=[DebugFrame(function='', path='', lineno=0)], arguments=list(self._args_inspection_failed(args, kwargs)), @@ -262,6 +220,47 @@ def _process_args(self, ex: 'Any', args: 'Any', kwargs: 'Any') -> 'Generator[Deb yield self.output_class.arg_class(value, name=name, variable=kw_arg_names.get(name)) +class DebugFrame: + __slots__ = 'function', 'path', 'lineno' + + def __init__(self, function: str, path: str, lineno: int): + self.function = function + self.path = path + self.lineno = lineno + + @staticmethod + def from_call_frame(call_frame: 'FrameType') -> 'DebugFrame': + from pathlib import Path + + function = call_frame.f_code.co_name + + path = Path(call_frame.f_code.co_filename) + if path.is_absolute(): + # make the path relative + cwd = Path('.').resolve() + try: + path = path.relative_to(cwd) + except ValueError: + # happens if filename path is not within CWD + pass + + lineno = call_frame.f_lineno + + return DebugFrame(function, str(path), lineno) + + def __str__(self) -> StrType: + return self.str() + + def str(self, highlight: bool = False) -> StrType: + if highlight: + return ( + f'{sformat(self.path, sformat.magenta)}:{sformat(self.lineno, sformat.green)} ' + f'{sformat(self.function, sformat.green, sformat.italic)}' + ) + else: + return f'{self.path}:{self.lineno} {self.function}' + + def _make_call_context(call_frame: 'Optional[FrameType]', trace: bool) -> 'List[DebugFrame]': call_context: 'List[DebugFrame]' = [] diff --git a/requirements/testing.in b/requirements/testing.in index 42c09e9..6a3232a 100644 --- a/requirements/testing.in +++ b/requirements/testing.in @@ -2,8 +2,6 @@ coverage[toml] pytest pytest-mock pytest-pretty -pytest-tmp-files -parametrize-from-file # these packages are used in tests so install the latest version # no binaries for 3.7 asyncpg; python_version>='3.8' diff --git a/requirements/testing.txt b/requirements/testing.txt index 5148b73..a3b4099 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -2,10 +2,8 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --output-file=requirements/testing.txt requirements/testing.in +# pip-compile --output-file=requirements/testing.txt --resolver=backtracking requirements/testing.in # -arrow==1.3.0 - # via inform asyncpg==0.27.0 ; python_version >= "3.8" # via -r requirements/testing.in attrs==22.2.0 @@ -16,38 +14,24 @@ click==8.1.3 # via black coverage[toml]==7.2.2 # via -r requirements/testing.in -decopatch==1.4.10 - # via parametrize-from-file exceptiongroup==1.1.3 # via pytest -greenlet==3.0.0 - # via sqlalchemy -inform==1.28 - # via nestedtext iniconfig==2.0.0 # via pytest -makefun==1.15.1 - # via decopatch markdown-it-py==2.2.0 # via rich mdurl==0.1.2 # via markdown-it-py -more-itertools==8.14.0 - # via parametrize-from-file multidict==6.0.4 ; python_version >= "3.8" # via -r requirements/testing.in mypy-extensions==1.0.0 # via black -nestedtext==3.6 - # via parametrize-from-file numpy==1.24.2 ; python_version >= "3.8" # via -r requirements/testing.in packaging==23.0 # via # black # pytest -parametrize-from-file==0.18.0 - # via -r requirements/testing.in pathspec==0.11.1 # via black platformdirs==3.2.0 @@ -61,41 +45,21 @@ pygments==2.15.0 pytest==7.2.2 # via # -r requirements/testing.in - # parametrize-from-file # pytest-mock # pytest-pretty - # pytest-tmp-files pytest-mock==3.10.0 # via -r requirements/testing.in pytest-pretty==1.2.0 # via -r requirements/testing.in -pytest-tmp-files==0.0.1 - # via -r requirements/testing.in -python-dateutil==2.8.2 - # via - # arrow - # pytest-tmp-files -pyyaml==6.0.1 - # via parametrize-from-file rich==13.3.3 # via pytest-pretty -six==1.16.0 - # via - # inform - # python-dateutil sqlalchemy==2.0.8 # via -r requirements/testing.in -tidyexc==0.10.0 - # via parametrize-from-file -toml==0.10.2 - # via parametrize-from-file tomli==2.0.1 # via # black # coverage # pytest -types-python-dateutil==2.8.19.14 - # via arrow typing-extensions==4.5.0 # via # pydantic diff --git a/tests/test_main.nt b/tests/test_main.nt deleted file mode 100644 index 6ec61ee..0000000 --- a/tests/test_main.nt +++ /dev/null @@ -1,109 +0,0 @@ -test_print_subprocess: - - - tmp_files: - test.py: - > from devtools import debug - > - > def test_func(v): - > debug('in test func', v) - > - > foobar = 'hello world' - > print('running debug...') - > debug(foobar) - > test_func(42) - > print('debug run.') - stdout: - > running debug... - > /path/to/test.py:8 - > foobar: 'hello world' (str) len=11 - > /path/to/test.py:4 test_func - > 'in test func' (str) len=12 - > v: 42 (int) - > debug run. - > - - - # Need to test the trace feature in a subprocess, because otherwise the - # stack trace would include a bunch of pytest frames. These frames would - # make the tests unreliable, because they could easily change between - # different versions of python. - - id: trace-call - tmp_files: - test.py: - > from devtools import debug - > - > def f(x): - > debug(x, trace_=True) - > g(x) - > - > def g(x): - > debug(x, trace_=True) - > - > x = 42 - > debug(x, trace_=True) - > f(x) - stdout: - > /path/to/test.py:11 - > x: 42 (int) - > /path/to/test.py:12 - > /path/to/test.py:4 f - > x: 42 (int) - > /path/to/test.py:12 - > /path/to/test.py:5 f - > /path/to/test.py:8 g - > x: 42 (int) - > - - - id: trace-format - tmp_files: - test.py: - > from devtools import debug - > - > def f(x): - > print(debug.format(x, trace_=True)) - > g(x) - > - > def g(x): - > print(debug.format(x, trace_=True)) - > - > x = 42 - > print(debug.format(x, trace_=True)) - > f(x) - stdout: - > /path/to/test.py:11 - > x: 42 (int) - > /path/to/test.py:12 - > /path/to/test.py:4 f - > x: 42 (int) - > /path/to/test.py:12 - > /path/to/test.py:5 f - > /path/to/test.py:8 g - > x: 42 (int) - > - - - id: trace-method - tmp_files: - test.py: - > from devtools import debug - > - > def f(x): - > debug.trace(x) - > g(x) - > - > def g(x): - > debug.trace(x) - > - > x = 42 - > debug.trace(x) - > f(x) - stdout: - > /path/to/test.py:11 - > x: 42 (int) - > /path/to/test.py:12 - > /path/to/test.py:4 f - > x: 42 (int) - > /path/to/test.py:12 - > /path/to/test.py:5 f - > /path/to/test.py:8 g - > x: 42 (int) - > diff --git a/tests/test_main.py b/tests/test_main.py index 5f46dd0..211360a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -61,15 +61,119 @@ def test_print_generator(capsys): assert list(result) == [] -@pff.parametrize( - indirect=['tmp_files'], +@pytest.mark.parametrize( + ['py_script', 'stdout'], [ + ( +'''\ +from devtools import debug + +def test_func(v): + debug('in test func', v) + +foobar = 'hello world' +print('running debug...') +debug(foobar) +test_func(42) +print('debug run.') +''', +'''\ +running debug... +/path/to/test.py:8 + foobar: 'hello world' (str) len=11 +/path/to/test.py:4 test_func + 'in test func' (str) len=12 + v: 42 (int) +debug run. +''', + ), ( +'''\ +from devtools import debug + +def f(x): + debug(x, trace_=True) + g(x) + +def g(x): + debug(x, trace_=True) + +x = 42 +debug(x, trace_=True) +f(x) +''', +'''\ +/path/to/test.py:11 + x: 42 (int) +/path/to/test.py:12 +/path/to/test.py:4 f + x: 42 (int) +/path/to/test.py:12 +/path/to/test.py:5 f +/path/to/test.py:8 g + x: 42 (int) +''' + ), ( +'''\ +from devtools import debug + +def f(x): + print(debug.format(x, trace_=True)) + g(x) + +def g(x): + print(debug.format(x, trace_=True)) + +x = 42 +print(debug.format(x, trace_=True)) +f(x) +''', +'''\ +/path/to/test.py:11 + x: 42 (int) +/path/to/test.py:12 +/path/to/test.py:4 f + x: 42 (int) +/path/to/test.py:12 +/path/to/test.py:5 f +/path/to/test.py:8 g + x: 42 (int) +''', + ), ( +'''\ +from devtools import debug + +def f(x): + debug.trace(x) + g(x) + +def g(x): + debug.trace(x) + +x = 42 +debug.trace(x) +f(x) +''', +'''\ +/path/to/test.py:11 + x: 42 (int) +/path/to/test.py:12 +/path/to/test.py:4 f + x: 42 (int) +/path/to/test.py:12 +/path/to/test.py:5 f +/path/to/test.py:8 g + x: 42 (int) +''' + ), + ], ) @pytest.mark.xfail( sys.platform == 'win32', reason='Fatal Python error: _Py_HashRandomization_Init: failed to get random numbers to initialize Python', ) -def test_print_subprocess(tmp_files, stdout): - f = tmp_files / 'test.py' +def test_print_subprocess(py_script, stdout, tmp_path): + f = tmp_path / 'test.py' + f.write_text(py_script) + p = run( [sys.executable, str(f)], capture_output=True, From 128a17817d9dd68c836d2d1f34a652e65582c26e Mon Sep 17 00:00:00 2001 From: Kale Kundert Date: Mon, 9 Oct 2023 16:29:46 -0400 Subject: [PATCH 4/4] try to satisfy the linter --- tests/test_main.py | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 211360a..acf9a2a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,7 +4,6 @@ from pathlib import Path from subprocess import run -import parametrize_from_file as pff import pytest from devtools import Debug, debug @@ -62,9 +61,10 @@ def test_print_generator(capsys): @pytest.mark.parametrize( - ['py_script', 'stdout'], [ + ['py_script', 'stdout'], + [ ( -'''\ + """\ from devtools import debug def test_func(v): @@ -75,8 +75,8 @@ def test_func(v): debug(foobar) test_func(42) print('debug run.') -''', -'''\ +""", + """\ running debug... /path/to/test.py:8 foobar: 'hello world' (str) len=11 @@ -84,9 +84,10 @@ def test_func(v): 'in test func' (str) len=12 v: 42 (int) debug run. -''', - ), ( -'''\ +""", + ), + ( + """\ from devtools import debug def f(x): @@ -99,8 +100,8 @@ def g(x): x = 42 debug(x, trace_=True) f(x) -''', -'''\ +""", + """\ /path/to/test.py:11 x: 42 (int) /path/to/test.py:12 @@ -110,9 +111,10 @@ def g(x): /path/to/test.py:5 f /path/to/test.py:8 g x: 42 (int) -''' - ), ( -'''\ +""", + ), + ( + """\ from devtools import debug def f(x): @@ -125,8 +127,8 @@ def g(x): x = 42 print(debug.format(x, trace_=True)) f(x) -''', -'''\ +""", + """\ /path/to/test.py:11 x: 42 (int) /path/to/test.py:12 @@ -136,9 +138,10 @@ def g(x): /path/to/test.py:5 f /path/to/test.py:8 g x: 42 (int) -''', - ), ( -'''\ +""", + ), + ( + """\ from devtools import debug def f(x): @@ -151,8 +154,8 @@ def g(x): x = 42 debug.trace(x) f(x) -''', -'''\ +""", + """\ /path/to/test.py:11 x: 42 (int) /path/to/test.py:12 @@ -162,7 +165,7 @@ def g(x): /path/to/test.py:5 f /path/to/test.py:8 g x: 42 (int) -''' +""", ), ], )