From 9c439755495b4e91ecb172ece29e727bdf0ac46b Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sun, 16 Feb 2020 22:21:15 +0200 Subject: [PATCH 01/15] Initial Formatter --- stack_data/__init__.py | 1 + stack_data/formatting.py | 185 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 stack_data/formatting.py diff --git a/stack_data/__init__.py b/stack_data/__init__.py index aa5be66..4aa21f2 100644 --- a/stack_data/__init__.py +++ b/stack_data/__init__.py @@ -1,3 +1,4 @@ __version__ = '0.0.6' from .core import Source, FrameInfo, markers_from_ranges, Options, LINE_GAP, Line, Variable, RangeInLine, RepeatedFrames, MarkerInLine, style_with_executing_node +from .formatting import Formatter diff --git a/stack_data/formatting.py b/stack_data/formatting.py new file mode 100644 index 0000000..27a04cf --- /dev/null +++ b/stack_data/formatting.py @@ -0,0 +1,185 @@ +import sys +import traceback +from types import FrameType, TracebackType +from typing import Union, Iterable + +from stack_data import style_with_executing_node, Options, Line, FrameInfo, LINE_GAP, Variable, RepeatedFrames + + +class Formatter: + def __init__( + self, *, + options=Options(), + pygmented=False, + show_executing_node=True, + pygments_formatter_cls=None, + pygments_formatter_kwargs=None, + pygments_style="monokai", + executing_node_modifier="bg:#005080", + executing_node_underline="^", + current_line_indicator="-->", + line_gap_string="(...)", + show_variables=False, + use_code_qualname=True, + show_linenos=True, + strip_leading_indent=True, + html=False, + chain=True, + collapse_repeated_frames=True, + ): + if pygmented and not options.pygments_formatter: + try: + import pygments + except ImportError: + pass + else: + if show_executing_node: + pygments_style = style_with_executing_node( + pygments_style, executing_node_modifier + ) + + if pygments_formatter_cls is None: + from pygments.formatters.terminal256 import Terminal256Formatter \ + as pygments_formatter_cls + + options.pygments_formatter = pygments_formatter_cls( + style=pygments_style, + **pygments_formatter_kwargs or {}, + ) + + self.pygmented = pygmented + self.show_executing_node = show_executing_node + assert len(executing_node_underline) == 1 + self.executing_node_underline = executing_node_underline + self.current_line_indicator = current_line_indicator or "" + self.line_gap_string = line_gap_string + self.show_variables = show_variables + self.show_linenos = show_linenos + self.use_code_qualname = use_code_qualname + self.strip_leading_indent = strip_leading_indent + self.html = html + self.chain = chain + self.options = options + self.collapse_repeated_frames = collapse_repeated_frames + + def set_hook(self): + def excepthook(_etype, evalue, _tb): + self.print_exception(evalue) + + sys.excepthook = excepthook + + def print_exception(self, e=None, *, file=None): + if file is None: + file = sys.stderr + for line in self.format_exception(e): + print(line, file=file, end="") + + def format_exception(self, e=None): + if e is None: + e = sys.exc_info()[1] + + if self.chain: + if e.__cause__ is not None: + yield from self.format_exception(e.__cause__) + yield traceback._cause_message + elif (e.__context__ is not None + and not e.__suppress_context__): + yield from self.format_exception(e.__context__) + yield traceback._context_message + + yield 'Traceback (most recent call last):\n' + + yield from self.format_stack_data( + FrameInfo.stack_data( + e.__traceback__, + self.options, + collapse_repeated_frames=self.collapse_repeated_frames, + ) + ) + + yield from traceback.format_exception_only(type(e), e) + + def format_stack_data(self, stack: Iterable[Union[FrameInfo, RepeatedFrames]]): + for item in stack: + if isinstance(item, FrameInfo): + yield from self.format_frame(item) + else: + return self.format_repeated_frames(item) + + def format_repeated_frames(self, repeated_frames: RepeatedFrames): + return ' [... skipping similar frames: {}]\n'.format( + repeated_frames.description + ) + + def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]): + if not isinstance(frame, FrameInfo): + frame = FrameInfo(frame, self.options) + + yield self.format_frame_header(frame) + + for line in frame.lines: + if isinstance(line, Line): + yield self.format_line(line) + else: + assert line is LINE_GAP + yield self.line_gap_string + "\n" + + if self.show_variables: + yield from self.format_variables(frame) + + def format_frame_header(self, frame_info: FrameInfo): + return ' File "{frame_info.filename}", line {frame_info.lineno}, in {name}\n'.format( + frame_info=frame_info, + name=( + frame_info.executing.code_qualname() + if self.use_code_qualname else + frame_info.code.co_name + ), + ) + + def format_line(self, line: Line): + result = "" + if self.current_line_indicator: + if line.is_current: + result = self.current_line_indicator + else: + result = " " * len(self.current_line_indicator) + result += " " + + if self.show_linenos: + result += "{:4} | ".format(line.lineno) + + result = result or " " + + prefix = result + + result += line.render( + pygmented=self.pygmented, + escape_html=self.html, + strip_leading_indent=self.strip_leading_indent, + ) + "\n" + + if self.show_executing_node and not self.pygmented: + for line_range in line.executing_node_ranges: + start = line_range.start - line.leading_indent + end = line_range.end - line.leading_indent + result += ( + " " * (start + len(prefix)) + + self.executing_node_underline * (end - start) + + "\n" + ) + + return result + + def format_variables(self, frame_info: FrameInfo): + for var in sorted(frame_info.variables, key=lambda v: v.name): + yield self.format_variable(var) + "\n" + + def format_variable(self, var: Variable): + return "{} = {}".format( + var.name, + self.format_variable_value(var.value), + ) + + def format_variable_value(self, value): + return repr(value) From beecad583847de997e097eff6084af9781c871ba Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sun, 16 Feb 2020 22:27:38 +0200 Subject: [PATCH 02/15] Remove trailing comma for 3.5 --- stack_data/formatting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_data/formatting.py b/stack_data/formatting.py index 27a04cf..e2caa6f 100644 --- a/stack_data/formatting.py +++ b/stack_data/formatting.py @@ -25,7 +25,7 @@ def __init__( strip_leading_indent=True, html=False, chain=True, - collapse_repeated_frames=True, + collapse_repeated_frames=True ): if pygmented and not options.pygments_formatter: try: From 08bf137546ab256b13d65c419b8c294f4d5f7945 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Wed, 12 Feb 2020 18:16:02 +0300 Subject: [PATCH 03/15] Moved the metadata into setup.cfg. + upgraded setuptools + now using pep517 for build + ignored some stuff + made coverage compute branch coverage + made pytest emit a file with the report --- .gitignore | 8 +++++- .travis.yml | 12 ++++++-- make_release.sh | 22 +++++++++++++++ pyproject.toml | 7 +++++ setup.cfg | 28 +++++++++++++++++++ setup.py | 62 ++---------------------------------------- stack_data/__init__.py | 2 +- 7 files changed, 76 insertions(+), 65 deletions(-) create mode 100755 make_release.sh create mode 100644 pyproject.toml create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index 1521c8b..6b47007 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -dist +/build +/dist +/*.egg-info +/stack_data/version.py +*.pyc +*.pyo +__pycache__ diff --git a/.travis.yml b/.travis.yml index 482e509..9c41082 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,12 +13,18 @@ env: - STACK_DATA_SLOW_TESTS=1 - COVERALLS_PARALLEL=true +before_install: + - pip install --upgrade coveralls setuptools>=44 setuptools_scm>=3.4.3 pep517 + install: - - pip install coveralls - - pip install .[tests] + - python3 -m pep517.build -b . + - ls -l + - export WHLNAME=./dist/stack_data-0.CI-py3-none-any.whl + - mv ./dist/*.whl $WHLNAME + - pip install --upgrade "$WHLNAME[tests]" script: - - coverage run --include='stack_data/*' -m pytest + - coverage run --branch --include='stack_data/*' -m pytest --junitxml=./rspec.xml - coverage report -m after_success: diff --git a/make_release.sh b/make_release.sh new file mode 100755 index 0000000..475c1bc --- /dev/null +++ b/make_release.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -eux; + +if [${1+x}]; then + if [[ ${1} =~ ^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?$ ]]; then + :; + else + echo "Not a valid release tag."; + exit 1; + fi; +else + echo "${0} .."; + exit 1; +fi; + +export TAG="v${1}"; +echo "$TAG" +git tag "${TAG}"; +git push origin master "${TAG}"; +rm -rf ./build ./dist; +python3 -m pep517.build -b .; +twine upload ./dist/*.whl; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e19fe75 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "stack_data/version.py" +write_to_template = "__version__ = '{version}'" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5ae26bd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = stack_data +author = Alex Hall +author_email = alex.mojaki@gmail.com +license = MIT +description = Extract data from python stack frames and tracebacks for informative displays +url = http://github.com/alexmojaki/stack_data +long_description = file: README.md +long_description_content_type = text/markdown +classifiers = + Intended Audience :: Developers + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Topic :: Software Development :: Debuggers + +[options] +packages = stack_data +install_requires = executing; asttokens; pure_eval +setup_requires = setuptools>=44; wheel; setuptools_scm[toml]>=3.4.3 +include_package_data = True +tests_require = pytest; typeguard; pygments + +[options.extras_require] +tests = pytest; typeguard; pygments; pep517 diff --git a/setup.py b/setup.py index 6fb0cc9..7f1a176 100644 --- a/setup.py +++ b/setup.py @@ -1,62 +1,4 @@ -import os -import re -from io import open - from setuptools import setup -package = 'stack_data' -dirname = os.path.dirname(__file__) - - -def file_to_string(*path): - with open(os.path.join(dirname, *path), encoding='utf8') as f: - return f.read() - - -# __version__ is defined inside the package, but we can't import -# it because it imports dependencies which may not be installed yet, -# so we extract it manually -contents = file_to_string(package, '__init__.py') -__version__ = re.search(r"__version__ = '([.\d]+)'", contents).group(1) - -install_requires = [ - 'executing', - 'asttokens', - 'pure_eval', -] - - -tests_require = [ - 'pytest', - 'typeguard', - 'pygments', -] - -setup( - name=package, - version=__version__, - description="Extract data from python stack frames and tracebacks for informative displays", - # long_description=file_to_string('README.md'), - # long_description_content_type='text/markdown', - url='http://github.com/alexmojaki/' + package, - author='Alex Hall', - author_email='alex.mojaki@gmail.com', - license='MIT', - include_package_data=True, - packages=[package], - install_requires=install_requires, - tests_require=tests_require, - extras_require={ - 'tests': tests_require, - }, - classifiers=[ - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Topic :: Software Development :: Debuggers', - ], -) +if __name__ == "__main__": + setup() diff --git a/stack_data/__init__.py b/stack_data/__init__.py index aa5be66..23be441 100644 --- a/stack_data/__init__.py +++ b/stack_data/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.0.6' +from .version import __version__ from .core import Source, FrameInfo, markers_from_ranges, Options, LINE_GAP, Line, Variable, RangeInLine, RepeatedFrames, MarkerInLine, style_with_executing_node From 0ae5e843936e6efd0621ed07ca2b96d8cfe7b1ee Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 6 Apr 2020 21:27:35 +0200 Subject: [PATCH 04/15] Test Formatter API --- stack_data/core.py | 8 ++-- stack_data/formatting.py | 73 +++++++++++++++++------------- tests/samples/formatter_example.py | 58 ++++++++++++++++++++++++ tests/test_formatter.py | 68 ++++++++++++++++++++++++++++ tests/utils.py | 16 +++++++ 5 files changed, 189 insertions(+), 34 deletions(-) create mode 100644 tests/samples/formatter_example.py create mode 100644 tests/test_formatter.py create mode 100644 tests/utils.py diff --git a/stack_data/core.py b/stack_data/core.py index f05753b..5ddbe9c 100644 --- a/stack_data/core.py +++ b/stack_data/core.py @@ -8,8 +8,8 @@ from types import FrameType, CodeType, TracebackType from typing import ( Iterator, List, Tuple, Optional, NamedTuple, - Any, Iterable, Callable, Union -) + Any, Iterable, Callable, Union, + Sequence) from typing import Mapping import executing @@ -50,7 +50,7 @@ class Variable( NamedTuple('_Variable', [('name', str), - ('nodes', List[ast.AST]), + ('nodes', Sequence[ast.AST]), ('value', Any)]) ): """ @@ -787,6 +787,8 @@ def variables(self) -> List[Variable]: nodes, values = zip(*group) value = values[0] text = get_text(nodes[0]) + if not text: + continue result.append(Variable(text, nodes, value)) return result diff --git a/stack_data/formatting.py b/stack_data/formatting.py index e2caa6f..aef3660 100644 --- a/stack_data/formatting.py +++ b/stack_data/formatting.py @@ -1,3 +1,4 @@ +import inspect import sys import traceback from types import FrameType, TracebackType @@ -28,25 +29,20 @@ def __init__( collapse_repeated_frames=True ): if pygmented and not options.pygments_formatter: - try: - import pygments - except ImportError: - pass - else: - if show_executing_node: - pygments_style = style_with_executing_node( - pygments_style, executing_node_modifier - ) - - if pygments_formatter_cls is None: - from pygments.formatters.terminal256 import Terminal256Formatter \ - as pygments_formatter_cls - - options.pygments_formatter = pygments_formatter_cls( - style=pygments_style, - **pygments_formatter_kwargs or {}, + if show_executing_node: + pygments_style = style_with_executing_node( + pygments_style, executing_node_modifier ) + if pygments_formatter_cls is None: + from pygments.formatters.terminal256 import Terminal256Formatter \ + as pygments_formatter_cls + + options.pygments_formatter = pygments_formatter_cls( + style=pygments_style, + **pygments_formatter_kwargs or {}, + ) + self.pygmented = pygmented self.show_executing_node = show_executing_node assert len(executing_node_underline) == 1 @@ -69,12 +65,21 @@ def excepthook(_etype, evalue, _tb): sys.excepthook = excepthook def print_exception(self, e=None, *, file=None): + self.print_lines(self.format_exception(e), file=file) + + def print_stack(self, frame_or_tb=None, *, file=None): + if frame_or_tb is None: + frame_or_tb = inspect.currentframe().f_back + + self.print_lines(self.format_stack(frame_or_tb), file=file) + + def print_lines(self, lines, *, file=None): if file is None: file = sys.stderr - for line in self.format_exception(e): + for line in lines: print(line, file=file, end="") - def format_exception(self, e=None): + def format_exception(self, e=None) -> Iterable[str]: if e is None: e = sys.exc_info()[1] @@ -88,30 +93,36 @@ def format_exception(self, e=None): yield traceback._context_message yield 'Traceback (most recent call last):\n' + yield from self.format_stack(e.__traceback__) + yield from traceback.format_exception_only(type(e), e) + + def format_stack(self, frame_or_tb=None) -> Iterable[str]: + if frame_or_tb is None: + frame_or_tb = inspect.currentframe().f_back yield from self.format_stack_data( FrameInfo.stack_data( - e.__traceback__, + frame_or_tb, self.options, collapse_repeated_frames=self.collapse_repeated_frames, ) ) - yield from traceback.format_exception_only(type(e), e) - - def format_stack_data(self, stack: Iterable[Union[FrameInfo, RepeatedFrames]]): + def format_stack_data( + self, stack: Iterable[Union[FrameInfo, RepeatedFrames]] + ) -> Iterable[str]: for item in stack: if isinstance(item, FrameInfo): yield from self.format_frame(item) else: - return self.format_repeated_frames(item) + yield self.format_repeated_frames(item) - def format_repeated_frames(self, repeated_frames: RepeatedFrames): + def format_repeated_frames(self, repeated_frames: RepeatedFrames) -> str: return ' [... skipping similar frames: {}]\n'.format( repeated_frames.description ) - def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]): + def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]) -> Iterable[str]: if not isinstance(frame, FrameInfo): frame = FrameInfo(frame, self.options) @@ -127,7 +138,7 @@ def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]): if self.show_variables: yield from self.format_variables(frame) - def format_frame_header(self, frame_info: FrameInfo): + def format_frame_header(self, frame_info: FrameInfo) -> str: return ' File "{frame_info.filename}", line {frame_info.lineno}, in {name}\n'.format( frame_info=frame_info, name=( @@ -137,7 +148,7 @@ def format_frame_header(self, frame_info: FrameInfo): ), ) - def format_line(self, line: Line): + def format_line(self, line: Line) -> str: result = "" if self.current_line_indicator: if line.is_current: @@ -171,15 +182,15 @@ def format_line(self, line: Line): return result - def format_variables(self, frame_info: FrameInfo): + def format_variables(self, frame_info: FrameInfo) -> Iterable[str]: for var in sorted(frame_info.variables, key=lambda v: v.name): yield self.format_variable(var) + "\n" - def format_variable(self, var: Variable): + def format_variable(self, var: Variable) -> str: return "{} = {}".format( var.name, self.format_variable_value(var.value), ) - def format_variable_value(self, value): + def format_variable_value(self, value) -> str: return repr(value) diff --git a/tests/samples/formatter_example.py b/tests/samples/formatter_example.py new file mode 100644 index 0000000..57fe848 --- /dev/null +++ b/tests/samples/formatter_example.py @@ -0,0 +1,58 @@ +import inspect + +from stack_data import Formatter + + +def foo(n=5): + if n > 0: + return foo(n - 1) + x = 1 + lst = ( + [ + x, + ] + + [] + + [] + + [] + + [] + + [] + ) + try: + return int(f"{lst}") + except: + try: + return 1 / 0 + except Exception as e: + raise TypeError from e + + +def bar(): + exec("foo()") + + +def print_stack1(formatter): + print_stack2(formatter) + + +def print_stack2(formatter): + formatter.print_stack() + + +def format_stack1(formatter): + return format_stack2(formatter) + + +def format_stack2(formatter): + return list(formatter.format_stack()) + + +def format_frame(formatter): + frame = inspect.currentframe() + return list(formatter.format_frame(frame)) + + +if __name__ == '__main__': + try: + bar() + except Exception: + Formatter(show_variables=True).print_exception() diff --git a/tests/test_formatter.py b/tests/test_formatter.py new file mode 100644 index 0000000..f679684 --- /dev/null +++ b/tests/test_formatter.py @@ -0,0 +1,68 @@ +import os +import re +import sys +from contextlib import contextmanager + +from stack_data import Formatter, FrameInfo +from tests.utils import compare_to_file + + +class BaseFormatter(Formatter): + def format_frame_header(self, frame_info: FrameInfo) -> str: + # noinspection PyPropertyAccess + frame_info.filename = os.path.basename(frame_info.filename) + return super().format_frame_header(frame_info) + + def format_variable_value(self, value) -> str: + result = super().format_variable_value(value) + result = re.sub(r'0x\w+', '0xABC', result) + return result + + +class MyFormatter(BaseFormatter): + def format_frame(self, frame): + if not frame.filename.endswith(("formatter_example.py", "")): + return + yield from super().format_frame(frame) + + +def test_example(capsys): + from .samples.formatter_example import bar, print_stack1, format_stack1, format_frame + + @contextmanager + def check_example(name): + yield + stderr = capsys.readouterr().err + compare_to_file(stderr, name) + + with check_example("variables"): + try: + bar() + except Exception: + MyFormatter(show_variables=True).print_exception() + + with check_example("pygmented"): + try: + bar() + except Exception: + MyFormatter(pygmented=True).print_exception() + + with check_example("plain"): + MyFormatter().set_hook() + try: + bar() + except Exception: + sys.excepthook(*sys.exc_info()) + + with check_example("print_stack"): + print_stack1(MyFormatter()) + + with check_example("format_stack"): + formatter = MyFormatter() + formatted = format_stack1(formatter) + formatter.print_lines(formatted) + + with check_example("format_frame"): + formatter = BaseFormatter() + formatted = format_frame(formatter) + formatter.print_lines(formatted) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..175c81e --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,16 @@ +import os + +from littleutils import string_to_file, file_to_string + + +def compare_to_file(text, name): + filename = os.path.join( + os.path.dirname(__file__), + 'golden_files', + name + '.txt', + ) + if os.environ.get('FIX_STACK_DATA_TESTS'): + string_to_file(text, filename) + else: + expected_output = file_to_string(filename) + assert text == expected_output From 2b548ef74b523b80b7b995a51aea2e890deb0c6a Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 6 Apr 2020 21:27:50 +0200 Subject: [PATCH 05/15] Golden files --- tests/golden_files/format_frame.txt | 5 ++ tests/golden_files/format_stack.txt | 8 ++++ tests/golden_files/plain.txt | 60 ++++++++++++++++++++++++ tests/golden_files/print_stack.txt | 8 ++++ tests/golden_files/pygmented.txt | 54 ++++++++++++++++++++++ tests/golden_files/variables.txt | 72 +++++++++++++++++++++++++++++ 6 files changed, 207 insertions(+) create mode 100644 tests/golden_files/format_frame.txt create mode 100644 tests/golden_files/format_stack.txt create mode 100644 tests/golden_files/plain.txt create mode 100644 tests/golden_files/print_stack.txt create mode 100644 tests/golden_files/pygmented.txt create mode 100644 tests/golden_files/variables.txt diff --git a/tests/golden_files/format_frame.txt b/tests/golden_files/format_frame.txt new file mode 100644 index 0000000..0d4364e --- /dev/null +++ b/tests/golden_files/format_frame.txt @@ -0,0 +1,5 @@ + File "formatter_example.py", line 51, in format_frame + 49 | def format_frame(formatter): + 50 | frame = inspect.currentframe() +--> 51 | return list(formatter.format_frame(frame)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/golden_files/format_stack.txt b/tests/golden_files/format_stack.txt new file mode 100644 index 0000000..5949510 --- /dev/null +++ b/tests/golden_files/format_stack.txt @@ -0,0 +1,8 @@ + File "formatter_example.py", line 42, in format_stack1 + 41 | def format_stack1(formatter): +--> 42 | return format_stack2(formatter) + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "formatter_example.py", line 46, in format_stack2 + 45 | def format_stack2(formatter): +--> 46 | return list(formatter.format_stack()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/golden_files/plain.txt b/tests/golden_files/plain.txt new file mode 100644 index 0000000..66ca3b6 --- /dev/null +++ b/tests/golden_files/plain.txt @@ -0,0 +1,60 @@ +Traceback (most recent call last): + File "formatter_example.py", line 21, in foo + 9 | x = 1 + 10 | lst = ( + 11 | [ + 12 | x, +(...) + 18 | + [] + 19 | ) + 20 | try: +--> 21 | return int(f"{lst}") + ^^^^^^^^^^^^^ + 22 | except: +ValueError: invalid literal for int() with base 10: '[1]' + +During handling of the above excption, another exception occurred: + +Traceback (most recent call last): + File "formatter_example.py", line 24, in foo + 21 | return int(f"{lst}") + 22 | except: + 23 | try: +--> 24 | return 1 / 0 + ^^^^^ + 25 | except Exception as e: +ZeroDivisionError: division by zero + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "formatter_example.py", line 30, in bar + 29 | def bar(): +--> 30 | exec("foo()") + ^^^^^^^^^^^^^ + File "", line 1, in + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 | if n > 0: +--> 8 | return foo(n - 1) + ^^^^^^^^^^ + 9 | x = 1 + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 | if n > 0: +--> 8 | return foo(n - 1) + ^^^^^^^^^^ + 9 | x = 1 + [... skipping similar frames: foo at line 8 (2 times)] + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 | if n > 0: +--> 8 | return foo(n - 1) + ^^^^^^^^^^ + 9 | x = 1 + File "formatter_example.py", line 26, in foo + 23 | try: + 24 | return 1 / 0 + 25 | except Exception as e: +--> 26 | raise TypeError from e +TypeError diff --git a/tests/golden_files/print_stack.txt b/tests/golden_files/print_stack.txt new file mode 100644 index 0000000..12f1f40 --- /dev/null +++ b/tests/golden_files/print_stack.txt @@ -0,0 +1,8 @@ + File "formatter_example.py", line 34, in print_stack1 + 33 | def print_stack1(formatter): +--> 34 | print_stack2(formatter) + ^^^^^^^^^^^^^^^^^^^^^^^ + File "formatter_example.py", line 38, in print_stack2 + 37 | def print_stack2(formatter): +--> 38 | formatter.print_stack() + ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/golden_files/pygmented.txt b/tests/golden_files/pygmented.txt new file mode 100644 index 0000000..421ce70 --- /dev/null +++ b/tests/golden_files/pygmented.txt @@ -0,0 +1,54 @@ +Traceback (most recent call last): + File "formatter_example.py", line 21, in foo + 9 | x = 1 + 10 | lst = ( + 11 |  [ + 12 |  x, +(...) + 18 |  + [] + 19 | ) + 20 | try: +--> 21 |  return int(f"{lst}") + 22 | except: +ValueError: invalid literal for int() with base 10: '[1]' + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "formatter_example.py", line 24, in foo + 21 |  return int(f"{lst}") + 22 | except: + 23 |  try: +--> 24 |  return 1 / 0 + 25 |  except Exception as e: +ZeroDivisionError: division by zero + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "formatter_example.py", line 30, in bar + 29 | def bar(): +--> 30 |  exec("foo()") + File "", line 1, in + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 |  if n > 0: +--> 8 |  return foo(n - 1) + 9 |  x = 1 + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 |  if n > 0: +--> 8 |  return foo(n - 1) + 9 |  x = 1 + [... skipping similar frames: foo at line 8 (2 times)] + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 |  if n > 0: +--> 8 |  return foo(n - 1) + 9 |  x = 1 + File "formatter_example.py", line 26, in foo + 23 | try: + 24 |  return 1 / 0 + 25 | except Exception as e: +--> 26 |  raise TypeError from e +TypeError diff --git a/tests/golden_files/variables.txt b/tests/golden_files/variables.txt new file mode 100644 index 0000000..6c800e1 --- /dev/null +++ b/tests/golden_files/variables.txt @@ -0,0 +1,72 @@ +Traceback (most recent call last): + File "formatter_example.py", line 21, in foo + 9 | x = 1 + 10 | lst = ( + 11 | [ + 12 | x, +(...) + 18 | + [] + 19 | ) + 20 | try: +--> 21 | return int(f"{lst}") + ^^^^^^^^^^^^^ + 22 | except: +lst = [1] +n = 0 +x = 1 +ValueError: invalid literal for int() with base 10: '[1]' + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "formatter_example.py", line 24, in foo + 21 | return int(f"{lst}") + 22 | except: + 23 | try: +--> 24 | return 1 / 0 + ^^^^^ + 25 | except Exception as e: +lst = [1] +n = 0 +x = 1 +ZeroDivisionError: division by zero + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "formatter_example.py", line 30, in bar + 29 | def bar(): +--> 30 | exec("foo()") + ^^^^^^^^^^^^^ + File "", line 1, in + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 | if n > 0: +--> 8 | return foo(n - 1) + ^^^^^^^^^^ + 9 | x = 1 +n = 5 + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 | if n > 0: +--> 8 | return foo(n - 1) + ^^^^^^^^^^ + 9 | x = 1 +n = 4 + [... skipping similar frames: foo at line 8 (2 times)] + File "formatter_example.py", line 8, in foo + 6 | def foo(n=5): + 7 | if n > 0: +--> 8 | return foo(n - 1) + ^^^^^^^^^^ + 9 | x = 1 +n = 1 + File "formatter_example.py", line 26, in foo + 23 | try: + 24 | return 1 / 0 + 25 | except Exception as e: +--> 26 | raise TypeError from e +lst = [1] +n = 0 +x = 1 +TypeError From 67aabff661f4552cca10a804c33873b81d67eb78 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 6 Apr 2020 21:30:09 +0200 Subject: [PATCH 06/15] Littleutils for tests --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6fb0cc9..4310ddd 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ def file_to_string(*path): 'pytest', 'typeguard', 'pygments', + 'littleutils', ] setup( From 06fac6208fcc1eba274912628fd4a416f1468917 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 6 Apr 2020 21:35:37 +0200 Subject: [PATCH 07/15] Minor test fixes --- tests/golden_files/plain.txt | 6 +++--- tests/golden_files/pygmented.txt | 4 ++-- tests/golden_files/variables.txt | 4 ++-- tests/samples/formatter_example.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/golden_files/plain.txt b/tests/golden_files/plain.txt index 66ca3b6..18eceb9 100644 --- a/tests/golden_files/plain.txt +++ b/tests/golden_files/plain.txt @@ -8,16 +8,16 @@ Traceback (most recent call last): 18 | + [] 19 | ) 20 | try: ---> 21 | return int(f"{lst}") +--> 21 | return int(str(lst)) ^^^^^^^^^^^^^ 22 | except: ValueError: invalid literal for int() with base 10: '[1]' -During handling of the above excption, another exception occurred: +During handling of the above exception, another exception occurred: Traceback (most recent call last): File "formatter_example.py", line 24, in foo - 21 | return int(f"{lst}") + 21 | return int(str(lst)) 22 | except: 23 | try: --> 24 | return 1 / 0 diff --git a/tests/golden_files/pygmented.txt b/tests/golden_files/pygmented.txt index 421ce70..920dabc 100644 --- a/tests/golden_files/pygmented.txt +++ b/tests/golden_files/pygmented.txt @@ -8,7 +8,7 @@ Traceback (most recent call last): 18 |  + [] 19 | ) 20 | try: ---> 21 |  return int(f"{lst}") +--> 21 |  return int(str(lst)) 22 | except: ValueError: invalid literal for int() with base 10: '[1]' @@ -16,7 +16,7 @@ During handling of the above exception, another exception occurred: Traceback (most recent call last): File "formatter_example.py", line 24, in foo - 21 |  return int(f"{lst}") + 21 |  return int(str(lst)) 22 | except: 23 |  try: --> 24 |  return 1 / 0 diff --git a/tests/golden_files/variables.txt b/tests/golden_files/variables.txt index 6c800e1..f27ab19 100644 --- a/tests/golden_files/variables.txt +++ b/tests/golden_files/variables.txt @@ -8,7 +8,7 @@ Traceback (most recent call last): 18 | + [] 19 | ) 20 | try: ---> 21 | return int(f"{lst}") +--> 21 | return int(str(lst)) ^^^^^^^^^^^^^ 22 | except: lst = [1] @@ -20,7 +20,7 @@ During handling of the above exception, another exception occurred: Traceback (most recent call last): File "formatter_example.py", line 24, in foo - 21 | return int(f"{lst}") + 21 | return int(str(lst)) 22 | except: 23 | try: --> 24 | return 1 / 0 diff --git a/tests/samples/formatter_example.py b/tests/samples/formatter_example.py index 57fe848..f3e6f80 100644 --- a/tests/samples/formatter_example.py +++ b/tests/samples/formatter_example.py @@ -18,7 +18,7 @@ def foo(n=5): + [] ) try: - return int(f"{lst}") + return int(str(lst)) except: try: return 1 / 0 From 7db074901c1073fca7fd6402a584430f85f6c78e Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Tue, 7 Apr 2020 20:13:31 +0200 Subject: [PATCH 08/15] Add newline to version.py for PEP8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e19fe75..8288d78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,4 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "stack_data/version.py" -write_to_template = "__version__ = '{version}'" +write_to_template = "__version__ = '{version}'\n" From ae01787190935aa6bcd3625edb0500b35865930d Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Tue, 7 Apr 2020 20:13:52 +0200 Subject: [PATCH 09/15] Handle missing version.py --- stack_data/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/stack_data/__init__.py b/stack_data/__init__.py index ffd958f..e9bc429 100644 --- a/stack_data/__init__.py +++ b/stack_data/__init__.py @@ -1,4 +1,9 @@ -from .version import __version__ - -from .core import Source, FrameInfo, markers_from_ranges, Options, LINE_GAP, Line, Variable, RangeInLine, RepeatedFrames, MarkerInLine, style_with_executing_node +from .core import Source, FrameInfo, markers_from_ranges, Options, LINE_GAP, Line, Variable, RangeInLine, \ + RepeatedFrames, MarkerInLine, style_with_executing_node from .formatting import Formatter + +try: + from .version import __version__ +except ImportError: + # version.py is auto-generated with the git tag when building + __version__ = "???" From 0303449f41eab7e4064e301bb9eb0d046ba45a84 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Tue, 7 Apr 2020 20:14:18 +0200 Subject: [PATCH 10/15] Fix check for version argument --- make_release.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/make_release.sh b/make_release.sh index 475c1bc..66a64cd 100755 --- a/make_release.sh +++ b/make_release.sh @@ -1,20 +1,21 @@ #!/usr/bin/env bash set -eux; -if [${1+x}]; then +if [ -z "${1+x}" ]; then + set +x + echo Provide a version argument + echo "${0} .."; + exit 1; +else if [[ ${1} =~ ^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?$ ]]; then :; else echo "Not a valid release tag."; exit 1; fi; -else - echo "${0} .."; - exit 1; fi; export TAG="v${1}"; -echo "$TAG" git tag "${TAG}"; git push origin master "${TAG}"; rm -rf ./build ./dist; From 3eb0c992d476c35bcdd930cc622078f21bed86a8 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sat, 30 May 2020 16:41:06 +0200 Subject: [PATCH 11/15] Remove semicolons from make_release --- make_release.sh | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/make_release.sh b/make_release.sh index 66a64cd..2bf7c9a 100755 --- a/make_release.sh +++ b/make_release.sh @@ -1,23 +1,23 @@ #!/usr/bin/env bash -set -eux; +set -eux if [ -z "${1+x}" ]; then set +x echo Provide a version argument - echo "${0} .."; - exit 1; + echo "${0} .." + exit 1 else if [[ ${1} =~ ^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?$ ]]; then - :; + : else - echo "Not a valid release tag."; - exit 1; - fi; -fi; + echo "Not a valid release tag." + exit 1 + fi +fi -export TAG="v${1}"; -git tag "${TAG}"; -git push origin master "${TAG}"; -rm -rf ./build ./dist; -python3 -m pep517.build -b .; -twine upload ./dist/*.whl; +export TAG="v${1}" +git tag "${TAG}" +git push origin master "${TAG}" +rm -rf ./build ./dist +python3 -m pep517.build -b . +twine upload ./dist/*.whl From da73792d5cae8c467715a3704cc0ed0304b50508 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sat, 30 May 2020 16:41:43 +0200 Subject: [PATCH 12/15] Update make_release --- make_release.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/make_release.sh b/make_release.sh index 2bf7c9a..228f628 100755 --- a/make_release.sh +++ b/make_release.sh @@ -1,13 +1,18 @@ #!/usr/bin/env bash set -eux -if [ -z "${1+x}" ]; then +# Ensure that there are no uncommitted changes +# which would mess up using the git tag as a version +[ -z "$(git status --porcelain)" ] + +if [ -z "${1+x}" ] +then set +x echo Provide a version argument echo "${0} .." exit 1 else - if [[ ${1} =~ ^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?$ ]]; then + if [[ ${1} =~ ^([0-9]+)(\.[0-9]+)?(\.[0-9]+)?$ ]]; then : else echo "Not a valid release tag." @@ -15,6 +20,8 @@ else fi fi +tox -p auto + export TAG="v${1}" git tag "${TAG}" git push origin master "${TAG}" From c3826ae086ede463444bb9a22d34475d60c3ce05 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sat, 30 May 2020 16:42:45 +0200 Subject: [PATCH 13/15] Simplify install stage on travis --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c41082..610a584 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,11 +17,7 @@ before_install: - pip install --upgrade coveralls setuptools>=44 setuptools_scm>=3.4.3 pep517 install: - - python3 -m pep517.build -b . - - ls -l - - export WHLNAME=./dist/stack_data-0.CI-py3-none-any.whl - - mv ./dist/*.whl $WHLNAME - - pip install --upgrade "$WHLNAME[tests]" + - pip install '.[tests]' script: - coverage run --branch --include='stack_data/*' -m pytest --junitxml=./rspec.xml From 6bf590cfdaed6309eeb47f8612a4b53a4361af3d Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sat, 30 May 2020 22:38:19 +0200 Subject: [PATCH 14/15] Remove asserts and test 3.9 --- .travis.yml | 1 + setup.cfg | 1 + stack_data/core.py | 8 ++++---- stack_data/formatting.py | 8 ++++++-- stack_data/utils.py | 9 ++++++++- tox.ini | 2 +- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 610a584..1a67d6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - 3.6 - 3.7 - 3.8-dev + - 3.9-dev env: global: diff --git a/setup.cfg b/setup.cfg index ede00ad..e9ac751 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 License :: OSI Approved :: MIT License Operating System :: OS Independent Topic :: Software Development :: Debuggers diff --git a/stack_data/core.py b/stack_data/core.py index 5ddbe9c..2e9b8fa 100644 --- a/stack_data/core.py +++ b/stack_data/core.py @@ -19,7 +19,7 @@ from stack_data.utils import ( truncate, unique_in_order, line_range, frame_and_lineno, iter_stack, collapse_repeated, group_by_key_func, - cached_property, is_frame, _pygmented_with_ranges) + cached_property, is_frame, _pygmented_with_ranges, assert_) RangeInLine = NamedTuple('RangeInLine', [('start', int), @@ -333,8 +333,7 @@ def render( common to all lines in this frame will be excluded. """ if pygmented: - assert not markers, "Cannot use pygmented with markers" - assert self.frame_info.options.pygments_formatter, "Must set a pygments formatter in Options" + assert_(not markers, ValueError("Cannot use pygmented with markers")) start_line, lines = self.frame_info._pygmented_scope_lines result = lines[self.lineno - start_line] if strip_leading_indent: @@ -721,7 +720,8 @@ def _pygmented_scope_lines(self) -> Optional[Tuple[int, List[str]]]: formatter = self.options.pygments_formatter scope = self.scope - assert scope and formatter + assert_(formatter, ValueError("Must set a pygments formatter in Options")) + assert_(scope) if isinstance(formatter, HtmlFormatter): formatter.nowrap = True diff --git a/stack_data/formatting.py b/stack_data/formatting.py index aef3660..ccb06b1 100644 --- a/stack_data/formatting.py +++ b/stack_data/formatting.py @@ -5,6 +5,7 @@ from typing import Union, Iterable from stack_data import style_with_executing_node, Options, Line, FrameInfo, LINE_GAP, Variable, RepeatedFrames +from stack_data.utils import assert_ class Formatter: @@ -45,7 +46,10 @@ def __init__( self.pygmented = pygmented self.show_executing_node = show_executing_node - assert len(executing_node_underline) == 1 + assert_( + len(executing_node_underline) == 1, + ValueError("executing_node_underline must be a single character"), + ) self.executing_node_underline = executing_node_underline self.current_line_indicator = current_line_indicator or "" self.line_gap_string = line_gap_string @@ -132,7 +136,7 @@ def format_frame(self, frame: Union[FrameInfo, FrameType, TracebackType]) -> Ite if isinstance(line, Line): yield self.format_line(line) else: - assert line is LINE_GAP + assert_(line is LINE_GAP) yield self.line_gap_string + "\n" if self.show_variables: diff --git a/stack_data/utils.py b/stack_data/utils.py index b84391d..12eabad 100644 --- a/stack_data/utils.py +++ b/stack_data/utils.py @@ -84,7 +84,7 @@ def collapse_repeated(lst, *, collapser, mapper=identity, key=identity): def is_frame(frame_or_tb: Union[FrameType, TracebackType]) -> bool: - assert isinstance(frame_or_tb, (types.FrameType, types.TracebackType)) + assert_(isinstance(frame_or_tb, (types.FrameType, types.TracebackType))) return isinstance(frame_or_tb, (types.FrameType,)) @@ -159,3 +159,10 @@ def get_tokens(self, text): lexer = MyLexer(stripnl=False) return pygments.highlight(code, lexer, formatter).splitlines() + + +def assert_(condition, error=""): + if not condition: + if isinstance(error, str): + error = AssertionError(error) + raise error diff --git a/tox.ini b/tox.ini index e2a9421..9040d03 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{35,36,37,38} +envlist = py{35,36,37,38,39} [testenv] commands = pytest From 5c539e88e16163e62c2992f483e11a5d35294d9b Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sat, 30 May 2020 23:09:00 +0200 Subject: [PATCH 15/15] Fix type hint for Token --- stack_data/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stack_data/core.py b/stack_data/core.py index 2e9b8fa..e222a38 100644 --- a/stack_data/core.py +++ b/stack_data/core.py @@ -4,7 +4,6 @@ import sys from collections import defaultdict, Counter from textwrap import dedent -from tokenize import TokenInfo from types import FrameType, CodeType, TracebackType from typing import ( Iterator, List, Tuple, Optional, NamedTuple, @@ -87,7 +86,7 @@ def pieces(self) -> List[range]: return list(self._clean_pieces()) @cached_property - def tokens_by_lineno(self) -> Mapping[int, List[TokenInfo]]: + def tokens_by_lineno(self) -> Mapping[int, List[Token]]: if not self.tree: raise AttributeError("This file doesn't contain valid Python, so .tokens_by_lineno doesn't exist") return group_by_key_func(