Skip to content

Commit 423fa59

Browse files
committedFeb 8, 2023
feat: simplify purges_files
Also, move tests to test_data.py, and finish covering the code.
1 parent cb7d679 commit 423fa59

File tree

5 files changed

+53
-130
lines changed

5 files changed

+53
-130
lines changed
 

‎CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ development at the same time, such as 4.5.x and 5.0.
2020
Unreleased
2121
----------
2222

23+
- Added a :meth:`.CoverageData.purge_files` method to remove recorded data for
24+
a particular file. Contributed by `Stephan Deibel <pull 1547_>`_.
25+
2326
- Fix: In some embedded environments, an IndexError could occur on stop() when
2427
the originating thread exits before completion. This is now fixed, thanks to
2528
`Russell Keith-Magee <pull 1543_>`_, closing `issue 1542`_.
@@ -29,6 +32,7 @@ Unreleased
2932

3033
.. _issue 1542: https://github.com/nedbat/coveragepy/issues/1542
3134
.. _pull 1543: https://github.com/nedbat/coveragepy/pull/1543
35+
.. _pull 1547: https://github.com/nedbat/coveragepy/pull/1547
3236
.. _pull 1550: https://github.com/nedbat/coveragepy/pull/1550
3337

3438

‎CONTRIBUTORS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ Sigve Tjora
149149
Simon Willison
150150
Stan Hu
151151
Stefan Behnel
152+
Stephan Deibel
152153
Stephan Richter
153154
Stephen Finucane
154155
Steve Dower

‎coverage/sqldata.py

