coverage 7.6.10__cp312-cp312-musllinux_1_2_aarch64.whl → 7.12.0__cp312-cp312-musllinux_1_2_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. coverage/__init__.py +3 -1
  2. coverage/__main__.py +3 -1
  3. coverage/annotate.py +2 -3
  4. coverage/bytecode.py +178 -4
  5. coverage/cmdline.py +330 -155
  6. coverage/collector.py +32 -43
  7. coverage/config.py +167 -63
  8. coverage/context.py +5 -6
  9. coverage/control.py +165 -86
  10. coverage/core.py +71 -34
  11. coverage/data.py +4 -5
  12. coverage/debug.py +113 -57
  13. coverage/disposition.py +2 -1
  14. coverage/env.py +29 -78
  15. coverage/exceptions.py +29 -7
  16. coverage/execfile.py +19 -14
  17. coverage/files.py +24 -19
  18. coverage/html.py +118 -75
  19. coverage/htmlfiles/coverage_html.js +12 -10
  20. coverage/htmlfiles/index.html +45 -10
  21. coverage/htmlfiles/pyfile.html +2 -2
  22. coverage/htmlfiles/style.css +54 -6
  23. coverage/htmlfiles/style.scss +85 -3
  24. coverage/inorout.py +62 -45
  25. coverage/jsonreport.py +22 -9
  26. coverage/lcovreport.py +16 -18
  27. coverage/misc.py +51 -47
  28. coverage/multiproc.py +12 -7
  29. coverage/numbits.py +4 -5
  30. coverage/parser.py +150 -251
  31. coverage/patch.py +166 -0
  32. coverage/phystokens.py +25 -26
  33. coverage/plugin.py +14 -14
  34. coverage/plugin_support.py +37 -36
  35. coverage/python.py +13 -14
  36. coverage/pytracer.py +31 -33
  37. coverage/regions.py +3 -2
  38. coverage/report.py +60 -44
  39. coverage/report_core.py +7 -10
  40. coverage/results.py +152 -68
  41. coverage/sqldata.py +261 -211
  42. coverage/sqlitedb.py +37 -29
  43. coverage/sysmon.py +237 -162
  44. coverage/templite.py +19 -7
  45. coverage/tomlconfig.py +13 -13
  46. coverage/tracer.cpython-312-aarch64-linux-musl.so +0 -0
  47. coverage/tracer.pyi +3 -1
  48. coverage/types.py +26 -23
  49. coverage/version.py +4 -19
  50. coverage/xmlreport.py +17 -14
  51. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/METADATA +50 -28
  52. coverage-7.12.0.dist-info/RECORD +59 -0
  53. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/WHEEL +1 -1
  54. coverage-7.6.10.dist-info/RECORD +0 -58
  55. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/entry_points.txt +0 -0
  56. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info/licenses}/LICENSE.txt +0 -0
  57. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/top_level.txt +0 -0
coverage/parser.py CHANGED
@@ -1,23 +1,21 @@
1
1
  # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2
- # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
2
+ # For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
3
3
 
4
4
  """Code parsing for coverage.py."""
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
8
  import ast
9
- import functools
10
9
  import collections
10
+ import functools
11
11
  import os
12
12
  import re
13
- import sys
14
13
  import token
15
14
  import tokenize
16
-
17
15
  from collections.abc import Iterable, Sequence
18
16
  from dataclasses import dataclass
19
17
  from types import CodeType
20
- from typing import cast, Callable, Optional, Protocol
18
+ from typing import Callable, Optional, Protocol, cast
21
19
 
22
20
  from coverage import env
23
21
  from coverage.bytecode import code_objects
@@ -37,6 +35,7 @@ class PythonParser:
37
35
  involved.
38
36
 
