Coverage for coverage / python.py: 97.884%

145 statements  

« prev     ^ index     » next       coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000

1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt 

3 

4"""Python source expertise for coverage.py""" 

5 

6from __future__ import annotations 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

7 

8import os.path 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

9import types 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

10import zipimport 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

11from collections.abc import Iterable 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

12from typing import TYPE_CHECKING 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

13 

14from coverage import env 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

15from coverage.exceptions import CoverageException, NoSource 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

16from coverage.files import canonical_filename, relative_filename, zip_location 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

17from coverage.misc import isolate_module, join_regex 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

18from coverage.parser import PythonParser 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

19from coverage.phystokens import source_encoding, source_token_lines 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

20from coverage.plugin import CodeRegion, FileReporter 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

21from coverage.regions import code_regions 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

22from coverage.types import TArc, TLineNo, TMorf, TSourceTokenLines 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

23 

24if TYPE_CHECKING: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

25 from coverage import Coverage 

26 

27# Protect ourselves against aggressive mocking. 

28os = isolate_module(os) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

29# Save the original `open` function so later mocks don't break us. 

30open = open # pylint: disable=redefined-builtin 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

31 

32 

33def read_python_source(filename: str) -> bytes: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

34 """Read the Python source text from `filename`. 

35 

36 Returns bytes. 

37 

38 """ 

39 with open(filename, "rb") as f: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

40 source = f.read() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

41 

42 return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

43 

44 

