diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cb2bd3e946..2acc7f5d0d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -342,6 +342,10 @@ jobs: - name: check wasm code with prettier # prettier doesn't handle ignore files very well: https://github.com/prettier/prettier/issues/8506 run: cd wasm && git ls-files -z | xargs -0 prettier --check -u + - name: Check update_asdl.sh consistency + run: bash scripts/update_asdl.sh && git diff --exit-code + - name: Check whats_left is not broken + run: python -I whats_left.py miri: name: Run tests under miri diff --git a/Lib/cgitb.py b/Lib/cgitb.py index 449bb1d43e..4f81271be3 100644 --- a/Lib/cgitb.py +++ b/Lib/cgitb.py @@ -1,321 +1,321 @@ -"""More comprehensive traceback formatting for Python scripts. - -To enable this module, do: - - import cgitb; cgitb.enable() - -at the top of your script. The optional arguments to enable() are: - - display - if true, tracebacks are displayed in the web browser - logdir - if set, tracebacks are written to files in this directory - context - number of lines of source code to show for each stack frame - format - 'text' or 'html' controls the output format - -By default, tracebacks are displayed but not saved, the context is 5 lines -and the output format is 'html' (for backwards compatibility with the -original use of this module) - -Alternatively, if you have caught an exception and want cgitb to display it -for you, call cgitb.handler(). The optional argument to handler() is a -3-item tuple (etype, evalue, etb) just like the value of sys.exc_info(). -The default handler displays output as HTML. - -""" -import inspect -import keyword -import linecache -import os -import pydoc -import sys -import tempfile -import time -import tokenize -import traceback - -def reset(): - """Return a string that resets the CGI and browser to a known state.""" - return ''' - --> --> - - ''' - -__UNDEF__ = [] # a special sentinel object -def small(text): - if text: - return '' + text + '' - else: - return '' - -def strong(text): - if text: - return '' + text + '' - else: - return '' - -def grey(text): - if text: - return '' + text + '' - else: - return '' - -def lookup(name, frame, locals): - """Find the value for a given name in the given environment.""" - if name in locals: - return 'local', locals[name] - if name in frame.f_globals: - return 'global', frame.f_globals[name] - if '__builtins__' in frame.f_globals: - builtins = frame.f_globals['__builtins__'] - if type(builtins) is type({}): - if name in builtins: - return 'builtin', builtins[name] - else: - if hasattr(builtins, name): - return 'builtin', getattr(builtins, name) - return None, __UNDEF__ - -def scanvars(reader, frame, locals): - """Scan one logical line of Python and look up values of variables used.""" - vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__ - for ttype, token, start, end, line in tokenize.generate_tokens(reader): - if ttype == tokenize.NEWLINE: break - if ttype == tokenize.NAME and token not in keyword.kwlist: - if lasttoken == '.': - if parent is not __UNDEF__: - value = getattr(parent, token, __UNDEF__) - vars.append((prefix + token, prefix, value)) - else: - where, value = lookup(token, frame, locals) - vars.append((token, where, value)) - elif token == '.': - prefix += lasttoken + '.' - parent = value - else: - parent, prefix = None, '' - lasttoken = token - return vars - -def html(einfo, context=5): - """Return a nice HTML document describing a given traceback.""" - etype, evalue, etb = einfo - if isinstance(etype, type): - etype = etype.__name__ - pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable - date = time.ctime(time.time()) - head = '' + pydoc.html.heading( - '%s' % - strong(pydoc.html.escape(str(etype))), - '#ffffff', '#6622aa', pyver + '
' + date) + ''' -

A problem occurred in a Python script. Here is the sequence of -function calls leading up to the error, in the order they occurred.

''' - - indent = '' + small(' ' * 5) + ' ' - frames = [] - records = inspect.getinnerframes(etb, context) - for frame, file, lnum, func, lines, index in records: - if file: - file = os.path.abspath(file) - link = '%s' % (file, pydoc.html.escape(file)) - else: - file = link = '?' - args, varargs, varkw, locals = inspect.getargvalues(frame) - call = '' - if func != '?': - call = 'in ' + strong(pydoc.html.escape(func)) - if func != "": - call += inspect.formatargvalues(args, varargs, varkw, locals, - formatvalue=lambda value: '=' + pydoc.html.repr(value)) - - highlight = {} - def reader(lnum=[lnum]): - highlight[lnum[0]] = 1 - try: return linecache.getline(file, lnum[0]) - finally: lnum[0] += 1 - vars = scanvars(reader, frame, locals) - - rows = ['%s%s %s' % - (' ', link, call)] - if index is not None: - i = lnum - index - for line in lines: - num = small(' ' * (5-len(str(i))) + str(i)) + ' ' - if i in highlight: - line = '=>%s%s' % (num, pydoc.html.preformat(line)) - rows.append('%s' % line) - else: - line = '  %s%s' % (num, pydoc.html.preformat(line)) - rows.append('%s' % grey(line)) - i += 1 - - done, dump = {}, [] - for name, where, value in vars: - if name in done: continue - done[name] = 1 - if value is not __UNDEF__: - if where in ('global', 'builtin'): - name = ('%s ' % where) + strong(name) - elif where == 'local': - name = strong(name) - else: - name = where + strong(name.split('.')[-1]) - dump.append('%s = %s' % (name, pydoc.html.repr(value))) - else: - dump.append(name + ' undefined') - - rows.append('%s' % small(grey(', '.join(dump)))) - frames.append(''' - -%s
''' % '\n'.join(rows)) - - exception = ['

%s: %s' % (strong(pydoc.html.escape(str(etype))), - pydoc.html.escape(str(evalue)))] - for name in dir(evalue): - if name[:1] == '_': continue - value = pydoc.html.repr(getattr(evalue, name)) - exception.append('\n
%s%s =\n%s' % (indent, name, value)) - - return head + ''.join(frames) + ''.join(exception) + ''' - - - -''' % pydoc.html.escape( - ''.join(traceback.format_exception(etype, evalue, etb))) - -def text(einfo, context=5): - """Return a plain text document describing a given traceback.""" - etype, evalue, etb = einfo - if isinstance(etype, type): - etype = etype.__name__ - pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable - date = time.ctime(time.time()) - head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + ''' -A problem occurred in a Python script. Here is the sequence of -function calls leading up to the error, in the order they occurred. -''' - - frames = [] - records = inspect.getinnerframes(etb, context) - for frame, file, lnum, func, lines, index in records: - file = file and os.path.abspath(file) or '?' - args, varargs, varkw, locals = inspect.getargvalues(frame) - call = '' - if func != '?': - call = 'in ' + func - if func != "": - call += inspect.formatargvalues(args, varargs, varkw, locals, - formatvalue=lambda value: '=' + pydoc.text.repr(value)) - - highlight = {} - def reader(lnum=[lnum]): - highlight[lnum[0]] = 1 - try: return linecache.getline(file, lnum[0]) - finally: lnum[0] += 1 - vars = scanvars(reader, frame, locals) - - rows = [' %s %s' % (file, call)] - if index is not None: - i = lnum - index - for line in lines: - num = '%5d ' % i - rows.append(num+line.rstrip()) - i += 1 - - done, dump = {}, [] - for name, where, value in vars: - if name in done: continue - done[name] = 1 - if value is not __UNDEF__: - if where == 'global': name = 'global ' + name - elif where != 'local': name = where + name.split('.')[-1] - dump.append('%s = %s' % (name, pydoc.text.repr(value))) - else: - dump.append(name + ' undefined') - - rows.append('\n'.join(dump)) - frames.append('\n%s\n' % '\n'.join(rows)) - - exception = ['%s: %s' % (str(etype), str(evalue))] - for name in dir(evalue): - value = pydoc.text.repr(getattr(evalue, name)) - exception.append('\n%s%s = %s' % (" "*4, name, value)) - - return head + ''.join(frames) + ''.join(exception) + ''' - -The above is a description of an error in a Python program. Here is -the original traceback: - -%s -''' % ''.join(traceback.format_exception(etype, evalue, etb)) - -class Hook: - """A hook to replace sys.excepthook that shows tracebacks in HTML.""" - - def __init__(self, display=1, logdir=None, context=5, file=None, - format="html"): - self.display = display # send tracebacks to browser if true - self.logdir = logdir # log tracebacks to files if not None - self.context = context # number of source code lines per frame - self.file = file or sys.stdout # place to send the output - self.format = format - - def __call__(self, etype, evalue, etb): - self.handle((etype, evalue, etb)) - - def handle(self, info=None): - info = info or sys.exc_info() - if self.format == "html": - self.file.write(reset()) - - formatter = (self.format=="html") and html or text - plain = False - try: - doc = formatter(info, self.context) - except: # just in case something goes wrong - doc = ''.join(traceback.format_exception(*info)) - plain = True - - if self.display: - if plain: - doc = pydoc.html.escape(doc) - self.file.write('

' + doc + '
\n') - else: - self.file.write(doc + '\n') - else: - self.file.write('

A problem occurred in a Python script.\n') - - if self.logdir is not None: - suffix = ['.txt', '.html'][self.format=="html"] - (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir) - - try: - with os.fdopen(fd, 'w') as file: - file.write(doc) - msg = '%s contains the description of this error.' % path - except: - msg = 'Tried to save traceback to %s, but failed.' % path - - if self.format == 'html': - self.file.write('

%s

\n' % msg) - else: - self.file.write(msg + '\n') - try: - self.file.flush() - except: pass - -handler = Hook().handle -def enable(display=1, logdir=None, context=5, format="html"): - """Install an exception handler that formats tracebacks as HTML. - - The optional argument 'display' can be set to 0 to suppress sending the - traceback to the browser, and 'logdir' can be set to a directory to cause - tracebacks to be written to files there.""" - sys.excepthook = Hook(display=display, logdir=logdir, - context=context, format=format) +"""More comprehensive traceback formatting for Python scripts. + +To enable this module, do: + + import cgitb; cgitb.enable() + +at the top of your script. The optional arguments to enable() are: + + display - if true, tracebacks are displayed in the web browser + logdir - if set, tracebacks are written to files in this directory + context - number of lines of source code to show for each stack frame + format - 'text' or 'html' controls the output format + +By default, tracebacks are displayed but not saved, the context is 5 lines +and the output format is 'html' (for backwards compatibility with the +original use of this module) + +Alternatively, if you have caught an exception and want cgitb to display it +for you, call cgitb.handler(). The optional argument to handler() is a +3-item tuple (etype, evalue, etb) just like the value of sys.exc_info(). +The default handler displays output as HTML. + +""" +import inspect +import keyword +import linecache +import os +import pydoc +import sys +import tempfile +import time +import tokenize +import traceback + +def reset(): + """Return a string that resets the CGI and browser to a known state.""" + return ''' + --> --> + + ''' + +__UNDEF__ = [] # a special sentinel object +def small(text): + if text: + return '' + text + '' + else: + return '' + +def strong(text): + if text: + return '' + text + '' + else: + return '' + +def grey(text): + if text: + return '' + text + '' + else: + return '' + +def lookup(name, frame, locals): + """Find the value for a given name in the given environment.""" + if name in locals: + return 'local', locals[name] + if name in frame.f_globals: + return 'global', frame.f_globals[name] + if '__builtins__' in frame.f_globals: + builtins = frame.f_globals['__builtins__'] + if type(builtins) is type({}): + if name in builtins: + return 'builtin', builtins[name] + else: + if hasattr(builtins, name): + return 'builtin', getattr(builtins, name) + return None, __UNDEF__ + +def scanvars(reader, frame, locals): + """Scan one logical line of Python and look up values of variables used.""" + vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__ + for ttype, token, start, end, line in tokenize.generate_tokens(reader): + if ttype == tokenize.NEWLINE: break + if ttype == tokenize.NAME and token not in keyword.kwlist: + if lasttoken == '.': + if parent is not __UNDEF__: + value = getattr(parent, token, __UNDEF__) + vars.append((prefix + token, prefix, value)) + else: + where, value = lookup(token, frame, locals) + vars.append((token, where, value)) + elif token == '.': + prefix += lasttoken + '.' + parent = value + else: + parent, prefix = None, '' + lasttoken = token + return vars + +def html(einfo, context=5): + """Return a nice HTML document describing a given traceback.""" + etype, evalue, etb = einfo + if isinstance(etype, type): + etype = etype.__name__ + pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable + date = time.ctime(time.time()) + head = '' + pydoc.html.heading( + '%s' % + strong(pydoc.html.escape(str(etype))), + '#ffffff', '#6622aa', pyver + '
' + date) + ''' +

