diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 76963c0eb..136da3195 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -7,7 +7,6 @@ from .code import getfslineno from .code import getrawcode from .code import Traceback from .code import TracebackEntry -from .source import compile_ as compile from .source import Source __all__ = [ @@ -19,6 +18,5 @@ __all__ = [ "getrawcode", "Traceback", "TracebackEntry", - "compile", "Source", ] diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 019da5765..6cb602a93 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -1,13 +1,9 @@ import ast import inspect -import linecache -import sys import textwrap import tokenize import warnings from bisect import bisect_right -from types import CodeType -from types import FrameType from typing import Iterable from typing import Iterator from typing import List @@ -15,13 +11,7 @@ from typing import Optional from typing import Tuple from typing import Union -import py - from _pytest.compat import overload -from _pytest.compat import TYPE_CHECKING - -if TYPE_CHECKING: - from typing_extensions import Literal class Source: @@ -30,8 +20,6 @@ class Source: When using Source(...), the source lines are deindented. """ - _compilecounter = 0 - def __init__(self, obj: object = None) -> None: if not obj: self.lines = [] # type: List[str] @@ -122,124 +110,6 @@ class Source: def __str__(self) -> str: return "\n".join(self.lines) - @overload - def compile( - self, - filename: Optional[str] = ..., - mode: str = ..., - flag: "Literal[0]" = ..., - dont_inherit: int = ..., - _genframe: Optional[FrameType] = ..., - ) -> CodeType: - raise NotImplementedError() - - @overload # noqa: F811 - def compile( # noqa: F811 - self, - filename: Optional[str] = ..., - mode: str = ..., - flag: int = ..., - dont_inherit: int = ..., - _genframe: Optional[FrameType] = ..., - ) -> Union[CodeType, ast.AST]: - raise NotImplementedError() - - def compile( # noqa: F811 - self, - filename: Optional[str] = None, - mode: str = "exec", - flag: int = 0, - dont_inherit: int = 0, - _genframe: Optional[FrameType] = None, - ) -> Union[CodeType, ast.AST]: - """ return compiled code object. if filename is None - invent an artificial filename which displays - the source/line position of the caller frame. - """ - if not filename or py.path.local(filename).check(file=0): - if _genframe is None: - _genframe = sys._getframe(1) # the caller - fn, lineno = _genframe.f_code.co_filename, _genframe.f_lineno - base = "<%d-codegen " % self._compilecounter - self.__class__._compilecounter += 1 - if not filename: - filename = base + "%s:%d>" % (fn, lineno) - else: - filename = base + "%r %s:%d>" % (filename, fn, lineno) - source = "\n".join(self.lines) + "\n" - try: - co = compile(source, filename, mode, flag) - except SyntaxError as ex: - # re-represent syntax errors from parsing python strings - msglines = self.lines[: ex.lineno] - if ex.offset: - msglines.append(" " * ex.offset + "^") - msglines.append("(code was compiled probably from here: %s)" % filename) - newex = SyntaxError("\n".join(msglines)) - newex.offset = ex.offset - newex.lineno = ex.lineno - newex.text = ex.text - raise newex from ex - else: - if flag & ast.PyCF_ONLY_AST: - assert isinstance(co, ast.AST) - return co - assert isinstance(co, CodeType) - lines = [(x + "\n") for x in self.lines] - # Type ignored because linecache.cache is private. - linecache.cache[filename] = (1, None, lines, filename) # type: ignore - return co - - -# -# public API shortcut functions -# - - -@overload -def compile_( - source: Union[str, bytes, ast.mod, ast.AST], - filename: Optional[str] = ..., - mode: str = ..., - flags: "Literal[0]" = ..., - dont_inherit: int = ..., -) -> CodeType: - raise NotImplementedError() - - -@overload # noqa: F811 -def compile_( # noqa: F811 - source: Union[str, bytes, ast.mod, ast.AST], - filename: Optional[str] = ..., - mode: str = ..., - flags: int = ..., - dont_inherit: int = ..., -) -> Union[CodeType, ast.AST]: - raise NotImplementedError() - - -def compile_( # noqa: F811 - source: Union[str, bytes, ast.mod, ast.AST], - filename: Optional[str] = None, - mode: str = "exec", - flags: int = 0, - dont_inherit: int = 0, -) -> Union[CodeType, ast.AST]: - """ compile the given source to a raw code object, - and maintain an internal cache which allows later - retrieval of the source code for the code object - and any recursively created code objects. - """ - if isinstance(source, ast.AST): - # XXX should Source support having AST? - assert filename is not None - co = compile(source, filename, mode, flags, dont_inherit) - assert isinstance(co, (CodeType, ast.AST)) - return co - _genframe = sys._getframe(1) # the caller - s = Source(source) - return s.compile(filename, mode, flags, _genframe=_genframe) - # # helper functions diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 75c937612..52d5286b8 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -127,24 +127,28 @@ class TestTraceback_f_g_h: assert s.endswith("raise ValueError") def test_traceback_entry_getsource_in_construct(self): - source = _pytest._code.Source( - """\ - def xyz(): - try: - raise ValueError - except somenoname: - pass - xyz() - """ - ) + def xyz(): + try: + raise ValueError + except somenoname: # type: ignore[name-defined] # noqa: F821 + pass + try: - exec(source.compile()) + xyz() except NameError: - tb = _pytest._code.ExceptionInfo.from_current().traceback - print(tb[-1].getsource()) - s = str(tb[-1].getsource()) - assert s.startswith("def xyz():\n try:") - assert s.strip().endswith("except somenoname:") + excinfo = _pytest._code.ExceptionInfo.from_current() + else: + assert False, "did not raise NameError" + + tb = excinfo.traceback + source = tb[-1].getsource() + assert source is not None + assert source.deindent().lines == [ + "def xyz():", + " try:", + " raise ValueError", + " except somenoname: # type: ignore[name-defined] # noqa: F821", + ] def test_traceback_cut(self): co = _pytest._code.Code(f) @@ -445,16 +449,6 @@ class TestFormattedExcinfo: return importasmod - def excinfo_from_exec(self, source): - source = _pytest._code.Source(source).strip() - try: - exec(source.compile()) - except KeyboardInterrupt: - raise - except BaseException: - return _pytest._code.ExceptionInfo.from_current() - assert 0, "did not raise" - def test_repr_source(self): pr = FormattedExcinfo() source = _pytest._code.Source( @@ -471,19 +465,29 @@ class TestFormattedExcinfo: def test_repr_source_excinfo(self) -> None: """ check if indentation is right """ - pr = FormattedExcinfo() - excinfo = self.excinfo_from_exec( - """ - def f(): - assert 0 - f() - """ - ) + try: + + def f(): + 1 / 0 + + f() + + except BaseException: + excinfo = _pytest._code.ExceptionInfo.from_current() + else: + assert 0, "did not raise" + pr = FormattedExcinfo() source = pr._getentrysource(excinfo.traceback[-1]) assert source is not None lines = pr.get_source(source, 1, excinfo) - assert lines == [" def f():", "> assert 0", "E AssertionError"] + for line in lines: + print(line) + assert lines == [ + " def f():", + "> 1 / 0", + "E ZeroDivisionError: division by zero", + ] def test_repr_source_not_existing(self): pr = FormattedExcinfo() diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 97a00964b..11f2f53cf 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -3,6 +3,7 @@ # or redundant on purpose and can't be disable on a line-by-line basis import ast import inspect +import linecache import sys import textwrap from types import CodeType @@ -33,14 +34,6 @@ def test_source_str_function() -> None: assert str(x) == "\n3" -def test_unicode() -> None: - x = Source("4") - assert str(x) == "4" - co = _pytest._code.compile('"å"', mode="eval") - val = eval(co) - assert isinstance(val, str) - - def test_source_from_function() -> None: source = _pytest._code.Source(test_source_str_function) assert str(source).startswith("def test_source_str_function() -> None:") @@ -83,15 +76,6 @@ def test_source_strip_multiline() -> None: assert source2.lines == [" hello"] -def test_syntaxerror_rerepresentation() -> None: - ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz") - assert ex is not None - assert ex.value.lineno == 1 - assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5 - assert ex.value.text - assert ex.value.text.rstrip("\n") == "xyz xyz" - - class TestAccesses: def setup_class(self) -> None: self.source = Source( @@ -124,7 +108,7 @@ class TestAccesses: assert len(values) == 4 -class TestSourceParsingAndCompiling: +class TestSourceParsing: def setup_class(self) -> None: self.source = Source( """\ @@ -135,39 +119,6 @@ class TestSourceParsingAndCompiling: """ ).strip() - def test_compile(self) -> None: - co = _pytest._code.compile("x=3") - d = {} # type: Dict[str, Any] - exec(co, d) - assert d["x"] == 3 - - def test_compile_and_getsource_simple(self) -> None: - co = _pytest._code.compile("x=3") - exec(co) - source = _pytest._code.Source(co) - assert str(source) == "x=3" - - def test_compile_and_getsource_through_same_function(self) -> None: - def gensource(source): - return _pytest._code.compile(source) - - co1 = gensource( - """ - def f(): - raise KeyError() - """ - ) - co2 = gensource( - """ - def f(): - raise ValueError() - """ - ) - source1 = inspect.getsource(co1) - assert "KeyError" in source1 - source2 = inspect.getsource(co2) - assert "ValueError" in source2 - def test_getstatement(self) -> None: # print str(self.source) ass = str(self.source[1:]) @@ -264,44 +215,6 @@ class TestSourceParsingAndCompiling: source = Source(":") pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) - def test_compile_to_ast(self) -> None: - source = Source("x = 4") - mod = source.compile(flag=ast.PyCF_ONLY_AST) - assert isinstance(mod, ast.Module) - compile(mod, "", "exec") - - def test_compile_and_getsource(self) -> None: - co = self.source.compile() - exec(co, globals()) - f(7) # type: ignore - excinfo = pytest.raises(AssertionError, f, 6) # type: ignore - assert excinfo is not None - frame = excinfo.traceback[-1].frame - assert isinstance(frame.code.fullsource, Source) - stmt = frame.code.fullsource.getstatement(frame.lineno) - assert str(stmt).strip().startswith("assert") - - @pytest.mark.parametrize("name", ["", None, "my"]) - def test_compilefuncs_and_path_sanity(self, name: Optional[str]) -> None: - def check(comp, name) -> None: - co = comp(self.source, name) - if not name: - expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) # type: ignore - else: - expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2) # type: ignore - fn = co.co_filename - assert fn.endswith(expected) - - mycode = _pytest._code.Code(self.test_compilefuncs_and_path_sanity) - mylineno = mycode.firstlineno - mypath = mycode.path - - for comp in _pytest._code.compile, _pytest._code.Source.compile: - check(comp, name) - - def test_offsetless_synerr(self): - pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval") - def test_getstartingblock_singleline() -> None: class A: @@ -331,18 +244,16 @@ def test_getline_finally() -> None: def test_getfuncsource_dynamic() -> None: - source = """ - def f(): - raise ValueError + def f(): + raise ValueError - def g(): pass - """ - co = _pytest._code.compile(source) - exec(co, globals()) - f_source = _pytest._code.Source(f) # type: ignore - g_source = _pytest._code.Source(g) # type: ignore + def g(): + pass + + f_source = _pytest._code.Source(f) + g_source = _pytest._code.Source(g) assert str(f_source).strip() == "def f():\n raise ValueError" - assert str(g_source).strip() == "def g(): pass" + assert str(g_source).strip() == "def g():\n pass" def test_getfuncsource_with_multine_string() -> None: @@ -405,23 +316,6 @@ def test_getsource_fallback() -> None: assert str(src) == expected -def test_idem_compile_and_getsource() -> None: - from _pytest._code.source import getsource - - expected = "def x(): pass" - co = _pytest._code.compile(expected) - src = getsource(co) - assert str(src) == expected - - -def test_compile_ast() -> None: - # We don't necessarily want to support this. - # This test was added just for coverage. - stmt = ast.parse("def x(): pass") - co = _pytest._code.compile(stmt, filename="foo.py") - assert isinstance(co, CodeType) - - def test_findsource_fallback() -> None: from _pytest._code.source import findsource @@ -431,15 +325,15 @@ def test_findsource_fallback() -> None: assert src[lineno] == " def x():" -def test_findsource() -> None: +def test_findsource(monkeypatch) -> None: from _pytest._code.source import findsource - co = _pytest._code.compile( - """if 1: - def x(): - pass -""" - ) + filename = "" + lines = ["if 1:\n", " def x():\n", " pass\n"] + co = compile("".join(lines), filename, "exec") + + # Type ignored because linecache.cache is private. + monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename)) # type: ignore[attr-defined] src, lineno = findsource(co) assert src is not None