39
37
  """
38
+
40
39
  def __init__(
41
40
  self,
42
41
  text: str | None = None,
@@ -55,6 +54,7 @@ class PythonParser:
55
54
  self.text: str = text
56
55
  else:
57
56
  from coverage.python import get_python_source
57
+
58
58
  try:
59
59
  self.text = get_python_source(self.filename)
60
60
  except OSError as err:
@@ -92,7 +92,7 @@ class PythonParser:
92
92
 
93
93
  # A dict mapping line numbers to lexical statement starts for
94
94
  # multi-line statements.
95
- self._multiline: dict[TLineNo, TLineNo] = {}
95
+ self.multiline_map: dict[TLineNo, TLineNo] = {}
96
96
 
97
97
  # Lazily-created arc data, and missing arc descriptions.
98
98
  self._all_arcs: set[TArc] | None = None
@@ -113,9 +113,11 @@ class PythonParser:
113
113
  last_start_line = 0
114
114
  for match in re.finditer(regex, self.text, flags=re.MULTILINE):
115
115
  start, end = match.span()
116
- start_line = last_start_line + self.text.count('\n', last_start, start)
117
- end_line = last_start_line + self.text.count('\n', last_start, end)
118
- matches.update(self._multiline.get(i, i) for i in range(start_line + 1, end_line + 2))
116
+ start_line = last_start_line + self.text.count("\n", last_start, start)
117
+ end_line = last_start_line + self.text.count("\n", last_start, end)
118
+ matches.update(
119
+ self.multiline_map.get(i, i) for i in range(start_line + 1, end_line + 2)
120
+ )
119
121
  last_start = start
120
122
  last_start_line = start_line
121
123
  return matches
@@ -147,20 +149,23 @@ class PythonParser:
147
149
  assert self.text is not None
148
150
  tokgen = generate_tokens(self.text)
149
151
  for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
150
- if self.show_tokens: # pragma: debugging
151
- print("%10s %5s %-20r %r" % (
152
- tokenize.tok_name.get(toktype, toktype),
153
- nice_pair((slineno, elineno)), ttext, ltext,
154
- ))
152
+ if self.show_tokens: # pragma: debugging
153
+ print(
154
+ "%10s %5s %-20r %r"
155
+ % (
156
+ tokenize.tok_name.get(toktype, toktype),
157
+ nice_pair((slineno, elineno)),
158
+ ttext,
159
+ ltext,
160
+ )
161
+ )
155
162
  if toktype == token.INDENT:
156
163
  indent += 1
157
164
  elif toktype == token.DEDENT:
158
165
  indent -= 1
159
166
  elif toktype == token.OP:
160
167
  if ttext == ":" and nesting == 0:
161
- should_exclude = (
162
- self.excluded.intersection(range(first_line, elineno + 1))
163
- )
168
+ should_exclude = self.excluded.intersection(range(first_line, elineno + 1))
164
169
  if not excluding and should_exclude:
165
170
  # Start excluding a suite. We trigger off of the colon
166
171
  # token so that the #pragma comment will be recognized on
@@ -177,8 +182,8 @@ class PythonParser:
177
182
  # We're at the end of a line, and we've ended on a
178
183
  # different line than the first line of the statement,
179
184
  # so record a multi-line range.
180
- for l in range(first_line, elineno+1):
181
- self._multiline[l] = first_line
185
+ for l in range(first_line, elineno + 1):
186
+ self.multiline_map[l] = first_line
182
187
  first_line = 0
183
188
 
184
189
  if ttext.strip() and toktype != tokenize.COMMENT:
@@ -198,12 +203,6 @@ class PythonParser:
198
203
  byte_parser = ByteParser(self.text, filename=self.filename)
199
204
  self.raw_statements.update(byte_parser._find_statements())
200
205
 
201
- # The first line of modules can lie and say 1 always, even if the first
202
- # line of code is later. If so, map 1 to the actual first line of the
203
- # module.
204
- if env.PYBEHAVIOR.module_firstline_1 and self._multiline:
205
- self._multiline[1] = min(self.raw_statements)
206
-
207
206
  self.excluded = self.first_lines(self.excluded)
208
207
 
209
208
  # AST lets us find classes, docstrings, and decorator-affected
@@ -233,9 +232,9 @@ class PythonParser:
233
232
  def first_line(self, lineno: TLineNo) -> TLineNo:
234
233
  """Return the first line number of the statement including `lineno`."""
235
234
  if lineno < 0:
236
- lineno = -self._multiline.get(-lineno, -lineno)
235
+ lineno = -self.multiline_map.get(-lineno, -lineno)
237
236
  else:
238
- lineno = self._multiline.get(lineno, lineno)
237
+ lineno = self.multiline_map.get(lineno, lineno)
239
238
  return lineno
240
239
 
241
240
  def first_lines(self, linenos: Iterable[TLineNo]) -> set[TLineNo]:
@@ -267,12 +266,12 @@ class PythonParser:
267
266
  self._raw_parse()
268
267
  except (tokenize.TokenError, IndentationError, SyntaxError) as err:
269
268
  if hasattr(err, "lineno"):
270
- lineno = err.lineno # IndentationError
269
+ lineno = err.lineno # IndentationError
271
270
  else:
272
- lineno = err.args[1][0] # TokenError
271
+ lineno = err.args[1][0] # TokenError
273
272
  raise NotPython(
274
- f"Couldn't parse '{self.filename}' as Python source: " +
275
- f"{err.args[0]!r} at line {lineno}",
273
+ f"Couldn't parse '{self.filename}' as Python source: "
274
+ + f"{err.args[0]!r} at line {lineno}",
276
275
  ) from err
277
276
 
278
277
  ignore = self.excluded | self.raw_docstrings
@@ -298,13 +297,12 @@ class PythonParser:
298
297
 
299
298
  """