A problem occurred in a Python script. Here is the sequence of +function calls leading up to the error, in the order they occurred.

''' + + indent = '' + small(' ' * 5) + ' ' + frames = [] + records = inspect.getinnerframes(etb, context) + for frame, file, lnum, func, lines, index in records: + if file: + file = os.path.abspath(file) + link = '%s' % (file, pydoc.html.escape(file)) + else: + file = link = '?' + args, varargs, varkw, locals = inspect.getargvalues(frame) + call = '' + if func != '?': + call = 'in ' + strong(pydoc.html.escape(func)) + if func != "": + call += inspect.formatargvalues(args, varargs, varkw, locals, + formatvalue=lambda value: '=' + pydoc.html.repr(value)) + + highlight = {} + def reader(lnum=[lnum]): + highlight[lnum[0]] = 1 + try: return linecache.getline(file, lnum[0]) + finally: lnum[0] += 1 + vars = scanvars(reader, frame, locals) + + rows = ['%s%s %s' % + (' ', link, call)] + if index is not None: + i = lnum - index + for line in lines: + num = small(' ' * (5-len(str(i))) + str(i)) + ' ' + if i in highlight: + line = '=>%s%s' % (num, pydoc.html.preformat(line)) + rows.append('%s' % line) + else: + line = '  %s%s' % (num, pydoc.html.preformat(line)) + rows.append('%s' % grey(line)) + i += 1 + + done, dump = {}, [] + for name, where, value in vars: + if name in done: continue + done[name] = 1 + if value is not __UNDEF__: + if where in ('global', 'builtin'): + name = ('%s ' % where) + strong(name) + elif where == 'local': + name = strong(name) + else: + name = where + strong(name.split('.')[-1]) + dump.append('%s = %s' % (name, pydoc.html.repr(value))) + else: + dump.append(name + ' undefined') + + rows.append('%s' % small(grey(', '.join(dump)))) + frames.append(''' + +%s
''' % '\n'.join(rows)) + + exception = ['

%s: %s' % (strong(pydoc.html.escape(str(etype))), + pydoc.html.escape(str(evalue)))] + for name in dir(evalue): + if name[:1] == '_': continue + value = pydoc.html.repr(getattr(evalue, name)) + exception.append('\n
%s%s =\n%s' % (indent, name, value)) + + return head + ''.join(frames) + ''.join(exception) + ''' + + + +''' % pydoc.html.escape( + ''.join(traceback.format_exception(etype, evalue, etb))) + +def text(einfo, context=5): + """Return a plain text document describing a given traceback.""" + etype, evalue, etb = einfo + if isinstance(etype, type): + etype = etype.__name__ + pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable + date = time.ctime(time.time()) + head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + ''' +A problem occurred in a Python script. Here is the sequence of +function calls leading up to the error, in the order they occurred. +''' + + frames = [] + records = inspect.getinnerframes(etb, context) + for frame, file, lnum, func, lines, index in records: + file = file and os.path.abspath(file) or '?' + args, varargs, varkw, locals = inspect.getargvalues(frame) + call = '' + if func != '?': + call = 'in ' + func + if func != "": + call += inspect.formatargvalues(args, varargs, varkw, locals, + formatvalue=lambda value: '=' + pydoc.text.repr(value)) + + highlight = {} + def reader(lnum=[lnum]): + highlight[lnum[0]] = 1 + try: return linecache.getline(file, lnum[0]) + finally: lnum[0] += 1 + vars = scanvars(reader, frame, locals) + + rows = [' %s %s' % (file, call)] + if index is not None: + i = lnum - index + for line in lines: + num = '%5d ' % i + rows.append(num+line.rstrip()) + i += 1 + + done, dump = {}, [] + for name, where, value in vars: + if name in done: continue + done[name] = 1 + if value is not __UNDEF__: + if where == 'global': name = 'global ' + name + elif where != 'local': name = where + name.split('.')[-1] + dump.append('%s = %s' % (name, pydoc.text.repr(value))) + else: + dump.append(name + ' undefined') + + rows.append('\n'.join(dump)) + frames.append('\n%s\n' % '\n'.join(rows)) + + exception = ['%s: %s' % (str(etype), str(evalue))] + for name in dir(evalue): + value = pydoc.text.repr(getattr(evalue, name)) + exception.append('\n%s%s = %s' % (" "*4, name, value)) + + return head + ''.join(frames) + ''.join(exception) + ''' + +The above is a description of an error in a Python program. Here is +the original traceback: + +%s +''' % ''.join(traceback.format_exception(etype, evalue, etb)) + +class Hook: + """A hook to replace sys.excepthook that shows tracebacks in HTML.""" + + def __init__(self, display=1, logdir=None, context=5, file=None, + format="html"): + self.display = display # send tracebacks to browser if true + self.logdir = logdir # log tracebacks to files if not None + self.context = context # number of source code lines per frame + self.file = file or sys.stdout # place to send the output + self.format = format + + def __call__(self, etype, evalue, etb): + self.handle((etype, evalue, etb)) + + def handle(self, info=None): + info = info or sys.exc_info() + if self.format == "html": + self.file.write(reset()) + + formatter = (self.format=="html") and html or text + plain = False + try: + doc = formatter(info, self.context) + except: # just in case something goes wrong + doc = ''.join(traceback.format_exception(*info)) + plain = True + + if self.display: + if plain: + doc = pydoc.html.escape(doc) + self.file.write('

' + doc + '
\n') + else: + self.file.write(doc + '\n') + else: + self.file.write('

A problem occurred in a Python script.\n') + + if self.logdir is not None: + suffix = ['.txt', '.html'][self.format=="html"] + (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir) + + try: + with os.fdopen(fd, 'w') as file: + file.write(doc) + msg = '%s contains the description of this error.' % path + except: + msg = 'Tried to save traceback to %s, but failed.' % path + + if self.format == 'html': + self.file.write('

%s

\n' % msg) + else: + self.file.write(msg + '\n') + try: + self.file.flush() + except: pass + +handler = Hook().handle +def enable(display=1, logdir=None, context=5, format="html"): + """Install an exception handler that formats tracebacks as HTML. + + The optional argument 'display' can be set to 0 to suppress sending the + traceback to the browser, and 'logdir' can be set to a directory to cause + tracebacks to be written to files there.""" + sys.excepthook = Hook(display=display, logdir=logdir, + context=context, format=format) diff --git a/Lib/keyword.py b/Lib/keyword.py index eab8204c91..cc2b46b722 100644 --- a/Lib/keyword.py +++ b/Lib/keyword.py @@ -1,63 +1,63 @@ -"""Keywords (from "Grammar/python.gram") - -This file is automatically generated; please don't muck it up! - -To update the symbols in this file, 'cd' to the top directory of -the python source tree and run: - - PYTHONPATH=Tools/peg_generator python3 -m pegen.keywordgen \ - Grammar/python.gram \ - Grammar/Tokens \ - Lib/keyword.py - -Alternatively, you can run 'make regen-keyword'. -""" - -__all__ = ["iskeyword", "issoftkeyword", "kwlist", "softkwlist"] - -kwlist = [ - 'False', - 'None', - 'True', - 'and', - 'as', - 'assert', - 'async', - 'await', - 'break', - 'class', - 'continue', - 'def', - 'del', - 'elif', - 'else', - 'except', - 'finally', - 'for', - 'from', - 'global', - 'if', - 'import', - 'in', - 'is', - 'lambda', - 'nonlocal', - 'not', - 'or', - 'pass', - 'raise', - 'return', - 'try', - 'while', - 'with', - 'yield' -] - -softkwlist = [ - '_', - 'case', - 'match' -] - -iskeyword = frozenset(kwlist).__contains__ -issoftkeyword = frozenset(softkwlist).__contains__ +"""Keywords (from "Grammar/python.gram") + +This file is automatically generated; please don't muck it up! + +To update the symbols in this file, 'cd' to the top directory of +the python source tree and run: + + PYTHONPATH=Tools/peg_generator python3 -m pegen.keywordgen \ + Grammar/python.gram \ + Grammar/Tokens \ + Lib/keyword.py + +Alternatively, you can run 'make regen-keyword'. +""" + +__all__ = ["iskeyword", "issoftkeyword", "kwlist", "softkwlist"] + +kwlist = [ + 'False', + 'None', + 'True', + 'and', + 'as', + 'assert', + 'async', + 'await', + 'break', + 'class', + 'continue', + 'def', + 'del', + 'elif', + 'else', + 'except', + 'finally', + 'for', + 'from', + 'global', + 'if', + 'import', + 'in', + 'is', + 'lambda', + 'nonlocal', + 'not', + 'or', + 'pass', + 'raise', + 'return', + 'try', + 'while', + 'with', + 'yield' +] + +softkwlist = [ + '_', + 'case', + 'match' +] + +iskeyword = frozenset(kwlist).__contains__ +issoftkeyword = frozenset(softkwlist).__contains__ diff --git a/Lib/platform.py b/Lib/platform.py index e32f9c11cd..fe88fa9d52 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1065,7 +1065,13 @@ def _sys_version(sys_version=None): repr(sys_version)) version, buildno, builddate, buildtime, compiler = \ match.groups() - name = 'CPython' + + # XXX: RUSTPYTHON support + if "rustc" in sys_version: + name = "RustPython" + else: + name = 'CPython' + if builddate is None: builddate = '' elif buildtime: diff --git a/Lib/trace.py b/Lib/trace.py index 85f8471b6d..89f17d485f 100644 --- a/Lib/trace.py +++ b/Lib/trace.py @@ -1,756 +1,756 @@ -#!/usr/bin/env python3 - -# portions copyright 2001, Autonomous Zones Industries, Inc., all rights... -# err... reserved and offered to the public under the terms of the -# Python 2.2 license. -# Author: Zooko O'Whielacronx -# http://zooko.com/ -# mailto:zooko@zooko.com -# -# Copyright 2000, Mojam Media, Inc., all rights reserved. -# Author: Skip Montanaro -# -# Copyright 1999, Bioreason, Inc., all rights reserved. -# Author: Andrew Dalke -# -# Copyright 1995-1997, Automatrix, Inc., all rights reserved. -# Author: Skip Montanaro -# -# Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved. -# -# -# Permission to use, copy, modify, and distribute this Python software and -# its associated documentation for any purpose without fee is hereby -# granted, provided that the above copyright notice appears in all copies, -# and that both that copyright notice and this permission notice appear in -# supporting documentation, and that the name of neither Automatrix, -# Bioreason or Mojam Media be used in advertising or publicity pertaining to -# distribution of the software without specific, written prior permission. -# -"""program/module to trace Python program or function execution - -Sample use, command line: - trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs - trace.py -t --ignore-dir '$prefix' spam.py eggs - trace.py --trackcalls spam.py eggs - -Sample use, programmatically - import sys - - # create a Trace object, telling it what to ignore, and whether to - # do tracing or line-counting or both. - tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,], - trace=0, count=1) - # run the new command using the given tracer - tracer.run('main()') - # make a report, placing output in /tmp - r = tracer.results() - r.write_results(show_missing=True, coverdir="/tmp") -""" -__all__ = ['Trace', 'CoverageResults'] - -import linecache -import os -import sys -import sysconfig -import token -import tokenize -import inspect -import gc -import dis -import pickle -from time import monotonic as _time - -import threading - -PRAGMA_NOCOVER = "#pragma NO COVER" - -class _Ignore: - def __init__(self, modules=None, dirs=None): - self._mods = set() if not modules else set(modules) - self._dirs = [] if not dirs else [os.path.normpath(d) - for d in dirs] - self._ignore = { '': 1 } - - def names(self, filename, modulename): - if modulename in self._ignore: - return self._ignore[modulename] - - # haven't seen this one before, so see if the module name is - # on the ignore list. - if modulename in self._mods: # Identical names, so ignore - self._ignore[modulename] = 1 - return 1 - - # check if the module is a proper submodule of something on - # the ignore list - for mod in self._mods: - # Need to take some care since ignoring - # "cmp" mustn't mean ignoring "cmpcache" but ignoring - # "Spam" must also mean ignoring "Spam.Eggs". - if modulename.startswith(mod + '.'): - self._ignore[modulename] = 1 - return 1 - - # Now check that filename isn't in one of the directories - if filename is None: - # must be a built-in, so we must ignore - self._ignore[modulename] = 1 - return 1 - - # Ignore a file when it contains one of the ignorable paths - for d in self._dirs: - # The '+ os.sep' is to ensure that d is a parent directory, - # as compared to cases like: - # d = "/usr/local" - # filename = "/usr/local.py" - # or - # d = "/usr/local.py" - # filename = "/usr/local.py" - if filename.startswith(d + os.sep): - self._ignore[modulename] = 1 - return 1 - - # Tried the different ways, so we don't ignore this module - self._ignore[modulename] = 0 - return 0 - -def _modname(path): - """Return a plausible module name for the patch.""" - - base = os.path.basename(path) - filename, ext = os.path.splitext(base) - return filename - -def _fullmodname(path): - """Return a plausible module name for the path.""" - - # If the file 'path' is part of a package, then the filename isn't - # enough to uniquely identify it. Try to do the right thing by - # looking in sys.path for the longest matching prefix. We'll - # assume that the rest is the package name. - - comparepath = os.path.normcase(path) - longest = "" - for dir in sys.path: - dir = os.path.normcase(dir) - if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep: - if len(dir) > len(longest): - longest = dir - - if longest: - base = path[len(longest) + 1:] - else: - base = path - # the drive letter is never part of the module name - drive, base = os.path.splitdrive(base) - base = base.replace(os.sep, ".") - if os.altsep: - base = base.replace(os.altsep, ".") - filename, ext = os.path.splitext(base) - return filename.lstrip(".") - -class CoverageResults: - def __init__(self, counts=None, calledfuncs=None, infile=None, - callers=None, outfile=None): - self.counts = counts - if self.counts is None: - self.counts = {} - self.counter = self.counts.copy() # map (filename, lineno) to count - self.calledfuncs = calledfuncs - if self.calledfuncs is None: - self.calledfuncs = {} - self.calledfuncs = self.calledfuncs.copy() - self.callers = callers - if self.callers is None: - self.callers = {} - self.callers = self.callers.copy() - self.infile = infile - self.outfile = outfile - if self.infile: - # Try to merge existing counts file. - try: - with open(self.infile, 'rb') as f: - counts, calledfuncs, callers = pickle.load(f) - self.update(self.__class__(counts, calledfuncs, callers)) - except (OSError, EOFError, ValueError) as err: - print(("Skipping counts file %r: %s" - % (self.infile, err)), file=sys.stderr) - - def is_ignored_filename(self, filename): - """Return True if the filename does not refer to a file - we want to have reported. - """ - return filename.startswith('<') and filename.endswith('>') - - def update(self, other): - """Merge in the data from another CoverageResults""" - counts = self.counts - calledfuncs = self.calledfuncs - callers = self.callers - other_counts = other.counts - other_calledfuncs = other.calledfuncs - other_callers = other.callers - - for key in other_counts: - counts[key] = counts.get(key, 0) + other_counts[key] - - for key in other_calledfuncs: - calledfuncs[key] = 1 - - for key in other_callers: - callers[key] = 1 - - def write_results(self, show_missing=True, summary=False, coverdir=None): - """ - Write the coverage results. - - :param show_missing: Show lines that had no hits. - :param summary: Include coverage summary per module. - :param coverdir: If None, the results of each module are placed in its - directory, otherwise it is included in the directory - specified. - """ - if self.calledfuncs: - print() - print("functions called:") - calls = self.calledfuncs - for filename, modulename, funcname in sorted(calls): - print(("filename: %s, modulename: %s, funcname: %s" - % (filename, modulename, funcname))) - - if self.callers: - print() - print("calling relationships:") - lastfile = lastcfile = "" - for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) \ - in sorted(self.callers): - if pfile != lastfile: - print() - print("***", pfile, "***") - lastfile = pfile - lastcfile = "" - if cfile != pfile and lastcfile != cfile: - print(" -->", cfile) - lastcfile = cfile - print(" %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc)) - - # turn the counts data ("(filename, lineno) = count") into something - # accessible on a per-file basis - per_file = {} - for filename, lineno in self.counts: - lines_hit = per_file[filename] = per_file.get(filename, {}) - lines_hit[lineno] = self.counts[(filename, lineno)] - - # accumulate summary info, if needed - sums = {} - - for filename, count in per_file.items(): - if self.is_ignored_filename(filename): - continue - - if filename.endswith(".pyc"): - filename = filename[:-1] - - if coverdir is None: - dir = os.path.dirname(os.path.abspath(filename)) - modulename = _modname(filename) - else: - dir = coverdir - if not os.path.exists(dir): - os.makedirs(dir) - modulename = _fullmodname(filename) - - # If desired, get a list of the line numbers which represent - # executable content (returned as a dict for better lookup speed) - if show_missing: - lnotab = _find_executable_linenos(filename) - else: - lnotab = {} - source = linecache.getlines(filename) - coverpath = os.path.join(dir, modulename + ".cover") - with open(filename, 'rb') as fp: - encoding, _ = tokenize.detect_encoding(fp.readline) - n_hits, n_lines = self.write_results_file(coverpath, source, - lnotab, count, encoding) - if summary and n_lines: - percent = int(100 * n_hits / n_lines) - sums[modulename] = n_lines, percent, modulename, filename - - - if summary and sums: - print("lines cov% module (path)") - for m in sorted(sums): - n_lines, percent, modulename, filename = sums[m] - print("%5d %3d%% %s (%s)" % sums[m]) - - if self.outfile: - # try and store counts and module info into self.outfile - try: - with open(self.outfile, 'wb') as f: - pickle.dump((self.counts, self.calledfuncs, self.callers), - f, 1) - except OSError as err: - print("Can't save counts files because %s" % err, file=sys.stderr) - - def write_results_file(self, path, lines, lnotab, lines_hit, encoding=None): - """Return a coverage results file in path.""" - # ``lnotab`` is a dict of executable lines, or a line number "table" - - try: - outfile = open(path, "w", encoding=encoding) - except OSError as err: - print(("trace: Could not open %r for writing: %s " - "- skipping" % (path, err)), file=sys.stderr) - return 0, 0 - - n_lines = 0 - n_hits = 0 - with outfile: - for lineno, line in enumerate(lines, 1): - # do the blank/comment match to try to mark more lines - # (help the reader find stuff that hasn't been covered) - if lineno in lines_hit: - outfile.write("%5d: " % lines_hit[lineno]) - n_hits += 1 - n_lines += 1 - elif lineno in lnotab and not PRAGMA_NOCOVER in line: - # Highlight never-executed lines, unless the line contains - # #pragma: NO COVER - outfile.write(">>>>>> ") - n_lines += 1 - else: - outfile.write(" ") - outfile.write(line.expandtabs(8)) - - return n_hits, n_lines - -def _find_lines_from_code(code, strs): - """Return dict where keys are lines in the line number table.""" - linenos = {} - - for _, lineno in dis.findlinestarts(code): - if lineno not in strs: - linenos[lineno] = 1 - - return linenos - -def _find_lines(code, strs): - """Return lineno dict for all code objects reachable from code.""" - # get all of the lineno information from the code of this scope level - linenos = _find_lines_from_code(code, strs) - - # and check the constants for references to other code objects - for c in code.co_consts: - if inspect.iscode(c): - # find another code object, so recurse into it - linenos.update(_find_lines(c, strs)) - return linenos - -def _find_strings(filename, encoding=None): - """Return a dict of possible docstring positions. - - The dict maps line numbers to strings. There is an entry for - line that contains only a string or a part of a triple-quoted - string. - """ - d = {} - # If the first token is a string, then it's the module docstring. - # Add this special case so that the test in the loop passes. - prev_ttype = token.INDENT - with open(filename, encoding=encoding) as f: - tok = tokenize.generate_tokens(f.readline) - for ttype, tstr, start, end, line in tok: - if ttype == token.STRING: - if prev_ttype == token.INDENT: - sline, scol = start - eline, ecol = end - for i in range(sline, eline + 1): - d[i] = 1 - prev_ttype = ttype - return d - -def _find_executable_linenos(filename): - """Return dict where keys are line numbers in the line number table.""" - try: - with tokenize.open(filename) as f: - prog = f.read() - encoding = f.encoding - except OSError as err: - print(("Not printing coverage data for %r: %s" - % (filename, err)), file=sys.stderr) - return {} - code = compile(prog, filename, "exec") - strs = _find_strings(filename, encoding) - return _find_lines(code, strs) - -class Trace: - def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0, - ignoremods=(), ignoredirs=(), infile=None, outfile=None, - timing=False): - """ - @param count true iff it should count number of times each - line is executed - @param trace true iff it should print out each line that is - being counted - @param countfuncs true iff it should just output a list of - (filename, modulename, funcname,) for functions - that were called at least once; This overrides - `count' and `trace' - @param ignoremods a list of the names of modules to ignore - @param ignoredirs a list of the names of directories to ignore - all of the (recursive) contents of - @param infile file from which to read stored counts to be - added into the results - @param outfile file in which to write the results - @param timing true iff timing information be displayed - """ - self.infile = infile - self.outfile = outfile - self.ignore = _Ignore(ignoremods, ignoredirs) - self.counts = {} # keys are (filename, linenumber) - self.pathtobasename = {} # for memoizing os.path.basename - self.donothing = 0 - self.trace = trace - self._calledfuncs = {} - self._callers = {} - self._caller_cache = {} - self.start_time = None - if timing: - self.start_time = _time() - if countcallers: - self.globaltrace = self.globaltrace_trackcallers - elif countfuncs: - self.globaltrace = self.globaltrace_countfuncs - elif trace and count: - self.globaltrace = self.globaltrace_lt - self.localtrace = self.localtrace_trace_and_count - elif trace: - self.globaltrace = self.globaltrace_lt - self.localtrace = self.localtrace_trace - elif count: - self.globaltrace = self.globaltrace_lt - self.localtrace = self.localtrace_count - else: - # Ahem -- do nothing? Okay. - self.donothing = 1 - - def run(self, cmd): - import __main__ - dict = __main__.__dict__ - self.runctx(cmd, dict, dict) - - def runctx(self, cmd, globals=None, locals=None): - if globals is None: globals = {} - if locals is None: locals = {} - if not self.donothing: - threading.settrace(self.globaltrace) - sys.settrace(self.globaltrace) - try: - exec(cmd, globals, locals) - finally: - if not self.donothing: - sys.settrace(None) - threading.settrace(None) - - def runfunc(*args, **kw): - if len(args) >= 2: - self, func, *args = args - elif not args: - raise TypeError("descriptor 'runfunc' of 'Trace' object " - "needs an argument") - elif 'func' in kw: - func = kw.pop('func') - self, *args = args - import warnings - warnings.warn("Passing 'func' as keyword argument is deprecated", - DeprecationWarning, stacklevel=2) - else: - raise TypeError('runfunc expected at least 1 positional argument, ' - 'got %d' % (len(args)-1)) - - result = None - if not self.donothing: - sys.settrace(self.globaltrace) - try: - result = func(*args, **kw) - finally: - if not self.donothing: - sys.settrace(None) - return result - runfunc.__text_signature__ = '($self, func, /, *args, **kw)' - - def file_module_function_of(self, frame): - code = frame.f_code - filename = code.co_filename - if filename: - modulename = _modname(filename) - else: - modulename = None - - funcname = code.co_name - clsname = None - if code in self._caller_cache: - if self._caller_cache[code] is not None: - clsname = self._caller_cache[code] - else: - self._caller_cache[code] = None - ## use of gc.get_referrers() was suggested by Michael Hudson - # all functions which refer to this code object - funcs = [f for f in gc.get_referrers(code) - if inspect.isfunction(f)] - # require len(func) == 1 to avoid ambiguity caused by calls to - # new.function(): "In the face of ambiguity, refuse the - # temptation to guess." - if len(funcs) == 1: - dicts = [d for d in gc.get_referrers(funcs[0]) - if isinstance(d, dict)] - if len(dicts) == 1: - classes = [c for c in gc.get_referrers(dicts[0]) - if hasattr(c, "__bases__")] - if len(classes) == 1: - # ditto for new.classobj() - clsname = classes[0].__name__ - # cache the result - assumption is that new.* is - # not called later to disturb this relationship - # _caller_cache could be flushed if functions in - # the new module get called. - self._caller_cache[code] = clsname - if clsname is not None: - funcname = "%s.%s" % (clsname, funcname) - - return filename, modulename, funcname - - def globaltrace_trackcallers(self, frame, why, arg): - """Handler for call events. - - Adds information about who called who to the self._callers dict. - """ - if why == 'call': - # XXX Should do a better job of identifying methods - this_func = self.file_module_function_of(frame) - parent_func = self.file_module_function_of(frame.f_back) - self._callers[(parent_func, this_func)] = 1 - - def globaltrace_countfuncs(self, frame, why, arg): - """Handler for call events. - - Adds (filename, modulename, funcname) to the self._calledfuncs dict. - """ - if why == 'call': - this_func = self.file_module_function_of(frame) - self._calledfuncs[this_func] = 1 - - def globaltrace_lt(self, frame, why, arg): - """Handler for call events. - - If the code block being entered is to be ignored, returns `None', - else returns self.localtrace. - """ - if why == 'call': - code = frame.f_code - filename = frame.f_globals.get('__file__', None) - if filename: - # XXX _modname() doesn't work right for packages, so - # the ignore support won't work right for packages - modulename = _modname(filename) - if modulename is not None: - ignore_it = self.ignore.names(filename, modulename) - if not ignore_it: - if self.trace: - print((" --- modulename: %s, funcname: %s" - % (modulename, code.co_name))) - return self.localtrace - else: - return None - - def localtrace_trace_and_count(self, frame, why, arg): - if why == "line": - # record the file name and line number of every trace - filename = frame.f_code.co_filename - lineno = frame.f_lineno - key = filename, lineno - self.counts[key] = self.counts.get(key, 0) + 1 - - if self.start_time: - print('%.2f' % (_time() - self.start_time), end=' ') - bname = os.path.basename(filename) - print("%s(%d): %s" % (bname, lineno, - linecache.getline(filename, lineno)), end='') - return self.localtrace - - def localtrace_trace(self, frame, why, arg): - if why == "line": - # record the file name and line number of every trace - filename = frame.f_code.co_filename - lineno = frame.f_lineno - - if self.start_time: - print('%.2f' % (_time() - self.start_time), end=' ') - bname = os.path.basename(filename) - print("%s(%d): %s" % (bname, lineno, - linecache.getline(filename, lineno)), end='') - return self.localtrace - - def localtrace_count(self, frame, why, arg): - if why == "line": - filename = frame.f_code.co_filename - lineno = frame.f_lineno - key = filename, lineno - self.counts[key] = self.counts.get(key, 0) + 1 - return self.localtrace - - def results(self): - return CoverageResults(self.counts, infile=self.infile, - outfile=self.outfile, - calledfuncs=self._calledfuncs, - callers=self._callers) - -def main(): - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument('--version', action='version', version='trace 2.0') - - grp = parser.add_argument_group('Main options', - 'One of these (or --report) must be given') - - grp.add_argument('-c', '--count', action='store_true', - help='Count the number of times each line is executed and write ' - 'the counts to .cover for each module executed, in ' - 'the module\'s directory. See also --coverdir, --file, ' - '--no-report below.') - grp.add_argument('-t', '--trace', action='store_true', - help='Print each line to sys.stdout before it is executed') - grp.add_argument('-l', '--listfuncs', action='store_true', - help='Keep track of which functions are executed at least once ' - 'and write the results to sys.stdout after the program exits. ' - 'Cannot be specified alongside --trace or --count.') - grp.add_argument('-T', '--trackcalls', action='store_true', - help='Keep track of caller/called pairs and write the results to ' - 'sys.stdout after the program exits.') - - grp = parser.add_argument_group('Modifiers') - - _grp = grp.add_mutually_exclusive_group() - _grp.add_argument('-r', '--report', action='store_true', - help='Generate a report from a counts file; does not execute any ' - 'code. --file must specify the results file to read, which ' - 'must have been created in a previous run with --count ' - '--file=FILE') - _grp.add_argument('-R', '--no-report', action='store_true', - help='Do not generate the coverage report files. ' - 'Useful if you want to accumulate over several runs.') - - grp.add_argument('-f', '--file', - help='File to accumulate counts over several runs') - grp.add_argument('-C', '--coverdir', - help='Directory where the report files go. The coverage report ' - 'for . will be written to file ' - '//.cover') - grp.add_argument('-m', '--missing', action='store_true', - help='Annotate executable lines that were not executed with ' - '">>>>>> "') - grp.add_argument('-s', '--summary', action='store_true', - help='Write a brief summary for each file to sys.stdout. ' - 'Can only be used with --count or --report') - grp.add_argument('-g', '--timing', action='store_true', - help='Prefix each line with the time since the program started. ' - 'Only used while tracing') - - grp = parser.add_argument_group('Filters', - 'Can be specified multiple times') - grp.add_argument('--ignore-module', action='append', default=[], - help='Ignore the given module(s) and its submodules ' - '(if it is a package). Accepts comma separated list of ' - 'module names.') - grp.add_argument('--ignore-dir', action='append', default=[], - help='Ignore files in the given directory ' - '(multiple directories can be joined by os.pathsep).') - - parser.add_argument('--module', action='store_true', default=False, - help='Trace a module. ') - parser.add_argument('progname', nargs='?', - help='file to run as main program') - parser.add_argument('arguments', nargs=argparse.REMAINDER, - help='arguments to the program') - - opts = parser.parse_args() - - if opts.ignore_dir: - _prefix = sysconfig.get_path("stdlib") - _exec_prefix = sysconfig.get_path("platstdlib") - - def parse_ignore_dir(s): - s = os.path.expanduser(os.path.expandvars(s)) - s = s.replace('$prefix', _prefix).replace('$exec_prefix', _exec_prefix) - return os.path.normpath(s) - - opts.ignore_module = [mod.strip() - for i in opts.ignore_module for mod in i.split(',')] - opts.ignore_dir = [parse_ignore_dir(s) - for i in opts.ignore_dir for s in i.split(os.pathsep)] - - if opts.report: - if not opts.file: - parser.error('-r/--report requires -f/--file') - results = CoverageResults(infile=opts.file, outfile=opts.file) - return results.write_results(opts.missing, opts.summary, opts.coverdir) - - if not any([opts.trace, opts.count, opts.listfuncs, opts.trackcalls]): - parser.error('must specify one of --trace, --count, --report, ' - '--listfuncs, or --trackcalls') - - if opts.listfuncs and (opts.count or opts.trace): - parser.error('cannot specify both --listfuncs and (--trace or --count)') - - if opts.summary and not opts.count: - parser.error('--summary can only be used with --count or --report') - - if opts.progname is None: - parser.error('progname is missing: required with the main options') - - t = Trace(opts.count, opts.trace, countfuncs=opts.listfuncs, - countcallers=opts.trackcalls, ignoremods=opts.ignore_module, - ignoredirs=opts.ignore_dir, infile=opts.file, - outfile=opts.file, timing=opts.timing) - try: - if opts.module: - import runpy - module_name = opts.progname - mod_name, mod_spec, code = runpy._get_module_details(module_name) - sys.argv = [code.co_filename, *opts.arguments] - globs = { - '__name__': '__main__', - '__file__': code.co_filename, - '__package__': mod_spec.parent, - '__loader__': mod_spec.loader, - '__spec__': mod_spec, - '__cached__': None, - } - else: - sys.argv = [opts.progname, *opts.arguments] - sys.path[0] = os.path.dirname(opts.progname) - - with open(opts.progname, 'rb') as fp: - code = compile(fp.read(), opts.progname, 'exec') - # try to emulate __main__ namespace as much as possible - globs = { - '__file__': opts.progname, - '__name__': '__main__', - '__package__': None, - '__cached__': None, - } - t.runctx(code, globs, globs) - except OSError as err: - sys.exit("Cannot run file %r because: %s" % (sys.argv[0], err)) - except SystemExit: - pass - - results = t.results() - - if not opts.no_report: - results.write_results(opts.missing, opts.summary, opts.coverdir) - -if __name__=='__main__': - main() +#!/usr/bin/env python3 + +# portions copyright 2001, Autonomous Zones Industries, Inc., all rights... +# err... reserved and offered to the public under the terms of the +# Python 2.2 license. +# Author: Zooko O'Whielacronx +# http://zooko.com/ +# mailto:zooko@zooko.com +# +# Copyright 2000, Mojam Media, Inc., all rights reserved. +# Author: Skip Montanaro +# +# Copyright 1999, Bioreason, Inc., all rights reserved. +# Author: Andrew Dalke +# +# Copyright 1995-1997, Automatrix, Inc., all rights reserved. +# Author: Skip Montanaro +# +# Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved. +# +# +# Permission to use, copy, modify, and distribute this Python software and +# its associated documentation for any purpose without fee is hereby +# granted, provided that the above copyright notice appears in all copies, +# and that both that copyright notice and this permission notice appear in +# supporting documentation, and that the name of neither Automatrix, +# Bioreason or Mojam Media be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior permission. +# +"""program/module to trace Python program or function execution + +Sample use, command line: + trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs + trace.py -t --ignore-dir '$prefix' spam.py eggs + trace.py --trackcalls spam.py eggs + +Sample use, programmatically + import sys + + # create a Trace object, telling it what to ignore, and whether to + # do tracing or line-counting or both. + tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,], + trace=0, count=1) + # run the new command using the given tracer + tracer.run('main()') + # make a report, placing output in /tmp + r = tracer.results() + r.write_results(show_missing=True, coverdir="/tmp") +""" +__all__ = ['Trace', 'CoverageResults'] + +import linecache +import os +import sys +import sysconfig +import token +import tokenize +import inspect +import gc +import dis +import pickle +from time import monotonic as _time + +import threading + +PRAGMA_NOCOVER = "#pragma NO COVER" + +class _Ignore: + def __init__(self, modules=None, dirs=None): + self._mods = set() if not modules else set(modules) + self._dirs = [] if not dirs else [os.path.normpath(d) + for d in dirs] + self._ignore = { '': 1 } + + def names(self, filename, modulename): + if modulename in self._ignore: + return self._ignore[modulename] + + # haven't seen this one before, so see if the module name is + # on the ignore list. + if modulename in self._mods: # Identical names, so ignore + self._ignore[modulename] = 1 + return 1 + + # check if the module is a proper submodule of something on + # the ignore list + for mod in self._mods: + # Need to take some care since ignoring + # "cmp" mustn't mean ignoring "cmpcache" but ignoring + # "Spam" must also mean ignoring "Spam.Eggs". + if modulename.startswith(mod + '.'): + self._ignore[modulename] = 1 + return 1 + + # Now check that filename isn't in one of the directories + if filename is None: + # must be a built-in, so we must ignore + self._ignore[modulename] = 1 + return 1 + + # Ignore a file when it contains one of the ignorable paths + for d in self._dirs: + # The '+ os.sep' is to ensure that d is a parent directory, + # as compared to cases like: + # d = "/usr/local" + # filename = "/usr/local.py" + # or + # d = "/usr/local.py" + # filename = "/usr/local.py" + if filename.startswith(d + os.sep): + self._ignore[modulename] = 1 + return 1 + + # Tried the different ways, so we don't ignore this module + self._ignore[modulename] = 0 + return 0 + +def _modname(path): + """Return a plausible module name for the patch.""" + + base = os.path.basename(path) + filename, ext = os.path.splitext(base) + return filename + +def _fullmodname(path): + """Return a plausible module name for the path.""" + + # If the file 'path' is part of a package, then the filename isn't + # enough to uniquely identify it. Try to do the right thing by + # looking in sys.path for the longest matching prefix. We'll + # assume that the rest is the package name. + + comparepath = os.path.normcase(path) + longest = "" + for dir in sys.path: + dir = os.path.normcase(dir) + if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep: + if len(dir) > len(longest): + longest = dir + + if longest: + base = path[len(longest) + 1:] + else: + base = path + # the drive letter is never part of the module name + drive, base = os.path.splitdrive(base) + base = base.replace(os.sep, ".") + if os.altsep: + base = base.replace(os.altsep, ".") + filename, ext = os.path.splitext(base) + return filename.lstrip(".") + +class CoverageResults: + def __init__(self, counts=None, calledfuncs=None, infile=None, + callers=None, outfile=None): + self.counts = counts + if self.counts is None: + self.counts = {} + self.counter = self.counts.copy() # map (filename, lineno) to count + self.calledfuncs = calledfuncs + if self.calledfuncs is None: + self.calledfuncs = {} + self.calledfuncs = self.calledfuncs.copy() + self.callers = callers + if self.callers is None: + self.callers = {} + self.callers = self.callers.copy() + self.infile = infile + self.outfile = outfile + if self.infile: + # Try to merge existing counts file. + try: + with open(self.infile, 'rb') as f: + counts, calledfuncs, callers = pickle.load(f) + self.update(self.__class__(counts, calledfuncs, callers)) + except (OSError, EOFError, ValueError) as err: + print(("Skipping counts file %r: %s" + % (self.infile, err)), file=sys.stderr) + + def is_ignored_filename(self, filename): + """Return True if the filename does not refer to a file + we want to have reported. + """ + return filename.startswith('<') and filename.endswith('>') + + def update(self, other): + """Merge in the data from another CoverageResults""" + counts = self.counts + calledfuncs = self.calledfuncs + callers = self.callers + other_counts = other.counts + other_calledfuncs = other.calledfuncs + other_callers = other.callers + + for key in other_counts: + counts[key] = counts.get(key, 0) + other_counts[key] + + for key in other_calledfuncs: + calledfuncs[key] = 1 + + for key in other_callers: + callers[key] = 1 + + def write_results(self, show_missing=True, summary=False, coverdir=None): + """ + Write the coverage results. + + :param show_missing: Show lines that had no hits. + :param summary: Include coverage summary per module. + :param coverdir: If None, the results of each module are placed in its + directory, otherwise it is included in the directory + specified. + """ + if self.calledfuncs: + print() + print("functions called:") + calls = self.calledfuncs + for filename, modulename, funcname in sorted(calls): + print(("filename: %s, modulename: %s, funcname: %s" + % (filename, modulename, funcname))) + + if self.callers: + print() + print("calling relationships:") + lastfile = lastcfile = "" + for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) \ + in sorted(self.callers): + if pfile != lastfile: + print() + print("***", pfile, "***") + lastfile = pfile + lastcfile = "" + if cfile != pfile and lastcfile != cfile: + print(" -->", cfile) + lastcfile = cfile + print(" %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc)) + + # turn the counts data ("(filename, lineno) = count") into something + # accessible on a per-file basis + per_file = {} + for filename, lineno in self.counts: + lines_hit = per_file[filename] = per_file.get(filename, {}) + lines_hit[lineno] = self.counts[(filename, lineno)] + + # accumulate summary info, if needed + sums = {} + + for filename, count in per_file.items(): + if self.is_ignored_filename(filename): + continue + + if filename.endswith(".pyc"): + filename = filename[:-1] + + if coverdir is None: + dir = os.path.dirname(os.path.abspath(filename)) + modulename = _modname(filename) + else: + dir = coverdir + if not os.path.exists(dir): + os.makedirs(dir) + modulename = _fullmodname(filename) + + # If desired, get a list of the line numbers which represent + # executable content (returned as a dict for better lookup speed) + if show_missing: + lnotab = _find_executable_linenos(filename) + else: + lnotab = {} + source = linecache.getlines(filename) + coverpath = os.path.join(dir, modulename + ".cover") + with open(filename, 'rb') as fp: + encoding, _ = tokenize.detect_encoding(fp.readline) + n_hits, n_lines = self.write_results_file(coverpath, source, + lnotab, count, encoding) + if summary and n_lines: + percent = int(100 * n_hits / n_lines) + sums[modulename] = n_lines, percent, modulename, filename + + + if summary and sums: + print("lines cov% module (path)") + for m in sorted(sums): + n_lines, percent, modulename, filename = sums[m] + print("%5d %3d%% %s (%s)" % sums[m]) + + if self.outfile: + # try and store counts and module info into self.outfile + try: + with open(self.outfile, 'wb') as f: + pickle.dump((self.counts, self.calledfuncs, self.callers), + f, 1) + except OSError as err: + print("Can't save counts files because %s" % err, file=sys.stderr) + + def write_results_file(self, path, lines, lnotab, lines_hit, encoding=None): + """Return a coverage results file in path.""" + # ``lnotab`` is a dict of executable lines, or a line number "table" + + try: + outfile = open(path, "w", encoding=encoding) + except OSError as err: + print(("trace: Could not open %r for writing: %s " + "- skipping" % (path, err)), file=sys.stderr) + return 0, 0 + + n_lines = 0 + n_hits = 0 + with outfile: + for lineno, line in enumerate(lines, 1): + # do the blank/comment match to try to mark more lines + # (help the reader find stuff that hasn't been covered) + if lineno in lines_hit: + outfile.write("%5d: " % lines_hit[lineno]) + n_hits += 1 + n_lines += 1 + elif lineno in lnotab and not PRAGMA_NOCOVER in line: + # Highlight never-executed lines, unless the line contains + # #pragma: NO COVER + outfile.write(">>>>>> ") + n_lines += 1 + else: + outfile.write(" ") + outfile.write(line.expandtabs(8)) + + return n_hits, n_lines + +def _find_lines_from_code(code, strs): + """Return dict where keys are lines in the line number table.""" + linenos = {} + + for _, lineno in dis.findlinestarts(code): + if lineno not in strs: + linenos[lineno] = 1 + + return linenos + +def _find_lines(code, strs): + """Return lineno dict for all code objects reachable from code.""" + # get all of the lineno information from the code of this scope level + linenos = _find_lines_from_code(code, strs) + + # and check the constants for references to other code objects + for c in code.co_consts: + if inspect.iscode(c): + # find another code object, so recurse into it + linenos.update(_find_lines(c, strs)) + return linenos + +def _find_strings(filename, encoding=None): + """Return a dict of possible docstring positions. + + The dict maps line numbers to strings. There is an entry for + line that contains only a string or a part of a triple-quoted + string. + """ + d = {} + # If the first token is a string, then it's the module docstring. + # Add this special case so that the test in the loop passes. + prev_ttype = token.INDENT + with open(filename, encoding=encoding) as f: + tok = tokenize.generate_tokens(f.readline) + for ttype, tstr, start, end, line in tok: + if ttype == token.STRING: + if prev_ttype == token.INDENT: + sline, scol = start + eline, ecol = end + for i in range(sline, eline + 1): + d[i] = 1 + prev_ttype = ttype + return d + +def _find_executable_linenos(filename): + """Return dict where keys are line numbers in the line number table.""" + try: + with tokenize.open(filename) as f: + prog = f.read() + encoding = f.encoding + except OSError as err: + print(("Not printing coverage data for %r: %s" + % (filename, err)), file=sys.stderr) + return {} + code = compile(prog, filename, "exec") + strs = _find_strings(filename, encoding) + return _find_lines(code, strs) + +class Trace: + def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0, + ignoremods=(), ignoredirs=(), infile=None, outfile=None, + timing=False): + """ + @param count true iff it should count number of times each + line is executed + @param trace true iff it should print out each line that is + being counted + @param countfuncs true iff it should just output a list of + (filename, modulename, funcname,) for functions + that were called at least once; This overrides + `count' and `trace' + @param ignoremods a list of the names of modules to ignore + @param ignoredirs a list of the names of directories to ignore + all of the (recursive) contents of + @param infile file from which to read stored counts to be + added into the results + @param outfile file in which to write the results + @param timing true iff timing information be displayed + """ + self.infile = infile + self.outfile = outfile + self.ignore = _Ignore(ignoremods, ignoredirs) + self.counts = {} # keys are (filename, linenumber) + self.pathtobasename = {} # for memoizing os.path.basename + self.donothing = 0 + self.trace = trace + self._calledfuncs = {} + self._callers = {} + self._caller_cache = {} + self.start_time = None + if timing: + self.start_time = _time() + if countcallers: + self.globaltrace = self.globaltrace_trackcallers + elif countfuncs: + self.globaltrace = self.globaltrace_countfuncs + elif trace and count: + self.globaltrace = self.globaltrace_lt + self.localtrace = self.localtrace_trace_and_count + elif trace: + self.globaltrace = self.globaltrace_lt + self.localtrace = self.localtrace_trace + elif count: + self.globaltrace = self.globaltrace_lt + self.localtrace = self.localtrace_count + else: + # Ahem -- do nothing? Okay. + self.donothing = 1 + + def run(self, cmd): + import __main__ + dict = __main__.__dict__ + self.runctx(cmd, dict, dict) + + def runctx(self, cmd, globals=None, locals=None): + if globals is None: globals = {} + if locals is None: locals = {} + if not self.donothing: + threading.settrace(self.globaltrace) + sys.settrace(self.globaltrace) + try: + exec(cmd, globals, locals) + finally: + if not self.donothing: + sys.settrace(None) + threading.settrace(None) + + def runfunc(*args, **kw): + if len(args) >= 2: + self, func, *args = args + elif not args: + raise TypeError("descriptor 'runfunc' of 'Trace' object " + "needs an argument") + elif 'func' in kw: + func = kw.pop('func') + self, *args = args + import warnings + warnings.warn("Passing 'func' as keyword argument is deprecated", + DeprecationWarning, stacklevel=2) + else: + raise TypeError('runfunc expected at least 1 positional argument, ' + 'got %d' % (len(args)-1)) + + result = None + if not self.donothing: + sys.settrace(self.globaltrace) + try: + result = func(*args, **kw) + finally: + if not self.donothing: + sys.settrace(None) + return result + runfunc.__text_signature__ = '($self, func, /, *args, **kw)' + + def file_module_function_of(self, frame): + code = frame.f_code + filename = code.co_filename + if filename: + modulename = _modname(filename) + else: + modulename = None + + funcname = code.co_name + clsname = None + if code in self._caller_cache: + if self._caller_cache[code] is not None: + clsname = self._caller_cache[code] + else: + self._caller_cache[code] = None + ## use of gc.get_referrers() was suggested by Michael Hudson + # all functions which refer to this code object + funcs = [f for f in gc.get_referrers(code) + if inspect.isfunction(f)] + # require len(func) == 1 to avoid ambiguity caused by calls to + # new.function(): "In the face of ambiguity, refuse the + # temptation to guess." + if len(funcs) == 1: + dicts = [d for d in gc.get_referrers(funcs[0]) + if isinstance(d, dict)] + if len(dicts) == 1: + classes = [c for c in gc.get_referrers(dicts[0]) + if hasattr(c, "__bases__")] + if len(classes) == 1: + # ditto for new.classobj() + clsname = classes[0].__name__ + # cache the result - assumption is that new.* is + # not called later to disturb this relationship + # _caller_cache could be flushed if functions in + # the new module get called. + self._caller_cache[code] = clsname + if clsname is not None: + funcname = "%s.%s" % (clsname, funcname) + + return filename, modulename, funcname + + def globaltrace_trackcallers(self, frame, why, arg): + """Handler for call events. + + Adds information about who called who to the self._callers dict. + """ + if why == 'call': + # XXX Should do a better job of identifying methods + this_func = self.file_module_function_of(frame) + parent_func = self.file_module_function_of(frame.f_back) + self._callers[(parent_func, this_func)] = 1 + + def globaltrace_countfuncs(self, frame, why, arg): + """Handler for call events. + + Adds (filename, modulename, funcname) to the self._calledfuncs dict. + """ + if why == 'call': + this_func = self.file_module_function_of(frame) + self._calledfuncs[this_func] = 1 + + def globaltrace_lt(self, frame, why, arg): + """Handler for call events. + + If the code block being entered is to be ignored, returns `None', + else returns self.localtrace. + """ + if why == 'call': + code = frame.f_code + filename = frame.f_globals.get('__file__', None) + if filename: + # XXX _modname() doesn't work right for packages, so + # the ignore support won't work right for packages + modulename = _modname(filename) + if modulename is not None: + ignore_it = self.ignore.names(filename, modulename) + if not ignore_it: + if self.trace: + print((" --- modulename: %s, funcname: %s" + % (modulename, code.co_name))) + return self.localtrace + else: + return None + + def localtrace_trace_and_count(self, frame, why, arg): + if why == "line": + # record the file name and line number of every trace + filename = frame.f_code.co_filename + lineno = frame.f_lineno + key = filename, lineno + self.counts[key] = self.counts.get(key, 0) + 1 + + if self.start_time: + print('%.2f' % (_time() - self.start_time), end=' ') + bname = os.path.basename(filename) + print("%s(%d): %s" % (bname, lineno, + linecache.getline(filename, lineno)), end='') + return self.localtrace + + def localtrace_trace(self, frame, why, arg): + if why == "line": + # record the file name and line number of every trace + filename = frame.f_code.co_filename + lineno = frame.f_lineno + + if self.start_time: + print('%.2f' % (_time() - self.start_time), end=' ') + bname = os.path.basename(filename) + print("%s(%d): %s" % (bname, lineno, + linecache.getline(filename, lineno)), end='') + return self.localtrace + + def localtrace_count(self, frame, why, arg): + if why == "line": + filename = frame.f_code.co_filename + lineno = frame.f_lineno + key = filename, lineno + self.counts[key] = self.counts.get(key, 0) + 1 + return self.localtrace + + def results(self): + return CoverageResults(self.counts, infile=self.infile, + outfile=self.outfile, + calledfuncs=self._calledfuncs, + callers=self._callers) + +def main(): + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--version', action='version', version='trace 2.0') + + grp = parser.add_argument_group('Main options', + 'One of these (or --report) must be given') + + grp.add_argument('-c', '--count', action='store_true', + help='Count the number of times each line is executed and write ' + 'the counts to .cover for each module executed, in ' + 'the module\'s directory. See also --coverdir, --file, ' + '--no-report below.') + grp.add_argument('-t', '--trace', action='store_true', + help='Print each line to sys.stdout before it is executed') + grp.add_argument('-l', '--listfuncs', action='store_true', + help='Keep track of which functions are executed at least once ' + 'and write the results to sys.stdout after the program exits. ' + 'Cannot be specified alongside --trace or --count.') + grp.add_argument('-T', '--trackcalls', action='store_true', + help='Keep track of caller/called pairs and write the results to ' + 'sys.stdout after the program exits.') + + grp = parser.add_argument_group('Modifiers') + + _grp = grp.add_mutually_exclusive_group() + _grp.add_argument('-r', '--report', action='store_true', + help='Generate a report from a counts file; does not execute any ' + 'code. --file must specify the results file to read, which ' + 'must have been created in a previous run with --count ' + '--file=FILE') + _grp.add_argument('-R', '--no-report', action='store_true', + help='Do not generate the coverage report files. ' + 'Useful if you want to accumulate over several runs.') + + grp.add_argument('-f', '--file', + help='File to accumulate counts over several runs') + grp.add_argument('-C', '--coverdir', + help='Directory where the report files go. The coverage report ' + 'for . will be written to file ' + '//.cover') + grp.add_argument('-m', '--missing', action='store_true', + help='Annotate executable lines that were not executed with ' + '">>>>>> "') + grp.add_argument('-s', '--summary', action='store_true', + help='Write a brief summary for each file to sys.stdout. ' + 'Can only be used with --count or --report') + grp.add_argument('-g', '--timing', action='store_true', + help='Prefix each line with the time since the program started. ' + 'Only used while tracing') + + grp = parser.add_argument_group('Filters', + 'Can be specified multiple times') + grp.add_argument('--ignore-module', action='append', default=[], + help='Ignore the given module(s) and its submodules ' + '(if it is a package). Accepts comma separated list of ' + 'module names.') + grp.add_argument('--ignore-dir', action='append', default=[], + help='Ignore files in the given directory ' + '(multiple directories can be joined by os.pathsep).') + + parser.add_argument('--module', action='store_true', default=False, + help='Trace a module. ') + parser.add_argument('progname', nargs='?', + help='file to run as main program') + parser.add_argument('arguments', nargs=argparse.REMAINDER, + help='arguments to the program') + + opts = parser.parse_args() + + if opts.ignore_dir: + _prefix = sysconfig.get_path("stdlib") + _exec_prefix = sysconfig.get_path("platstdlib") + + def parse_ignore_dir(s): + s = os.path.expanduser(os.path.expandvars(s)) + s = s.replace('$prefix', _prefix).replace('$exec_prefix', _exec_prefix) + return os.path.normpath(s) + + opts.ignore_module = [mod.strip() + for i in opts.ignore_module for mod in i.split(',')] + opts.ignore_dir = [parse_ignore_dir(s) + for i in opts.ignore_dir for s in i.split(os.pathsep)] + + if opts.report: + if not opts.file: + parser.error('-r/--report requires -f/--file') + results = CoverageResults(infile=opts.file, outfile=opts.file) + return results.write_results(opts.missing, opts.summary, opts.coverdir) + + if not any([opts.trace, opts.count, opts.listfuncs, opts.trackcalls]): + parser.error('must specify one of --trace, --count, --report, ' + '--listfuncs, or --trackcalls') + + if opts.listfuncs and (opts.count or opts.trace): + parser.error('cannot specify both --listfuncs and (--trace or --count)') + + if opts.summary and not opts.count: + parser.error('--summary can only be used with --count or --report') + + if opts.progname is None: + parser.error('progname is missing: required with the main options') + + t = Trace(opts.count, opts.trace, countfuncs=opts.listfuncs, + countcallers=opts.trackcalls, ignoremods=opts.ignore_module, + ignoredirs=opts.ignore_dir, infile=opts.file, + outfile=opts.file, timing=opts.timing) + try: + if opts.module: + import runpy + module_name = opts.progname + mod_name, mod_spec, code = runpy._get_module_details(module_name) + sys.argv = [code.co_filename, *opts.arguments] + globs = { + '__name__': '__main__', + '__file__': code.co_filename, + '__package__': mod_spec.parent, + '__loader__': mod_spec.loader, + '__spec__': mod_spec, + '__cached__': None, + } + else: + sys.argv = [opts.progname, *opts.arguments] + sys.path[0] = os.path.dirname(opts.progname) + + with open(opts.progname, 'rb') as fp: + code = compile(fp.read(), opts.progname, 'exec') + # try to emulate __main__ namespace as much as possible + globs = { + '__file__': opts.progname, + '__name__': '__main__', + '__package__': None, + '__cached__': None, + } + t.runctx(code, globs, globs) + except OSError as err: + sys.exit("Cannot run file %r because: %s" % (sys.argv[0], err)) + except SystemExit: + pass + + results = t.results() + + if not opts.no_report: + results.write_results(opts.missing, opts.summary, opts.coverdir) + +if __name__=='__main__': + main() diff --git a/vm/src/version.rs b/vm/src/version.rs index b98f8c28cd..452f659efc 100644 --- a/vm/src/version.rs +++ b/vm/src/version.rs @@ -1,8 +1,7 @@ /* Several function to retrieve version information. */ -use chrono::prelude::DateTime; -use chrono::Local; +use chrono::{prelude::DateTime, Local}; use std::time::{Duration, UNIX_EPOCH}; // = 3.10.0alpha @@ -18,7 +17,7 @@ pub const VERSION_HEX: usize = pub fn get_version() -> String { format!( - "{:.80} ({:.80}) \n[{:.80}]", + "{:.80} ({:.80}) \n[{:.80}]", // \n is PyPy convention get_version_number(), get_build_info(), get_compiler()