diff --git a/readme.md b/readme.md index 55bef1e..9d89949 100644 --- a/readme.md +++ b/readme.md @@ -36,6 +36,9 @@ This code will be executed ``` # Changelog +#### 0.6 (04.04.2022) +- Fixed an issue where the line numbers for error messages were not correct + #### 0.5 (10.03.2022) - Marked as safe for parallel reading diff --git a/src/sphinx_exec_code/__version__.py b/src/sphinx_exec_code/__version__.py index 5a6f84c..27fda16 100644 --- a/src/sphinx_exec_code/__version__.py +++ b/src/sphinx_exec_code/__version__.py @@ -1 +1 @@ -__version__ = '0.5' +__version__ = '0.6' diff --git a/src/sphinx_exec_code/code_exec.py b/src/sphinx_exec_code/code_exec.py index ba49a5b..854aa60 100644 --- a/src/sphinx_exec_code/code_exec.py +++ b/src/sphinx_exec_code/code_exec.py @@ -18,7 +18,7 @@ def setup_code_env(cwd: Path, folders: Iterable[Path]): ADDITIONAL_FOLDERS = tuple(map(str, folders)) -def execute_code(code: str, file: str, line: int) -> str: +def execute_code(code: str, file: Path, first_loc: int) -> str: if WORKING_DIR is None or ADDITIONAL_FOLDERS is None: raise ConfigError('Working dir or additional folders are not set!') @@ -30,7 +30,7 @@ def execute_code(code: str, file: str, line: int) -> str: run = subprocess.run([sys.executable, '-c', code], capture_output=True, cwd=WORKING_DIR, env=env) if run.returncode != 0: - raise CodeException(code, (file, line), run.returncode, run.stderr.decode()) from None + raise CodeException(code, file, first_loc, run.returncode, run.stderr.decode()) from None ret = (run.stdout.decode() + run.stderr.decode()).strip() diff --git a/src/sphinx_exec_code/code_exec_error.py b/src/sphinx_exec_code/code_exec_error.py index c409a85..4d1e6f0 100644 --- a/src/sphinx_exec_code/code_exec_error.py +++ b/src/sphinx_exec_code/code_exec_error.py @@ -1,14 +1,16 @@ import re from pathlib import Path -from typing import List, Tuple +from typing import List -re_line = re.compile(r'^\s*File "()", line (\d+), in ') +re_line = re.compile(r'^\s*File "()", line (\d+), in ', re.MULTILINE) class CodeException(Exception): - def __init__(self, code: str, loc: Tuple[str, int], ret: int, stderr: str): + def __init__(self, code: str, file: Path, first_loc: int, ret: int, stderr: str): self.code = code - self.loc: Tuple[str, int] = loc + + self.file: Path = file + self.first_loc: int = first_loc self.exec_ret = ret self.exec_err = stderr @@ -18,19 +20,20 @@ def _err_line(self, lines: List[str]) -> int: err_line = len(lines) for m in re_line.finditer(self.exec_err): err_line = int(m.group(2)) - return err_line + return err_line - 1 def pformat(self) -> List[str]: - filename = Path(self.loc[0]).name + filename = self.file.name code_lines = self.code.splitlines() err_line = self._err_line(code_lines) ret = [] # add code snippet - for i in range(max(0, err_line - 8), err_line - 1): - ret.append(f' {code_lines[i]}') - ret.append(f' {code_lines[err_line - 1]} <--') + snip_start = max(0, err_line - 8) + snip_last = min(err_line + 1, len(code_lines)) + for i in range(snip_start, snip_last): + ret.append(f' {code_lines[i]}' if i != err_line else f' {code_lines[err_line]} <--') # add traceback ret.append('') @@ -40,6 +43,6 @@ def pformat(self) -> List[str]: tb_line = tb_line.replace('File ""', f'File "{filename}"') tb_line = tb_line.replace('File ""', f'File "{filename}"') tb_line = tb_line.replace(f', line {m.group(2)}, in ', - f', line {int(m.group(2)) + self.loc[1] + 1}') + f', line {int(m.group(2)) + self.first_loc - 1}') ret.append(tb_line) return ret diff --git a/src/sphinx_exec_code/sphinx_exec.py b/src/sphinx_exec_code/sphinx_exec.py index 7641e6a..abdc1b7 100644 --- a/src/sphinx_exec_code/sphinx_exec.py +++ b/src/sphinx_exec_code/sphinx_exec.py @@ -1,6 +1,6 @@ import traceback from pathlib import Path -from typing import Optional +from typing import List, Optional from docutils import nodes from sphinx.errors import ExtensionError @@ -61,20 +61,39 @@ def run(self) -> list: log.error(line) raise ExtensionError(f'Error while running {name}!', orig_exc=e) + def _get_code_line(self, line_no: int, content: List[str]) -> int: + """Get the first line number of the code""" + if not content: + return line_no + + i = 0 + first_line = content[0] + + for i, raw_line in enumerate(self.block_text.splitlines()): + # raw line contains the leading white spaces + if raw_line.lstrip() == first_line: + break + + return line_no + i + def _run(self) -> list: """ Executes python code for an RST document, taking input from content or from a filename :return: """ output = [] - file, line = self.get_source_info() + raw_file, raw_line = self.get_source_info() content = self.content + file = Path(raw_file) + line = self._get_code_line(raw_line, content) + code_spec = SpecCode.from_options(self.options) # Read from example files if code_spec.filename: filename = (EXAMPLE_DIR / code_spec.filename).resolve() content = filename.read_text(encoding='utf-8').splitlines() + file, line = filename, 1 # format the code try: diff --git a/tests/test_code_exec.py b/tests/test_code_exec.py index ac2cd19..21a2574 100644 --- a/tests/test_code_exec.py +++ b/tests/test_code_exec.py @@ -25,7 +25,7 @@ def test_err(setup_env): code = "print('Line1')\nprint('Line2')\n1/0" with pytest.raises(CodeException) as e: - execute_code(code, 'my_file', 5) + execute_code(code, Path('/my_file'), 5) msg = e.value.pformat() assert msg == [ @@ -34,6 +34,6 @@ def test_err(setup_env): ' 1/0 <--', '', 'Traceback (most recent call last):', - ' File "my_file", line 9', + ' File "my_file", line 7', 'ZeroDivisionError: division by zero' ]