300
299
  assert self._ast_root is not None
301
- aaa = AstArcAnalyzer(self.filename, self._ast_root, self.raw_statements, self._multiline)
300
+ aaa = AstArcAnalyzer(self.filename, self._ast_root, self.raw_statements, self.multiline_map)
302
301
  aaa.analyze()
303
302
  arcs = aaa.arcs
304
- if env.PYBEHAVIOR.exit_through_with:
305
- self._with_jump_fixers = aaa.with_jump_fixers()
306
- if self._with_jump_fixers:
307
- arcs = self.fix_with_jumps(arcs)
303
+ self._with_jump_fixers = aaa.with_jump_fixers()
304
+ if self._with_jump_fixers:
305
+ arcs = self.fix_with_jumps(arcs)
308
306
 
309
307
  self._all_arcs = set()
310
308
  for l1, l2 in arcs:
@@ -453,33 +451,11 @@ class ByteParser:
453
451
  def _line_numbers(self) -> Iterable[TLineNo]:
454
452
  """Yield the line numbers possible in this code object.
455
453
 
456
- Uses co_lnotab described in Python/compile.c to find the
457
- line numbers. Produces a sequence: l0, l1, ...
454
+ Uses co_lines() to produce a sequence: l0, l1, ...
458
455
  """
459
- if hasattr(self.code, "co_lines"):
460
- # PYVERSIONS: new in 3.10
461
- for _, _, line in self.code.co_lines():
462
- if line:
463
- yield line
464
- else:
465
- # Adapted from dis.py in the standard library.
466
- byte_increments = self.code.co_lnotab[0::2]
467
- line_increments = self.code.co_lnotab[1::2]
468
-
469
- last_line_num = None
470
- line_num = self.code.co_firstlineno
471
- byte_num = 0
472
- for byte_incr, line_incr in zip(byte_increments, line_increments):
473
- if byte_incr:
474
- if line_num != last_line_num:
475
- yield line_num
476
- last_line_num = line_num
477
- byte_num += byte_incr
478
- if line_incr >= 0x80:
479
- line_incr -= 0x100
480
- line_num += line_incr
481
- if line_num != last_line_num:
482
- yield line_num
456
+ for _, _, line in self.code.co_lines():
457
+ if line:
458
+ yield line
483
459
 
484
460
  def _find_statements(self) -> Iterable[TLineNo]:
485
461
  """Find the statements in `self.code`.
@@ -497,6 +473,7 @@ class ByteParser:
497
473
  # AST analysis
498
474
  #
499
475
 
476
+
500
477
  @dataclass(frozen=True, order=True)
501
478
  class ArcStart:
502
479
  """The information needed to start an arc.
@@ -527,12 +504,14 @@ class ArcStart:
527
504
  "line 1 didn't jump to line 2 because the condition on line 1 was never true."
528
505
 
529
506
  """
507
+
530
508
  lineno: TLineNo
531
509
  cause: str = ""
532
510
 
533
511
 
534
512
  class TAddArcFn(Protocol):
535
513
  """The type for AstArcAnalyzer.add_arc()."""
514
+
536
515
  def __call__(
537
516
  self,
538
517
  start: TLineNo,
@@ -555,6 +534,7 @@ class TAddArcFn(Protocol):
555
534
 
556
535
  TArcFragments = dict[TArc, list[tuple[Optional[str], Optional[str]]]]
557
536
 
537
+
558
538
  class Block:
559
539
  """