45def get_python_source(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

46 """Return the source code, as unicode.""" 

47 base, ext = os.path.splitext(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

48 if ext == ".py" and env.WINDOWS: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

49 exts = [".py", ".pyw"] 1abcdefghijkl%mn'op(qr)s

50 else: 

51 exts = [ext] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

52 

53 source_bytes: bytes | None 

54 for ext in exts: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

55 try_filename = base + ext 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

56 if os.path.exists(try_filename): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

57 # A regular text file: open it. 

58 source_bytes = read_python_source(try_filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

59 break 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

60 

61 # Maybe it's in a zip file? 

62 source_bytes = get_zip_bytes(try_filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

63 if source_bytes is not None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

64 break 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

65 else: 

66 # Couldn't find source. 

67 raise NoSource(f"No source for code: '{filename}'.", slug="no-source") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

68 

69 # Replace \f because of http://bugs.python.org/issue19035 

70 source_bytes = source_bytes.replace(b"\f", b" ") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

71 source = source_bytes.decode(source_encoding(source_bytes), "replace") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

72 

73 # Python code should always end with a line with a newline. 

74 if source and source[-1] != "\n": 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

75 source += "\n" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

76 

77 return source 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

78 

79 

80def get_zip_bytes(filename: str) -> bytes | None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

81 """Get data from `filename` if it is a zip file path. 

82 

83 Returns the bytestring data read from the zip file, or None if no zip file 

84 could be found or `filename` isn't in it. The data returned will be 

85 an empty string if the file is empty. 

86 

87 """ 

88 zipfile_inner = zip_location(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

89 if zipfile_inner is not None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

90 zipfile, inner = zipfile_inner 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

91 try: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

92 zi = zipimport.zipimporter(zipfile) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

93 except zipimport.ZipImportError: 

94 return None 

95 try: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

96 data = zi.get_data(inner) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

97 except OSError: 

98 return None 

99 return data 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

100 return None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

101 

102 

103def source_for_file(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

104 """Return the source filename for `filename`. 

105 

106 Given a file name being traced, return the best guess as to the source 

107 file to attribute it to. 

108 

109 """ 

110 if filename.endswith(".py"): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

111 # .py files are themselves source files. 

112 return filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

113 

114 elif filename.endswith((".pyc", ".pyo")): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

115 # Bytecode files probably have source files near them. 

116 py_filename = filename[:-1] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ0125qr34s

117 if os.path.exists(py_filename): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ0125qr34s

118 # Found a .py file, use that. 

119 return py_filename 1abcdefghijklmnopqrs

120 if env.WINDOWS: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ0125qr34s

121 # On Windows, it could be a .pyw file. 

122 pyw_filename = py_filename + "w" 1abcdefghijklmnopqrs

123 if os.path.exists(pyw_filename): 1abcdefghijklmnopqrs

124 return pyw_filename 1abcdefghijklmnopqrs

125 # Didn't find source, but it's probably the .py file we want. 

126 return py_filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ0125qr34s

127 

128 # No idea, just use the file name as-is. 

129 return filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

130 

131 

132def source_for_morf(morf: TMorf) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

133 """Get the source filename for the module-or-file `morf`.""" 

134 if hasattr(morf, "__file__") and morf.__file__: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

135 filename = morf.__file__ 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

136 elif isinstance(morf, types.ModuleType): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

137 # A module should have had .__file__, otherwise we can't use it. 

138 # This could be a PEP-420 namespace package. 

139 raise CoverageException(f"Module {morf} has no file") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

140 else: 

141 filename = morf 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

142 

143 filename = source_for_file(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

144 return filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

145 

146 

147class PythonFileReporter(FileReporter): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

148 """Report support for a Python file.""" 

149 

150 def __init__(self, morf: TMorf, coverage: Coverage | None = None) -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

151 self.coverage = coverage 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

152 

153 filename = source_for_morf(morf) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

154 

155 fname = filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

156 canonicalize = True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

157 if self.coverage is not None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

158 if self.coverage.config.relative_files: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

159 canonicalize = False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXY6opZ012qr34s

160 if canonicalize: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

161 fname = canonical_filename(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

162 super().__init__(fname) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

163 

164 if hasattr(morf, "__name__"): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

165 name = morf.__name__.replace(".", os.sep) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8klRS9TU!mnVW#XY6opZ0$125qr34s

166 if os.path.basename(filename).startswith("__init__."): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8klRS9TU!mnVW#XY6opZ0$125qr34s

167 name += os.sep + "__init__" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

168 name += ".py" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8klRS9TU!mnVW#XY6opZ0$125qr34s

169 else: 

170 name = relative_filename(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

171 self.relname = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

172 

173 self._source: str | None = None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

174 self._parser: PythonParser | None = None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

175 self._excluded = None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

176 

177 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

178 return f"<PythonFileReporter {self.filename!r}>" 

179 

180 def relative_filename(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

181 return self.relname 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

182 

183 @property 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

184 def parser(self) -> PythonParser: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

185 """Lazily create a :class:`PythonParser`.""" 

186 assert self.coverage is not None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

187 if self._parser is None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

188 self._parser = PythonParser( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

189 filename=self.filename, 

190 exclude=self.coverage._exclude_regex("exclude"), 

191 ) 

192 self._parser.parse_source() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

193 return self._parser 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

194 

195 def lines(self) -> set[TLineNo]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

196 """Return the line numbers of statements in the file.""" 

197 return self.parser.statements 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

198 

199 def multiline_map(self) -> dict[TLineNo, TLineNo]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

200 """A map of line numbers to first-line in a multi-line statement.""" 

201 return self.parser.multiline_map 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

202 

203 def excluded_lines(self) -> set[TLineNo]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

204 """Return the line numbers of statements in the file.""" 

205 return self.parser.excluded 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

206 

207 def translate_lines(self, lines: Iterable[TLineNo]) -> set[TLineNo]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

208 return self.parser.translate_lines(lines) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

209 

210 def translate_arcs(self, arcs: Iterable[TArc]) -> set[TArc]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

211 return self.parser.translate_arcs(arcs) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

212 

213 def no_branch_lines(self) -> set[TLineNo]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

214 assert self.coverage is not None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

215 no_branch = self.parser.lines_matching( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

216 join_regex(self.coverage.config.partial_list + self.coverage.config.partial_always_list) 

217 ) 

218 return no_branch 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

219 

220 def arcs(self) -> set[TArc]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

221 return self.parser.arcs() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

222 

223 def exit_counts(self) -> dict[TLineNo, int]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

224 return self.parser.exit_counts() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

225 

226 def missing_arc_description( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr

227 self, 

228 start: TLineNo, 

229 end: TLineNo, 

230 executed_arcs: Iterable[TArc] | None = None, 

231 ) -> str: 

232 return self.parser.missing_arc_description(start, end) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

233 

234 def arc_description(self, start: TLineNo, end: TLineNo) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

235 return self.parser.arc_description(start, end) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

236 

237 def source(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

238 if self._source is None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

239 self._source = get_python_source(self.filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

240 return self._source 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

241 

242 def should_be_python(self) -> bool: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

243 """Does it seem like this file should contain Python? 

244 

245 This is used to decide if a file reported as part of the execution of 

246 a program was really likely to have contained Python in the first 

247 place. 

248 

249 """ 

250 # Get the file extension. 

251 _, ext = os.path.splitext(self.filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

252 

253 # Anything named *.py* should be Python. 

254 if ext.startswith(".py"): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

255 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

256 # A file with no extension should be Python. 

257 if not ext: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

258 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

259 # Everything else is probably not Python. 

260 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

261 

262 def source_token_lines(self) -> TSourceTokenLines: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

263 return source_token_lines(self.source()) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

264 

265 def code_regions(self) -> Iterable[CodeRegion]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

266 return code_regions(self.source()) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

267 

268 def code_region_kinds(self) -> Iterable[tuple[str, str]]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

269 return [ 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO7PQ8kl%RS9TU!mn'VW#XY6op(Z0$125qr)34s

270 ("function", "functions"), 

271 ("class", "classes"), 

272 ]