eric7/DebugClients/Python/coverage/sqldata.py

branch
eric7
changeset 8775
0802ae193343
parent 8527
2bd1325d727e
child 8929
fcca2fa618bf
equal deleted inserted replaced
8774:d728227e8ebb 8775:0802ae193343
6 # TODO: factor out dataop debugging to a wrapper class? 6 # TODO: factor out dataop debugging to a wrapper class?
7 # TODO: make sure all dataop debugging is in place somehow 7 # TODO: make sure all dataop debugging is in place somehow
8 8
9 import collections 9 import collections
10 import datetime 10 import datetime
11 import functools
11 import glob 12 import glob
12 import itertools 13 import itertools
13 import os 14 import os
14 import re 15 import re
15 import sqlite3 16 import sqlite3
16 import sys 17 import sys
18 import threading
17 import zlib 19 import zlib
18 20
19 from coverage import env
20 from coverage.backward import get_thread_id, iitems, to_bytes, to_string
21 from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr 21 from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr
22 from coverage.exceptions import CoverageException
22 from coverage.files import PathAliases 23 from coverage.files import PathAliases
23 from coverage.misc import CoverageException, contract, file_be_gone, filename_suffix, isolate_module 24 from coverage.misc import contract, file_be_gone, filename_suffix, isolate_module
24 from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits 25 from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits
25 from coverage.version import __version__ 26 from coverage.version import __version__
26 27
27 os = isolate_module(os) 28 os = isolate_module(os)
28 29
177 passing it the other. 178 passing it the other.
178 179
179 Data in a :class:`CoverageData` can be serialized and deserialized with 180 Data in a :class:`CoverageData` can be serialized and deserialized with
180 :meth:`dumps` and :meth:`loads`. 181 :meth:`dumps` and :meth:`loads`.
181 182
183 The methods used during the coverage.py collection phase
184 (:meth:`add_lines`, :meth:`add_arcs`, :meth:`set_context`, and
185 :meth:`add_file_tracers`) are thread-safe. Other methods may not be.
186
182 """ 187 """
183 188
184 def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None): 189 def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None):
185 """Create a :class:`CoverageData` object to hold coverage-measured data. 190 """Create a :class:`CoverageData` object to hold coverage-measured data.
186 191
205 self._choose_filename() 210 self._choose_filename()
206 self._file_map = {} 211 self._file_map = {}
207 # Maps thread ids to SqliteDb objects. 212 # Maps thread ids to SqliteDb objects.
208 self._dbs = {} 213 self._dbs = {}
209 self._pid = os.getpid() 214 self._pid = os.getpid()
215 # Synchronize the operations used during collection.
216 self._lock = threading.Lock()
210 217
211 # Are we in sync with the data file? 218 # Are we in sync with the data file?
212 self._have_used = False 219 self._have_used = False
213 220
214 self._has_lines = False 221 self._has_lines = False
215 self._has_arcs = False 222 self._has_arcs = False
216 223
217 self._current_context = None 224 self._current_context = None
218 self._current_context_id = None 225 self._current_context_id = None
219 self._query_context_ids = None 226 self._query_context_ids = None
227
228 def _locked(method): # pylint: disable=no-self-argument
229 """A decorator for methods that should hold self._lock."""
230 @functools.wraps(method)
231 def _wrapped(self, *args, **kwargs):
232 with self._lock:
233 # pylint: disable=not-callable
234 return method(self, *args, **kwargs)
235 return _wrapped
220 236
221 def _choose_filename(self): 237 def _choose_filename(self):
222 """Set self._filename based on inited attributes.""" 238 """Set self._filename based on inited attributes."""
223 if self._no_disk: 239 if self._no_disk:
224 self._filename = ":memory:" 240 self._filename = ":memory:"
241 def _create_db(self): 257 def _create_db(self):
242 """Create a db file that doesn't exist yet. 258 """Create a db file that doesn't exist yet.
243 259
244 Initializes the schema and certain metadata. 260 Initializes the schema and certain metadata.
245 """ 261 """
246 if self._debug.should('dataio'): 262 if self._debug.should("dataio"):
247 self._debug.write("Creating data file {!r}".format(self._filename)) 263 self._debug.write(f"Creating data file {self._filename!r}")
248 self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) 264 self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug)
249 with db: 265 with db:
250 db.executescript(SCHEMA) 266 db.executescript(SCHEMA)
251 db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) 267 db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,))
252 db.executemany( 268 db.executemany(
253 "insert into meta (key, value) values (?, ?)", 269 "insert into meta (key, value) values (?, ?)",
254 [ 270 [
255 ('sys_argv', str(getattr(sys, 'argv', None))), 271 ("sys_argv", str(getattr(sys, "argv", None))),
256 ('version', __version__), 272 ("version", __version__),
257 ('when', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')), 273 ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
258 ] 274 ]
259 ) 275 )
260 276
261 def _open_db(self): 277 def _open_db(self):
262 """Open an existing db file, and read its metadata.""" 278 """Open an existing db file, and read its metadata."""
263 if self._debug.should('dataio'): 279 if self._debug.should("dataio"):
264 self._debug.write("Opening data file {!r}".format(self._filename)) 280 self._debug.write(f"Opening data file {self._filename!r}")
265 self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) 281 self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug)
266 self._read_db() 282 self._read_db()
267 283
268 def _read_db(self): 284 def _read_db(self):
269 """Read the metadata from a database so that we are ready to use it.""" 285 """Read the metadata from a database so that we are ready to use it."""
270 with self._dbs[get_thread_id()] as db: 286 with self._dbs[threading.get_ident()] as db:
271 try: 287 try:
272 schema_version, = db.execute_one("select version from coverage_schema") 288 schema_version, = db.execute_one("select version from coverage_schema")
273 except Exception as exc: 289 except Exception as exc:
274 raise CoverageException( 290 raise CoverageException(
275 "Data file {!r} doesn't seem to be a coverage data file: {}".format( 291 "Data file {!r} doesn't seem to be a coverage data file: {}".format(
276 self._filename, exc 292 self._filename, exc
277 ) 293 )
278 ) 294 ) from exc
279 else: 295 else:
280 if schema_version != SCHEMA_VERSION: 296 if schema_version != SCHEMA_VERSION:
281 raise CoverageException( 297 raise CoverageException(
282 "Couldn't use data file {!r}: wrong schema: {} instead of {}".format( 298 "Couldn't use data file {!r}: wrong schema: {} instead of {}".format(
283 self._filename, schema_version, SCHEMA_VERSION 299 self._filename, schema_version, SCHEMA_VERSION
291 for path, file_id in db.execute("select path, id from file"): 307 for path, file_id in db.execute("select path, id from file"):
292 self._file_map[path] = file_id 308 self._file_map[path] = file_id
293 309
294 def _connect(self): 310 def _connect(self):
295 """Get the SqliteDb object to use.""" 311 """Get the SqliteDb object to use."""
296 if get_thread_id() not in self._dbs: 312 if threading.get_ident() not in self._dbs:
297 if os.path.exists(self._filename): 313 if os.path.exists(self._filename):
298 self._open_db() 314 self._open_db()
299 else: 315 else:
300 self._create_db() 316 self._create_db()
301 return self._dbs[get_thread_id()] 317 return self._dbs[threading.get_ident()]
302 318
303 def __nonzero__(self): 319 def __nonzero__(self):
304 if (get_thread_id() not in self._dbs and not os.path.exists(self._filename)): 320 if (threading.get_ident() not in self._dbs and not os.path.exists(self._filename)):
305 return False 321 return False
306 try: 322 try:
307 with self._connect() as con: 323 with self._connect() as con:
308 rows = con.execute("select * from file limit 1") 324 rows = con.execute("select * from file limit 1")
309 return bool(list(rows)) 325 return bool(list(rows))
310 except CoverageException: 326 except CoverageException:
311 return False 327 return False
312 328
313 __bool__ = __nonzero__ 329 __bool__ = __nonzero__
314 330
315 @contract(returns='bytes') 331 @contract(returns="bytes")
316 def dumps(self): 332 def dumps(self):
317 """Serialize the current data to a byte string. 333 """Serialize the current data to a byte string.
318 334
319 The format of the serialized data is not documented. It is only 335 The format of the serialized data is not documented. It is only
320 suitable for use with :meth:`loads` in the same version of 336 suitable for use with :meth:`loads` in the same version of
321 coverage.py. 337 coverage.py.
322 338
339 Note that this serialization is not what gets stored in coverage data
340 files. This method is meant to produce bytes that can be transmitted
341 elsewhere and then deserialized with :meth:`loads`.
342
323 Returns: 343 Returns:
324 A byte string of serialized data. 344 A byte string of serialized data.
325 345
326 .. versionadded:: 5.0 346 .. versionadded:: 5.0
327 347
328 """ 348 """
329 if self._debug.should('dataio'): 349 if self._debug.should("dataio"):
330 self._debug.write("Dumping data from data file {!r}".format(self._filename)) 350 self._debug.write(f"Dumping data from data file {self._filename!r}")
331 with self._connect() as con: 351 with self._connect() as con:
332 return b'z' + zlib.compress(to_bytes(con.dump())) 352 return b"z" + zlib.compress(con.dump().encode("utf-8"))
333 353
334 @contract(data='bytes') 354 @contract(data="bytes")
335 def loads(self, data): 355 def loads(self, data):
336 """Deserialize data from :meth:`dumps` 356 """Deserialize data from :meth:`dumps`.
337 357
338 Use with a newly-created empty :class:`CoverageData` object. It's 358 Use with a newly-created empty :class:`CoverageData` object. It's
339 undefined what happens if the object already has data in it. 359 undefined what happens if the object already has data in it.
340 360
361 Note that this is not for reading data from a coverage data file. It
362 is only for use on data you produced with :meth:`dumps`.
363
341 Arguments: 364 Arguments:
342 data: A byte string of serialized data produced by :meth:`dumps`. 365 data: A byte string of serialized data produced by :meth:`dumps`.
343 366
344 .. versionadded:: 5.0 367 .. versionadded:: 5.0
345 368
346 """ 369 """
347 if self._debug.should('dataio'): 370 if self._debug.should("dataio"):
348 self._debug.write("Loading data into data file {!r}".format(self._filename)) 371 self._debug.write(f"Loading data into data file {self._filename!r}")
349 if data[:1] != b'z': 372 if data[:1] != b"z":
350 raise CoverageException( 373 raise CoverageException(
351 "Unrecognized serialization: {!r} (head of {} bytes)".format(data[:40], len(data)) 374 f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)"
352 ) 375 )
353 script = to_string(zlib.decompress(data[1:])) 376 script = zlib.decompress(data[1:]).decode("utf-8")
354 self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) 377 self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug)
355 with db: 378 with db:
356 db.executescript(script) 379 db.executescript(script)
357 self._read_db() 380 self._read_db()
358 self._have_used = True 381 self._have_used = True
359 382
379 if row is not None: 402 if row is not None:
380 return row[0] 403 return row[0]
381 else: 404 else:
382 return None 405 return None
383 406
407 @_locked
384 def set_context(self, context): 408 def set_context(self, context):
385 """Set the current context for future :meth:`add_lines` etc. 409 """Set the current context for future :meth:`add_lines` etc.
386 410
387 `context` is a str, the name of the context to use for the next data 411 `context` is a str, the name of the context to use for the next data
388 additions. The context persists until the next :meth:`set_context`. 412 additions. The context persists until the next :meth:`set_context`.
389 413
390 .. versionadded:: 5.0 414 .. versionadded:: 5.0
391 415
392 """ 416 """
393 if self._debug.should('dataop'): 417 if self._debug.should("dataop"):
394 self._debug.write("Setting context: %r" % (context,)) 418 self._debug.write(f"Setting context: {context!r}")
395 self._current_context = context 419 self._current_context = context
396 self._current_context_id = None 420 self._current_context_id = None
397 421
398 def _set_context_id(self): 422 def _set_context_id(self):
399 """Use the _current_context to set _current_context_id.""" 423 """Use the _current_context to set _current_context_id."""
420 .. versionadded:: 5.0 444 .. versionadded:: 5.0
421 445
422 """ 446 """
423 return self._filename 447 return self._filename
424 448
449 @_locked
425 def add_lines(self, line_data): 450 def add_lines(self, line_data):
426 """Add measured line data. 451 """Add measured line data.
427 452
428 `line_data` is a dictionary mapping file names to dictionaries:: 453 `line_data` is a dictionary mapping file names to iterables of ints::
429 454
430 { filename: { lineno: None, ... }, ...} 455 { filename: { line1, line2, ... }, ...}
431 456
432 """ 457 """
433 if self._debug.should('dataop'): 458 if self._debug.should("dataop"):
434 self._debug.write("Adding lines: %d files, %d lines total" % ( 459 self._debug.write("Adding lines: %d files, %d lines total" % (
435 len(line_data), sum(len(lines) for lines in line_data.values()) 460 len(line_data), sum(len(lines) for lines in line_data.values())
436 )) 461 ))
437 self._start_using() 462 self._start_using()
438 self._choose_lines_or_arcs(lines=True) 463 self._choose_lines_or_arcs(lines=True)
439 if not line_data: 464 if not line_data:
440 return 465 return
441 with self._connect() as con: 466 with self._connect() as con:
442 self._set_context_id() 467 self._set_context_id()
443 for filename, linenos in iitems(line_data): 468 for filename, linenos in line_data.items():
444 linemap = nums_to_numbits(linenos) 469 linemap = nums_to_numbits(linenos)
445 file_id = self._file_id(filename, add=True) 470 file_id = self._file_id(filename, add=True)
446 query = "select numbits from line_bits where file_id = ? and context_id = ?" 471 query = "select numbits from line_bits where file_id = ? and context_id = ?"
447 existing = list(con.execute(query, (file_id, self._current_context_id))) 472 existing = list(con.execute(query, (file_id, self._current_context_id)))
448 if existing: 473 if existing:
449 linemap = numbits_union(linemap, existing[0][0]) 474 linemap = numbits_union(linemap, existing[0][0])
450 475
451 con.execute( 476 con.execute(
452 "insert or replace into line_bits " 477 "insert or replace into line_bits " +
453 " (file_id, context_id, numbits) values (?, ?, ?)", 478 " (file_id, context_id, numbits) values (?, ?, ?)",
454 (file_id, self._current_context_id, linemap), 479 (file_id, self._current_context_id, linemap),
455 ) 480 )
456 481
482 @_locked
457 def add_arcs(self, arc_data): 483 def add_arcs(self, arc_data):
458 """Add measured arc data. 484 """Add measured arc data.
459 485
460 `arc_data` is a dictionary mapping file names to dictionaries:: 486 `arc_data` is a dictionary mapping file names to iterables of pairs of
461 487 ints::
462 { filename: { (l1,l2): None, ... }, ...} 488
463 489 { filename: { (l1,l2), (l1,l2), ... }, ...}
464 """ 490
465 if self._debug.should('dataop'): 491 """
492 if self._debug.should("dataop"):
466 self._debug.write("Adding arcs: %d files, %d arcs total" % ( 493 self._debug.write("Adding arcs: %d files, %d arcs total" % (
467 len(arc_data), sum(len(arcs) for arcs in arc_data.values()) 494 len(arc_data), sum(len(arcs) for arcs in arc_data.values())
468 )) 495 ))
469 self._start_using() 496 self._start_using()
470 self._choose_lines_or_arcs(arcs=True) 497 self._choose_lines_or_arcs(arcs=True)
471 if not arc_data: 498 if not arc_data:
472 return 499 return
473 with self._connect() as con: 500 with self._connect() as con:
474 self._set_context_id() 501 self._set_context_id()
475 for filename, arcs in iitems(arc_data): 502 for filename, arcs in arc_data.items():
476 file_id = self._file_id(filename, add=True) 503 file_id = self._file_id(filename, add=True)
477 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] 504 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs]
478 con.executemany( 505 con.executemany(
479 "insert or ignore into arc " 506 "insert or ignore into arc " +
480 "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", 507 "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
481 data, 508 data,
482 ) 509 )
483 510
484 def _choose_lines_or_arcs(self, lines=False, arcs=False): 511 def _choose_lines_or_arcs(self, lines=False, arcs=False):
493 self._has_lines = lines 520 self._has_lines = lines
494 self._has_arcs = arcs 521 self._has_arcs = arcs
495 with self._connect() as con: 522 with self._connect() as con:
496 con.execute( 523 con.execute(
497 "insert into meta (key, value) values (?, ?)", 524 "insert into meta (key, value) values (?, ?)",
498 ('has_arcs', str(int(arcs))) 525 ("has_arcs", str(int(arcs)))
499 ) 526 )
500 527
528 @_locked
501 def add_file_tracers(self, file_tracers): 529 def add_file_tracers(self, file_tracers):
502 """Add per-file plugin information. 530 """Add per-file plugin information.
503 531
504 `file_tracers` is { filename: plugin_name, ... } 532 `file_tracers` is { filename: plugin_name, ... }
505 533
506 """ 534 """
507 if self._debug.should('dataop'): 535 if self._debug.should("dataop"):
508 self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) 536 self._debug.write("Adding file tracers: %d files" % (len(file_tracers),))
509 if not file_tracers: 537 if not file_tracers:
510 return 538 return
511 self._start_using() 539 self._start_using()
512 with self._connect() as con: 540 with self._connect() as con:
513 for filename, plugin_name in iitems(file_tracers): 541 for filename, plugin_name in file_tracers.items():
514 file_id = self._file_id(filename) 542 file_id = self._file_id(filename)
515 if file_id is None: 543 if file_id is None:
516 raise CoverageException( 544 raise CoverageException(
517 "Can't add file tracer data for unmeasured file '%s'" % (filename,) 545 f"Can't add file tracer data for unmeasured file '{filename}'"
518 ) 546 )
519 547
520 existing_plugin = self.file_tracer(filename) 548 existing_plugin = self.file_tracer(filename)
521 if existing_plugin: 549 if existing_plugin:
522 if existing_plugin != plugin_name: 550 if existing_plugin != plugin_name:
523 raise CoverageException( 551 raise CoverageException(
524 "Conflicting file tracer name for '%s': %r vs %r" % ( 552 "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
525 filename, existing_plugin, plugin_name, 553 filename, existing_plugin, plugin_name,
526 ) 554 )
527 ) 555 )
528 elif plugin_name: 556 elif plugin_name:
529 con.execute( 557 con.execute(
543 """Ensure that `filenames` appear in the data, empty if needed. 571 """Ensure that `filenames` appear in the data, empty if needed.
544 572
545 `plugin_name` is the name of the plugin responsible for these files. It is used 573 `plugin_name` is the name of the plugin responsible for these files. It is used
546 to associate the right filereporter, etc. 574 to associate the right filereporter, etc.
547 """ 575 """
548 if self._debug.should('dataop'): 576 if self._debug.should("dataop"):
549 self._debug.write("Touching %r" % (filenames,)) 577 self._debug.write(f"Touching {filenames!r}")
550 self._start_using() 578 self._start_using()
551 with self._connect(): # Use this to get one transaction. 579 with self._connect(): # Use this to get one transaction.
552 if not self._has_arcs and not self._has_lines: 580 if not self._has_arcs and not self._has_lines:
553 raise CoverageException("Can't touch files in an empty CoverageData") 581 raise CoverageException("Can't touch files in an empty CoverageData")
554 582
562 """Update this data with data from several other :class:`CoverageData` instances. 590 """Update this data with data from several other :class:`CoverageData` instances.
563 591
564 If `aliases` is provided, it's a `PathAliases` object that is used to 592 If `aliases` is provided, it's a `PathAliases` object that is used to
565 re-map paths to match the local machine's. 593 re-map paths to match the local machine's.
566 """ 594 """
567 if self._debug.should('dataop'): 595 if self._debug.should("dataop"):
568 self._debug.write("Updating with data from %r" % ( 596 self._debug.write("Updating with data from {!r}".format(
569 getattr(other_data, '_filename', '???'), 597 getattr(other_data, "_filename", "???"),
570 )) 598 ))
571 if self._has_lines and other_data._has_arcs: 599 if self._has_lines and other_data._has_arcs:
572 raise CoverageException("Can't combine arc data with line data") 600 raise CoverageException("Can't combine arc data with line data")
573 if self._has_arcs and other_data._has_lines: 601 if self._has_arcs and other_data._has_lines:
574 raise CoverageException("Can't combine line data with arc data") 602 raise CoverageException("Can't combine line data with arc data")
581 609
582 # Collector for all arcs, lines and tracers 610 # Collector for all arcs, lines and tracers
583 other_data.read() 611 other_data.read()
584 with other_data._connect() as conn: 612 with other_data._connect() as conn:
585 # Get files data. 613 # Get files data.
586 cur = conn.execute('select path from file') 614 cur = conn.execute("select path from file")
587 files = {path: aliases.map(path) for (path,) in cur} 615 files = {path: aliases.map(path) for (path,) in cur}
588 cur.close() 616 cur.close()
589 617
590 # Get contexts data. 618 # Get contexts data.
591 cur = conn.execute('select context from context') 619 cur = conn.execute("select context from context")
592 contexts = [context for (context,) in cur] 620 contexts = [context for (context,) in cur]
593 cur.close() 621 cur.close()
594 622
595 # Get arc data. 623 # Get arc data.
596 cur = conn.execute( 624 cur = conn.execute(
597 'select file.path, context.context, arc.fromno, arc.tono ' 625 "select file.path, context.context, arc.fromno, arc.tono " +
598 'from arc ' 626 "from arc " +
599 'inner join file on file.id = arc.file_id ' 627 "inner join file on file.id = arc.file_id " +
600 'inner join context on context.id = arc.context_id' 628 "inner join context on context.id = arc.context_id"
601 ) 629 )
602 arcs = [(files[path], context, fromno, tono) for (path, context, fromno, tono) in cur] 630 arcs = [(files[path], context, fromno, tono) for (path, context, fromno, tono) in cur]
603 cur.close() 631 cur.close()
604 632
605 # Get line data. 633 # Get line data.
606 cur = conn.execute( 634 cur = conn.execute(
607 'select file.path, context.context, line_bits.numbits ' 635 "select file.path, context.context, line_bits.numbits " +
608 'from line_bits ' 636 "from line_bits " +
609 'inner join file on file.id = line_bits.file_id ' 637 "inner join file on file.id = line_bits.file_id " +
610 'inner join context on context.id = line_bits.context_id' 638 "inner join context on context.id = line_bits.context_id"
611 ) 639 )
612 lines = { 640 lines = {(files[path], context): numbits for (path, context, numbits) in cur}
613 (files[path], context): numbits
614 for (path, context, numbits) in cur
615 }
616 cur.close() 641 cur.close()
617 642
618 # Get tracer data. 643 # Get tracer data.
619 cur = conn.execute( 644 cur = conn.execute(
620 'select file.path, tracer ' 645 "select file.path, tracer " +
621 'from tracer ' 646 "from tracer " +
622 'inner join file on file.id = tracer.file_id' 647 "inner join file on file.id = tracer.file_id"
623 ) 648 )
624 tracers = {files[path]: tracer for (path, tracer) in cur} 649 tracers = {files[path]: tracer for (path, tracer) in cur}
625 cur.close() 650 cur.close()
626 651
627 with self._connect() as conn: 652 with self._connect() as conn:
628 conn.con.isolation_level = 'IMMEDIATE' 653 conn.con.isolation_level = "IMMEDIATE"
629 654
630 # Get all tracers in the DB. Files not in the tracers are assumed 655 # Get all tracers in the DB. Files not in the tracers are assumed
631 # to have an empty string tracer. Since Sqlite does not support 656 # to have an empty string tracer. Since Sqlite does not support
632 # full outer joins, we have to make two queries to fill the 657 # full outer joins, we have to make two queries to fill the
633 # dictionary. 658 # dictionary.
634 this_tracers = {path: '' for path, in conn.execute('select path from file')} 659 this_tracers = {path: "" for path, in conn.execute("select path from file")}
635 this_tracers.update({ 660 this_tracers.update({
636 aliases.map(path): tracer 661 aliases.map(path): tracer
637 for path, tracer in conn.execute( 662 for path, tracer in conn.execute(
638 'select file.path, tracer from tracer ' 663 "select file.path, tracer from tracer " +
639 'inner join file on file.id = tracer.file_id' 664 "inner join file on file.id = tracer.file_id"
640 ) 665 )
641 }) 666 })
642 667
643 # Create all file and context rows in the DB. 668 # Create all file and context rows in the DB.
644 conn.executemany( 669 conn.executemany(
645 'insert or ignore into file (path) values (?)', 670 "insert or ignore into file (path) values (?)",
646 ((file,) for file in files.values()) 671 ((file,) for file in files.values())
647 ) 672 )
648 file_ids = { 673 file_ids = {
649 path: id 674 path: id
650 for id, path in conn.execute('select id, path from file') 675 for id, path in conn.execute("select id, path from file")
651 } 676 }
652 conn.executemany( 677 conn.executemany(
653 'insert or ignore into context (context) values (?)', 678 "insert or ignore into context (context) values (?)",
654 ((context,) for context in contexts) 679 ((context,) for context in contexts)
655 ) 680 )
656 context_ids = { 681 context_ids = {
657 context: id 682 context: id
658 for id, context in conn.execute('select id, context from context') 683 for id, context in conn.execute("select id, context from context")
659 } 684 }
660 685
661 # Prepare tracers and fail, if a conflict is found. 686 # Prepare tracers and fail, if a conflict is found.
662 # tracer_paths is used to ensure consistency over the tracer data 687 # tracer_paths is used to ensure consistency over the tracer data
663 # and tracer_map tracks the tracers to be inserted. 688 # and tracer_map tracks the tracers to be inserted.
664 tracer_map = {} 689 tracer_map = {}
665 for path in files.values(): 690 for path in files.values():
666 this_tracer = this_tracers.get(path) 691 this_tracer = this_tracers.get(path)
667 other_tracer = tracers.get(path, '') 692 other_tracer = tracers.get(path, "")
668 # If there is no tracer, there is always the None tracer. 693 # If there is no tracer, there is always the None tracer.
669 if this_tracer is not None and this_tracer != other_tracer: 694 if this_tracer is not None and this_tracer != other_tracer:
670 raise CoverageException( 695 raise CoverageException(
671 "Conflicting file tracer name for '%s': %r vs %r" % ( 696 "Conflicting file tracer name for '{}': {!r} vs {!r}".format(
672 path, this_tracer, other_tracer 697 path, this_tracer, other_tracer
673 ) 698 )
674 ) 699 )
675 tracer_map[path] = other_tracer 700 tracer_map[path] = other_tracer
676 701
682 for file, context, fromno, tono in arcs 707 for file, context, fromno, tono in arcs
683 ) 708 )
684 709
685 # Get line data. 710 # Get line data.
686 cur = conn.execute( 711 cur = conn.execute(
687 'select file.path, context.context, line_bits.numbits ' 712 "select file.path, context.context, line_bits.numbits " +
688 'from line_bits ' 713 "from line_bits " +
689 'inner join file on file.id = line_bits.file_id ' 714 "inner join file on file.id = line_bits.file_id " +
690 'inner join context on context.id = line_bits.context_id' 715 "inner join context on context.id = line_bits.context_id"
691 ) 716 )
692 for path, context, numbits in cur: 717 for path, context, numbits in cur:
693 key = (aliases.map(path), context) 718 key = (aliases.map(path), context)
694 if key in lines: 719 if key in lines:
695 numbits = numbits_union(lines[key], numbits) 720 numbits = numbits_union(lines[key], numbits)
699 if arcs: 724 if arcs:
700 self._choose_lines_or_arcs(arcs=True) 725 self._choose_lines_or_arcs(arcs=True)
701 726
702 # Write the combined data. 727 # Write the combined data.
703 conn.executemany( 728 conn.executemany(
704 'insert or ignore into arc ' 729 "insert or ignore into arc " +
705 '(file_id, context_id, fromno, tono) values (?, ?, ?, ?)', 730 "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)",
706 arc_rows 731 arc_rows
707 ) 732 )
708 733
709 if lines: 734 if lines:
710 self._choose_lines_or_arcs(lines=True) 735 self._choose_lines_or_arcs(lines=True)
711 conn.execute("delete from line_bits") 736 conn.execute("delete from line_bits")
712 conn.executemany( 737 conn.executemany(
713 "insert into line_bits " 738 "insert into line_bits " +
714 "(file_id, context_id, numbits) values (?, ?, ?)", 739 "(file_id, context_id, numbits) values (?, ?, ?)",
715 [ 740 [
716 (file_ids[file], context_ids[context], numbits) 741 (file_ids[file], context_ids[context], numbits)
717 for (file, context), numbits in lines.items() 742 for (file, context), numbits in lines.items()
718 ] 743 ]
719 ) 744 )
720 conn.executemany( 745 conn.executemany(
721 'insert or ignore into tracer (file_id, tracer) values (?, ?)', 746 "insert or ignore into tracer (file_id, tracer) values (?, ?)",
722 ((file_ids[filename], tracer) for filename, tracer in tracer_map.items()) 747 ((file_ids[filename], tracer) for filename, tracer in tracer_map.items())
723 ) 748 )
724 749
725 # Update all internal cache data. 750 # Update all internal cache data.
726 self._reset() 751 self._reset()
734 759
735 """ 760 """
736 self._reset() 761 self._reset()
737 if self._no_disk: 762 if self._no_disk:
738 return 763 return
739 if self._debug.should('dataio'): 764 if self._debug.should("dataio"):
740 self._debug.write("Erasing data file {!r}".format(self._filename)) 765 self._debug.write(f"Erasing data file {self._filename!r}")
741 file_be_gone(self._filename) 766 file_be_gone(self._filename)
742 if parallel: 767 if parallel:
743 data_dir, local = os.path.split(self._filename) 768 data_dir, local = os.path.split(self._filename)
744 localdot = local + '.*' 769 localdot = local + ".*"
745 pattern = os.path.join(os.path.abspath(data_dir), localdot) 770 pattern = os.path.join(os.path.abspath(data_dir), localdot)
746 for filename in glob.glob(pattern): 771 for filename in glob.glob(pattern):
747 if self._debug.should('dataio'): 772 if self._debug.should("dataio"):
748 self._debug.write("Erasing parallel data file {!r}".format(filename)) 773 self._debug.write(f"Erasing parallel data file {filename!r}")
749 file_be_gone(filename) 774 file_be_gone(filename)
750 775
751 def read(self): 776 def read(self):
752 """Start using an existing data file.""" 777 """Start using an existing data file."""
753 with self._connect(): # TODO: doesn't look right 778 with self._connect(): # TODO: doesn't look right
834 859
835 """ 860 """
836 self._start_using() 861 self._start_using()
837 if contexts: 862 if contexts:
838 with self._connect() as con: 863 with self._connect() as con:
839 context_clause = ' or '.join(['context regexp ?'] * len(contexts)) 864 context_clause = " or ".join(["context regexp ?"] * len(contexts))
840 cur = con.execute("select id from context where " + context_clause, contexts) 865 cur = con.execute("select id from context where " + context_clause, contexts)
841 self._query_context_ids = [row[0] for row in cur.fetchall()] 866 self._query_context_ids = [row[0] for row in cur.fetchall()]
842 else: 867 else:
843 self._query_context_ids = None 868 self._query_context_ids = None
844 869
845 def lines(self, filename): 870 def lines(self, filename):
846 """Get the list of lines executed for a file. 871 """Get the list of lines executed for a source file.
847 872
848 If the file was not measured, returns None. A file might be measured, 873 If the file was not measured, returns None. A file might be measured,
849 and have no lines executed, in which case an empty list is returned. 874 and have no lines executed, in which case an empty list is returned.
850 875
851 If the file was executed, returns a list of integers, the line numbers 876 If the file was executed, returns a list of integers, the line numbers
865 return None 890 return None
866 else: 891 else:
867 query = "select numbits from line_bits where file_id = ?" 892 query = "select numbits from line_bits where file_id = ?"
868 data = [file_id] 893 data = [file_id]
869 if self._query_context_ids is not None: 894 if self._query_context_ids is not None:
870 ids_array = ', '.join('?' * len(self._query_context_ids)) 895 ids_array = ", ".join("?" * len(self._query_context_ids))
871 query += " and context_id in (" + ids_array + ")" 896 query += " and context_id in (" + ids_array + ")"
872 data += self._query_context_ids 897 data += self._query_context_ids
873 bitmaps = list(con.execute(query, data)) 898 bitmaps = list(con.execute(query, data))
874 nums = set() 899 nums = set()
875 for row in bitmaps: 900 for row in bitmaps:
900 return None 925 return None
901 else: 926 else:
902 query = "select distinct fromno, tono from arc where file_id = ?" 927 query = "select distinct fromno, tono from arc where file_id = ?"
903 data = [file_id] 928 data = [file_id]
904 if self._query_context_ids is not None: 929 if self._query_context_ids is not None:
905 ids_array = ', '.join('?' * len(self._query_context_ids)) 930 ids_array = ", ".join("?" * len(self._query_context_ids))
906 query += " and context_id in (" + ids_array + ")" 931 query += " and context_id in (" + ids_array + ")"
907 data += self._query_context_ids 932 data += self._query_context_ids
908 arcs = con.execute(query, data) 933 arcs = con.execute(query, data)
909 return list(arcs) 934 return list(arcs)
910 935
915 A dict mapping line numbers to a list of context names. 940 A dict mapping line numbers to a list of context names.
916 941
917 .. versionadded:: 5.0 942 .. versionadded:: 5.0
918 943
919 """ 944 """
920 lineno_contexts_map = collections.defaultdict(list)
921 self._start_using() 945 self._start_using()
922 with self._connect() as con: 946 with self._connect() as con:
923 file_id = self._file_id(filename) 947 file_id = self._file_id(filename)
924 if file_id is None: 948 if file_id is None:
925 return lineno_contexts_map 949 return {}
950
951 lineno_contexts_map = collections.defaultdict(set)
926 if self.has_arcs(): 952 if self.has_arcs():
927 query = ( 953 query = (
928 "select arc.fromno, arc.tono, context.context " 954 "select arc.fromno, arc.tono, context.context " +
929 "from arc, context " 955 "from arc, context " +
930 "where arc.file_id = ? and arc.context_id = context.id" 956 "where arc.file_id = ? and arc.context_id = context.id"
931 ) 957 )
932 data = [file_id] 958 data = [file_id]
933 if self._query_context_ids is not None: 959 if self._query_context_ids is not None:
934 ids_array = ', '.join('?' * len(self._query_context_ids)) 960 ids_array = ", ".join("?" * len(self._query_context_ids))
935 query += " and arc.context_id in (" + ids_array + ")" 961 query += " and arc.context_id in (" + ids_array + ")"
936 data += self._query_context_ids 962 data += self._query_context_ids
937 for fromno, tono, context in con.execute(query, data): 963 for fromno, tono, context in con.execute(query, data):
938 if context not in lineno_contexts_map[fromno]: 964 if fromno > 0:
939 lineno_contexts_map[fromno].append(context) 965 lineno_contexts_map[fromno].add(context)
940 if context not in lineno_contexts_map[tono]: 966 if tono > 0:
941 lineno_contexts_map[tono].append(context) 967 lineno_contexts_map[tono].add(context)
942 else: 968 else:
943 query = ( 969 query = (
944 "select l.numbits, c.context from line_bits l, context c " 970 "select l.numbits, c.context from line_bits l, context c " +
945 "where l.context_id = c.id " 971 "where l.context_id = c.id " +
946 "and file_id = ?" 972 "and file_id = ?"
947 ) 973 )
948 data = [file_id] 974 data = [file_id]
949 if self._query_context_ids is not None: 975 if self._query_context_ids is not None:
950 ids_array = ', '.join('?' * len(self._query_context_ids)) 976 ids_array = ", ".join("?" * len(self._query_context_ids))
951 query += " and l.context_id in (" + ids_array + ")" 977 query += " and l.context_id in (" + ids_array + ")"
952 data += self._query_context_ids 978 data += self._query_context_ids
953 for numbits, context in con.execute(query, data): 979 for numbits, context in con.execute(query, data):
954 for lineno in numbits_to_nums(numbits): 980 for lineno in numbits_to_nums(numbits):
955 lineno_contexts_map[lineno].append(context) 981 lineno_contexts_map[lineno].add(context)
956 return lineno_contexts_map 982
983 return {lineno: list(contexts) for lineno, contexts in lineno_contexts_map.items()}
957 984
958 @classmethod 985 @classmethod
959 def sys_info(cls): 986 def sys_info(cls):
960 """Our information for `Coverage.sys_info`. 987 """Our information for `Coverage.sys_info`.
961 988
962 Returns a list of (key, value) pairs. 989 Returns a list of (key, value) pairs.
963 990
964 """ 991 """
965 with SqliteDb(":memory:", debug=NoDebugging()) as db: 992 with SqliteDb(":memory:", debug=NoDebugging()) as db:
966 temp_store = [row[0] for row in db.execute("pragma temp_store")] 993 temp_store = [row[0] for row in db.execute("pragma temp_store")]
967 compile_options = [row[0] for row in db.execute("pragma compile_options")] 994 copts = [row[0] for row in db.execute("pragma compile_options")]
995 # Yes, this is overkill. I don't like the long list of options
996 # at the end of "debug sys", but I don't want to omit information.
997 copts = ["; ".join(copts[i:i + 3]) for i in range(0, len(copts), 3)]
968 998
969 return [ 999 return [
970 ('sqlite3_version', sqlite3.version), 1000 ("sqlite3_version", sqlite3.version),
971 ('sqlite3_sqlite_version', sqlite3.sqlite_version), 1001 ("sqlite3_sqlite_version", sqlite3.sqlite_version),
972 ('sqlite3_temp_store', temp_store), 1002 ("sqlite3_temp_store", temp_store),
973 ('sqlite3_compile_options', compile_options), 1003 ("sqlite3_compile_options", copts),
974 ] 1004 ]
975 1005
976 1006
977 class SqliteDb(SimpleReprMixin): 1007 class SqliteDb(SimpleReprMixin):
978 """A simple abstraction over a SQLite database. 1008 """A simple abstraction over a SQLite database.
983 with SqliteDb(filename, debug_control) as db: 1013 with SqliteDb(filename, debug_control) as db:
984 db.execute("insert into schema (version) values (?)", (SCHEMA_VERSION,)) 1014 db.execute("insert into schema (version) values (?)", (SCHEMA_VERSION,))
985 1015
986 """ 1016 """
987 def __init__(self, filename, debug): 1017 def __init__(self, filename, debug):
988 self.debug = debug if debug.should('sql') else None 1018 self.debug = debug if debug.should("sql") else None
989 self.filename = filename 1019 self.filename = filename
990 self.nest = 0 1020 self.nest = 0
991 self.con = None 1021 self.con = None
992 1022
993 def _connect(self): 1023 def _connect(self):
994 """Connect to the db and do universal initialization.""" 1024 """Connect to the db and do universal initialization."""
995 if self.con is not None: 1025 if self.con is not None:
996 return 1026 return
997
998 # SQLite on Windows on py2 won't open a file if the filename argument
999 # has non-ascii characters in it. Opening a relative file name avoids
1000 # a problem if the current directory has non-ascii.
1001 filename = self.filename
1002 if env.WINDOWS and env.PY2:
1003 try:
1004 filename = os.path.relpath(self.filename)
1005 except ValueError:
1006 # ValueError can be raised under Windows when os.getcwd() returns a
1007 # folder from a different drive than the drive of self.filename in
1008 # which case we keep the original value of self.filename unchanged,
1009 # hoping that we won't face the non-ascii directory problem.
1010 pass
1011 1027
1012 # It can happen that Python switches threads while the tracer writes 1028 # It can happen that Python switches threads while the tracer writes
1013 # data. The second thread will also try to write to the data, 1029 # data. The second thread will also try to write to the data,
1014 # effectively causing a nested context. However, given the idempotent 1030 # effectively causing a nested context. However, given the idempotent
1015 # nature of the tracer operations, sharing a connection among threads 1031 # nature of the tracer operations, sharing a connection among threads
1016 # is not a problem. 1032 # is not a problem.
1017 if self.debug: 1033 if self.debug:
1018 self.debug.write("Connecting to {!r}".format(self.filename)) 1034 self.debug.write(f"Connecting to {self.filename!r}")
1019 self.con = sqlite3.connect(filename, check_same_thread=False) 1035 try:
1020 self.con.create_function('REGEXP', 2, _regexp) 1036 self.con = sqlite3.connect(self.filename, check_same_thread=False)
1037 except sqlite3.Error as exc:
1038 raise CoverageException(f"Couldn't use data file {self.filename!r}: {exc}") from exc
1039
1040 self.con.create_function("REGEXP", 2, _regexp)
1021 1041
1022 # This pragma makes writing faster. It disables rollbacks, but we never need them. 1042 # This pragma makes writing faster. It disables rollbacks, but we never need them.
1023 # PyPy needs the .close() calls here, or sqlite gets twisted up: 1043 # PyPy needs the .close() calls here, or sqlite gets twisted up:
1024 # https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on 1044 # https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on
1025 self.execute("pragma journal_mode=off").close() 1045 self.execute("pragma journal_mode=off").close()
1045 try: 1065 try:
1046 self.con.__exit__(exc_type, exc_value, traceback) 1066 self.con.__exit__(exc_type, exc_value, traceback)
1047 self.close() 1067 self.close()
1048 except Exception as exc: 1068 except Exception as exc:
1049 if self.debug: 1069 if self.debug:
1050 self.debug.write("EXCEPTION from __exit__: {}".format(exc)) 1070 self.debug.write(f"EXCEPTION from __exit__: {exc}")
1051 raise 1071 raise CoverageException(f"Couldn't end data file {self.filename!r}: {exc}") from exc
1052 1072
1053 def execute(self, sql, parameters=()): 1073 def execute(self, sql, parameters=()):
1054 """Same as :meth:`python:sqlite3.Connection.execute`.""" 1074 """Same as :meth:`python:sqlite3.Connection.execute`."""
1055 if self.debug: 1075 if self.debug:
1056 tail = " with {!r}".format(parameters) if parameters else "" 1076 tail = f" with {parameters!r}" if parameters else ""
1057 self.debug.write("Executing {!r}{}".format(sql, tail)) 1077 self.debug.write(f"Executing {sql!r}{tail}")
1058 try: 1078 try:
1059 try: 1079 try:
1060 return self.con.execute(sql, parameters) 1080 return self.con.execute(sql, parameters)
1061 except Exception: 1081 except Exception:
1062 # In some cases, an error might happen that isn't really an 1082 # In some cases, an error might happen that isn't really an
1070 # hard to provide useful hints if something goes wrong now. 1090 # hard to provide useful hints if something goes wrong now.
1071 with open(self.filename, "rb") as bad_file: 1091 with open(self.filename, "rb") as bad_file:
1072 cov4_sig = b"!coverage.py: This is a private format" 1092 cov4_sig = b"!coverage.py: This is a private format"
1073 if bad_file.read(len(cov4_sig)) == cov4_sig: 1093 if bad_file.read(len(cov4_sig)) == cov4_sig:
1074 msg = ( 1094 msg = (
1075 "Looks like a coverage 4.x data file. " 1095 "Looks like a coverage 4.x data file. " +
1076 "Are you mixing versions of coverage?" 1096 "Are you mixing versions of coverage?"
1077 ) 1097 )
1078 except Exception: 1098 except Exception: # pragma: cant happen
1079 pass 1099 pass
1080 if self.debug: 1100 if self.debug:
1081 self.debug.write("EXCEPTION from execute: {}".format(msg)) 1101 self.debug.write(f"EXCEPTION from execute: {msg}")
1082 raise CoverageException("Couldn't use data file {!r}: {}".format(self.filename, msg)) 1102 raise CoverageException(f"Couldn't use data file {self.filename!r}: {msg}") from exc
1083 1103
1084 def execute_one(self, sql, parameters=()): 1104 def execute_one(self, sql, parameters=()):
1085 """Execute a statement and return the one row that results. 1105 """Execute a statement and return the one row that results.
1086 1106
1087 This is like execute(sql, parameters).fetchone(), except it is 1107 This is like execute(sql, parameters).fetchone(), except it is
1094 if len(rows) == 0: 1114 if len(rows) == 0:
1095 return None 1115 return None
1096 elif len(rows) == 1: 1116 elif len(rows) == 1:
1097 return rows[0] 1117 return rows[0]
1098 else: 1118 else:
1099 raise CoverageException("Sql {!r} shouldn't return {} rows".format(sql, len(rows))) 1119 raise AssertionError(f"SQL {sql!r} shouldn't return {len(rows)} rows")
1100 1120
1101 def executemany(self, sql, data): 1121 def executemany(self, sql, data):
1102 """Same as :meth:`python:sqlite3.Connection.executemany`.""" 1122 """Same as :meth:`python:sqlite3.Connection.executemany`."""
1103 if self.debug: 1123 if self.debug:
1104 data = list(data) 1124 data = list(data)
1105 self.debug.write("Executing many {!r} with {} rows".format(sql, len(data))) 1125 self.debug.write(f"Executing many {sql!r} with {len(data)} rows")
1106 return self.con.executemany(sql, data) 1126 try:
1127 return self.con.executemany(sql, data)
1128 except Exception: # pragma: cant happen
1129 # In some cases, an error might happen that isn't really an
1130 # error. Try again immediately.
1131 # https://github.com/nedbat/coveragepy/issues/1010
1132 return self.con.executemany(sql, data)
1107 1133
1108 def executescript(self, script): 1134 def executescript(self, script):
1109 """Same as :meth:`python:sqlite3.Connection.executescript`.""" 1135 """Same as :meth:`python:sqlite3.Connection.executescript`."""
1110 if self.debug: 1136 if self.debug:
1111 self.debug.write("Executing script with {} chars: {}".format( 1137 self.debug.write("Executing script with {} chars: {}".format(

eric ide

mercurial