560
540
  Blocks need to handle various exiting statements in their own ways.
@@ -564,6 +544,7 @@ class Block:
564
544
  exits are handled, or False if the search should continue up the block
565
545
  stack.
566
546
  """
547
+
567
548
  # pylint: disable=unused-argument
568
549
  def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
569
550
  """Process break exits."""
@@ -584,6 +565,7 @@ class Block:
584
565
 
585
566
  class LoopBlock(Block):
586
567
  """A block on the block stack representing a `for` or `while` loop."""
568
+
587
569
  def __init__(self, start: TLineNo) -> None:
588
570
  # The line number where the loop starts.
589
571
  self.start = start
@@ -602,6 +584,7 @@ class LoopBlock(Block):
602
584
 
603
585
  class FunctionBlock(Block):
604
586
  """A block on the block stack representing a function definition."""
587
+
605
588
  def __init__(self, start: TLineNo, name: str) -> None:
606
589
  # The line number where the function starts.
607
590
  self.start = start
@@ -611,7 +594,9 @@ class FunctionBlock(Block):
611
594
  def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
612
595
  for xit in exits:
613
596
  add_arc(
614
- xit.lineno, -self.start, xit.cause,
597
+ xit.lineno,
598
+ -self.start,
599
+ xit.cause,
615
600
  f"except from function {self.name!r}",
616
601
  )
617
602
  return True
@@ -619,7 +604,9 @@ class FunctionBlock(Block):
619
604
  def process_return_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
620
605
  for xit in exits:
621
606
  add_arc(
622
- xit.lineno, -self.start, xit.cause,
607
+ xit.lineno,
608
+ -self.start,
609
+ xit.cause,
623
610
  f"return from function {self.name!r}",
624
611
  )
625
612
  return True
@@ -627,6 +614,7 @@ class FunctionBlock(Block):
627
614
 
628
615
  class TryBlock(Block):
629
616
  """A block on the block stack representing a `try` block."""
617
+
630
618
  def __init__(self, handler_start: TLineNo | None, final_start: TLineNo | None) -> None:
631
619
  # The line number of the first "except" handler, if any.
632
620
  self.handler_start = handler_start
@@ -640,18 +628,33 @@ class TryBlock(Block):
640
628
  return True
641
629
 
642
630
 
643
- class NodeList(ast.AST):
644
- """A synthetic fictitious node, containing a sequence of nodes.
631
+ # TODO: Shouldn't the cause messages join with "and" instead of "or"?
645
632
 
646
- This is used when collapsing optimized if-statements, to represent the
647
- unconditional execution of one of the clauses.
648
633
 
649
- """
650
- def __init__(self, body: Sequence[ast.AST]) -> None:
651
- self.body = body
652
- self.lineno = body[0].lineno # type: ignore[attr-defined]
634
+ def is_constant_test_expr(node: ast.AST) -> tuple[bool, bool]:
635
+ """Is this a compile-time constant test expression?
653
636
 