+19-24
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,11 @@ class CoverageData(AutoReprMixin):
197197
198198
Write the data to its file with :meth:`write`.
199199
200-
You can clear the data in memory with :meth:`erase`. Two data collections
201-
can be combined by using :meth:`update` on one :class:`CoverageData`,
202-
passing it the other.
200+
You can clear the data in memory with :meth:`erase`. Data for specific
201+
files can be removed from the database with :meth:`purge_files`.
202+
203+
Two data collections can be combined by using :meth:`update` on one
204+
:class:`CoverageData`, passing it the other.
203205
204206
Data in a :class:`CoverageData` can be serialized and deserialized with
205207
:meth:`dumps` and :meth:`loads`.
@@ -615,41 +617,29 @@ def touch_files(self, filenames: Collection[str], plugin_name: Optional[str] = N
615617
# Set the tracer for this file
616618
self.add_file_tracers({filename: plugin_name})
617619

618-
def purge_files(self, filenames: Iterable[str], context: Optional[str] = None) -> None:
620+
def purge_files(self, filenames: Collection[str]) -> None:
619621
"""Purge any existing coverage data for the given `filenames`.
620622
621-
If `context` is given, purge only data associated with that measurement context.
622-
"""
623+
.. versionadded:: 7.2
623624
625+
"""
624626
if self._debug.should("dataop"):
625-
self._debug.write(f"Purging {filenames!r} for context {context}")
627+
self._debug.write(f"Purging data for {filenames!r}")
626628
self._start_using()
627629
with self._connect() as con:
628630

629-
if context is not None:
630-
context_id = self._context_id(context)
631-
if context_id is None:
632-
raise DataError("Unknown context {context}")
633-
else:
634-
context_id = None
635-
636631
if self._has_lines:
637-
table = 'line_bits'
632+
sql = "delete from line_bits where file_id=?"
638633
elif self._has_arcs:
639-
table = 'arcs'
634+
sql = "delete from arc where file_id=?"
640635
else:
641-
return
636+
raise DataError("Can't purge files in an empty CoverageData")
642637

643638
for filename in filenames:
644639
file_id = self._file_id(filename, add=False)
645640
if file_id is None:
646641
continue
647-
self._file_map.pop(filename, None)
648-
if context_id is None:
649-
q = f'delete from {table} where file_id={file_id}'
650-
else:
651-
q = f'delete from {table} where file_id={file_id} and context_id={context_id}'
652-
con.execute(q)
642+
con.execute_void(sql, (file_id,))
653643

654644
def update(self, other_data: CoverageData, aliases: Optional[PathAliases] = None) -> None:
655645
"""Update this data with data from several other :class:`CoverageData` instances.
@@ -868,7 +858,12 @@ def has_arcs(self) -> bool:
868858
return bool(self._has_arcs)
869859

870860
def measured_files(self) -> Set[str]:
871-
"""A set of all files that had been measured."""
861+
"""A set of all files that have been measured.
862+
863+
Note that a file may be mentioned as measured even though no lines or
864+
arcs for that file are present in the data.
865+
866+
"""
872867
return set(self._file_map)
873868

874869
def measured_contexts(self) -> Set[str]:

‎tests/test_api.py

-106
Original file line numberDiff line numberDiff line change
@@ -754,112 +754,6 @@ def test_run_debug_sys(self) -> None:
754754
cov.stop() # pragma: nested
755755
assert cast(str, d['data_file']).endswith(".coverage")
756756

757-
def test_purge_filenames(self) -> None:
758-
759-
fn1 = self.make_file("mymain.py", """\
760-
import mymod
761-
a = 1
762-
""")
763-
fn1 = os.path.join(self.temp_dir, fn1)
764-
765-
fn2 = self.make_file("mymod.py", """\
766-
fooey = 17
767-
""")
768-
fn2 = os.path.join(self.temp_dir, fn2)
769-
770-
cov = coverage.Coverage()
771-
self.start_import_stop(cov, "mymain")
772-
773-
data = cov.get_data()
774-
775-
# Initial measurement was for two files
776-
assert len(data.measured_files()) == 2
777-
assert [1, 2] == sorted_lines(data, fn1)
778-
assert [1,] == sorted_lines(data, fn2)
779-
780-
# Purge one file's data and one should remain
781-
data.purge_files([fn1])
782-
assert len(data.measured_files()) == 1
783-
assert [] == sorted_lines(data, fn1)
784-
assert [1,] == sorted_lines(data, fn2)
785-
786-
# Purge second file's data and none should remain
787-
data.purge_files([fn2])
788-
assert len(data.measured_files()) == 0
789-
assert [] == sorted_lines(data, fn1)
790-
assert [] == sorted_lines(data, fn2)
791-
792-
def test_purge_filenames_context(self) -> None:
793-
794-
fn1 = self.make_file("mymain.py", """\
795-
import mymod
796-
a = 1
797-
""")
798-
fn1 = os.path.join(self.temp_dir, fn1)
799-
800-
fn2 = self.make_file("mymod.py", """\
801-
fooey = 17
802-
""")
803-
fn2 = os.path.join(self.temp_dir, fn2)
804-
805-
def dummy_function() -> None:
806-
unused = 42
807-
808-
# Start/stop since otherwise cantext
809-
cov = coverage.Coverage()
810-
cov.start()
811-
cov.switch_context('initialcontext')
812-
dummy_function()
813-
cov.switch_context('testcontext')
814-
cov.stop()
815-
self.start_import_stop(cov, "mymain")
816-
817-
data = cov.get_data()
818-
819-
# Initial measurement was for three files and two contexts
820-
assert len(data.measured_files()) == 3
821-
assert [1, 2] == sorted_lines(data, fn1)
822-
assert [1,] == sorted_lines(data, fn2)
823-
assert len(sorted_lines(data, __file__)) == 1
824-
assert len(data.measured_contexts()) == 2
825-
826-
# Remove specifying wrong context should raise exception and not remove anything
827-
try:
828-
data.purge_files([fn1], 'wrongcontext')
829-
except coverage.sqldata.DataError:
830-
pass
831-
else:
832-
assert 0, "exception expected"
833-
assert len(data.measured_files()) == 3
834-
assert [1, 2] == sorted_lines(data, fn1)
835-
assert [1,] == sorted_lines(data, fn2)
836-
assert len(sorted_lines(data, __file__)) == 1
837-
assert len(data.measured_contexts()) == 2
838-
839-
# Remove one file specifying correct context
840-
data.purge_files([fn1], 'testcontext')
841-
assert len(data.measured_files()) == 2
842-
assert [] == sorted_lines(data, fn1)
843-
assert [1,] == sorted_lines(data, fn2)
844-
assert len(sorted_lines(data, __file__)) == 1
845-
assert len(data.measured_contexts()) == 2
846-
847-
# Remove second file with other correct context
848-
data.purge_files([__file__], 'initialcontext')
849-
assert len(data.measured_files()) == 1
850-
assert [] == sorted_lines(data, fn1)
851-
assert [1,] == sorted_lines(data, fn2)
852-
assert len(sorted_lines(data, __file__)) == 0
853-
assert len(data.measured_contexts()) == 2
854-
855-
# Remove last file specifying correct context
856-
data.purge_files([fn2], 'testcontext')
857-
assert len(data.measured_files()) == 0
858-
assert [] == sorted_lines(data, fn1)
859-
assert [] == sorted_lines(data, fn2)
860-
assert len(sorted_lines(data, __file__)) == 0
861-
assert len(data.measured_contexts()) == 2
862-
863757

864758
class CurrentInstanceTest(CoverageTest):
865759
"""Tests of Coverage.current()."""

‎tests/test_data.py

+29
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,35 @@ def thread_main() -> None:
588588
assert_lines1_data(covdata)
589589
assert not exceptions
590590

591+
def test_purge_files_lines(self) -> None:
592+
covdata = DebugCoverageData()
593+
covdata.add_lines(LINES_1)
594+
covdata.add_lines(LINES_2)
595+
assert_line_counts(covdata, SUMMARY_1_2)
596+
covdata.purge_files(["a.py", "b.py"])
597+
assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 1})
598+
covdata.purge_files(["c.py"])
599+
assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0})
600+
# It's OK to "purge" a file that wasn't measured.
601+
covdata.purge_files(["xyz.py"])
602+
assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0})
603+
604+
def test_purge_files_arcs(self) -> None:
605+
covdata = CoverageData()
606+
covdata.add_arcs(ARCS_3)
607+
covdata.add_arcs(ARCS_4)
608+
assert_line_counts(covdata, SUMMARY_3_4)
609+
covdata.purge_files(["x.py", "y.py"])
610+
assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 1})
611+
covdata.purge_files(["z.py"])
612+
assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 0})
613+
614+
def test_cant_purge_in_empty_data(self) -> None:
615+
covdata = DebugCoverageData()
616+
msg = "Can't purge files in an empty CoverageData"
617+
with pytest.raises(DataError, match=msg):
618+
covdata.purge_files(["abc.py"])
619+
591620

592621
class CoverageDataInTempDirTest(CoverageTest):
593622
"""Tests of CoverageData that need a temporary directory to make files."""

0 commit comments

Comments
 (0)
Please sign in to comment.