654
- # TODO: Shouldn't the cause messages join with "and" instead of "or"?
637
+ We don't try to mimic all of CPython's optimizations. We just have to
638
+ handle the kinds of constant expressions people might actually use.
639
+
640
+ """
641
+ match node:
642
+ case ast.Constant():
643
+ return True, bool(node.value)
644
+ case ast.Name():
645
+ if node.id in ["True", "False", "None", "__debug__"]:
646
+ return True, eval(node.id) # pylint: disable=eval-used
647
+ case ast.UnaryOp():
648
+ if isinstance(node.op, ast.Not):
649
+ is_constant, val = is_constant_test_expr(node.operand)
650
+ return is_constant, not val
651
+ case ast.BoolOp():
652
+ rets = [is_constant_test_expr(v) for v in node.values]
653
+ is_constant = all(is_const for is_const, _ in rets)
654
+ if is_constant:
655
+ op = any if isinstance(node.op, ast.Or) else all
656
+ return True, op(v for _, v in rets)
657
+ return False, False
655
658
 
656
659
 
657
660
  class AstArcAnalyzer:
@@ -691,7 +694,7 @@ class AstArcAnalyzer:
691
694
  # $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
692
695
  dump_ast = bool(int(os.getenv("COVERAGE_AST_DUMP", "0")))
693
696
 
694
- if dump_ast: # pragma: debugging
697
+ if dump_ast: # pragma: debugging
695
698
  # Dump the AST so that failing tests have helpful output.
696
699
  print(f"Statements: {self.statements}")
697
700
  print(f"Multiline map: {self.multiline}")
@@ -717,7 +720,7 @@ class AstArcAnalyzer:
717
720
  """Examine the AST tree from `self.root_node` to determine possible arcs."""
718
721
  for node in ast.walk(self.root_node):
719
722
  node_name = node.__class__.__name__
720
- code_object_handler = getattr(self, "_code_object__" + node_name, None)
723
+ code_object_handler = getattr(self, f"_code_object__{node_name}", None)
721
724
  if code_object_handler is not None:
722
725
  code_object_handler(node)
723
726
 
@@ -785,7 +788,7 @@ class AstArcAnalyzer:
785
788
  action_msg: str | None = None,
786
789
  ) -> None:
787
790
  """Add an arc, including message fragments to use if it is missing."""
788
- if self.debug: # pragma: debugging
791
+ if self.debug: # pragma: debugging
789
792
  print(f"Adding possible arc: ({start}, {end}): {missing_cause_msg!r}, {action_msg!r}")
790
793
  print(short_stack(), end="\n\n")
791
794
  self.arcs.add((start, end))
@@ -808,12 +811,12 @@ class AstArcAnalyzer:
808
811
  node_name = node.__class__.__name__
809
812
  handler = cast(
810
813
  Optional[Callable[[ast.AST], TLineNo]],
811
- getattr(self, "_line__" + node_name, None),
814
+ getattr(self, f"_line__{node_name}", None),
812
815
  )
813
816
  if handler is not None:
814
817
  line = handler(node)
815
818
  else:
816
- line = node.lineno # type: ignore[attr-defined]
819
+ line = node.lineno # type: ignore[attr-defined]
817
820
  return self.multiline.get(line, line)
818
821
 
819
822
  # First lines: _line__*
@@ -854,19 +857,22 @@ class AstArcAnalyzer:
854
857
  else:
855
858
  return node.lineno
856
859
 
857
- def _line__Module(self, node: ast.Module) -> TLineNo:
858
- if env.PYBEHAVIOR.module_firstline_1:
859
- return 1
860
- elif node.body:
861
- return self.line_for_node(node.body[0])
862
- else:
863
- # Empty modules have no line number, they always start at 1.
864
- return 1
860
+ def _line__Module(self, node: ast.Module) -> TLineNo: # pylint: disable=unused-argument
861
+ return 1
865
862
 
866
863
  # The node types that just flow to the next node with no complications.
867
864
  OK_TO_DEFAULT = {
868
- "AnnAssign", "Assign", "Assert", "AugAssign", "Delete", "Expr", "Global",
869
- "Import", "ImportFrom", "Nonlocal", "Pass",
865
+ "AnnAssign",
866
+ "Assign",
867
+ "Assert",
868
+ "AugAssign",
869
+ "Delete",
870
+ "Expr",
871
+ "Global",
872
+ "Import",
873
+ "ImportFrom",
874
+ "Nonlocal",
875
+ "Pass",
870
876
  }
871
877
 
872
878
  def node_exits(self, node: ast.AST) -> set[ArcStart]:
@@ -889,7 +895,7 @@ class AstArcAnalyzer:
889
895
  node_name = node.__class__.__name__
890
896
  handler = cast(
891
897
  Optional[Callable[[ast.AST], set[ArcStart]]],
892
- getattr(self, "_handle__" + node_name, None),
898
+ getattr(self, f"_handle__{node_name}", None),
893
899
  )
894
900
  if handler is not None:
895
901
  arc_starts = handler(node)
@@ -898,7 +904,7 @@ class AstArcAnalyzer:
898
904
  # statement), or it's something we overlooked.
899
905
  if env.TESTING:
900
906
  if node_name not in self.OK_TO_DEFAULT:
901
- raise RuntimeError(f"*** Unhandled: {node}") # pragma: only failure
907
+ raise RuntimeError(f"*** Unhandled: {node}") # pragma: only failure
902
908
 
903
909
  # Default for simple statements: one exit from this node.
904
910
  arc_starts = {ArcStart(self.line_for_node(node))}
@@ -937,108 +943,12 @@ class AstArcAnalyzer:
937
943
  for body_node in body:
938
944
  lineno = self.line_for_node(body_node)
939
945
  if lineno not in self.statements:
940
- maybe_body_node = self.find_non_missing_node(body_node)
941
- if maybe_body_node is None:
942
- continue
943
- body_node = maybe_body_node
944
- lineno = self.line_for_node(body_node)
946
+ continue
945
947
  for prev_start in prev_starts:
946
948
  self.add_arc(prev_start.lineno, lineno, prev_start.cause)
947
949
  prev_starts = self.node_exits(body_node)
948
950
  return prev_starts
949
951
 
950
- def find_non_missing_node(self, node: ast.AST) -> ast.AST | None:
951
- """Search `node` looking for a child that has not been optimized away.
952
-
953
- This might return the node you started with, or it will work recursively
954
- to find a child node in self.statements.
955
-
956
- Returns a node, or None if none of the node remains.
957
-
958
- """
959
- # This repeats work just done in process_body, but this duplication
960
- # means we can avoid a function call in the 99.9999% case of not
961
- # optimizing away statements.
962
- lineno = self.line_for_node(node)
963
- if lineno in self.statements:
964
- return node
965
-
966
- missing_fn = cast(
967
- Optional[Callable[[ast.AST], Optional[ast.AST]]],
968
- getattr(self, "_missing__" + node.__class__.__name__, None),
969
- )
970
- if missing_fn is not None:
971
- ret_node = missing_fn(node)
972
- else:
973
- ret_node = None
974
- return ret_node
975
-
976
- # Missing nodes: _missing__*
977
- #
978
- # Entire statements can be optimized away by Python. They will appear in
979
- # the AST, but not the bytecode. These functions are called (by
980
- # find_non_missing_node) to find a node to use instead of the missing
981
- # node. They can return None if the node should truly be gone.
982
-
983
- def _missing__If(self, node: ast.If) -> ast.AST | None:
984
- # If the if-node is missing, then one of its children might still be
985
- # here, but not both. So return the first of the two that isn't missing.
986
- # Use a NodeList to hold the clauses as a single node.
987
- non_missing = self.find_non_missing_node(NodeList(node.body))
988
- if non_missing:
989
- return non_missing
990
- if node.orelse:
991
- return self.find_non_missing_node(NodeList(node.orelse))
992
- return None
993
-
994
- def _missing__NodeList(self, node: NodeList) -> ast.AST | None:
995
- # A NodeList might be a mixture of missing and present nodes. Find the
996
- # ones that are present.
997
- non_missing_children = []
998
- for child in node.body:
999
- maybe_child = self.find_non_missing_node(child)
1000
- if maybe_child is not None:
1001
- non_missing_children.append(maybe_child)
1002
-
1003
- # Return the simplest representation of the present children.
1004
- if not non_missing_children:
1005
- return None
1006
- if len(non_missing_children) == 1:
1007
- return non_missing_children[0]
1008
- return NodeList(non_missing_children)
1009
-
1010
- def _missing__While(self, node: ast.While) -> ast.AST | None:
1011
- body_nodes = self.find_non_missing_node(NodeList(node.body))
1012
- if not body_nodes:
1013
- return None
1014
- # Make a synthetic While-true node.
1015
- new_while = ast.While() # type: ignore[call-arg]
1016
- new_while.lineno = body_nodes.lineno # type: ignore[attr-defined]
1017
- new_while.test = ast.Name() # type: ignore[call-arg]
1018
- new_while.test.lineno = body_nodes.lineno # type: ignore[attr-defined]
1019
- new_while.test.id = "True"
1020
- assert hasattr(body_nodes, "body")
1021
- new_while.body = body_nodes.body
1022
- new_while.orelse = []
1023
- return new_while
1024
-
1025
- def is_constant_expr(self, node: ast.AST) -> str | None:
1026
- """Is this a compile-time constant?"""
1027
- node_name = node.__class__.__name__
1028
- if node_name in ["Constant", "NameConstant", "Num"]:
1029
- return "Num"
1030
- elif isinstance(node, ast.Name):
1031
- if node.id in ["True", "False", "None", "__debug__"]:
1032
- return "Name"
1033
- return None
1034
-
1035
- # In the fullness of time, these might be good tests to write:
1036
- # while EXPR:
1037
- # while False:
1038
- # listcomps hidden deep in other expressions
1039
- # listcomps hidden in lists: x = [[i for i in range(10)]]
1040
- # nested function definitions
1041
-
1042
952
  # Exit processing: process_*_exits
1043
953
  #
1044
954
  # These functions process the four kinds of jump exits: break, continue,
@@ -1049,13 +959,13 @@ class AstArcAnalyzer:
1049
959
 
1050
960
  def process_break_exits(self, exits: set[ArcStart]) -> None:
1051
961
  """Add arcs due to jumps from `exits` being breaks."""
1052
- for block in self.nearest_blocks(): # pragma: always breaks
962
+ for block in self.nearest_blocks(): # pragma: always breaks
1053
963
  if block.process_break_exits(exits, self.add_arc):
1054
964
  break
1055
965
 
1056
966
  def process_continue_exits(self, exits: set[ArcStart]) -> None:
1057
967
  """Add arcs due to jumps from `exits` being continues."""
1058
- for block in self.nearest_blocks(): # pragma: always breaks
968
+ for block in self.nearest_blocks(): # pragma: always breaks
1059
969
  if block.process_continue_exits(exits, self.add_arc):
1060
970
  break
1061
971
 
@@ -1067,7 +977,7 @@ class AstArcAnalyzer:
1067
977
 
1068
978
  def process_return_exits(self, exits: set[ArcStart]) -> None:
1069
979
  """Add arcs due to jumps from `exits` being returns."""
1070
- for block in self.nearest_blocks(): # pragma: always breaks
980
+ for block in self.nearest_blocks(): # pragma: always breaks
1071
981
  if block.process_return_exits(exits, self.add_arc):
1072
982
  break
1073
983
 
@@ -1097,8 +1007,8 @@ class AstArcAnalyzer:
1097
1007
  last = None
1098
1008
  for dec_node in decs:
1099
1009
  dec_start = self.line_for_node(dec_node)
1100
- if last is not None and dec_start != last: # type: ignore[unreachable]
1101
- self.add_arc(last, dec_start) # type: ignore[unreachable]
1010
+ if last is not None and dec_start != last:
1011
+ self.add_arc(last, dec_start)
1102
1012
  last = dec_start
1103
1013
  assert last is not None
1104
1014
  self.add_arc(last, main_line)
@@ -1147,48 +1057,44 @@ class AstArcAnalyzer:
1147
1057
 
1148
1058
  def _handle__If(self, node: ast.If) -> set[ArcStart]:
1149
1059
  start = self.line_for_node(node.test)
1150
- from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
1151
- exits = self.process_body(node.body, from_start=from_start)
1152
- from_start = ArcStart(start, cause="the condition on line {lineno} was always true")
1153
- exits |= self.process_body(node.orelse, from_start=from_start)
1060
+ constant_test, val = is_constant_test_expr(node.test)
1061
+ exits = set()
1062
+ if not constant_test or val:
1063
+ from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
1064
+ exits |= self.process_body(node.body, from_start=from_start)
1065
+ if not constant_test or not val:
1066
+ from_start = ArcStart(start, cause="the condition on line {lineno} was always true")
1067
+ exits |= self.process_body(node.orelse, from_start=from_start)
1154
1068
  return exits
1155
1069
 
1156
- if sys.version_info >= (3, 10):
1157
- def _handle__Match(self, node: ast.Match) -> set[ArcStart]:
1158
- start = self.line_for_node(node)
1159
- last_start = start
1160
- exits = set()
1161
- for case in node.cases:
1162
- case_start = self.line_for_node(case.pattern)
1163
- self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched")
1164
- from_start = ArcStart(
1165
- case_start,
1166
- cause="the pattern on line {lineno} never matched",
1167
- )
1168
- exits |= self.process_body(case.body, from_start=from_start)
1169
- last_start = case_start
1170
-
1171
- # case is now the last case, check for wildcard match.
1172
- pattern = case.pattern # pylint: disable=undefined-loop-variable
1173
- while isinstance(pattern, ast.MatchOr):
1174
- pattern = pattern.patterns[-1]
1175
- while isinstance(pattern, ast.MatchAs) and pattern.pattern is not None:
1176
- pattern = pattern.pattern
1177
- had_wildcard = (
1178
- isinstance(pattern, ast.MatchAs)
1179
- and pattern.pattern is None
1180
- and case.guard is None # pylint: disable=undefined-loop-variable
1070
+ def _handle__Match(self, node: ast.Match) -> set[ArcStart]:
1071
+ start = self.line_for_node(node)
1072
+ last_start = start
1073
+ exits = set()
1074
+ for case in node.cases:
1075
+ case_start = self.line_for_node(case.pattern)
1076
+ self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched")
1077
+ from_start = ArcStart(
1078
+ case_start,
1079
+ cause="the pattern on line {lineno} never matched",
1181
1080
  )
1081
+ exits |= self.process_body(case.body, from_start=from_start)
1082
+ last_start = case_start
1083
+
1084
+ # case is now the last case, check for wildcard match.
1085
+ pattern = case.pattern # pylint: disable=undefined-loop-variable
1086
+ while isinstance(pattern, ast.MatchOr):
1087
+ pattern = pattern.patterns[-1]
1088
+ while isinstance(pattern, ast.MatchAs) and pattern.pattern is not None:
1089
+ pattern = pattern.pattern
1090
+ had_wildcard = (
1091
+ isinstance(pattern, ast.MatchAs) and pattern.pattern is None and case.guard is None # pylint: disable=undefined-loop-variable
1092
+ )
1182
1093
 
1183
- if not had_wildcard:
1184
- exits.add(
1185
- ArcStart(case_start, cause="the pattern on line {lineno} always matched"),
1186
- )
1187
- return exits
1188
-
1189
- def _handle__NodeList(self, node: NodeList) -> set[ArcStart]:
1190
- start = self.line_for_node(node)
1191
- exits = self.process_body(node.body, from_start=ArcStart(start))
1094
+ if not had_wildcard:
1095
+ exits.add(
1096
+ ArcStart(case_start, cause="the pattern on line {lineno} always matched"),
1097
+ )
1192
1098
  return exits
1193
1099
 
1194
1100
  def _handle__Raise(self, node: ast.Raise) -> set[ArcStart]:
@@ -1260,16 +1166,11 @@ class AstArcAnalyzer:
1260
1166
 
1261
1167
  return exits
1262
1168
 
1169
+ _handle__TryStar = _handle__Try
1170
+
1263
1171
  def _handle__While(self, node: ast.While) -> set[ArcStart]:
1264
1172
  start = to_top = self.line_for_node(node.test)
1265
- constant_test = self.is_constant_expr(node.test)
1266
- top_is_body0 = False
1267
- if constant_test:
1268
- top_is_body0 = True
1269
- if env.PYBEHAVIOR.keep_constant_test:
1270
- top_is_body0 = False
1271
- if top_is_body0:
1272
- to_top = self.line_for_node(node.body[0])
1173
+ constant_test, _ = is_constant_test_expr(node.test)
1273
1174
  self.block_stack.append(LoopBlock(start=to_top))
1274
1175
  from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
1275
1176
  exits = self.process_body(node.body, from_start=from_start)
@@ -1294,22 +1195,20 @@ class AstArcAnalyzer:
1294
1195
  starts = [self.line_for_node(item.context_expr) for item in node.items]
1295
1196
  else:
1296
1197
  starts = [self.line_for_node(node)]
1297
- if env.PYBEHAVIOR.exit_through_with:
1298
- for start in starts:
1299
- self.current_with_starts.add(start)
1300
- self.all_with_starts.add(start)
1198
+ for start in starts:
1199
+ self.current_with_starts.add(start)
1200
+ self.all_with_starts.add(start)
1301
1201
 
1302
1202
  exits = self.process_body(node.body, from_start=ArcStart(starts[-1]))
1303
1203
 
1304
- if env.PYBEHAVIOR.exit_through_with:
1305
- start = starts[-1]
1306
- self.current_with_starts.remove(start)
1307
- with_exit = {ArcStart(start)}
1308
- if exits:
1309
- for xit in exits:
1310
- self.add_arc(xit.lineno, start)
1311
- self.with_exits.add((xit.lineno, start))
1312
- exits = with_exit
1204
+ start = starts[-1]
1205
+ self.current_with_starts.remove(start)
1206
+ with_exit = {ArcStart(start)}
1207
+ if exits:
1208
+ for xit in exits:
1209
+ self.add_arc(xit.lineno, start)
1210
+ self.with_exits.add((xit.lineno, start))
1211
+ exits = with_exit
1313
1212
 
1314
1213
  return exits
1315
1214