From d04c14fea97314d7041dcde275e9fb4f4a8e1778 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 8 Feb 2026 18:02:10 -0500 Subject: [PATCH] ruff format --- dev/autoprofile-poc.py | 26 +- dev/maintain/port_utilities.py | 27 +- docs/source/conf.py | 181 +++-- kernprof.py | 482 +++++++++----- line_profiler/__init__.py | 27 +- line_profiler/_diagnostics.py | 27 +- line_profiler/_line_profiler.pyi | 3 - line_profiler/_logger.py | 31 +- .../autoprofile/ast_profile_transformer.py | 60 +- .../autoprofile/ast_tree_profiler.py | 66 +- line_profiler/autoprofile/autoprofile.py | 28 +- line_profiler/autoprofile/eager_preimports.py | 119 ++-- .../autoprofile/line_profiler_utils.py | 44 +- .../autoprofile/profmod_extractor.py | 39 +- line_profiler/autoprofile/run_module.py | 10 +- line_profiler/autoprofile/util_static.py | 122 ++-- line_profiler/cli_utils.py | 82 ++- line_profiler/explicit_profiler.py | 50 +- line_profiler/ipython_extension.py | 171 +++-- line_profiler/line_profiler.py | 541 ++++++++++----- line_profiler/line_profiler_utils.py | 9 +- line_profiler/profiler_mixin.py | 252 ++++--- line_profiler/scoping_policy.py | 99 +-- line_profiler/toml_config.py | 77 ++- pyproject.toml | 20 + run_tests.py | 42 +- setup.py | 197 +++--- tests/complex_example.py | 16 +- tests/test_autoprofile.py | 624 ++++++++++++------ tests/test_cli.py | 134 ++-- tests/test_complex_case.py | 81 ++- tests/test_cython.py | 42 +- tests/test_eager_preimports.py | 159 +++-- tests/test_explicit_profile.py | 406 +++++++----- tests/test_import.py | 7 +- tests/test_ipython.py | 109 +-- tests/test_kernprof.py | 338 ++++++---- tests/test_line_profiler.py | 331 ++++++---- tests/test_sys_monitoring.py | 252 ++++--- tests/test_sys_trace.py | 244 ++++--- tests/test_toml_config.py | 48 +- 41 files changed, 3611 insertions(+), 2012 deletions(-) diff --git a/dev/autoprofile-poc.py b/dev/autoprofile-poc.py index cfb78edf..084275db 100644 --- a/dev/autoprofile-poc.py +++ b/dev/autoprofile-poc.py @@ -17,16 +17,16 @@ def create_poc(dry_run=False): root = ub.Path.appdir('line_profiler/test/poc/') - repo = (root / 'repo') + repo = root / 'repo' modpaths = {} - modpaths['script'] = (root / 'repo/script.py') - modpaths['foo'] = (root / 'repo/foo') - modpaths['foo.__init__'] = (root / 'repo/foo/__init__.py') - modpaths['foo.bar'] = (root / 'repo/foo/bar.py') - modpaths['foo.baz'] = (root / 'repo/foo/baz') - modpaths['foo.baz.__init__'] = (root / 'repo/foo/baz/__init__.py') - modpaths['foo.baz.spam'] = (root / 'repo/foo/baz/spam.py') - modpaths['foo.baz.eggs'] = (root / 'repo/foo/baz/eggs.py') + modpaths['script'] = root / 'repo/script.py' + modpaths['foo'] = root / 'repo/foo' + modpaths['foo.__init__'] = root / 'repo/foo/__init__.py' + modpaths['foo.bar'] = root / 'repo/foo/bar.py' + modpaths['foo.baz'] = root / 'repo/foo/baz' + modpaths['foo.baz.__init__'] = root / 'repo/foo/baz/__init__.py' + modpaths['foo.baz.spam'] = root / 'repo/foo/baz/spam.py' + modpaths['foo.baz.eggs'] = root / 'repo/foo/baz/eggs.py' if not dry_run: root.delete().ensuredir() @@ -45,7 +45,7 @@ def create_poc(dry_run=False): """different import variations to handle""" script_text = ub.codeblock( - ''' + """ import foo # mod import foo.bar # py from foo import bar # py @@ -69,7 +69,8 @@ def main(): main() test() # asdf() - ''') + """ + ) ub.writeto(modpaths['script'], script_text) return root, repo, modpaths @@ -115,11 +116,13 @@ def main(): import sys import os import builtins + __file__ = script_file __name__ = '__main__' script_directory = os.path.realpath(os.path.dirname(script_file)) sys.path.insert(0, script_directory) import line_profiler + prof = line_profiler.LineProfiler() builtins.__dict__['profile'] = prof ns = locals() @@ -130,5 +133,6 @@ def main(): print('=' * 10) prof.print_stats(output_unit=1e-6, stripzeros=True, stream=sys.stdout) + if __name__ == '__main__': main() diff --git a/dev/maintain/port_utilities.py b/dev/maintain/port_utilities.py index a94f80b2..d7725f46 100644 --- a/dev/maintain/port_utilities.py +++ b/dev/maintain/port_utilities.py @@ -7,6 +7,7 @@ ~/code/mkinit/dev/maintain/port_ubelt_code.py ~/code/line_profiler/dev/maintain/port_utilities.py """ + import ubelt as ub import liberator import re @@ -45,7 +46,8 @@ def generate_util_static(): :py:mod:`xdoctest` via dev/maintain/port_utilities.py in the line_profiler repo. """ - ''') + ''' + ) # Remove doctest references to ubelt new_lines = [] @@ -53,7 +55,9 @@ def generate_util_static(): if line.strip().startswith('>>> from ubelt'): continue if line.strip().startswith('>>> import ubelt as ub'): - line = re.sub('>>> .*', '>>> # xdoctest: +SKIP("ubelt dependency")', line) + line = re.sub( + '>>> .*', '>>> # xdoctest: +SKIP("ubelt dependency")', line + ) new_lines.append(line) text = '\n'.join(new_lines) @@ -67,13 +71,26 @@ def main(): import parso import line_profiler - target_fpath = ub.Path(line_profiler.__file__).parent / 'autoprofile' / 'util_static.py' + + target_fpath = ( + ub.Path(line_profiler.__file__).parent + / 'autoprofile' + / 'util_static.py' + ) new_module = parso.parse(text) if target_fpath.exists(): old_module = parso.parse(target_fpath.read_text()) - new_names = [child.name.value for child in new_module.children if child.type in {'funcdef', 'classdef'}] - old_names = [child.name.value for child in old_module.children if child.type in {'funcdef', 'classdef'}] + new_names = [ + child.name.value + for child in new_module.children + if child.type in {'funcdef', 'classdef'} + ] + old_names = [ + child.name.value + for child in old_module.children + if child.type in {'funcdef', 'classdef'} + ] print(set(old_names) - set(new_names)) print(set(new_names) - set(old_names)) diff --git a/docs/source/conf.py b/docs/source/conf.py index e9328ff9..b81e1786 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -110,7 +110,6 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) - # -- Project information ----------------------------------------------------- import sphinx_rtd_theme from os.path import exists @@ -123,20 +122,24 @@ def parse_version(fpath): Statically parse the version number from a python file """ import ast + if not exists(fpath): raise ValueError('fpath={!r} does not exist'.format(fpath)) with open(fpath, 'r') as file_: sourcecode = file_.read() pt = ast.parse(sourcecode) + class VersionVisitor(ast.NodeVisitor): def visit_Assign(self, node): for target in node.targets: if getattr(target, 'id', None) == '__version__': self.version = node.value.s + visitor = VersionVisitor() visitor.visit(pt) return visitor.version + project = 'line_profiler' copyright = '2025, Robert Kern' author = 'Robert Kern' @@ -182,8 +185,8 @@ def visit_Assign(self, node): napoleon_use_param = False napoleon_use_ivar = True -#autoapi_type = 'python' -#autoapi_dirs = [mod_dpath] +# autoapi_type = 'python' +# autoapi_dirs = [mod_dpath] autodoc_inherit_docstrings = False @@ -198,7 +201,8 @@ def visit_Assign(self, node): ] autodoc_default_options = { # Document callable classes - 'special-members': '__call__'} + 'special-members': '__call__' +} autodoc_member_order = 'bysource' autoclass_content = 'both' @@ -233,16 +237,13 @@ def visit_Assign(self, node): 'networkx': ('https://networkx.org/documentation/stable/', None), 'scriptconfig': ('https://scriptconfig.readthedocs.io/en/latest/', None), 'rich': ('https://rich.readthedocs.io/en/latest/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), 'sympy': ('https://docs.sympy.org/latest/', None), 'scikit-learn': ('https://scikit-learn.org/stable/', None), 'pandas': ('https://pandas.pydata.org/docs/', None), 'matplotlib': ('https://matplotlib.org/stable/', None), - 'pytest': ('https://docs.pytest.org/en/latest/', None), 'platformdirs': ('https://platformdirs.readthedocs.io/en/latest/', None), - 'timerit': ('https://timerit.readthedocs.io/en/latest/', None), 'progiter': ('https://progiter.readthedocs.io/en/latest/', None), 'dateutil': ('https://dateutil.readthedocs.io/en/latest/', None), @@ -358,15 +359,12 @@ def visit_Assign(self, node): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -376,8 +374,13 @@ def visit_Assign(self, node): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'line_profiler.tex', 'line_profiler Documentation', - 'Robert Kern', 'manual'), + ( + master_doc, + 'line_profiler.tex', + 'line_profiler Documentation', + 'Robert Kern', + 'manual', + ), ] @@ -386,8 +389,7 @@ def visit_Assign(self, node): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'line_profiler', 'line_profiler Documentation', - [author], 1) + (master_doc, 'line_profiler', 'line_profiler Documentation', [author], 1) ] @@ -397,14 +399,21 @@ def visit_Assign(self, node): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'line_profiler', 'line_profiler Documentation', - author, 'line_profiler', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + 'line_profiler', + 'line_profiler Documentation', + author, + 'line_profiler', + 'One line description of project.', + 'Miscellaneous', + ), ] # -- Extension configuration ------------------------------------------------- from sphinx.domains.python import PythonDomain # NOQA + # from sphinx.application import Sphinx # NOQA from typing import Any, List # NOQA @@ -414,6 +423,7 @@ def visit_Assign(self, node): MAX_TIME_MINUTES = None if MAX_TIME_MINUTES: import ubelt # NOQA + TIMER = ubelt.Timer() TIMER.tic() @@ -423,7 +433,10 @@ class PatchedPythonDomain(PythonDomain): References: https://github.com/sphinx-doc/sphinx/issues/3866 """ - def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): + + def resolve_xref( + self, env, fromdocname, builder, typ, target, node, contnode + ): """ Helps to resolves cross-references """ @@ -432,7 +445,8 @@ def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): if target.startswith('xdoc.'): target = 'xdoctest.' + target[3] return_value = super(PatchedPythonDomain, self).resolve_xref( - env, fromdocname, builder, typ, target, node, contnode) + env, fromdocname, builder, typ, target, node, contnode + ) return return_value @@ -460,6 +474,7 @@ def register_section(self, tag, alias=None): alias = [alias] if not isinstance(alias, (list, tuple, set)) else alias alias.append(tag) alias = tuple(alias) + # TODO: better tag patterns def _wrap(func): self.registry[tag] = { @@ -468,6 +483,7 @@ def _wrap(func): 'func': func, } return func + return _wrap def _register_builtins(self): @@ -485,9 +501,12 @@ def commandline(lines): new_lines.extend(lines[1:]) return new_lines - @self.register_section(tag='SpecialExample', alias=['Benchmark', 'Sympy', 'Doctest']) + @self.register_section( + tag='SpecialExample', alias=['Benchmark', 'Sympy', 'Doctest'] + ) def benchmark(lines): import textwrap + new_lines = [] tag = lines[0].replace(':', '').strip() # new_lines.append(lines[0]) # TODO: it would be nice to change the tagline. @@ -560,7 +579,7 @@ def process(self, lines): accum = [] def accept(): - """ called when we finish reading a section """ + """called when we finish reading a section""" if curr_mode == '__doc__': # Keep the lines as-is new_lines.extend(accum) @@ -574,7 +593,6 @@ def accept(): accum[:] = [] for line in orig_lines: - found = None for regitem in self.registry.values(): if line.startswith(regitem['alias']): @@ -604,8 +622,15 @@ def accept(): return lines - def process_docstring_callback(self, app, what_: str, name: str, obj: Any, - options: Any, lines: List[str]) -> None: + def process_docstring_callback( + self, + app, + what_: str, + name: str, + obj: Any, + options: Any, + lines: List[str], + ) -> None: """ Callback to be registered to autodoc-process-docstring @@ -634,7 +659,9 @@ def process_docstring_callback(self, app, what_: str, name: str, obj: Any, https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html """ if self.debug: - print(f'ProcessDocstring: name={name}, what_={what_}, num_lines={len(lines)}') + print( + f'ProcessDocstring: name={name}, what_={what_}, num_lines={len(lines)}' + ) # print('BEFORE:') # import ubelt as ub @@ -666,9 +693,9 @@ def process_docstring_callback(self, app, what_: str, name: str, obj: Any, FIX_EXAMPLE_FORMATTING = 1 if FIX_EXAMPLE_FORMATTING: for idx, line in enumerate(lines): - if line == "Example:": - lines[idx] = "**Example:**" - lines.insert(idx + 1, "") + if line == 'Example:': + lines[idx] = '**Example:**' + lines.insert(idx + 1, '') REFORMAT_SECTIONS = 0 if REFORMAT_SECTIONS: @@ -710,7 +737,7 @@ def process_docstring_callback(self, app, what_: str, name: str, obj: Any, text = found['text'] new_lines = [] for para in text.split('\n\n'): - indent = para[:len(para) - len(para.lstrip())] + indent = para[: len(para) - len(para.lstrip())] new_paragraph = indent + paragraph(para) new_lines.append(new_paragraph) new_lines.append('') @@ -729,11 +756,13 @@ class SphinxDocstring: """ Helper to parse and modify sphinx docstrings """ + def __init__(docstr, lines): docstr.lines = lines # FORMAT THE RETURNS SECTION A BIT NICER import re + tag_pat = re.compile(r'^:(\w*):') directive_pat = re.compile(r'^.. (\w*)::\s*(\w*)') @@ -744,16 +773,22 @@ def __init__(docstr, lines): directive_match = directive_pat.search(line) if tag_match: tag = tag_match.groups()[0] - sphinx_parts.append({ - 'tag': tag, 'start_offset': idx, - 'type': 'tag', - }) + sphinx_parts.append( + { + 'tag': tag, + 'start_offset': idx, + 'type': 'tag', + } + ) elif directive_match: tag = directive_match.groups()[0] - sphinx_parts.append({ - 'tag': tag, 'start_offset': idx, - 'type': 'directive', - }) + sphinx_parts.append( + { + 'tag': tag, + 'start_offset': idx, + 'type': 'directive', + } + ) prev_offset = len(lines) for part in sphinx_parts[::-1]: @@ -793,6 +828,7 @@ def paragraph(text): str: the reduced text block """ import re + out = re.sub(r'\s\s*', ' ', text).strip() return out @@ -805,6 +841,7 @@ def create_doctest_figure(app, obj, name, lines): import xdoctest import sys import types + if isinstance(obj, types.ModuleType): module = obj else: @@ -818,14 +855,15 @@ def create_doctest_figure(app, obj, name, lines): # print(doctest.format_src()) import pathlib + # HACK: write to the srcdir doc_outdir = pathlib.Path(app.outdir) doc_srcdir = pathlib.Path(app.srcdir) doc_static_outdir = doc_outdir / '_static' doc_static_srcdir = doc_srcdir / '_static' - src_fig_dpath = (doc_static_srcdir / 'images') + src_fig_dpath = doc_static_srcdir / 'images' src_fig_dpath.mkdir(exist_ok=True, parents=True) - out_fig_dpath = (doc_static_outdir / 'images') + out_fig_dpath = doc_static_outdir / 'images' out_fig_dpath.mkdir(exist_ok=True, parents=True) # fig_dpath = (doc_outdir / 'autofigs' / name).mkdir(exist_ok=True) @@ -833,6 +871,7 @@ def create_doctest_figure(app, obj, name, lines): fig_num = 1 import kwplot + kwplot.autompl(force='agg') plt = kwplot.autoplt() @@ -843,7 +882,10 @@ def create_doctest_figure(app, obj, name, lines): # so we can get different figures. But we can hack it for now. import re - split_parts = re.split('({}\\s*\n)'.format(re.escape('.. rubric:: Example')), docstr) + + split_parts = re.split( + '({}\\s*\n)'.format(re.escape('.. rubric:: Example')), docstr + ) # split_parts = docstr.split('.. rubric:: Example') # import xdev @@ -853,7 +895,9 @@ def doctest_line_offsets(doctest): # Where the doctests starts and ends relative to the file start_line_offset = doctest.lineno - 1 last_part = doctest._parts[-1] - last_line_offset = start_line_offset + last_part.line_offset + last_part.n_lines - 1 + last_line_offset = ( + start_line_offset + last_part.line_offset + last_part.n_lines - 1 + ) offsets = { 'start': start_line_offset, 'end': last_line_offset, @@ -870,10 +914,14 @@ def doctest_line_offsets(doctest): for part in split_parts: num_lines = part.count('\n') - doctests = list(xdoctest.core.parse_docstr_examples( - part, modpath=modpath, callname=name, - # style='google' - )) + doctests = list( + xdoctest.core.parse_docstr_examples( + part, + modpath=modpath, + callname=name, + # style='google' + ) + ) # print(doctests) # doctests = list(xdoctest.core.parse_docstr_examples( @@ -894,6 +942,7 @@ def doctest_line_offsets(doctest): # Define dummy skipped exception if pytest is not available class Skipped(Exception): pass + try: doctest.mode = 'native' doctest.run(verbose=0, on_error='raise') @@ -913,19 +962,23 @@ class Skipped(Exception): fig_num += 1 # path_name = path_sanatize(name) path_name = (name).replace('.', '_') - fig_fpath = src_fig_dpath / f'fig_{path_name}_{fig_num:03d}.jpeg' + fig_fpath = ( + src_fig_dpath / f'fig_{path_name}_{fig_num:03d}.jpeg' + ) fig.savefig(fig_fpath) print(f'Wrote figure: {fig_fpath}') - to_insert_fpaths.append({ - 'insert_line_index': insert_line_index, - 'fpath': fig_fpath, - }) + to_insert_fpaths.append( + { + 'insert_line_index': insert_line_index, + 'fpath': fig_fpath, + } + ) for fig in figures: plt.close(fig) # kwplot.close_figures(figures) - curr_line_offset += (num_lines) + curr_line_offset += num_lines # if len(doctests) > 1: # doctests @@ -938,6 +991,7 @@ class Skipped(Exception): end_index = len(lines) # Reverse order for inserts import shutil + for info in to_insert_fpaths[::-1]: src_abs_fpath = info['fpath'] @@ -966,7 +1020,9 @@ class Skipped(Exception): insert_index = end_index else: raise KeyError(INSERT_AT) - lines.insert(insert_index, '.. image:: {}'.format('..' / rel_to_root_fpath)) + lines.insert( + insert_index, '.. image:: {}'.format('..' / rel_to_root_fpath) + ) # lines.insert(insert_index, '.. image:: {}'.format(rel_to_root_fpath)) # lines.insert(insert_index, '.. image:: {}'.format(rel_to_static_fpath)) lines.insert(insert_index, '') @@ -981,6 +1037,7 @@ def postprocess_hyperlinks(app, doctree, docname): # Your hyperlink postprocessing logic here from docutils import nodes import pathlib + for node in doctree.traverse(nodes.reference): if 'refuri' in node.attributes: refuri = node.attributes['refuri'] @@ -989,7 +1046,9 @@ def postprocess_hyperlinks(app, doctree, docname): fpath = pathlib.Path(node.document['source']) parent_dpath = fpath.parent if (parent_dpath / refuri).exists(): - node.attributes['refuri'] = refuri.replace('.rst', '.html') + node.attributes['refuri'] = refuri.replace( + '.rst', '.html' + ) else: raise AssertionError @@ -1003,17 +1062,22 @@ def fix_rst_todo_section(lines): def setup(app): import sphinx - app : sphinx.application.Sphinx = app + + app: sphinx.application.Sphinx = app app.add_domain(PatchedPythonDomain, override=True) - app.connect("doctree-resolved", postprocess_hyperlinks) + app.connect('doctree-resolved', postprocess_hyperlinks) docstring_processor = GoogleStyleDocstringProcessor() # https://stackoverflow.com/questions/26534184/can-sphinx-ignore-certain-tags-in-python-docstrings - app.connect('autodoc-process-docstring', docstring_processor.process_docstring_callback) + app.connect( + 'autodoc-process-docstring', + docstring_processor.process_docstring_callback, + ) def copy(src, dst): import shutil + print(f'Copy {src} -> {dst}') assert src.exists() if not dst.parent.exists(): @@ -1024,16 +1088,17 @@ def copy(src, dst): HACK_FOR_KWCOCO = 0 if HACK_FOR_KWCOCO: import pathlib + doc_outdir = pathlib.Path(app.outdir) / 'auto' doc_srcdir = pathlib.Path(app.srcdir) / 'auto' mod_dpath = doc_srcdir / '../../../kwcoco' - src_fpath = (mod_dpath / 'coco_schema.json') + src_fpath = mod_dpath / 'coco_schema.json' copy(src_fpath, doc_outdir / src_fpath.name) copy(src_fpath, doc_srcdir / src_fpath.name) - src_fpath = (mod_dpath / 'coco_schema_informal.rst') + src_fpath = mod_dpath / 'coco_schema_informal.rst' copy(src_fpath, doc_outdir / src_fpath.name) copy(src_fpath, doc_srcdir / src_fpath.name) return app diff --git a/kernprof.py b/kernprof.py index ade6db02..222f05f0 100755 --- a/kernprof.py +++ b/kernprof.py @@ -185,6 +185,7 @@ def main(): To restore the old behavior, pass the :option:`!--no-preimports` flag. """ # noqa: E501 + import atexit import builtins import functools @@ -221,9 +222,12 @@ def main(): import line_profiler from line_profiler.cli_utils import ( - add_argument, get_cli_config, + add_argument, + get_cli_config, get_python_executable as _python_command, # Compatibility - positive_float, short_string_path) + positive_float, + short_string_path, +) from line_profiler.profiler_mixin import ByCountProfilerMixin from line_profiler._logger import Logger from line_profiler import _diagnostics as diagnostics @@ -233,16 +237,19 @@ def main(): def execfile(filename, globals=None, locals=None): - """ Python 3.x doesn't have :py:func:`execfile` builtin """ + """Python 3.x doesn't have :py:func:`execfile` builtin""" with open(filename, 'rb') as f: exec(compile(f.read(), filename, 'exec'), globals, locals) + + # ===================================== class ContextualProfile(ByCountProfilerMixin, Profile): - """ A subclass of :py:class:`Profile` that adds a context manager + """A subclass of :py:class:`Profile` that adds a context manager for Python 2.5 with: statements and a decorator. """ + def __init__(self, *args, **kwds): super(ByCountProfilerMixin, self).__init__(*args, **kwds) self.enable_count = 0 @@ -251,14 +258,13 @@ def __call__(self, func): return self.wrap_callable(func) def enable_by_count(self, subcalls=True, builtins=True): - """ Enable the profiler if it hasn't been enabled before. - """ + """Enable the profiler if it hasn't been enabled before.""" if self.enable_count == 0: self.enable(subcalls=subcalls, builtins=builtins) self.enable_count += 1 def disable_by_count(self): - """ Disable the profiler if the number of disable requests matches the + """Disable the profiler if the number of disable requests matches the number of enable requests. """ if self.enable_count > 0: @@ -280,6 +286,7 @@ class RepeatedTimer: References: .. [SO474528] https://stackoverflow.com/questions/474528/execute-function-every-x-seconds/40965385#40965385 """ # noqa: E501 + def __init__(self, interval, dump_func, outfile): self._timer = None self.interval = interval @@ -297,8 +304,9 @@ def _run(self): def start(self): if not self.is_running: self.next_call += self.interval - self._timer = threading.Timer(self.next_call - time.time(), - self._run) + self._timer = threading.Timer( + self.next_call - time.time(), self._run + ) self._timer.start() self.is_running = True @@ -339,7 +347,7 @@ def resolve_module_path(mod_name): # type: (str) -> str | None def find_script(script_name, *, exit_on_error=True): - """ Find the script. + """Find the script. If the input is not a file, then :envvar:`PATH` will be searched. """ @@ -373,6 +381,7 @@ def _normalize_profiling_targets(targets): string. * Removing duplicates. """ + def find(path): try: path = find_script(path, exit_on_error=False) @@ -400,6 +409,7 @@ class _restore: Restore a collection like :py:data:`sys.path` after running code which potentially modifies it. """ + def __init__(self, obj, getter, setter): self.obj = obj self.setter = setter @@ -442,6 +452,7 @@ def sequence(cls, seq): >>> l [1, 2, 3] """ + def set_list(orig, copy): orig[:] = copy @@ -468,6 +479,7 @@ def mapping(cls, mpg): >>> d {1: 2} """ + def set_mapping(orig, copy): orig.clear() orig.update(copy) @@ -549,7 +561,7 @@ def pre_parse_single_arg_directive(args, flag, sep='--'): pass else: pre[:] = args[:i_sep] - post[:] = args[i_sep + 1:] + post[:] = args[i_sep + 1 :] pre_pre, arg, pre_post = pre_parse_single_arg_directive(pre, flag) if arg is None: assert not pre_post @@ -562,7 +574,7 @@ def pre_parse_single_arg_directive(args, flag, sep='--'): return args, None, [] if i_flag == len(args) - 1: # Last element raise ValueError(f'argument expected for the {flag} option') - args, thing, post_args = args[:i_flag], args[i_flag + 1], args[i_flag + 2:] + args, thing, post_args = args[:i_flag], args[i_flag + 1], args[i_flag + 2 :] return args, thing, post_args @@ -576,113 +588,189 @@ def _add_core_parser_arguments(parser): :py:class:`~argparse.ArgumentParser`. """ default = get_cli_config('kernprof') - add_argument(parser, '-V', '--version', - action='version', version=__version__) - add_argument(parser, '--config', - help='Path to the TOML file, from the ' - '`tool.line_profiler.kernprof` table of which to load ' - 'defaults for the options. ' - f'(Default: {short_string_path(default.path)!r})') - add_argument(parser, '--no-config', - action='store_const', dest='config', const=False, - help='Disable the loading of configuration files other ' - 'than the default one') + add_argument( + parser, '-V', '--version', action='version', version=__version__ + ) + add_argument( + parser, + '--config', + help='Path to the TOML file, from the ' + '`tool.line_profiler.kernprof` table of which to load ' + 'defaults for the options. ' + f'(Default: {short_string_path(default.path)!r})', + ) + add_argument( + parser, + '--no-config', + action='store_const', + dest='config', + const=False, + help='Disable the loading of configuration files other than the default one', + ) prof_opts = parser.add_argument_group('profiling options') - add_argument(prof_opts, '-l', '--line-by-line', action='store_true', - help='Use the line-by-line profiler instead of cProfile. ' - 'Implies `--builtin`. ' - f'(Default: {default.conf_dict["line_by_line"]})') - add_argument(prof_opts, '-b', '--builtin', action='store_true', - help="Put `profile` in the builtins. " - "Use `profile.enable()`/`.disable()` to " - "toggle profiling, " - "`@profile` to decorate functions, " - "or `with profile:` to profile a section of code. " - f"(Default: {default.conf_dict['builtin']})") + add_argument( + prof_opts, + '-l', + '--line-by-line', + action='store_true', + help='Use the line-by-line profiler instead of cProfile. ' + 'Implies `--builtin`. ' + f'(Default: {default.conf_dict["line_by_line"]})', + ) + add_argument( + prof_opts, + '-b', + '--builtin', + action='store_true', + help='Put `profile` in the builtins. ' + 'Use `profile.enable()`/`.disable()` to ' + 'toggle profiling, ' + '`@profile` to decorate functions, ' + 'or `with profile:` to profile a section of code. ' + f'(Default: {default.conf_dict["builtin"]})', + ) if default.conf_dict['setup']: def_setupfile = repr(default.conf_dict['setup']) else: def_setupfile = 'N/A' - add_argument(prof_opts, '-s', '--setup', - help='Path to the Python source file containing setup ' - 'code to execute before the code to profile. ' - f'(Default: {def_setupfile})') + add_argument( + prof_opts, + '-s', + '--setup', + help='Path to the Python source file containing setup ' + 'code to execute before the code to profile. ' + f'(Default: {def_setupfile})', + ) if default.conf_dict['prof_mod']: def_prof_mod = repr(default.conf_dict['prof_mod']) else: def_prof_mod = 'N/A' - add_argument(prof_opts, '-p', '--prof-mod', action='append', - help="List of modules, functions and/or classes to profile " - "specified by their name or path. These profiling targets " - "can be supplied both as comma-separated items, or " - "separately with multiple copies of this flag. Packages " - "are automatically recursed into unless they are specified " - "with `.__init__`. Adding the current script/module " - "profiles the entirety of it. Only works with line " - "profiling (`-l`/`--line-by-line`). " - f"(Default: {def_prof_mod}; " - "pass an empty string to clear the defaults (or any `-p` " - "target specified earlier))") - add_argument(prof_opts, '--preimports', action='store_true', - help="Eagerly import all profiling targets specified via " - "`-p` and profile them, instead of only profiling those " - "that are directly imported in the profiled code. " - "Only works with line profiling (`-l`/`--line-by-line`). " - f"(Default: {default.conf_dict['preimports']})") - add_argument(prof_opts, '--prof-imports', action='store_true', - help="If the script/module profiled is in `--prof-mod`, " - "autoprofile all its imports. " - "Only works with line profiling (`-l`/`--line-by-line`). " - f"(Default: {default.conf_dict['prof_imports']})") + add_argument( + prof_opts, + '-p', + '--prof-mod', + action='append', + help='List of modules, functions and/or classes to profile ' + 'specified by their name or path. These profiling targets ' + 'can be supplied both as comma-separated items, or ' + 'separately with multiple copies of this flag. Packages ' + 'are automatically recursed into unless they are specified ' + 'with `.__init__`. Adding the current script/module ' + 'profiles the entirety of it. Only works with line ' + 'profiling (`-l`/`--line-by-line`). ' + f'(Default: {def_prof_mod}; ' + 'pass an empty string to clear the defaults (or any `-p` ' + 'target specified earlier))', + ) + add_argument( + prof_opts, + '--preimports', + action='store_true', + help='Eagerly import all profiling targets specified via ' + '`-p` and profile them, instead of only profiling those ' + 'that are directly imported in the profiled code. ' + 'Only works with line profiling (`-l`/`--line-by-line`). ' + f'(Default: {default.conf_dict["preimports"]})', + ) + add_argument( + prof_opts, + '--prof-imports', + action='store_true', + help='If the script/module profiled is in `--prof-mod`, ' + 'autoprofile all its imports. ' + 'Only works with line profiling (`-l`/`--line-by-line`). ' + f'(Default: {default.conf_dict["prof_imports"]})', + ) out_opts = parser.add_argument_group('output options') if default.conf_dict['outfile']: def_outfile = repr(default.conf_dict['outfile']) else: def_outfile = ( "'.lprof' in line-profiling mode " - "(`-l`/`--line-by-line`); " - "'.prof' otherwise") - add_argument(out_opts, '-o', '--outfile', - help=f'Save stats to OUTFILE. (Default: {def_outfile})') - add_argument(out_opts, '-v', '--verbose', '--view', - action='count', default=default.conf_dict['verbose'], - help="Increase verbosity level " - f"(default: {default.conf_dict['verbose']}). " - "At level 1, view the profiling results in addition to " - "saving them; " - "at level 2, show other diagnostic info.") - add_argument(out_opts, '-q', '--quiet', - action='count', default=0, - help='Decrease verbosity level ' - f"(default: {default.conf_dict['verbose']}). " - 'At level -1, disable ' - 'helpful messages (e.g. "Wrote profile results to <...>"); ' - 'at level -2, silence the stdout; ' - 'at level -3, silence the stderr.') - add_argument(out_opts, '-r', '--rich', action='store_true', - help='Use rich formatting if viewing output. ' - f'(Default: {default.conf_dict["rich"]})') - add_argument(out_opts, '-u', '--unit', type=positive_float, - help='Output unit (in seconds) in which ' - 'the timing info is displayed. ' - f'(Default: {default.conf_dict["unit"]} s)') - add_argument(out_opts, '-z', '--skip-zero', action='store_true', - help="Hide functions which have not been called. " - f"(Default: {default.conf_dict['skip_zero']})") - add_argument(out_opts, '--summarize', action='store_true', - help='Print a summary of total function time. ' - f'(Default: {default.conf_dict["summarize"]})') + '(`-l`/`--line-by-line`); ' + "'.prof' otherwise" + ) + add_argument( + out_opts, + '-o', + '--outfile', + help=f'Save stats to OUTFILE. (Default: {def_outfile})', + ) + add_argument( + out_opts, + '-v', + '--verbose', + '--view', + action='count', + default=default.conf_dict['verbose'], + help='Increase verbosity level ' + f'(default: {default.conf_dict["verbose"]}). ' + 'At level 1, view the profiling results in addition to ' + 'saving them; ' + 'at level 2, show other diagnostic info.', + ) + add_argument( + out_opts, + '-q', + '--quiet', + action='count', + default=0, + help='Decrease verbosity level ' + f'(default: {default.conf_dict["verbose"]}). ' + 'At level -1, disable ' + 'helpful messages (e.g. "Wrote profile results to <...>"); ' + 'at level -2, silence the stdout; ' + 'at level -3, silence the stderr.', + ) + add_argument( + out_opts, + '-r', + '--rich', + action='store_true', + help='Use rich formatting if viewing output. ' + f'(Default: {default.conf_dict["rich"]})', + ) + add_argument( + out_opts, + '-u', + '--unit', + type=positive_float, + help='Output unit (in seconds) in which ' + 'the timing info is displayed. ' + f'(Default: {default.conf_dict["unit"]} s)', + ) + add_argument( + out_opts, + '-z', + '--skip-zero', + action='store_true', + help='Hide functions which have not been called. ' + f'(Default: {default.conf_dict["skip_zero"]})', + ) + add_argument( + out_opts, + '--summarize', + action='store_true', + help='Print a summary of total function time. ' + f'(Default: {default.conf_dict["summarize"]})', + ) if default.conf_dict['output_interval']: def_out_int = f'{default.conf_dict["output_interval"]} s' else: def_out_int = '0 s (disabled)' - add_argument(out_opts, '-i', '--output-interval', - type=int, const=1, nargs='?', - help="Enables outputting of cumulative profiling results " - "to OUTFILE every OUTPUT_INTERVAL seconds. " - "Uses the threading module. " - "Minimum value (and the value implied if the bare option " - f"is given) is 1 s. (Default: {def_out_int})") + add_argument( + out_opts, + '-i', + '--output-interval', + type=int, + const=1, + nargs='?', + help='Enables outputting of cumulative profiling results ' + 'to OUTFILE every OUTPUT_INTERVAL seconds. ' + 'Uses the threading module. ' + 'Minimum value (and the value implied if the bare option ' + f'is given) is 1 s. (Default: {def_out_int})', + ) def _build_parsers(args=None): @@ -709,7 +797,7 @@ def _build_parsers(args=None): module, literal_code = None, thing if module is literal_code is None: # Normal execution - real_parser, = parsers = [ArgumentParser(**parser_kwargs)] + (real_parser,) = parsers = [ArgumentParser(**parser_kwargs)] help_parser = None else: # We've already consumed the `-m `, so we need a dummy @@ -725,13 +813,15 @@ def _build_parsers(args=None): _add_core_parser_arguments(parser) if parser is help_parser or module is literal_code is None: - add_argument(parser, 'script', - metavar='{path/to/script' - ' | -m path.to.module | -c "literal code"}', - help='The python script file, module, or ' - 'literal code to run') - add_argument(parser, 'args', - nargs='...', help='Optional script arguments') + add_argument( + parser, + 'script', + metavar='{path/to/script | -m path.to.module | -c "literal code"}', + help='The python script file, module, or literal code to run', + ) + add_argument( + parser, 'args', nargs='...', help='Optional script arguments' + ) special_info = { 'module': module, 'literal_code': literal_code, @@ -742,7 +832,8 @@ def _build_parsers(args=None): def _parse_arguments( - real_parser, help_parser, special_info, args, exit_on_error): + real_parser, help_parser, special_info, args, exit_on_error +): module = special_info['module'] literal_code = special_info['literal_code'] @@ -800,8 +891,9 @@ def _parse_arguments( # Handle output options.verbose -= options.quiet - options.debug = (diagnostics.DEBUG - or options.verbose >= DIAGNOSITICS_VERBOSITY) + options.debug = ( + diagnostics.DEBUG or options.verbose >= DIAGNOSITICS_VERBOSITY + ) logger_kwargs = {'name': 'kernprof'} logger_kwargs['backend'] = 'auto' if options.debug: @@ -825,7 +917,8 @@ def _parse_arguments( diagnostics.log.debug('`rich` not installed, unsetting --rich') diagnostics.log.debug( - f'Loaded configs from {short_string_path(options.config)!r}') + f'Loaded configs from {short_string_path(options.config)!r}' + ) return options, tempfile_source_and_content @@ -846,16 +939,19 @@ def main(args=None, *, exit_on_error=True): module = special_info['module'] options, tempfile_source_and_content = _parse_arguments( - real_parser, help_parser, special_info, args, exit_on_error) + real_parser, help_parser, special_info, args, exit_on_error + ) if module is not None: diagnostics.log.debug(f'Profiling module: {module}') elif tempfile_source_and_content: diagnostics.log.debug( - f'Profiling script read from: {tempfile_source_and_content[0]}') + f'Profiling script read from: {tempfile_source_and_content[0]}' + ) else: diagnostics.log.debug( - f'Profiling script: {short_string_path(options.script)!r}') + f'Profiling script: {short_string_path(options.script)!r}' + ) with contextlib.ExitStack() as stack: enter = stack.enter_context @@ -872,7 +968,10 @@ def main(args=None, *, exit_on_error=True): cleanup = no_op else: cleanup = functools.partial( - _remove, tmpdir, recursive=True, missing_ok=True, + _remove, + tmpdir, + recursive=True, + missing_ok=True, ) if tempfile_source_and_content: try: @@ -924,7 +1023,8 @@ def _write_tempfile(source, content, options): with open(fname, mode='w') as fobj: print(content, file=fobj) diagnostics.log.debug( - f'Wrote temporary script file to {short_string_path(fname)!r}:') + f'Wrote temporary script file to {short_string_path(fname)!r}:' + ) options.script = fname # Add the tempfile to `--prof-mod` if options.prof_mod: @@ -935,12 +1035,12 @@ def _write_tempfile(source, content, options): # filename clash) if not options.outfile: extension = 'lprof' if options.line_by_line else 'prof' - options.outfile = _touch_tempfile(dir=os.curdir, - prefix=file_prefix + '-', - suffix='.' + extension) + options.outfile = _touch_tempfile( + dir=os.curdir, prefix=file_prefix + '-', suffix='.' + extension + ) diagnostics.log.debug( - 'Using default output destination ' - f'{short_string_path(options.outfile)!r}') + f'Using default output destination {short_string_path(options.outfile)!r}' + ) def _gather_preimport_targets(options, exclude): @@ -949,6 +1049,7 @@ def _gather_preimport_targets(options, exclude): """ from line_profiler.autoprofile.util_static import modpath_to_modname from line_profiler.autoprofile.eager_preimports import is_dotted_path + filtered_targets = [] recurse_targets = [] invalid_targets = [] @@ -977,11 +1078,14 @@ def _gather_preimport_targets(options, exclude): recurse_targets.append(modname) if invalid_targets: invalid_targets = sorted(set(invalid_targets)) - msg = ('{} profile-on-import target{} cannot be converted to ' - 'dotted-path form: {!r}' - .format(len(invalid_targets), - '' if len(invalid_targets) == 1 else 's', - invalid_targets)) + msg = ( + '{} profile-on-import target{} cannot be converted to ' + 'dotted-path form: {!r}'.format( + len(invalid_targets), + '' if len(invalid_targets) == 1 else 's', + invalid_targets, + ) + ) warnings.warn(msg) diagnostics.log.warning(msg) @@ -993,20 +1097,25 @@ def _write_preimports(prof, options, exclude): Called by :py:func:`main()` to handle eager pre-imports; not to be invoked on its own. """ - from line_profiler.autoprofile.eager_preimports import write_eager_import_module + from line_profiler.autoprofile.eager_preimports import ( + write_eager_import_module, + ) from line_profiler.autoprofile.autoprofile import ( - _extend_line_profiler_for_profiling_imports as upgrade_profiler) + _extend_line_profiler_for_profiling_imports as upgrade_profiler, + ) - filtered_targets, recurse_targets = _gather_preimport_targets(options, exclude) + filtered_targets, recurse_targets = _gather_preimport_targets( + options, exclude + ) if not (filtered_targets or recurse_targets): return # We could've done everything in-memory with `io.StringIO` and `exec()`, # but that results in indecipherable tracebacks should anything goes wrong; # so we write to a tempfile and `execfile()` it upgrade_profiler(prof) - temp_mod_path = _touch_tempfile(dir=options.tmpdir, - prefix='kernprof-eager-preimports-', - suffix='.py') + temp_mod_path = _touch_tempfile( + dir=options.tmpdir, prefix='kernprof-eager-preimports-', suffix='.py' + ) write_module_kwargs = { 'dotted_paths': filtered_targets, 'recurse': recurse_targets, @@ -1021,7 +1130,8 @@ def _write_preimports(prof, options, exclude): print(code, file=fobj) diagnostics.log.debug( 'Wrote temporary module for pre-imports to ' - f'{short_string_path(temp_mod_path)!r}') + f'{short_string_path(temp_mod_path)!r}' + ) else: with temp_file as fobj: write_eager_import_module(stream=fobj, **write_module_kwargs) @@ -1084,29 +1194,29 @@ def _dump_filtered_stats(tmpdir, prof, filename): def _format_call_message(func, *args, **kwargs): if isinstance(func, functools.partial): return _format_call_message( - func.func, *func.args, *args, **{**func.keywords, **kwargs}) + func.func, *func.args, *args, **{**func.keywords, **kwargs} + ) if isinstance(func, MethodType): obj = func.__self__ - func_repr = ( - '{0.__module__}.{0.__qualname__}(...).{1.__name__}' - .format(type(obj), func.__func__)) + func_repr = '{0.__module__}.{0.__qualname__}(...).{1.__name__}'.format( + type(obj), func.__func__ + ) else: try: func_repr = '{0.__module__}.{0.__qualname__}'.format(func) except Exception: # Fallback func_repr = repr(func) - args_repr = dedent(' ' + pformat(args)[len('['):-len(']')]) + args_repr = dedent(' ' + pformat(args)[len('[') : -len(']')]) lprefix = len('namespace(') kwargs_repr = dedent( - ' ' * lprefix - + pformat(SimpleNamespace(**kwargs))[lprefix:-len(')')]) + ' ' * lprefix + pformat(SimpleNamespace(**kwargs))[lprefix : -len(')')] + ) if args_repr and kwargs_repr: all_args_repr = f'{args_repr},\n{kwargs_repr}' else: all_args_repr = args_repr or kwargs_repr if all_args_repr: - call = '{}(\n{})'.format( - func_repr, indent(all_args_repr, ' ')) + call = '{}(\n{})'.format(func_repr, indent(all_args_repr, ' ')) else: call = func_repr + '()' return call @@ -1132,8 +1242,8 @@ def _pre_profile(options, module, exit_on_error): extension = 'lprof' if options.line_by_line else 'prof' options.outfile = f'{os.path.basename(options.script)}.{extension}' diagnostics.log.debug( - 'Using default output destination ' - f'{short_string_path(options.outfile)!r}') + f'Using default output destination {short_string_path(options.outfile)!r}' + ) sys.argv = [options.script] + options.args if module: @@ -1151,8 +1261,8 @@ def _pre_profile(options, module, exit_on_error): sys.path.insert(0, os.path.dirname(setup_file)) ns = {'__file__': setup_file, '__name__': '__main__'} diagnostics.log.debug( - f'Executing file {short_string_path(setup_file)!r} ' - 'as pre-profiling setup') + f'Executing file {short_string_path(setup_file)!r} as pre-profiling setup' + ) if not options.dryrun: execfile(setup_file, ns, ns) @@ -1160,8 +1270,10 @@ def _pre_profile(options, module, exit_on_error): prof = line_profiler.LineProfiler() options.builtin = True elif Profile.__module__ == 'profile': - raise RuntimeError('non-line-by-line profiling depends on cProfile, ' - 'which is not available on this platform') + raise RuntimeError( + 'non-line-by-line profiling depends on cProfile, ' + 'which is not available on this platform' + ) else: prof = ContextualProfile() @@ -1175,7 +1287,8 @@ def _pre_profile(options, module, exit_on_error): if module: script_file = find_module_script( - options.script, static=options.static, exit_on_error=exit_on_error) + options.script, static=options.static, exit_on_error=exit_on_error + ) else: script_file = find_script(options.script, exit_on_error=exit_on_error) # Make sure the script's directory is on sys.path instead of @@ -1203,8 +1316,9 @@ def _pre_profile(options, module, exit_on_error): options.global_profiler = global_profiler options.install_profiler = install_profiler if options.output_interval and not options.dryrun: - options.rt = RepeatedTimer(max(options.output_interval, 1), - prof.dump_stats, options.outfile) + options.rt = RepeatedTimer( + max(options.output_interval, 1), prof.dump_stats, options.outfile + ) else: options.rt = None options.original_stdout = sys.stdout @@ -1218,19 +1332,28 @@ def _main_profile(options, module=False, exit_on_error=True): """ script_file, prof = _pre_profile(options, module, exit_on_error) try: - rmod = functools.partial(run_module, - run_name='__main__', alter_sys=True) - ns = {'__file__': script_file, '__name__': '__main__', - 'execfile': execfile, 'rmod': rmod, - 'prof': prof} + rmod = functools.partial( + run_module, run_name='__main__', alter_sys=True + ) + ns = { + '__file__': script_file, + '__name__': '__main__', + 'execfile': execfile, + 'rmod': rmod, + 'prof': prof, + } if options.prof_mod and options.line_by_line: from line_profiler.autoprofile import autoprofile + _call_with_diagnostics( options, - autoprofile.run, script_file, ns, + autoprofile.run, + script_file, + ns, prof_mod=options.prof_mod, profile_imports=options.prof_imports, - as_module=module is not None) + as_module=module is not None, + ) else: if module: runner, target = 'rmod', options.script @@ -1241,8 +1364,12 @@ def _main_profile(options, module=False, exit_on_error=True): _call_with_diagnostics(options, ns[runner], target, ns) else: _call_with_diagnostics( - options, prof.runctx, - f'{runner}({target!r}, globals())', ns, ns) + options, + prof.runctx, + f'{runner}({target!r}, globals())', + ns, + ns, + ) finally: _post_profile(options, prof) @@ -1257,19 +1384,24 @@ def _post_profile(options, prof): _dump_filtered_stats(options.tmpdir, prof, options.outfile) short_outfile = short_string_path(options.outfile) diagnostics.log.info( - ('Profile results would have been written to ' - if options.dryrun else - 'Wrote profile results ') - + f'to {short_outfile!r}') + ( + 'Profile results would have been written to ' + if options.dryrun + else 'Wrote profile results ' + ) + + f'to {short_outfile!r}' + ) if options.verbose > 0 and not options.dryrun: kwargs = {} if not isinstance(prof, ContextualProfile): - kwargs.update(output_unit=options.unit, - stripzeros=options.skip_zero, - summarize=options.summarize, - rich=options.rich, - stream=options.original_stdout, - config=options.config) + kwargs.update( + output_unit=options.unit, + stripzeros=options.skip_zero, + summarize=options.summarize, + rich=options.rich, + stream=options.original_stdout, + config=options.config, + ) _call_with_diagnostics(options, prof.print_stats, **kwargs) else: py_exe = _python_command() @@ -1277,9 +1409,11 @@ def _post_profile(options, prof): show_mod = 'pstats' else: show_mod = 'line_profiler -rmt' - diagnostics.log.info('Inspect results with:\n' - f'{quote(py_exe)} -m {show_mod} ' - f'{quote(short_outfile)}') + diagnostics.log.info( + 'Inspect results with:\n' + f'{quote(py_exe)} -m {show_mod} ' + f'{quote(short_outfile)}' + ) # Fully disable the profiler for _ in range(prof.enable_count): prof.disable_by_count() diff --git a/line_profiler/__init__.py b/line_profiler/__init__.py index 4ba71204..6896cebf 100644 --- a/line_profiler/__init__.py +++ b/line_profiler/__init__.py @@ -251,14 +251,29 @@ def main(): # NOTE: This needs to be in sync with ../kernprof.py and line_profiler.py __version__ = '5.0.2' -from .line_profiler import (LineProfiler, LineStats, - load_ipython_extension, load_stats, main, - show_func, show_text,) +from .line_profiler import ( + LineProfiler, + LineStats, + load_ipython_extension, + load_stats, + main, + show_func, + show_text, +) from .explicit_profiler import profile -__all__ = ['LineProfiler', 'LineStats', 'line_profiler', - 'load_ipython_extension', 'load_stats', 'main', 'show_func', - 'show_text', '__version__', 'profile'] +__all__ = [ + 'LineProfiler', + 'LineStats', + 'line_profiler', + 'load_ipython_extension', + 'load_stats', + 'main', + 'show_func', + 'show_text', + '__version__', + 'profile', +] diff --git a/line_profiler/_diagnostics.py b/line_profiler/_diagnostics.py index a2639746..6ae2b1a8 100644 --- a/line_profiler/_diagnostics.py +++ b/line_profiler/_diagnostics.py @@ -2,6 +2,7 @@ Global state initialized at import time. Used for hidden arguments and developer features. """ + import os import sys from types import ModuleType @@ -9,10 +10,11 @@ def _boolean_environ( - envvar, - truey=frozenset({'1', 'on', 'true', 'yes'}), - falsy=frozenset({'0', 'off', 'false', 'no'}), - default=False): + envvar, + truey=frozenset({'1', 'on', 'true', 'yes'}), + falsy=frozenset({'0', 'off', 'false', 'no'}), + default=False, +): r""" Args: envvar (str) @@ -91,13 +93,14 @@ def _boolean_environ( WRAP_TRACE = _boolean_environ('LINE_PROFILER_WRAP_TRACE') SET_FRAME_LOCAL_TRACE = _boolean_environ('LINE_PROFILER_SET_FRAME_LOCAL_TRACE') _MUST_USE_LEGACY_TRACE = not isinstance( - getattr(sys, 'monitoring', None), ModuleType) -USE_LEGACY_TRACE = ( - _MUST_USE_LEGACY_TRACE - or _boolean_environ('LINE_PROFILER_CORE', - # Also provide `coverage-style` aliases - truey={'old', 'legacy', 'ctrace'}, - falsy={'new', 'sys.monitoring', 'sysmon'}, - default=_MUST_USE_LEGACY_TRACE)) + getattr(sys, 'monitoring', None), ModuleType +) +USE_LEGACY_TRACE = _MUST_USE_LEGACY_TRACE or _boolean_environ( + 'LINE_PROFILER_CORE', + # Also provide `coverage-style` aliases + truey={'old', 'legacy', 'ctrace'}, + falsy={'new', 'sys.monitoring', 'sysmon'}, + default=_MUST_USE_LEGACY_TRACE, +) log = _logger.Logger('line_profiler', backend='auto') diff --git a/line_profiler/_line_profiler.pyi b/line_profiler/_line_profiler.pyi index a51cfffd..5598ffbe 100644 --- a/line_profiler/_line_profiler.pyi +++ b/line_profiler/_line_profiler.pyi @@ -2,7 +2,6 @@ from __future__ import annotations from typing import Any, Mapping - class LineStats: timings: Mapping[tuple[str, int, str], list[tuple[int, int, int]]] unit: float @@ -13,7 +12,6 @@ class LineStats: unit: float, ) -> None: ... - class LineProfiler: def enable_by_count(self) -> None: ... def disable_by_count(self) -> None: ... @@ -21,5 +19,4 @@ class LineProfiler: def get_stats(self) -> LineStats: ... def dump_stats(self, filename: str) -> None: ... - def label(code: Any) -> Any: ... diff --git a/line_profiler/_logger.py b/line_profiler/_logger.py index 83fac0fb..c632e925 100644 --- a/line_profiler/_logger.py +++ b/line_profiler/_logger.py @@ -1,6 +1,7 @@ """ # Ported from kwutil """ + # import os import logging from abc import ABC, abstractmethod @@ -13,6 +14,7 @@ class _LogBackend(ABC): """ Abstract base class for our logger implementations. """ + backend: ClassVar[str] def __init__(self, name): @@ -58,9 +60,10 @@ class _PrintLogBackend(_LogBackend): Hello world >>> pl.debug('Should not appear') """ + backend = 'print' - def __init__(self, name="", level=logging.INFO): + def __init__(self, name='', level=logging.INFO): super().__init__(name) self.level = level @@ -121,6 +124,7 @@ class _StdlibLogBackend(_LogBackend): >>> print(text) >>> assert text.strip().endswith('Hello world') """ + backend = 'stdlib' def __init__(self, name): @@ -162,7 +166,7 @@ def configure( # Default settings for file and stream handlers fileinfo = { 'path': None, - 'format': '%(asctime)s : [file] %(levelname)s : %(message)s' + 'format': '%(asctime)s : [file] %(levelname)s : %(message)s', } streaminfo = { '__enable__': None, # will be determined below @@ -262,10 +266,15 @@ class Logger: >>> logger.debug('Debug %d', 123) >>> logger.info('Hello %d', 123) """ - def __init__(self, name="Logger", verbose=1, backend="auto", file=None, stream=True): + + def __init__( + self, name='Logger', verbose=1, backend='auto', file=None, stream=True + ): # Map verbose level to logging levels. If verbose > 1, show DEBUG, else INFO. self.name = name - self.configure(verbose=verbose, backend=backend, file=file, stream=stream) + self.configure( + verbose=verbose, backend=backend, file=file, stream=stream + ) def configure(self, backend='auto', verbose=1, file=None, stream=None): name = self.name @@ -273,20 +282,22 @@ def configure(self, backend='auto', verbose=1, file=None, stream=None): kwargs['level'] = { 0: logging.CRITICAL, 1: logging.INFO, - 2: logging.DEBUG}.get(verbose, logging.DEBUG) - if backend == "auto": + 2: logging.DEBUG, + }.get(verbose, logging.DEBUG) + if backend == 'auto': # Choose _StdlibLogBackend if a logger with handlers exists. if logging.getLogger(name).handlers: backend = 'stdlib' else: backend = 'print' try: - Backend = {'print': _PrintLogBackend, - 'stdlib': _StdlibLogBackend}[backend] + Backend = {'print': _PrintLogBackend, 'stdlib': _StdlibLogBackend}[ + backend + ] except KeyError: raise ValueError( - "Unsupported backend. " - "Use 'auto', 'print', or 'stdlib'.") from None + "Unsupported backend. Use 'auto', 'print', or 'stdlib'." + ) from None self._backend = Backend(name).configure(**kwargs) return self diff --git a/line_profiler/autoprofile/ast_profile_transformer.py b/line_profiler/autoprofile/ast_profile_transformer.py index 95827aeb..96b65c49 100644 --- a/line_profiler/autoprofile/ast_profile_transformer.py +++ b/line_profiler/autoprofile/ast_profile_transformer.py @@ -5,8 +5,10 @@ def ast_create_profile_node( - modname: str, profiler_name: str = 'profile', - attr: str = 'add_imported_function_or_module') -> ast.Expr: + modname: str, + profiler_name: str = 'profile', + attr: str = 'add_imported_function_or_module', +) -> ast.Expr: """Create an abstract syntax tree node that adds an object to the profiler to be profiled. An abstract syntax tree node is created which calls the attr method from profile and @@ -32,7 +34,11 @@ def ast_create_profile_node( (_ast.Expr): expr AST node that adds modname to profiler. """ - func = ast.Attribute(value=ast.Name(id=profiler_name, ctx=ast.Load()), attr=attr, ctx=ast.Load()) + func = ast.Attribute( + value=ast.Name(id=profiler_name, ctx=ast.Load()), + attr=attr, + ctx=ast.Load(), + ) names = modname.split('.') value: ast.expr = ast.Name(id=names[0], ctx=ast.Load()) for name in names[1:]: @@ -50,9 +56,12 @@ class AstProfileTransformer(ast.NodeTransformer): immediately after the import. """ - def __init__(self, profile_imports: bool = False, - profiled_imports: list[str] | None = None, - profiler_name: str = 'profile') -> None: + def __init__( + self, + profile_imports: bool = False, + profiled_imports: list[str] | None = None, + profiler_name: str = 'profile', + ) -> None: """Initializes the AST transformer with the profiler name. Args: @@ -67,11 +76,13 @@ def __init__(self, profile_imports: bool = False, to the profiler. """ self._profile_imports = bool(profile_imports) - self._profiled_imports = profiled_imports if profiled_imports is not None else [] + self._profiled_imports = ( + profiled_imports if profiled_imports is not None else [] + ) self._profiler_name = profiler_name def _visit_func_def( - self, node: ast.FunctionDef | ast.AsyncFunctionDef + self, node: ast.FunctionDef | ast.AsyncFunctionDef ) -> ast.FunctionDef | ast.AsyncFunctionDef: """Decorate functions/methods with profiler. @@ -93,16 +104,21 @@ def _visit_func_def( if isinstance(decor, ast.Name): decor_ids.add(decor.id) if self._profiler_name not in decor_ids: - node.decorator_list.append(ast.Name(id=self._profiler_name, ctx=ast.Load())) + node.decorator_list.append( + ast.Name(id=self._profiler_name, ctx=ast.Load()) + ) self.generic_visit(node) return node visit_FunctionDef = visit_AsyncFunctionDef = _visit_func_def def _visit_import( - self, node: ast.Import | ast.ImportFrom - ) -> (ast.Import | ast.ImportFrom - | list[ast.Import | ast.ImportFrom | ast.Expr]): + self, node: ast.Import | ast.ImportFrom + ) -> ( + ast.Import + | ast.ImportFrom + | list[ast.Import | ast.ImportFrom | ast.Expr] + ): """Add a node that profiles an import If profile_imports is True and the import is not in profiled_imports, @@ -123,7 +139,9 @@ def _visit_import( if not self._profile_imports: self.generic_visit(node) return node - this_visit = cast(Union[ast.Import, ast.ImportFrom], self.generic_visit(node)) + this_visit = cast( + Union[ast.Import, ast.ImportFrom], self.generic_visit(node) + ) visited: list[ast.Import | ast.ImportFrom | ast.Expr] = [this_visit] for names in node.names: node_name = names.name if names.asname is None else names.asname @@ -135,7 +153,7 @@ def _visit_import( return visited def visit_Import( - self, node: ast.Import + self, node: ast.Import ) -> ast.Import | list[ast.Import | ast.Expr]: """Add a node that profiles an object imported using the "import foo" sytanx @@ -150,11 +168,13 @@ def visit_Import( if profile_imports is True: returns list containing the import node and the profiling node """ - return cast(Union[ast.Import, List[Union[ast.Import, ast.Expr]]], - self._visit_import(node)) + return cast( + Union[ast.Import, List[Union[ast.Import, ast.Expr]]], + self._visit_import(node), + ) def visit_ImportFrom( - self, node: ast.ImportFrom + self, node: ast.ImportFrom ) -> ast.ImportFrom | list[ast.ImportFrom | ast.Expr]: """Add a node that profiles an object imported using the "from foo import bar" syntax @@ -169,5 +189,7 @@ def visit_ImportFrom( if profile_imports is True: returns list containing the import node and the profiling node """ - return cast(Union[ast.ImportFrom, List[Union[ast.ImportFrom, ast.Expr]]], - self._visit_import(node)) + return cast( + Union[ast.ImportFrom, List[Union[ast.ImportFrom, ast.Expr]]], + self._visit_import(node), + ) diff --git a/line_profiler/autoprofile/ast_tree_profiler.py b/line_profiler/autoprofile/ast_tree_profiler.py index 994074f6..9b69580a 100644 --- a/line_profiler/autoprofile/ast_tree_profiler.py +++ b/line_profiler/autoprofile/ast_tree_profiler.py @@ -4,8 +4,10 @@ import os from typing import Type -from .ast_profile_transformer import (AstProfileTransformer, - ast_create_profile_node) +from .ast_profile_transformer import ( + AstProfileTransformer, + ast_create_profile_node, +) from .profmod_extractor import ProfmodExtractor __docstubs__ = """ @@ -22,12 +24,14 @@ class AstTreeProfiler: classes & modules in prof_mod to the profiler to be profiled. """ - def __init__(self, - script_file: str, - prof_mod: list[str], - profile_imports: bool, - ast_transformer_class_handler: Type = AstProfileTransformer, - profmod_extractor_class_handler: Type = ProfmodExtractor) -> None: + def __init__( + self, + script_file: str, + prof_mod: list[str], + profile_imports: bool, + ast_transformer_class_handler: Type = AstProfileTransformer, + profmod_extractor_class_handler: Type = ProfmodExtractor, + ) -> None: """Initializes the AST tree profiler instance with the script file path Args: @@ -56,7 +60,8 @@ def __init__(self, @staticmethod def _check_profile_full_script( - script_file: str, prof_mod: list[str]) -> bool: + script_file: str, prof_mod: list[str] + ) -> bool: """Check whether whole script should be profiled. Checks whether path to script has been passed to prof_mod indicating that @@ -76,7 +81,9 @@ def _check_profile_full_script( if True, profile whole script. """ script_file_realpath = os.path.realpath(script_file) - profile_full_script = script_file_realpath in map(os.path.realpath, prof_mod) + profile_full_script = script_file_realpath in map( + os.path.realpath, prof_mod + ) return profile_full_script @staticmethod @@ -96,11 +103,13 @@ def _get_script_ast_tree(script_file: str) -> ast.Module: tree = ast.parse(script_text, filename=script_file) return tree - def _profile_ast_tree(self, - tree: ast.Module, - tree_imports_to_profile_dict: dict[int, str], - profile_full_script: bool = False, - profile_imports: bool = False) -> ast.Module: + def _profile_ast_tree( + self, + tree: ast.Module, + tree_imports_to_profile_dict: dict[int, str], + profile_full_script: bool = False, + profile_imports: bool = False, + ) -> ast.Module: """Add profiling to an abstract syntax tree. Adds nodes to the AST that adds the specified objects to the profiler. @@ -131,15 +140,19 @@ def _profile_ast_tree(self, abstract syntax tree with profiling. """ profiled_imports = [] - argsort_tree_indexes = sorted(list(tree_imports_to_profile_dict), reverse=True) + argsort_tree_indexes = sorted( + list(tree_imports_to_profile_dict), reverse=True + ) for tree_index in argsort_tree_indexes: name = tree_imports_to_profile_dict[tree_index] expr = ast_create_profile_node(name) tree.body.insert(tree_index + 1, expr) profiled_imports.append(name) if profile_full_script: - tree = self._ast_transformer_class_handler(profile_imports=profile_imports, - profiled_imports=profiled_imports).visit(tree) + tree = self._ast_transformer_class_handler( + profile_imports=profile_imports, + profiled_imports=profiled_imports, + ).visit(tree) ast.fix_missing_locations(tree) return tree @@ -158,14 +171,19 @@ def profile(self) -> ast.Module: (_ast.Module): tree abstract syntax tree with profiling. """ - profile_full_script = self._check_profile_full_script(self._script_file, self._prof_mod) + profile_full_script = self._check_profile_full_script( + self._script_file, self._prof_mod + ) tree = self._get_script_ast_tree(self._script_file) tree_imports_to_profile_dict = self._profmod_extractor_class_handler( - tree, self._script_file, self._prof_mod).run() - tree_profiled = self._profile_ast_tree(tree, - tree_imports_to_profile_dict, - profile_full_script=profile_full_script, - profile_imports=self._profile_imports) + tree, self._script_file, self._prof_mod + ).run() + tree_profiled = self._profile_ast_tree( + tree, + tree_imports_to_profile_dict, + profile_full_script=profile_full_script, + profile_imports=self._profile_imports, + ) return tree_profiled diff --git a/line_profiler/autoprofile/autoprofile.py b/line_profiler/autoprofile/autoprofile.py index 57b118bf..558cd385 100644 --- a/line_profiler/autoprofile/autoprofile.py +++ b/line_profiler/autoprofile/autoprofile.py @@ -44,6 +44,7 @@ def main(): python -m kernprof -p demo.py -l demo.py python -m line_profiler -rmt demo.py.lprof """ + from __future__ import annotations import contextlib import functools @@ -74,12 +75,18 @@ def _extend_line_profiler_for_profiling_imports(prof: Any) -> None: prof (LineProfiler): instance of LineProfiler. """ - prof.add_imported_function_or_module = types.MethodType(add_imported_function_or_module, prof) - - -def run(script_file: str, ns: MutableMapping[str, Any], - prof_mod: list[str], profile_imports: bool = False, - as_module: bool = False) -> None: + prof.add_imported_function_or_module = types.MethodType( + add_imported_function_or_module, prof + ) + + +def run( + script_file: str, + ns: MutableMapping[str, Any], + prof_mod: list[str], + profile_imports: bool = False, + as_module: bool = False, +) -> None: """Automatically profile a script and run it. Profile functions, classes & modules specified in prof_mod without needing to add @@ -103,6 +110,7 @@ def run(script_file: str, ns: MutableMapping[str, Any], as_module (bool): Whether we're running script_file as a module """ + class restore_dict: def __init__(self, d: MutableMapping[str, Any], target=None): self.d = d @@ -128,8 +136,9 @@ def __exit__(self, *_, **__): Profiler = AstTreeModuleProfiler module_name = modpath_to_modname(script_file) if not module_name: - raise ModuleNotFoundError(f'script_file = {script_file!r}: ' - 'cannot find corresponding module') + raise ModuleNotFoundError( + f'script_file = {script_file!r}: cannot find corresponding module' + ) module_obj = types.ModuleType(module_name) namespace = vars(module_obj) @@ -141,7 +150,8 @@ def __exit__(self, *_, **__): # Set the module object to `sys.modules` via a callback, and # then restore it via the context manager callback = functools.partial( - operator.setitem, sys.modules, '__main__', module_obj) + operator.setitem, sys.modules, '__main__', module_obj + ) ctx = restore_dict(sys.modules, callback) else: Profiler = AstTreeProfiler diff --git a/line_profiler/autoprofile/eager_preimports.py b/line_profiler/autoprofile/eager_preimports.py index d03f617b..c407e0c4 100644 --- a/line_profiler/autoprofile/eager_preimports.py +++ b/line_profiler/autoprofile/eager_preimports.py @@ -2,6 +2,7 @@ Tools for eagerly pre-importing everything as specified in ``line_profiler.autoprof.run(prof_mod=...)``. """ + from __future__ import annotations import ast @@ -15,11 +16,18 @@ from warnings import warn from typing import Any, Generator, NamedTuple, TextIO from .util_static import ( - modname_to_modpath, modpath_to_modname, package_modpaths) + modname_to_modpath, + modpath_to_modname, + package_modpaths, +) -__all__ = ('is_dotted_path', 'split_dotted_path', - 'resolve_profiling_targets', 'write_eager_import_module') +__all__ = ( + 'is_dotted_path', + 'split_dotted_path', + 'resolve_profiling_targets', + 'write_eager_import_module', +) def is_dotted_path(obj: Any) -> bool: @@ -58,7 +66,8 @@ def get_expression(obj: Any) -> ast.Expression | None: def split_dotted_path( - dotted_path: str, static: bool = True) -> tuple[str, str | None]: + dotted_path: str, static: bool = True +) -> tuple[str, str | None]: """ Arguments: dotted_path (str): @@ -114,9 +123,11 @@ def split_dotted_path( ['foo.bar.baz', 'foo.bar', 'foo'] """ if not is_dotted_path(dotted_path): - raise TypeError(f'dotted_path = {dotted_path!r}: ' - 'expected a dotted path ' - '(string of period-joined identifiers)') + raise TypeError( + f'dotted_path = {dotted_path!r}: ' + 'expected a dotted path ' + '(string of period-joined identifiers)' + ) chunks = dotted_path.split('.') checked_locs = [] check = modname_to_modpath if static else find_spec @@ -131,9 +142,11 @@ def split_dotted_path( checked_locs.append(module) continue return module, target - raise ModuleNotFoundError(f'dotted_path = {dotted_path!r}: ' - 'none of the below looks like an importable ' - f'module: {checked_locs!r}') + raise ModuleNotFoundError( + f'dotted_path = {dotted_path!r}: ' + 'none of the below looks like an importable ' + f'module: {checked_locs!r}' + ) def strip(s: str) -> str: @@ -166,6 +179,7 @@ class LoadedNameFinder(ast.NodeVisitor): >>> names = LoadedNameFinder.find(ast.parse(module)) >>> assert names == {'bar', 'foobar', 'a', 'str'}, names """ + def __init__(self) -> None: self.names: set[str] = set() self.contexts: list[set[str]] = [] @@ -178,13 +192,14 @@ def visit_Name(self, node: ast.Name) -> None: self.names.add(node.id) def _visit_func_def( - self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda + self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda ) -> None: args = node.args arg_names = { arg.arg for arg_list in (args.posonlyargs, args.args, args.kwonlyargs) - for arg in arg_list} + for arg in arg_list + } if args.vararg: arg_names.add(args.vararg.arg) if args.kwarg: @@ -227,22 +242,24 @@ def propose_names(prefixes: Collection[str]) -> Generator[str, None, None]: """ prefixes = list(dict.fromkeys(prefixes)) # Preserve order if not all(is_dotted_path(p) and '.' not in p for p in prefixes): - raise TypeError(f'prefixes = {prefixes!r}: ' - 'expected string identifiers') + raise TypeError(f'prefixes = {prefixes!r}: expected string identifiers') # Yield all the provided prefixes yield from prefixes # Yield the prefixes in order with numeric suffixes prefixes_and_patterns = [ (prefix, ('{}{}' if len(prefix) == 1 else '{}_{}').format) - for prefix in prefixes] + for prefix in prefixes + ] for i in itertools.count(): for prefix, pattern in prefixes_and_patterns: yield pattern(prefix, i) def resolve_profiling_targets( - dotted_paths: Collection[str], static: bool = True, - recurse: Collection[str] | bool = False) -> ResolvedResult: + dotted_paths: Collection[str], + static: bool = True, + recurse: Collection[str] | bool = False, +) -> ResolvedResult: """ Arguments: dotted_paths (Collection[str]): @@ -286,6 +303,7 @@ def resolve_profiling_targets( ``dotted_paths`` and ``recurse`` to be imported (and hence alter the state of :py:data:`sys.modules`. """ + def walk_packages_static(pkg): # Note: this probably can't handle namespace packages path = modname_to_modpath(pkg) @@ -335,11 +353,14 @@ def walk_packages_import_sys(pkg): def write_eager_import_module( - dotted_paths: Collection[str], stream: TextIO | None = None, *, - static: bool = True, - recurse: Collection[str] | bool = False, - adder: str = 'profile.add_imported_function_or_module', - indent: str = ' ') -> None: + dotted_paths: Collection[str], + stream: TextIO | None = None, + *, + static: bool = True, + recurse: Collection[str] | bool = False, + adder: str = 'profile.add_imported_function_or_module', + indent: str = ' ', +) -> None: r""" Write a module which autoprofiles all its imports. @@ -465,9 +486,11 @@ def write_eager_import_module( else: AdderError = ValueError if AdderError: - raise AdderError(f'adder = {adder!r}: ' - 'expected a single-line string parsable to a single ' - 'expression') + raise AdderError( + f'adder = {adder!r}: ' + 'expected a single-line string parsable to a single ' + 'expression' + ) if not isinstance(indent, str): IndentError = TypeError elif len(indent.splitlines()) == 1 and indent.isspace(): @@ -475,8 +498,9 @@ def write_eager_import_module( else: IndentError = ValueError if IndentError: - raise IndentError(f'indent = {indent!r}: ' - 'expected a single-line non-empty whitespace string') + raise IndentError( + f'indent = {indent!r}: expected a single-line non-empty whitespace string' + ) # Get the names loaded by `adder`; # these names are not allowed in the namespace @@ -487,28 +511,35 @@ def write_eager_import_module( # - One for a list of failed targets # - One for the imported module adder_name = next( - name for name in propose_names(['add', 'add_func', 'a', 'f']) - if name not in forbidden_names) + name + for name in propose_names(['add', 'add_func', 'a', 'f']) + if name not in forbidden_names + ) forbidden_names.add(adder_name) failures_name = next( name for name in propose_names(['failures', 'failed_targets', 'f', '_']) - if name not in forbidden_names) + if name not in forbidden_names + ) forbidden_names.add(failures_name) module_name = next( - name for name in propose_names(['module', 'mod', 'imported', 'm', '_']) - if name not in forbidden_names) + name + for name in propose_names(['module', 'mod', 'imported', 'm', '_']) + if name not in forbidden_names + ) # Figure out the import targets to profile resolved = resolve_profiling_targets( - dotted_paths, static=static, recurse=recurse) + dotted_paths, static=static, recurse=recurse + ) # Warn against failed imports if resolved.unresolved: msg = '{} import target{} cannot be resolved: {!r}'.format( len(resolved.unresolved), '' if len(resolved.unresolved) == 1 else 's', - resolved.unresolved) + resolved.unresolved, + ) warn(msg, stacklevel=2) # Do the imports and add them with `adder` @@ -537,14 +568,16 @@ def write_eager_import_module( on_error = f'{failures_name}.append({module!r})' else: on_error = 'pass' - write('\n' - + strip(f""" + write( + '\n' + + strip(f""" try: {indent}import {module} as {module_name} except {allowed_error}: {indent}{on_error} else: - """)) + """) + ) chunks = [] if profile_whole_module: chunks.append(f'{adder_name}({module_name})') @@ -552,18 +585,21 @@ def write_eager_import_module( targets_ = sorted(t for t in targets if t is not None) for target in sorted(targets_): path = f'{module}.{target}' - chunks.append(strip(f""" + chunks.append( + strip(f""" try: {indent}{adder_name}({module_name}.{target}) except AttributeError: {indent}{failures_name}.append({path!r}) - """)) + """) + ) for chunk in chunks: write(indent_(chunk, indent)) # Issue a warning if any of the targets doesn't exist if resolved.targets: write('\n') - write(strip(f""" + write( + strip(f""" if {failures_name}: {indent}import warnings @@ -572,7 +608,8 @@ def write_eager_import_module( {indent * 2}'' if len({failures_name}) == 1 else 's', {indent * 2}{failures_name}) {indent}warnings.warn(msg, stacklevel=2) - """)) + """) + ) class ResolvedResult(NamedTuple): diff --git a/line_profiler/autoprofile/line_profiler_utils.py b/line_profiler/autoprofile/line_profiler_utils.py index a84811ec..31d95c7e 100644 --- a/line_profiler/autoprofile/line_profiler_utils.py +++ b/line_profiler/autoprofile/line_profiler_utils.py @@ -12,27 +12,43 @@ @overload def add_imported_function_or_module( - self, item: CLevelCallable | Any, *, - scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, - wrap: bool = False) -> Literal[0]: - ... + self, + item: CLevelCallable | Any, + *, + scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, + wrap: bool = False, +) -> Literal[0]: ... @overload def add_imported_function_or_module( - self, - item: (FunctionType | CythonCallable | type | partial | property - | cached_property | MethodType | staticmethod | classmethod - | partialmethod | ModuleType), - *, scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, - wrap: bool = False) -> Literal[0, 1]: - ... + self, + item: ( + FunctionType + | CythonCallable + | type + | partial + | property + | cached_property + | MethodType + | staticmethod + | classmethod + | partialmethod + | ModuleType + ), + *, + scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, + wrap: bool = False, +) -> Literal[0, 1]: ... def add_imported_function_or_module( - self, item: object, *, - scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, - wrap: bool = False) -> Literal[0, 1]: + self, + item: object, + *, + scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, + wrap: bool = False, +) -> Literal[0, 1]: """ Method to add an object to :py:class:`~.line_profiler.LineProfiler` to be profiled. diff --git a/line_profiler/autoprofile/profmod_extractor.py b/line_profiler/autoprofile/profmod_extractor.py index f19319dc..bb03db06 100644 --- a/line_profiler/autoprofile/profmod_extractor.py +++ b/line_profiler/autoprofile/profmod_extractor.py @@ -4,8 +4,11 @@ import os import sys from typing import List, cast, Any, Union -from .util_static import (modname_to_modpath, modpath_to_modname, - package_modpaths) +from .util_static import ( + modname_to_modpath, + modpath_to_modname, + package_modpaths, +) class ProfmodExtractor: @@ -15,8 +18,9 @@ class ProfmodExtractor: abstract syntax tree. """ - def __init__(self, tree: ast.Module, script_file: str, - prof_mod: list[str]) -> None: + def __init__( + self, tree: ast.Module, script_file: str, prof_mod: list[str] + ) -> None: """Initializes the AST tree profiler instance with the AST, script file path and prof_mod Args: @@ -54,7 +58,8 @@ def _is_path(text: str) -> bool: @classmethod def _get_modnames_to_profile_from_prof_mod( - cls, script_file: str, prof_mod: list[str]) -> list[str]: + cls, script_file: str, prof_mod: list[str] + ) -> list[str]: """Grab the valid paths and all dotted paths in prof_mod and their subpackages and submodules, in the form of dotted paths. @@ -99,7 +104,9 @@ def _get_modnames_to_profile_from_prof_mod( if it fails, the item may point to an installed module rather than local script so we check if the item is path and whether that path exists, else skip the item. """ - modpath = modname_to_modpath(mod, sys_path=cast(List[Union[str, os.PathLike]], new_sys_path)) + modpath = modname_to_modpath( + mod, sys_path=cast(List[Union[str, os.PathLike]], new_sys_path) + ) if modpath is None: """if cannot convert to modpath, check if already path and if invalid""" if not os.path.exists(mod): @@ -132,7 +139,8 @@ def _get_modnames_to_profile_from_prof_mod( @staticmethod def _ast_get_imports_from_tree( - tree: ast.Module) -> list[dict[str, str | int | None]]: + tree: ast.Module, + ) -> list[dict[str, str | int | None]]: """Get all imports in an abstract syntax tree. Args: @@ -182,8 +190,8 @@ def _ast_get_imports_from_tree( @staticmethod def _find_modnames_in_tree_imports( - modnames_to_profile: list[str], - module_dict_list: list[dict[str, str | int | None]] + modnames_to_profile: list[str], + module_dict_list: list[dict[str, str | int | None]], ) -> dict[int, str]: """Map modnames to imports from an abstract sytax tree. @@ -219,7 +227,10 @@ def _find_modnames_in_tree_imports( if modname in modname_added_list: continue """check if either the parent module or submodule are in modnames_to_profile""" - if modname not in modnames_to_profile and modname.rsplit('.', 1)[0] not in modnames_to_profile: + if ( + modname not in modnames_to_profile + and modname.rsplit('.', 1)[0] not in modnames_to_profile + ): continue name = module_dict['alias'] or modname if not isinstance(name, str): @@ -245,9 +256,13 @@ def run(self) -> dict[int, str]: value (str): alias (or name if no alias used) of import """ - modnames_to_profile = self._get_modnames_to_profile_from_prof_mod(self._script_file, self._prof_mod) + modnames_to_profile = self._get_modnames_to_profile_from_prof_mod( + self._script_file, self._prof_mod + ) module_dict_list = self._ast_get_imports_from_tree(self._tree) - tree_imports_to_profile_dict = self._find_modnames_in_tree_imports(modnames_to_profile, module_dict_list) + tree_imports_to_profile_dict = self._find_modnames_in_tree_imports( + modnames_to_profile, module_dict_list + ) return tree_imports_to_profile_dict diff --git a/line_profiler/autoprofile/run_module.py b/line_profiler/autoprofile/run_module.py index 6e545126..9cde259c 100644 --- a/line_profiler/autoprofile/run_module.py +++ b/line_profiler/autoprofile/run_module.py @@ -56,6 +56,7 @@ def get_module_from_importfrom(node: ast.ImportFrom, module: str) -> str: class ImportFromTransformer(ast.NodeTransformer): """Turn all the relative imports into absolute imports.""" + def __init__(self, module: str) -> None: self.module = module @@ -78,13 +79,15 @@ class AstTreeModuleProfiler(AstTreeProfiler): and/or decorators to the AST that adds the specified functions/methods, classes & modules in prof_mod to the profiler to be profiled. """ + @classmethod def _get_script_ast_tree(cls, script_file: str) -> ast.Module: tree = super()._get_script_ast_tree(script_file) # Note: don't drop the `.__init__` or `.__main__` suffix, lest # the relative imports fail - module = modpath_to_modname(script_file, - hide_main=False, hide_init=False) + module = modpath_to_modname( + script_file, hide_main=False, hide_init=False + ) return ImportFromTransformer(module).visit(tree) @staticmethod @@ -93,7 +96,8 @@ def _is_main(fname: str) -> bool: @classmethod def _check_profile_full_script( - cls, script_file: str, prof_mod: list[str]) -> bool: + cls, script_file: str, prof_mod: list[str] + ) -> bool: rp = os.path.realpath paths_to_check = {rp(script_file)} if cls._is_main(script_file): diff --git a/line_profiler/autoprofile/util_static.py b/line_profiler/autoprofile/util_static.py index 4eaf224b..50495e7a 100644 --- a/line_profiler/autoprofile/util_static.py +++ b/line_profiler/autoprofile/util_static.py @@ -3,6 +3,7 @@ :py:mod:`xdoctest` via dev/maintain/port_utilities.py in the line_profiler repo. """ + from __future__ import annotations from os.path import abspath from os.path import dirname @@ -67,25 +68,25 @@ def package_modpaths( (yield pkgpath) else: if with_pkg: - root_path = join(pkgpath, "__init__.py") + root_path = join(pkgpath, '__init__.py') if (not check) or exists(root_path): (yield root_path) - valid_exts = [".py"] + valid_exts = ['.py'] if with_libs: valid_exts += _platform_pylib_exts() for dpath, dnames, fnames in os.walk(pkgpath, followlinks=followlinks): - ispkg = exists(join(dpath, "__init__.py")) + ispkg = exists(join(dpath, '__init__.py')) if ispkg or (not check): check = True if with_mod: for fname in fnames: if splitext(fname)[1] in valid_exts: - if fname != "__init__.py": + if fname != '__init__.py': path = join(dpath, fname) (yield path) if with_pkg: for dname in dnames: - path = join(dpath, dname, "__init__.py") + path = join(dpath, dname, '__init__.py') if exists(path): (yield path) else: @@ -109,13 +110,15 @@ def _parse_static_node_value(node): from collections import OrderedDict if IS_PY_GE_308: - if isinstance(node, ast.Constant) and isinstance(node.value, numbers.Number): + if isinstance(node, ast.Constant) and isinstance( + node.value, numbers.Number + ): return node.value if isinstance(node, ast.Constant) and isinstance(node.value, str): return node.value else: - num_type = getattr(ast, "Num", None) - str_type = getattr(ast, "Str", None) + num_type = getattr(ast, 'Num', None) + str_type = getattr(ast, 'Str', None) if (num_type is not None) and isinstance(node, num_type): return node.n if (str_type is not None) and isinstance(node, str_type): @@ -129,13 +132,13 @@ def _parse_static_node_value(node): values = map(_parse_static_node_value, node.values) return OrderedDict(zip(keys, values)) if IS_PY_LT_314: - nameconst_type = getattr(ast, "NameConstant", None) + nameconst_type = getattr(ast, 'NameConstant', None) if (nameconst_type is not None) and isinstance(node, nameconst_type): return node.value if isinstance(node, ast.Constant): return node.value raise TypeError( - "Cannot parse a static value from non-static node of type: {!r}".format( + 'Cannot parse a static value from non-static node of type: {!r}'.format( type(node) ) ) @@ -151,8 +154,8 @@ def _extension_module_tags(): import sysconfig tags = [] - tags.append(sysconfig.get_config_var("SOABI")) - tags.append("abi3") + tags.append(sysconfig.get_config_var('SOABI')) + tags.append('abi3') tags = [t for t in tags if t] return tags @@ -199,22 +202,21 @@ def _static_parse(varname, fpath): import ast if not exists(fpath): - raise ValueError("fpath={!r} does not exist".format(fpath)) - with open(fpath, "r") as file_: + raise ValueError('fpath={!r} does not exist'.format(fpath)) + with open(fpath, 'r') as file_: sourcecode = file_.read() pt = ast.parse(sourcecode) class StaticVisitor(ast.NodeVisitor): - def visit_Assign(self, node): for target in node.targets: - target_id = getattr(target, "id", None) + target_id = getattr(target, 'id', None) if target_id == varname: self.static_value = _parse_static_node_value(node.value) def visit_AnnAssign(self, node): target = node.target - target_id = getattr(target, "id", None) + target_id = getattr(target, 'id', None) if target_id == varname: self.static_value = _parse_static_node_value(node.value) @@ -223,7 +225,7 @@ def visit_AnnAssign(self, node): try: value = visitor.static_value except AttributeError: - value = "Unknown {}".format(varname) + value = 'Unknown {}'.format(varname) raise AttributeError(value) return value @@ -241,14 +243,16 @@ def _platform_pylib_exts(): import sysconfig valid_exts = [] - base_ext = "." + sysconfig.get_config_var("EXT_SUFFIX").split(".")[(-1)] + base_ext = '.' + sysconfig.get_config_var('EXT_SUFFIX').split('.')[(-1)] for tag in _extension_module_tags(): - valid_exts.append((("." + tag) + base_ext)) + valid_exts.append((('.' + tag) + base_ext)) valid_exts.append(base_ext) return tuple(valid_exts) -def _syspath_modname_to_modpath(modname, sys_path=None, exclude=None) -> str | None: +def _syspath_modname_to_modpath( + modname, sys_path=None, exclude=None +) -> str | None: """ syspath version of modname_to_modpath @@ -314,20 +318,20 @@ def _syspath_modname_to_modpath(modname, sys_path=None, exclude=None) -> str | N def _isvalid(modpath, base): subdir = dirname(modpath) while subdir and (subdir != base): - if not exists(join(subdir, "__init__.py")): + if not exists(join(subdir, '__init__.py')): return False subdir = dirname(subdir) return True - _fname_we = modname.replace(".", os.path.sep) - candidate_fnames = [_fname_we + ".py"] + _fname_we = modname.replace('.', os.path.sep) + candidate_fnames = [_fname_we + '.py'] candidate_fnames += [(_fname_we + ext) for ext in _platform_pylib_exts()] if sys_path is None: sys_path = sys.path - candidate_dpaths = [("." if (p == "") else p) for p in sys_path] + candidate_dpaths = [('.' if (p == '') else p) for p in sys_path] def normalize(p): - if sys.platform.startswith("win32"): + if sys.platform.startswith('win32'): return realpath(p).lower() else: return realpath(p) @@ -342,7 +346,7 @@ def normalize(p): def check_dpath(dpath): modpath = join(dpath, _fname_we) if exists(modpath): - if isfile(join(modpath, "__init__.py")): + if isfile(join(modpath, '__init__.py')): if _isvalid(modpath, dpath): return modpath for fname in candidate_fnames: @@ -352,11 +356,11 @@ def check_dpath(dpath): return modpath _pkg_name = _fname_we.split(os.path.sep)[0] - _pkg_name_hypen = _pkg_name.replace("_", "-") - _egglink_fname1 = _pkg_name + ".egg-link" - _egglink_fname2 = _pkg_name_hypen + ".egg-link" - _editable_fname_pth_pat = ("__editable__." + _pkg_name) + "-*.pth" - _editable_fname_finder_py_pat = "__editable___*_*finder.py" + _pkg_name_hypen = _pkg_name.replace('_', '-') + _egglink_fname1 = _pkg_name + '.egg-link' + _egglink_fname2 = _pkg_name_hypen + '.egg-link' + _editable_fname_pth_pat = ('__editable__.' + _pkg_name) + '-*.pth' + _editable_fname_finder_py_pat = '__editable___*_*finder.py' found_modpath = None for dpath in candidate_dpaths: modpath = check_dpath(dpath) @@ -369,7 +373,7 @@ def check_dpath(dpath): if new_editable_finder_paths: for finder_fpath in new_editable_finder_paths: try: - mapping = _static_parse("MAPPING", finder_fpath) + mapping = _static_parse('MAPPING', finder_fpath) except AttributeError: ... else: @@ -378,20 +382,24 @@ def check_dpath(dpath): except KeyError: ... else: - if (not exclude) or (normalize(target) not in real_exclude): + if (not exclude) or ( + normalize(target) not in real_exclude + ): modpath = check_dpath(target) if modpath: found_modpath = modpath break if found_modpath is not None: break - new_editable_pth_paths = sorted(glob.glob(join(dpath, _editable_fname_pth_pat))) + new_editable_pth_paths = sorted( + glob.glob(join(dpath, _editable_fname_pth_pat)) + ) if new_editable_pth_paths: import pathlib for editable_pth in new_editable_pth_paths: editable_pth_ = pathlib.Path(editable_pth) - target = editable_pth_.read_text().strip().split("\n")[(-1)] + target = editable_pth_.read_text().strip().split('\n')[(-1)] if (not exclude) or (normalize(target) not in real_exclude): modpath = check_dpath(target) if modpath: @@ -407,7 +415,7 @@ def check_dpath(dpath): elif isfile(linkpath2): linkpath = linkpath2 if linkpath is not None: - with open(linkpath, "r") as file: + with open(linkpath, 'r') as file: target = file.readline().strip() if (not exclude) or (normalize(target) not in real_exclude): modpath = check_dpath(target) @@ -470,13 +478,17 @@ def modname_to_modpath( modpath = _syspath_modname_to_modpath(modname, sys_path) if modpath is None: return None - modpath_ = normalize_modpath(modpath, hide_init=hide_init, hide_main=hide_main) + modpath_ = normalize_modpath( + modpath, hide_init=hide_init, hide_main=hide_main + ) if typing.TYPE_CHECKING: modpath_ = typing.cast(str, modpath_) return modpath_ -def split_modpath(modpath: str | os.PathLike, check: bool = True) -> tuple[(str, str)]: +def split_modpath( + modpath: str | os.PathLike, check: bool = True +) -> tuple[(str, str)]: """ Splits the modpath into the dir that must be in PYTHONPATH for the module to be imported and the modulepath relative to this directory. @@ -505,14 +517,14 @@ def split_modpath(modpath: str | os.PathLike, check: bool = True) -> tuple[(str, if check: if not exists(modpath_): if not exists(modpath): - raise ValueError("modpath={} does not exist".format(modpath)) - raise ValueError("modpath={} is not a module".format(modpath)) - if isdir(modpath_) and (not exists(join(modpath, "__init__.py"))): - raise ValueError("modpath={} is not a module".format(modpath)) + raise ValueError('modpath={} does not exist'.format(modpath)) + raise ValueError('modpath={} is not a module'.format(modpath)) + if isdir(modpath_) and (not exists(join(modpath, '__init__.py'))): + raise ValueError('modpath={} is not a module'.format(modpath)) (full_dpath, fname_ext) = split(modpath_) _relmod_parts = [fname_ext] dpath = full_dpath - while exists(join(dpath, "__init__.py")): + while exists(join(dpath, '__init__.py')): (dpath, dname) = split(dpath) _relmod_parts.append(dname) relmod_parts = _relmod_parts[::(-1)] @@ -559,16 +571,16 @@ def normalize_modpath( >>> assert not res3.endswith('.py') """ if hide_init: - if basename(modpath) == "__init__.py": + if basename(modpath) == '__init__.py': modpath = dirname(modpath) hide_main = True else: - modpath_with_init = join(modpath, "__init__.py") + modpath_with_init = join(modpath, '__init__.py') if exists(modpath_with_init): modpath = modpath_with_init if hide_main: - if basename(modpath) == "__main__.py": - parallel_init = join(dirname(modpath), "__init__.py") + if basename(modpath) == '__main__.py': + parallel_init = join(dirname(modpath), '__init__.py') if exists(parallel_init): modpath = dirname(modpath) return modpath @@ -643,17 +655,19 @@ def modpath_to_modname( """ if check and (relativeto is None): if not exists(modpath): - raise ValueError("modpath={} does not exist".format(modpath)) + raise ValueError('modpath={} does not exist'.format(modpath)) modpath__ = abspath(expanduser(modpath)) - modpath_ = normalize_modpath(modpath__, hide_init=hide_init, hide_main=hide_main) + modpath_ = normalize_modpath( + modpath__, hide_init=hide_init, hide_main=hide_main + ) if relativeto: dpath = dirname(abspath(expanduser(relativeto))) rel_modpath = relpath(modpath_, dpath) else: (dpath, rel_modpath) = split_modpath(modpath_, check=check) modname = splitext(rel_modpath)[0] - if "." in modname: - (modname, abi_tag) = modname.split(".", 1) - modname = modname.replace("/", ".") - modname = modname.replace("\\", ".") + if '.' in modname: + (modname, abi_tag) = modname.split('.', 1) + modname = modname.replace('/', '.') + modname = modname.replace('\\', '.') return modname diff --git a/line_profiler/cli_utils.py b/line_profiler/cli_utils.py index 0238c602..bc9b6fa1 100644 --- a/line_profiler/cli_utils.py +++ b/line_profiler/cli_utils.py @@ -2,6 +2,7 @@ Shared utilities between the :command:`python -m line_profiler` and :command:`kernprof` CLI tools. """ + from __future__ import annotations import argparse @@ -15,15 +16,20 @@ from .toml_config import ConfigSource -_BOOLEAN_VALUES = {**{k.casefold(): False - for k in ('', '0', 'off', 'False', 'F', 'no', 'N')}, - **{k.casefold(): True - for k in ('1', 'on', 'True', 'T', 'yes', 'Y')}} +_BOOLEAN_VALUES = { + **{k.casefold(): False for k in ('', '0', 'off', 'False', 'F', 'no', 'N')}, + **{k.casefold(): True for k in ('1', 'on', 'True', 'T', 'yes', 'Y')}, +} -def add_argument(parser_like, arg: str, /, *args: str, - hide_complementary_options: bool = True, - **kwargs: object) -> argparse.Action: +def add_argument( + parser_like, + arg: str, + /, + *args: str, + hide_complementary_options: bool = True, + **kwargs: object, +) -> argparse.Action: """ Override the ``'store_true'`` and ``'store_false'`` actions so that they are turned into options which: @@ -66,6 +72,7 @@ def add_argument(parser_like, arg: str, /, *args: str, action's help text is updated to mention the corresponding short flag(s). """ + def negate_result(func): @functools.wraps(func) def negated(*args, **kwargs): @@ -95,9 +102,9 @@ def negated(*args, **kwargs): kwargs['const'] = const = kwargs.pop('action') == 'store_true' for key, value in dict( - default=None, - metavar='Y[es] | N[o] | T[rue] | F[alse] ' - '| on | off | 1 | 0').items(): + default=None, + metavar='Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0', + ).items(): kwargs.setdefault(key, value) long_kwargs = kwargs.copy() short_kwargs = {**kwargs, 'action': 'store_const'} @@ -108,12 +115,13 @@ def negated(*args, **kwargs): # Mention the short options in the long options' documentation, and # suppress the short options in the help if ( - long_flags - and short_flags - and long_kwargs.get('help') != argparse.SUPPRESS): + long_flags + and short_flags + and long_kwargs.get('help') != argparse.SUPPRESS + ): additional_msg = 'Short {}: {}'.format( - 'form' if len(short_flags) == 1 else 'forms', - ', '.join(short_flags)) + 'form' if len(short_flags) == 1 else 'forms', ', '.join(short_flags) + ) if long_kwargs.get('help'): raw_help = long_kwargs['help'] help_text = raw_help if isinstance(raw_help, str) else str(raw_help) @@ -124,7 +132,8 @@ def negated(*args, **kwargs): help_text[:-1], additional_msg[0].lower(), additional_msg[1:], - help_text[-1]) + help_text[-1], + ) else: help_text = f'{help_text} ({additional_msg})' long_kwargs['help'] = help_text @@ -157,18 +166,24 @@ def negated(*args, **kwargs): falsy_help_text = 'Negate these flags: ' + ', '.join(args) parser_like.add_argument( *(flag[:2] + 'no-' + flag[2:] for flag in long_flags), - **{**long_kwargs, - 'const': False, - 'dest': action.dest, - 'type': negate_result(action.type), - 'help': falsy_help_text}) + **{ + **long_kwargs, + 'const': False, + 'dest': action.dest, + 'type': negate_result(action.type), + 'help': falsy_help_text, + }, + ) return action def get_cli_config( - subtable: str, /, - config: str | PathLike[str] | bool | None = None, - *, read_env: bool = True) -> ConfigSource: + subtable: str, + /, + config: str | PathLike[str] | bool | None = None, + *, + read_env: bool = True, +) -> ConfigSource: """ Get the ``tool.line_profiler.`` configs and normalize its keys (``some-key`` -> ``some_key``). @@ -186,10 +201,12 @@ def get_cli_config( instance """ config_source = ConfigSource.from_config( - config, read_env=read_env).get_subconfig(subtable) + config, read_env=read_env + ).get_subconfig(subtable) config_source.conf_dict = { key.replace('-', '_'): value - for key, value in config_source.conf_dict.items()} + for key, value in config_source.conf_dict.items() + } return config_source @@ -228,8 +245,9 @@ def positive_float(value: str) -> float: return val -def boolean(value: str, *, fallback: bool | None = None, - invert: bool = False) -> bool: +def boolean( + value: str, *, fallback: bool | None = None, invert: bool = False +) -> bool: """ Arguments: value (str) @@ -284,9 +302,11 @@ def boolean(value: str, *, fallback: bool | None = None, else: return (not result) if invert else result if fallback is None: - raise ValueError(f'value = {value!r}: ' - 'cannot be parsed into a boolean; valid values are' - f'({{string: bool}}): {_BOOLEAN_VALUES!r}') + raise ValueError( + f'value = {value!r}: ' + 'cannot be parsed into a boolean; valid values are' + f'({{string: bool}}): {_BOOLEAN_VALUES!r}' + ) return fallback diff --git a/line_profiler/explicit_profiler.py b/line_profiler/explicit_profiler.py index 0d88ca42..0f8afebe 100644 --- a/line_profiler/explicit_profiler.py +++ b/line_profiler/explicit_profiler.py @@ -163,6 +163,7 @@ def func4(): The core functionality in this module was ported from :mod:`xdev`. """ + from __future__ import annotations import atexit import multiprocessing @@ -326,8 +327,9 @@ def _implicit_setup(self) -> None: """ environ_flags = self.setup_config['environ_flags'] cli_flags = self.setup_config['cli_flags'] - is_profiling = any(boolean(os.environ.get(f, ''), fallback=True) - for f in environ_flags) + is_profiling = any( + boolean(os.environ.get(f, ''), fallback=True) for f in environ_flags + ) is_profiling |= any(f in sys.argv for f in cli_flags) if is_profiling: self.enable() @@ -344,22 +346,22 @@ def enable(self, output_prefix: str | None = None) -> None: must not claim ownership or register an atexit hook, otherwise they can clobber output from the real script process. """ - self._debug("enable:ENTER") + self._debug('enable:ENTER') if is_mp_bootstrap(): - self._debug("enable:skip-mp-bootstrap") + self._debug('enable:skip-mp-bootstrap') self.enabled = False return if self._should_skip_due_to_owner(): - self._debug("enable:skip-due-to-owner") + self._debug('enable:skip-due-to-owner') self.enabled = False return owner_pid = os.getpid() os.environ[_OWNER_PID_ENVVAR] = str(owner_pid) self._owner_pid = owner_pid - self._debug("enable:owner-claimed", owner_pid=owner_pid) + self._debug('enable:owner-claimed', owner_pid=owner_pid) if self._profile is None: atexit.register(self.show) @@ -378,20 +380,22 @@ def _should_skip_due_to_owner(self) -> bool: """ owner = os.environ.get(_OWNER_PID_ENVVAR) if not owner: - self._debug("owner:no-owner-env") + self._debug('owner:no-owner-env') return False current = str(os.getpid()) if owner == current: - self._debug("owner:is-us", owner=owner) + self._debug('owner:is-us', owner=owner) return False if is_mp_bootstrap(): - self._debug("owner:skip-mp-bootstrap", owner=owner, current=current) + self._debug('owner:skip-mp-bootstrap', owner=owner, current=current) return True # Standalone run: allow this interpreter to become the owner. - self._debug("owner:allow-standalone-reset", owner=owner, current=current) + self._debug( + 'owner:allow-standalone-reset', owner=owner, current=current + ) return False def _debug(self, message: str, **extra: Any) -> None: @@ -480,7 +484,11 @@ def show(self) -> None: if write_text or write_timestamped_text: stream = io.StringIO() # Text output always contains details, and cannot be rich. - text_kwargs: dict[str, Any] = {**kwargs, 'rich': False, 'details': True} + text_kwargs: dict[str, Any] = { + **kwargs, + 'rich': False, + 'details': True, + } self._profile.print_stats(stream=stream, **text_kwargs) raw_text = stream.getvalue() @@ -491,10 +499,12 @@ def show(self) -> None: if write_timestamped_text: from datetime import datetime as datetime_cls + now = datetime_cls.now() timestamp = now.strftime('%Y-%m-%dT%H%M%S') txt_output_fpath2 = pathlib.Path( - f'{self.output_prefix}_{timestamp}.txt') + f'{self.output_prefix}_{timestamp}.txt' + ) txt_output_fpath2.write_text(raw_text, encoding='utf-8') print('Wrote profile results to %s' % txt_output_fpath2) @@ -504,8 +514,7 @@ def show(self) -> None: print('Wrote profile results to %s' % lprof_output_fpath) print('To view details run:') py_exe = _python_command() - print(py_exe + ' -m line_profiler -rtmz ' - + str(lprof_output_fpath)) + print(py_exe + ' -m line_profiler -rtmz ' + str(lprof_output_fpath)) def is_mp_bootstrap() -> bool: @@ -541,21 +550,22 @@ def is_mp_bootstrap() -> bool: """ try: import multiprocessing.spawn as mp_spawn - if getattr(mp_spawn, "_inheriting", False): + + if getattr(mp_spawn, '_inheriting', False): return True except Exception: pass - orig = getattr(sys, "orig_argv", None) or [] - if any(a.startswith("--multiprocessing") for a in orig): + orig = getattr(sys, 'orig_argv', None) or [] + if any(a.startswith('--multiprocessing') for a in orig): return True - if any("multiprocessing.forkserver" in a for a in orig): + if any('multiprocessing.forkserver' in a for a in orig): return True - if any("multiprocessing.spawn" in a for a in orig): + if any('multiprocessing.spawn' in a for a in orig): return True try: - if multiprocessing.current_process().name != "MainProcess": + if multiprocessing.current_process().name != 'MainProcess': return True except Exception: pass diff --git a/line_profiler/ipython_extension.py b/line_profiler/ipython_extension.py index 4e99213f..4b5b307d 100644 --- a/line_profiler/ipython_extension.py +++ b/line_profiler/ipython_extension.py @@ -32,6 +32,7 @@ .. |lprun_all| replace:: :py:data:`%%lprun_all ` .. |builtins| replace:: :py:mod:`__builtins__ ` """ + from __future__ import annotations import ast @@ -46,9 +47,15 @@ from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Union + if TYPE_CHECKING: # pragma: no cover - from typing import (Callable, ParamSpec, # noqa: F401 - Any, ClassVar, TypeVar) + from typing import ( + Callable, + ParamSpec, # noqa: F401 + Any, + ClassVar, + TypeVar, + ) PS = ParamSpec('PS') PD = TypeVar('PD', bound='_PatchDict') @@ -73,7 +80,7 @@ @dataclass class _ParseParamResult: - """ Class for holding parsed info relevant to the behaviors of both + """Class for holding parsed info relevant to the behaviors of both the ``%lprun`` and ``%%lprun_all`` magics. Attributes: @@ -102,11 +109,12 @@ class _ParseParamResult: :py:class:`line_profiler.LineProfiler` instance is to be returned. """ + opts: Struct arg_str: str def __getattr__(self, attr): # type: (str) -> Any - """ Defers to :py:attr:`_ParseParamResult.opts`.""" + """Defers to :py:attr:`_ParseParamResult.opts`.""" return getattr(self.opts, attr) @functools.cached_property @@ -130,22 +138,23 @@ def output_unit(self): # type: () -> float | None try: return float(self.opts.u[0]) except Exception: - raise TypeError("Timer unit setting must be a float.") + raise TypeError('Timer unit setting must be a float.') @functools.cached_property def strip_zero(self): # type: () -> bool - return "z" in self.opts + return 'z' in self.opts @functools.cached_property def return_profiler(self): # type: () -> bool - return "r" in self.opts + return 'r' in self.opts @dataclass class _RunAndProfileResult: - """ Class for holding the results of both the ``%lprun`` and + """Class for holding the results of both the ``%lprun`` and ``%%lprun_all`` magics. """ + stats: LineStats parse_result: _ParseParamResult message: Union[str, None] = None @@ -172,22 +181,26 @@ def _make_show_func_wrapper(self, show_func): @functools.wraps(show_func) def show_func_wrapper( - filename, start_lineno, func_name, *args, **kwargs): - call = functools.partial(show_func, - filename, start_lineno, func_name, - *args, **kwargs) - show_entire_module = (start_lineno == 1 - and func_name == _LPRUN_ALL_CODE_OBJ_NAME - and tmp is not None - and tmp.samefile(filename)) + filename, start_lineno, func_name, *args, **kwargs + ): + call = functools.partial( + show_func, filename, start_lineno, func_name, *args, **kwargs + ) + show_entire_module = ( + start_lineno == 1 + and func_name == _LPRUN_ALL_CODE_OBJ_NAME + and tmp is not None + and tmp.samefile(filename) + ) if not show_entire_module: return call() with _PatchDict.from_module( - line_profiler, get_code_block=get_code_block_wrapper): + line_profiler, get_code_block=get_code_block_wrapper + ): return call() def get_code_block_wrapper(filename, lineno): - """ Return the entire content of :py:attr:`~.tempfile`.""" + """Return the entire content of :py:attr:`~.tempfile`.""" with tmp.open(mode='r') as fobj: return fobj.read().splitlines(keepends=True) @@ -199,11 +212,14 @@ def output(self): # type: () -> str cap = stack.enter_context(StringIO()) # Trap text output patch_show_func = _PatchDict.from_module( line_profiler, - show_func=self._make_show_func_wrapper(line_profiler.show_func)) + show_func=self._make_show_func_wrapper(line_profiler.show_func), + ) stack.enter_context(patch_show_func) - self.stats.print(cap, - output_unit=self.parse_result.output_unit, - stripzeros=self.parse_result.strip_zero) + self.stats.print( + cap, + output_unit=self.parse_result.output_unit, + stripzeros=self.parse_result.strip_zero, + ) return cap.getvalue().rstrip() @@ -230,6 +246,7 @@ class _PatchProfilerIntoBuiltins: skip this doctest if :py:mod:`IPython` (and hence this module) can't be imported. """ + def __init__(self, prof=None): # type: (LineProfiler | None) -> None if prof is None: @@ -290,21 +307,23 @@ def _parse_parameters(self, parameter_s, getopt_spec, opts_def): # type: (str, str, Struct) -> _ParseParamResult # FIXME: There is a chance that this handling will need to be # updated to handle single-quoted characters better (#382) - parameter_s = parameter_s.replace('"', r"\"").replace("'", r"\"") + parameter_s = parameter_s.replace('"', r'\"').replace("'", r'\"') opts, arg_str = self.parse_options( - parameter_s, getopt_spec, list_all=True) + parameter_s, getopt_spec, list_all=True + ) opts.merge(opts_def) return _ParseParamResult(opts, arg_str) @staticmethod - def _run_and_profile(prof, # type: LineProfiler - parse_result, # type: _ParseParamResult - tempfile, # type: str | None - method, # type: Callable[PS, Any] - *args, # type: PS.args - **kwargs, # type: PS.kwargs - ): # type: (...) -> _RunAndProfileResult + def _run_and_profile( + prof, # type: LineProfiler + parse_result, # type: _ParseParamResult + tempfile, # type: str | None + method, # type: Callable[PS, Any] + *args, # type: PS.args + **kwargs, # type: PS.kwargs + ): # type: (...) -> _RunAndProfileResult # Use the time module because it's easier than parsing the # output from `show_text()`. # `perf_counter()` is a monotonically increasing alternative to @@ -314,32 +333,37 @@ def _run_and_profile(prof, # type: LineProfiler method(*args, **kwargs) message = None except (SystemExit, KeyboardInterrupt) as e: - message = (f"{type(e).__name__} exception caught in " - "code being profiled.") + message = ( + f'{type(e).__name__} exception caught in code being profiled.' + ) # Capture and save total runtime total_time = time.perf_counter() - start_time return _RunAndProfileResult( - prof.get_stats(), parse_result, - message=message, time_elapsed=total_time, tempfile=tempfile) + prof.get_stats(), + parse_result, + message=message, + time_elapsed=total_time, + tempfile=tempfile, + ) @classmethod def _lprun_all_get_rewritten_profiled_code(cls, tmpfile): # type: (str) -> types.CodeType - """ Transform and compile the AST of the profiled code. This is + """Transform and compile the AST of the profiled code. This is similar to :py:meth:`.LineProfiler.runctx`, """ at = AstTreeProfiler(tmpfile, [tmpfile], profile_imports=False) tree = at.profile() - return compile(tree, tmpfile, "exec") + return compile(tree, tmpfile, 'exec') @classmethod def _lprun_get_top_level_profiled_code(cls, tmpfile): # type: (str) -> types.CodeType - """ Compile the profiled code.""" + """Compile the profiled code.""" with open(tmpfile, mode='r') as fobj: - return compile(fobj.read(), tmpfile, "exec") + return compile(fobj.read(), tmpfile, 'exec') @staticmethod def _handle_end(prof, run_result): @@ -349,22 +373,23 @@ def _handle_end(prof, run_result): dump_file = run_result.parse_result.dump_raw_dest if dump_file is not None: prof.dump_stats(dump_file) - print(f"\n*** Profile stats pickled to file {str(dump_file)!r}.") + print(f'\n*** Profile stats pickled to file {str(dump_file)!r}.') text_file = run_result.parse_result.dump_text_dest if text_file is not None: - with text_file.open("w", encoding="utf-8") as pfile: + with text_file.open('w', encoding='utf-8') as pfile: print(run_result.output, file=pfile) - print("\n*** Profile printout saved to text file " - f"{str(text_file)!r}.") + print( + f'\n*** Profile printout saved to text file {str(text_file)!r}.' + ) if run_result.message: - print("\n*** " + run_result.message) + print('\n*** ' + run_result.message) return prof if run_result.parse_result.return_profiler else None @line_magic - def lprun(self, parameter_s=""): + def lprun(self, parameter_s=''): """Execute a statement under the line-by-line profiler from the :py:mod:`line_profiler` module. @@ -411,10 +436,10 @@ def lprun(self, parameter_s=""): ``-u``: specify time unit for the print-out in seconds. """ - opts_def = Struct(D=[""], T=[""], f=[], m=[], u=None) - parsed = self._parse_parameters(parameter_s, "rszf:m:D:T:u:", opts_def) - if "s" in parsed.opts: # Handle alias - parsed.opts["z"] = True + opts_def = Struct(D=[''], T=[''], f=[], m=[], u=None) + parsed = self._parse_parameters(parameter_s, 'rszf:m:D:T:u:', opts_def) + if 's' in parsed.opts: # Handle alias + parsed.opts['z'] = True assert self.shell is not None global_ns = self.shell.user_global_ns @@ -427,7 +452,7 @@ def lprun(self, parameter_s=""): funcs.append(eval(name, global_ns, local_ns)) except Exception as e: raise UsageError( - f"Could not find function {name}.\n{e.__class__.__name__}: {e}" + f'Could not find function {name}.\n{e.__class__.__name__}: {e}' ) profile = LineProfiler(*funcs) @@ -435,22 +460,28 @@ def lprun(self, parameter_s=""): # Get the modules, too for modname in parsed.m: try: - mod = __import__(modname, fromlist=[""]) + mod = __import__(modname, fromlist=['']) profile.add_module(mod) except Exception as e: raise UsageError( - f"Could not find module {modname}.\n{e.__class__.__name__}: {e}" + f'Could not find module {modname}.\n{e.__class__.__name__}: {e}' ) with _PatchProfilerIntoBuiltins(profile): run = self._run_and_profile( - profile, parsed, None, profile.runctx, parsed.arg_str, - globals=global_ns, locals=local_ns) + profile, + parsed, + None, + profile.runctx, + parsed.arg_str, + globals=global_ns, + locals=local_ns, + ) return self._handle_end(profile, run) @cell_magic - def lprun_all(self, parameter_s="", cell=""): + def lprun_all(self, parameter_s='', cell=''): """Execute the whole notebook cell under the line-by-line profiler from the :py:mod:`line_profiler` module. @@ -494,32 +525,32 @@ def lprun_all(self, parameter_s="", cell=""): Using this can bypass any issues with :py:mod:`ast` transformations. """ - opts_def = Struct(D=[""], T=[""], u=None) - parsed = self._parse_parameters(parameter_s, "rzptD:T:u:", opts_def) + opts_def = Struct(D=[''], T=[''], u=None) + parsed = self._parse_parameters(parameter_s, 'rzptD:T:u:', opts_def) ip = get_ipython() if not cell.strip(): # Edge case - cell = "..." + cell = '...' # Write the cell to a temporary file so `show_text()` inside # `print_stats()` can open it. with tempfile.NamedTemporaryFile( - suffix=".py", delete=False, mode="w", encoding="utf-8" + suffix='.py', delete=False, mode='w', encoding='utf-8' ) as tf: tf.write(textwrap.dedent(cell).strip('\n')) try: - if "p" not in parsed.opts: # This is the default case. + if 'p' not in parsed.opts: # This is the default case. get_code = self._lprun_all_get_rewritten_profiled_code else: get_code = self._lprun_get_top_level_profiled_code # Inject a fresh LineProfiler into @profile. with _PatchProfilerIntoBuiltins() as prof: code = get_code(tf.name).replace( - co_name=_LPRUN_ALL_CODE_OBJ_NAME) + co_name=_LPRUN_ALL_CODE_OBJ_NAME + ) try: - code = code.replace( - co_qualname=_LPRUN_ALL_CODE_OBJ_NAME) + code = code.replace(co_qualname=_LPRUN_ALL_CODE_OBJ_NAME) except TypeError: # Python < 3.11 pass # "Register" the profiled code object with the profiler @@ -543,15 +574,21 @@ def lprun_all(self, parameter_s="", cell=""): # items to be profiled. with prof: run = self._run_and_profile( - prof, parsed, tf.name, exec, code, + prof, + parsed, + tf.name, + exec, + code, # `globals` and `locals` - ip.user_global_ns, ip.user_ns) + ip.user_global_ns, + ip.user_ns, + ) finally: os.unlink(tf.name) - if "t" in parsed.opts: + if 't' in parsed.opts: # I know it seems redundant to include this because users # could just use -r to get the info, but see the docstring # for why -t is included anyway. - ip.user_ns["_total_time_taken"] = run.time_elapsed + ip.user_ns['_total_time_taken'] = run.time_elapsed return self._handle_end(prof, run) diff --git a/line_profiler/line_profiler.py b/line_profiler/line_profiler.py index 399ebfd2..ceb595b8 100755 --- a/line_profiler/line_profiler.py +++ b/line_profiler/line_profiler.py @@ -4,6 +4,7 @@ inspect its output. This depends on the :py:mod:`line_profiler._line_profiler` Cython backend. """ + from __future__ import annotations import functools @@ -20,12 +21,24 @@ from argparse import ArgumentParser from datetime import datetime from os import PathLike -from typing import (TYPE_CHECKING, IO, Callable, Literal, Mapping, Protocol, - Sequence, TypeVar, cast, Tuple) +from typing import ( + TYPE_CHECKING, + IO, + Callable, + Literal, + Mapping, + Protocol, + Sequence, + TypeVar, + cast, + Tuple, +) try: - from ._line_profiler import (LineProfiler as CLineProfiler, - LineStats as CLineStats) + from ._line_profiler import ( + LineProfiler as CLineProfiler, + LineStats as CLineStats, + ) except ImportError as ex: raise ImportError( 'The line_profiler._line_profiler c-extension is not importable. ' @@ -33,7 +46,11 @@ ) from . import _diagnostics as diagnostics from .cli_utils import ( - add_argument, get_cli_config, positive_float, short_string_path) + add_argument, + get_cli_config, + positive_float, + short_string_path, +) from .profiler_mixin import ByCountProfilerMixin, is_c_level_callable from .scoping_policy import ScopingPolicy, ScopingPolicyDict from .toml_config import ConfigSource @@ -42,8 +59,7 @@ from typing_extensions import ParamSpec, Self class _IPythonLike(Protocol): - def register_magics(self, magics: type) -> None: - ... + def register_magics(self, magics: type) -> None: ... PS = ParamSpec('PS') _TimingsMap = Mapping[Tuple[str, int, str], list[Tuple[int, int, int]]] @@ -59,7 +75,7 @@ def register_magics(self, magics: type) -> None: @functools.lru_cache() def get_column_widths( - config: bool | str | None = False + config: bool | str | None = False, ) -> Mapping[ColumnLiterals, int]: """ Arguments @@ -70,16 +86,18 @@ def get_column_widths( * The default value (:py:data:`False`) loads the config from the default TOML file that the package ships with. """ - subconf = (ConfigSource.from_config(config) - .get_subconfig('show', 'column_widths')) + subconf = ConfigSource.from_config(config).get_subconfig( + 'show', 'column_widths' + ) return types.MappingProxyType( - cast(Mapping[ColumnLiterals, int], subconf.conf_dict)) + cast(Mapping[ColumnLiterals, int], subconf.conf_dict) + ) def load_ipython_extension(ip: object) -> None: - """ API for IPython to recognize this module as an IPython extension. - """ + """API for IPython to recognize this module as an IPython extension.""" from .ipython_extension import LineProfilerMagics + if TYPE_CHECKING: ip = cast(_IPythonLike, ip) ip.register_magics(LineProfilerMagics) @@ -171,7 +189,9 @@ def get_code_block(filename: os.PathLike[str] | str, lineno: int) -> list[str]: namespace = inspect.getblock.__globals__ namespace['BlockFinder'] = _CythonBlockFinder try: - return inspect.getblock(linecache.getlines(os.fspath(filename))[lineno - 1:]) + return inspect.getblock( + linecache.getlines(os.fspath(filename))[lineno - 1 :] + ) finally: namespace['BlockFinder'] = BlockFinder @@ -187,14 +207,20 @@ class _CythonBlockFinder(inspect.BlockFinder): is public but undocumented API. See similar caveat in :py:func:`~.get_code_block`. """ + def tokeneater( - self, type: int, token: str, - srowcol: tuple[int, int], erowcol: tuple[int, int], - line: str) -> None: + self, + type: int, + token: str, + srowcol: tuple[int, int], + erowcol: tuple[int, int], + line: str, + ) -> None: if ( - not self.started - and type == tokenize.NAME - and token in ('cdef', 'cpdef', 'property')): + not self.started + and type == tokenize.NAME + and token in ('cdef', 'cpdef', 'property') + ): # Fudge the token to get the desired 'scoping' behavior token = 'def' return super().tokeneater(type, token, srowcol, erowcol, line) @@ -210,6 +236,7 @@ class _WrapperInfo: profiler_id (int) ID of the `LineProfiler`. """ + def __init__(self, func: types.FunctionType, profiler_id: int) -> None: self.func = func self.profiler_id = profiler_id @@ -229,7 +256,8 @@ def __init__(self, timings: _TimingsMap, unit: float) -> None: def __repr__(self) -> str: return '{}({}, {:.2G})'.format( - type(self).__name__, self.timings, self.unit) + type(self).__name__, self.timings, self.unit + ) def __eq__(self, other: object) -> bool: """ @@ -310,25 +338,39 @@ def __iadd__(self, other: _StatsLike) -> Self: return self def print( - self, stream: io.TextIOBase | None = None, - output_unit: float | None = None, - stripzeros: bool = False, details: bool = True, - summarize: bool = False, sort: bool = False, rich: bool = False, - *, config: str | PathLike[str] | bool | None = None) -> None: - show_text(self.timings, self.unit, output_unit=output_unit, - stream=stream, stripzeros=stripzeros, details=details, - summarize=summarize, sort=sort, rich=rich, config=config) + self, + stream: io.TextIOBase | None = None, + output_unit: float | None = None, + stripzeros: bool = False, + details: bool = True, + summarize: bool = False, + sort: bool = False, + rich: bool = False, + *, + config: str | PathLike[str] | bool | None = None, + ) -> None: + show_text( + self.timings, + self.unit, + output_unit=output_unit, + stream=stream, + stripzeros=stripzeros, + details=details, + summarize=summarize, + sort=sort, + rich=rich, + config=config, + ) def to_file(self, filename: PathLike[str] | str) -> None: - """ Pickle the instance to the given filename. - """ + """Pickle the instance to the given filename.""" with open(filename, 'wb') as f: pickle.dump(self, f, pickle.HIGHEST_PROTOCOL) @classmethod def from_files( - cls, file: PathLike[str] | str, /, - *files: PathLike[str] | str) -> Self: + cls, file: PathLike[str] | str, /, *files: PathLike[str] | str + ) -> Self: """ Utility function to load an instance from the given filenames. """ @@ -340,8 +382,8 @@ def from_files( @classmethod def from_stats_objects( - cls, stats: _StatsLike, /, - *more_stats: _StatsLike) -> Self: + cls, stats: _StatsLike, /, *more_stats: _StatsLike + ) -> Self: """ Example: >>> stats1 = LineStats( @@ -371,7 +413,7 @@ def _get_aggregated_timings(stats_objs): if not stats_objs: raise ValueError(f'stats_objs = {stats_objs!r}: empty') try: - stats, = stats_objs + (stats,) = stats_objs except ValueError: # > 1 obj # Add from small scaling factors to large to minimize # rounding errors @@ -384,15 +426,21 @@ def _get_aggregated_timings(stats_objs): entry_dict = timing_dict.setdefault(key, {}) for lineno, nhits, time in entries: prev_nhits, prev_time = entry_dict.get(lineno, (0, 0)) - entry_dict[lineno] = (prev_nhits + nhits, - prev_time + factor * time) + entry_dict[lineno] = ( + prev_nhits + nhits, + prev_time + factor * time, + ) timings = { - key: [(lineno, nhits, int(round(time, 0))) - for lineno, (nhits, time) in sorted(entry_dict.items())] - for key, entry_dict in timing_dict.items()} + key: [ + (lineno, nhits, int(round(time, 0))) + for lineno, (nhits, time) in sorted(entry_dict.items()) + ] + for key, entry_dict in timing_dict.items() + } else: - timings = {key: entries.copy() - for key, entries in stats.timings.items()} + timings = { + key: entries.copy() for key, entries in stats.timings.items() + } unit = stats.unit return timings, unit @@ -437,9 +485,11 @@ def wrap_callable(self, func: Callable) -> Callable: return super().wrap_callable(func) def add_callable( - self, func: object, - guard: Callable[[Callable], bool] | None = None, - name: str | None = None) -> Literal[0, 1]: + self, + func: object, + guard: Callable[[Callable], bool] | None = None, + name: str | None = None, + ) -> Literal[0, 1]: """ Register a function, method, :py:class:`property`, :py:func:`~functools.partial` object, etc. with the underlying @@ -499,7 +549,8 @@ def _repr_for_log(obj, name=None): real_name, '()' if callable(obj) and not isinstance(obj, type) else '', f'(=`{name}`) ' if name and name != real_name else '', - id(obj)) + id(obj), + ) def _debug(self, msg): self_repr = f'{type(self).__name__} @ {id(self):#x}' @@ -515,35 +566,52 @@ def get_stats(self) -> LineStats: return LineStats.from_stats_objects(super().get_stats()) def dump_stats(self, filename: os.PathLike[str] | str) -> None: - """ Dump a representation of the data to a file as a pickled + """Dump a representation of the data to a file as a pickled :py:class:`~.LineStats` object from :py:meth:`~.get_stats()`. """ self.get_stats().to_file(filename) def print_stats( - self, stream: io.TextIOBase | None = None, - output_unit: float | None = None, stripzeros: bool = False, - details: bool = True, summarize: bool = False, - sort: bool = False, rich: bool = False, *, - config: str | PathLike[str] | bool | None = None) -> None: - """ Show the gathered statistics. - """ + self, + stream: io.TextIOBase | None = None, + output_unit: float | None = None, + stripzeros: bool = False, + details: bool = True, + summarize: bool = False, + sort: bool = False, + rich: bool = False, + *, + config: str | PathLike[str] | bool | None = None, + ) -> None: + """Show the gathered statistics.""" self.get_stats().print( - stream=stream, output_unit=output_unit, - stripzeros=stripzeros, details=details, summarize=summarize, - sort=sort, rich=rich, config=config) + stream=stream, + output_unit=output_unit, + stripzeros=stripzeros, + details=details, + summarize=summarize, + sort=sort, + rich=rich, + config=config, + ) def _add_namespace( - self, namespace: type | types.ModuleType, *, - seen: set[int] | None = None, - func_scoping_policy: ScopingPolicy = cast( - ScopingPolicy, ScopingPolicy.NONE), - class_scoping_policy: ScopingPolicy = cast( - ScopingPolicy, ScopingPolicy.NONE), - module_scoping_policy: ScopingPolicy = cast( - ScopingPolicy, ScopingPolicy.NONE), - wrap: bool = False, - name: str | None = None) -> int: + self, + namespace: type | types.ModuleType, + *, + seen: set[int] | None = None, + func_scoping_policy: ScopingPolicy = cast( + ScopingPolicy, ScopingPolicy.NONE + ), + class_scoping_policy: ScopingPolicy = cast( + ScopingPolicy, ScopingPolicy.NONE + ), + module_scoping_policy: ScopingPolicy = cast( + ScopingPolicy, ScopingPolicy.NONE + ), + wrap: bool = False, + name: str | None = None, + ) -> int: def func_guard(func): return self._already_a_wrapper(func) or not func_check(func) @@ -556,7 +624,8 @@ def func_guard(func): func_scoping_policy=func_scoping_policy, class_scoping_policy=class_scoping_policy, module_scoping_policy=module_scoping_policy, - wrap=wrap) + wrap=wrap, + ) members_to_wrap = {} func_check = func_scoping_policy.get_filter(namespace, 'func') cls_check = class_scoping_policy.get_filter(namespace, 'class') @@ -574,17 +643,22 @@ def func_guard(func): continue seen.add(id(value)) if isinstance(value, type): - if not (cls_check(value) - and add_namespace(value, name=f'{name}.{attr}')): + if not ( + cls_check(value) + and add_namespace(value, name=f'{name}.{attr}') + ): continue elif isinstance(value, types.ModuleType): - if not (mod_check(value) - and add_namespace(value, name=f'{name}.{attr}')): + if not ( + mod_check(value) + and add_namespace(value, name=f'{name}.{attr}') + ): continue else: try: if not self.add_callable( - value, guard=func_guard, name=f'{name}.{attr}'): + value, guard=func_guard, name=f'{name}.{attr}' + ): continue except TypeError: # Not a callable (wrapper) continue @@ -592,20 +666,26 @@ def func_guard(func): members_to_wrap[attr] = value count += 1 if wrap and members_to_wrap: - self._wrap_namespace_members(namespace, members_to_wrap, - warning_stack_level=3) + self._wrap_namespace_members( + namespace, members_to_wrap, warning_stack_level=3 + ) if count: self._debug( 'added {} member{} in {}'.format( count, '' if count == 1 else 's', - self._repr_for_log(namespace, name))) + self._repr_for_log(namespace, name), + ) + ) return count def add_class( - self, cls: type, *, - scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, - wrap: bool = False) -> int: + self, + cls: type, + *, + scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, + wrap: bool = False, + ) -> int: """ Add the members (callables (wrappers), methods, classes, ...) in a class' local namespace and profile them. @@ -644,16 +724,21 @@ def add_class( Number of members added to the profiler. """ policies = ScopingPolicy.to_policies(scoping_policy) - return self._add_namespace(cls, - func_scoping_policy=policies['func'], - class_scoping_policy=policies['class'], - module_scoping_policy=policies['module'], - wrap=wrap) + return self._add_namespace( + cls, + func_scoping_policy=policies['func'], + class_scoping_policy=policies['class'], + module_scoping_policy=policies['module'], + wrap=wrap, + ) def add_module( - self, mod: types.ModuleType, *, - scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, - wrap: bool = False) -> int: + self, + mod: types.ModuleType, + *, + scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, + wrap: bool = False, + ) -> int: """ Add the members (callables (wrappers), methods, classes, ...) in a module's local namespace and profile them. @@ -692,11 +777,13 @@ def add_module( Number of members added to the profiler. """ policies = ScopingPolicy.to_policies(scoping_policy) - return self._add_namespace(mod, - func_scoping_policy=policies['func'], - class_scoping_policy=policies['class'], - module_scoping_policy=policies['module'], - wrap=wrap) + return self._add_namespace( + mod, + func_scoping_policy=policies['func'], + class_scoping_policy=policies['class'], + module_scoping_policy=policies['module'], + wrap=wrap, + ) def _get_wrapper_info(self, func): info = getattr(func, self._profiler_wrapped_marker, None) @@ -721,26 +808,32 @@ def _mark_wrapper(self, wrapper): # This could be in the ipython_extension submodule, # but it doesn't depend on the IPython module so it's easier to just let it stay here. def is_generated_code(filename: str) -> bool: - """ Return True if a filename corresponds to generated code, such as a + """Return True if a filename corresponds to generated code, such as a Jupyter Notebook cell. """ filename = os.path.normcase(filename) temp_dir = os.path.normcase(tempfile.gettempdir()) return ( - filename.startswith(' None: +def show_func( + filename: str, + start_lineno: int, + func_name: str, + timings: Sequence[tuple[int, int, int | float]], + unit: float, + output_unit: float | None = None, + stream: io.TextIOBase | None = None, + stripzeros: bool = False, + rich: bool = False, + *, + config: str | PathLike[str] | bool | None = None, +) -> None: """ Show results for a single function. @@ -821,7 +914,8 @@ def show_func(filename: str, start_lineno: int, func_name: str, Syntax = importlib.import_module('rich.syntax').Syntax ReprHighlighter = importlib.import_module( - 'rich.highlighter').ReprHighlighter + 'rich.highlighter' + ).ReprHighlighter Text = importlib.import_module('rich.text').Text Console = importlib.import_module('rich.console').Console Table = importlib.import_module('rich.table').Table @@ -845,11 +939,17 @@ def show_func(filename: str, start_lineno: int, func_name: str, else: stream.write('\n') stream.write(f'Could not find file {filename}\n') - stream.write('Are you sure you are running this program from the same directory\n') + stream.write( + 'Are you sure you are running this program from the same directory\n' + ) stream.write('that you ran the profiler from?\n') stream.write("Continuing without the function's contents.\n") # Fake empty lines so we can see the timings, if not the code. - nlines = 1 if not linenos else max(linenos) - min(min(linenos), start_lineno) + 1 + nlines = ( + 1 + if not linenos + else max(linenos) - min(min(linenos), start_lineno) + 1 + ) sublines = [''] * nlines # Define minimum column sizes so text fits and usually looks consistent @@ -858,7 +958,8 @@ def show_func(filename: str, start_lineno: int, func_name: str, conf_column_sizes = get_column_widths(config) default_column_sizes = { col: max(width, conf_column_sizes.get(col, width)) - for col, width in get_column_widths().items()} + for col, width in get_column_widths().items() + } display = {} @@ -878,7 +979,7 @@ def show_func(filename: str, start_lineno: int, func_name: str, if len(perhit_disp) > default_column_sizes['perhit']: perhit_disp = '%5.3g' % (float(time) * scalar / nhits) - nhits_disp = "%d" % nhits + nhits_disp = '%d' % nhits if len(nhits_disp) > default_column_sizes['hits']: nhits_disp = '%g' % nhits @@ -894,8 +995,16 @@ def show_func(filename: str, start_lineno: int, func_name: str, column_sizes['time'] = max(column_sizes['time'], max_timelen) column_sizes['perhit'] = max(column_sizes['perhit'], max_perhitlen) - col_order: list[ColumnLiterals] = ['line', 'hits', 'time', 'perhit', 'percent'] - lhs_template = ' '.join(['%' + str(column_sizes[k]) + 's' for k in col_order]) + col_order: list[ColumnLiterals] = [ + 'line', + 'hits', + 'time', + 'perhit', + 'percent', + ] + lhs_template = ' '.join( + ['%' + str(column_sizes[k]) + 's' for k in col_order] + ) template = lhs_template + ' %-s' linenos = list(range(start_lineno, start_lineno + len(sublines))) @@ -931,49 +1040,71 @@ def show_func(filename: str, start_lineno: int, func_name: str, # Use a table to horizontally concatenate the text # reference: https://github.com/Textualize/rich/discussions/3076 - table = Table(box=None, - padding=0, - collapse_padding=True, - show_header=False, - show_footer=False, - show_edge=False, - pad_edge=False, - expand=False) + table = Table( + box=None, + padding=0, + collapse_padding=True, + show_header=False, + show_footer=False, + show_edge=False, + pad_edge=False, + expand=False, + ) table.add_row(lhs, ' ', rhs) # Use a Console to render to the stream # Not sure if we should force-terminal or just specify the color system # write_console = Console(file=stream, force_terminal=True, soft_wrap=True) write_console = Console( - file=cast(IO[str], stream), - soft_wrap=True, - color_system='standard') + file=cast(IO[str], stream), soft_wrap=True, color_system='standard' + ) write_console.print(table) stream.write('\n') else: for lineno, line in zip(linenos, sublines): nhits_s, time_s, per_hit_s, percent_s = display.get(lineno, empty) line_ = line.rstrip('\n').rstrip('\r') - txt = template % (lineno, nhits_s, time_s, per_hit_s, percent_s, line_) + txt = template % ( + lineno, + nhits_s, + time_s, + per_hit_s, + percent_s, + line_, + ) try: stream.write(txt) except UnicodeEncodeError: # todo: better handling of windows encoding issue # for now just work around it line_ = 'UnicodeEncodeError - help wanted for a fix' - txt = template % (lineno, nhits_s, time_s, per_hit_s, percent_s, line_) + txt = template % ( + lineno, + nhits_s, + time_s, + per_hit_s, + percent_s, + line_, + ) stream.write(txt) stream.write('\n') stream.write('\n') -def show_text(stats: _TimingsMap, unit: float, - output_unit: float | None = None, - stream: io.TextIOBase | None = None, - stripzeros: bool = False, details: bool = True, - summarize: bool = False, sort: bool = False, rich: bool = False, - *, config: str | PathLike[str] | bool | None = None) -> None: +def show_text( + stats: _TimingsMap, + unit: float, + output_unit: float | None = None, + stream: io.TextIOBase | None = None, + stripzeros: bool = False, + details: bool = True, + summarize: bool = False, + sort: bool = False, + rich: bool = False, + *, + config: str | PathLike[str] | bool | None = None, +) -> None: """ Show text for the given timings. @@ -997,7 +1128,9 @@ def show_text(stats: _TimingsMap, unit: float, if sort: # Order by ascending duration - stats_order = sorted(stats.items(), key=lambda kv: sum(t[2] for t in kv[1])) + stats_order = sorted( + stats.items(), key=lambda kv: sum(t[2] for t in kv[1]) + ) else: # Default ordering stats_order = list(stats.items()) @@ -1008,9 +1141,18 @@ def show_text(stats: _TimingsMap, unit: float, if details: # Show detailed per-line information for each function. for (fn, lineno, name), timings in stats_order: - show_func(fn, lineno, name, stats[fn, lineno, name], unit, - output_unit=output_unit, stream=stream, - stripzeros=stripzeros, rich=rich, config=config) + show_func( + fn, + lineno, + name, + stats[fn, lineno, name], + unit, + output_unit=output_unit, + stream=stream, + stripzeros=stripzeros, + rich=rich, + config=config, + ) if summarize: # Summarize the total time for each function @@ -1024,15 +1166,23 @@ def show_text(stats: _TimingsMap, unit: float, rich = False line_template = '%6.2f seconds - %s:%s - %s' if rich: - write_console = Console(file=cast(IO[str], stream), soft_wrap=True, - color_system='standard') + write_console = Console( + file=cast(IO[str], stream), + soft_wrap=True, + color_system='standard', + ) for (fn, lineno, name), timings in stats_order: total_time = sum(t[2] for t in timings) * unit if not stripzeros or total_time: # Wrap the filename with link markup to allow the user to # open the file fn_link = f'[link={fn}]{escape(fn)}[/link]' - line = line_template % (total_time, fn_link, lineno, escape(name)) + line = line_template % ( + total_time, + fn_link, + lineno, + escape(name), + ) write_console.print(line) else: for (fn, lineno, name), timings in stats_order: @@ -1052,40 +1202,76 @@ def main() -> None: parser = ArgumentParser( description='Read and show line profiling results (`.lprof` files) ' 'as generated by the CLI application `kernprof` or by ' - '`LineProfiler.dump_stats()`.') + '`LineProfiler.dump_stats()`.' + ) get_main_config = functools.partial(get_cli_config, 'cli') default = config = get_main_config() - add_argument(parser, '-V', '--version', - action='version', version=__version__) - add_argument(parser, '-c', '--config', - help='Path to the TOML file, from the ' - '`tool.line_profiler.cli` table of which to load ' - 'defaults for the options. ' - f'(Default: {short_string_path(default.path)!r})') - add_argument(parser, '--no-config', - action='store_const', dest='config', const=False, - help='Disable the loading of configuration files other than ' - 'the default one') - add_argument(parser, '-u', '--unit', type=positive_float, - help='Output unit (in seconds) in which ' - 'the timing info is displayed. ' - f'(Default: {default.conf_dict["unit"]} s)') - add_argument(parser, '-r', '--rich', action='store_true', - help='Use rich formatting. ' - f'(Default: {default.conf_dict["rich"]})') - add_argument(parser, '-z', '--skip-zero', action='store_true', - help='Hide functions which have not been called. ' - f'(Default: {default.conf_dict["skip_zero"]})') - add_argument(parser, '-t', '--sort', action='store_true', - help='Sort by ascending total time. ' - f'(Default: {default.conf_dict["sort"]})') - add_argument(parser, '-m', '--summarize', action='store_true', - help='Print a summary of total function time. ' - f'(Default: {default.conf_dict["summarize"]})') - add_argument(parser, 'profile_output', - nargs='+', - help="'*.lprof' file(s) created by `kernprof`") + add_argument( + parser, '-V', '--version', action='version', version=__version__ + ) + add_argument( + parser, + '-c', + '--config', + help='Path to the TOML file, from the ' + '`tool.line_profiler.cli` table of which to load ' + 'defaults for the options. ' + f'(Default: {short_string_path(default.path)!r})', + ) + add_argument( + parser, + '--no-config', + action='store_const', + dest='config', + const=False, + help='Disable the loading of configuration files other than the default one', + ) + add_argument( + parser, + '-u', + '--unit', + type=positive_float, + help='Output unit (in seconds) in which ' + 'the timing info is displayed. ' + f'(Default: {default.conf_dict["unit"]} s)', + ) + add_argument( + parser, + '-r', + '--rich', + action='store_true', + help=f'Use rich formatting. (Default: {default.conf_dict["rich"]})', + ) + add_argument( + parser, + '-z', + '--skip-zero', + action='store_true', + help='Hide functions which have not been called. ' + f'(Default: {default.conf_dict["skip_zero"]})', + ) + add_argument( + parser, + '-t', + '--sort', + action='store_true', + help=f'Sort by ascending total time. (Default: {default.conf_dict["sort"]})', + ) + add_argument( + parser, + '-m', + '--summarize', + action='store_true', + help='Print a summary of total function time. ' + f'(Default: {default.conf_dict["summarize"]})', + ) + add_argument( + parser, + 'profile_output', + nargs='+', + help="'*.lprof' file(s) created by `kernprof`", + ) args = parser.parse_args() if args.config: @@ -1096,13 +1282,16 @@ def main() -> None: setattr(args, key, default) lstats = LineStats.from_files(*args.profile_output) - show_text(lstats.timings, lstats.unit, - output_unit=args.unit, - stripzeros=args.skip_zero, - rich=args.rich, - sort=args.sort, - summarize=args.summarize, - config=args.config) + show_text( + lstats.timings, + lstats.unit, + output_unit=args.unit, + stripzeros=args.skip_zero, + rich=args.rich, + sort=args.sort, + summarize=args.summarize, + config=args.config, + ) if __name__ == '__main__': diff --git a/line_profiler/line_profiler_utils.py b/line_profiler/line_profiler_utils.py index 8d15f060..b42bb647 100644 --- a/line_profiler/line_profiler_utils.py +++ b/line_profiler/line_profiler_utils.py @@ -1,6 +1,7 @@ """ Miscellaneous utilities that :py:mod:`line_profiler` uses. """ + from __future__ import annotations import enum @@ -33,6 +34,7 @@ class _StrEnumBase(str, enum.Enum): ... ValueError: 'baz' is not a valid MyEnum """ + @staticmethod def _generate_next_value_(name: str, *_, **__) -> str: return name.lower() @@ -70,10 +72,13 @@ class StringEnum(getattr(enum, 'StrEnum', _StrEnumBase)): # type: ignore[misc] >>> str(bar) 'bar' """ + @classmethod def _missing_(cls, value: object) -> Self | None: if not isinstance(value, str): return None - members = {name.casefold(): instance - for name, instance in cls.__members__.items()} + members = { + name.casefold(): instance + for name, instance in cls.__members__.items() + } return members.get(value.casefold()) diff --git a/line_profiler/profiler_mixin.py b/line_profiler/profiler_mixin.py index 1f66773d..62b75e97 100644 --- a/line_profiler/profiler_mixin.py +++ b/line_profiler/profiler_mixin.py @@ -5,8 +5,16 @@ import types from functools import cached_property, partial, partialmethod from sys import version_info -from typing import (TYPE_CHECKING, Any, Callable, Mapping, Protocol, TypeVar, - cast, Sequence) +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Mapping, + Protocol, + TypeVar, + cast, + Sequence, +) from warnings import warn from ._line_profiler import label from .scoping_policy import ScopingPolicy @@ -19,12 +27,14 @@ # These objects are callables, but are defined in C(-ython) so we can't # handle them anyway -C_LEVEL_CALLABLE_TYPES = (types.BuiltinFunctionType, - types.BuiltinMethodType, - types.ClassMethodDescriptorType, - types.MethodDescriptorType, - types.MethodWrapperType, - types.WrapperDescriptorType) +C_LEVEL_CALLABLE_TYPES = ( + types.BuiltinFunctionType, + types.BuiltinMethodType, + types.ClassMethodDescriptorType, + types.MethodDescriptorType, + types.MethodWrapperType, + types.WrapperDescriptorType, +) # Can't line-profile Cython in 3.12 since the old C API was upended # without an appropriate replacement (which only came in 3.13); @@ -34,104 +44,91 @@ if TYPE_CHECKING: from typing_extensions import ParamSpec, TypeIs + UnparametrizedCallableLike = TypeVar( 'UnparametrizedCallableLike', - types.FunctionType, property, types.MethodType) + types.FunctionType, + property, + types.MethodType, + ) T = TypeVar('T') T_co = TypeVar('T_co', covariant=True) PS = ParamSpec('PS') class CythonCallable(Protocol[PS, T_co]): - def __call__(self, *args: PS.args, **kwargs: PS.kwargs) -> T_co: - ... + def __call__(self, *args: PS.args, **kwargs: PS.kwargs) -> T_co: ... @property - def __code__(self) -> types.CodeType: - ... + def __code__(self) -> types.CodeType: ... @property - def func_code(self) -> types.CodeType: - ... + def func_code(self) -> types.CodeType: ... @property - def __name__(self) -> str: - ... + def __name__(self) -> str: ... @property - def func_name(self) -> str: - ... + def func_name(self) -> str: ... @property - def __qualname__(self) -> str: - ... + def __qualname__(self) -> str: ... @property - def __doc__(self) -> str | None: - ... + def __doc__(self) -> str | None: ... @__doc__.setter - def __doc__(self, doc: str | None) -> None: - ... + def __doc__(self, doc: str | None) -> None: ... @property - def func_doc(self) -> str | None: - ... + def func_doc(self) -> str | None: ... @property - def __globals__(self) -> dict[str, Any]: - ... + def __globals__(self) -> dict[str, Any]: ... @property - def func_globals(self) -> dict[str, Any]: - ... + def func_globals(self) -> dict[str, Any]: ... @property - def __dict__(self) -> dict[str, Any]: - ... + def __dict__(self) -> dict[str, Any]: ... @__dict__.setter - def __dict__(self, dict: dict[str, Any]) -> None: - ... + def __dict__(self, dict: dict[str, Any]) -> None: ... @property - def func_dict(self) -> dict[str, Any]: - ... + def func_dict(self) -> dict[str, Any]: ... @property - def __annotations__(self) -> dict[str, Any]: - ... + def __annotations__(self) -> dict[str, Any]: ... @__annotations__.setter - def __annotations__(self, annotations: dict[str, Any]) -> None: - ... + def __annotations__(self, annotations: dict[str, Any]) -> None: ... @property - def __defaults__(self): - ... + def __defaults__(self): ... @property - def func_defaults(self): - ... + def func_defaults(self): ... @property - def __kwdefaults__(self): - ... + def __kwdefaults__(self): ... @property - def __closure__(self): - ... + def __closure__(self): ... @property - def func_closure(self): - ... + def func_closure(self): ... else: CythonCallable = type(label) CLevelCallable = TypeVar( 'CLevelCallable', - types.BuiltinFunctionType, types.BuiltinMethodType, - types.ClassMethodDescriptorType, types.MethodDescriptorType, - types.MethodWrapperType, types.WrapperDescriptorType) + types.BuiltinFunctionType, + types.BuiltinMethodType, + types.ClassMethodDescriptorType, + types.MethodDescriptorType, + types.MethodWrapperType, + types.WrapperDescriptorType, +) def is_c_level_callable(func: Any) -> TypeIs[CLevelCallable]: @@ -151,8 +148,10 @@ def is_cython_callable(func: Any) -> TypeIs[CythonCallable]: # said type depends on the Cython version used for building the # Cython code; # just check for what is common between Cython versions - return (type(func).__name__ - in ('cython_function_or_method', 'fused_cython_function')) + return type(func).__name__ in ( + 'cython_function_or_method', + 'fused_cython_function', + ) def is_classmethod(f: Any) -> TypeIs[classmethod]: @@ -229,12 +228,14 @@ def wrap_callable(self, func: Callable) -> Callable: return self.wrap_class(func) if callable(func): return self.wrap_function(func) - raise TypeError(f'func = {func!r}: does not look like a callable or ' - 'callable wrapper') + raise TypeError( + f'func = {func!r}: does not look like a callable or callable wrapper' + ) @classmethod def get_underlying_functions( - cls, func: object) -> list[types.FunctionType | CythonCallable]: + cls, func: object + ) -> list[types.FunctionType | CythonCallable]: """ Get the underlying function objects of a callable or an adjacent object. @@ -251,34 +252,46 @@ def get_underlying_functions( @classmethod def _get_underlying_functions( - cls, func: object, seen: set[int] | None = None, - stop_at_classes: bool = False + cls, + func: object, + seen: set[int] | None = None, + stop_at_classes: bool = False, ) -> Sequence[Callable]: if seen is None: seen = set() # Extract inner functions if is_boundmethod(func): return cls._get_underlying_functions( - func.__func__, seen=seen, stop_at_classes=stop_at_classes) + func.__func__, seen=seen, stop_at_classes=stop_at_classes + ) if is_classmethod(func) or is_staticmethod(func): return cls._get_underlying_functions( - func.__func__, seen=seen, stop_at_classes=stop_at_classes) - if is_partial(func) or is_partialmethod(func) or is_cached_property(func): + func.__func__, seen=seen, stop_at_classes=stop_at_classes + ) + if ( + is_partial(func) + or is_partialmethod(func) + or is_cached_property(func) + ): return cls._get_underlying_functions( - func.func, seen=seen, stop_at_classes=stop_at_classes) + func.func, seen=seen, stop_at_classes=stop_at_classes + ) # Dispatch to specific handlers if is_property(func): return cls._get_underlying_functions_from_property( - func, seen, stop_at_classes) + func, seen, stop_at_classes + ) if isinstance(func, type): if stop_at_classes: return [func] return cls._get_underlying_functions_from_type( - func, seen, stop_at_classes) + func, seen, stop_at_classes + ) # Otherwise, the object should either be a function... if not callable(func): - raise TypeError(f'func = {func!r}: ' - f'cannot get functions from {type(func)} objects') + raise TypeError( + f'func = {func!r}: cannot get functions from {type(func)} objects' + ) if id(func) in seen: return [] seen.add(id(func)) @@ -296,20 +309,19 @@ def _get_underlying_functions( @classmethod def _get_underlying_functions_from_property( - cls, prop: property, seen: set[int], - stop_at_classes: bool + cls, prop: property, seen: set[int], stop_at_classes: bool ) -> Sequence[Callable]: result: list[Callable] = [] for impl in prop.fget, prop.fset, prop.fdel: if impl is not None: result.extend( - cls._get_underlying_functions(impl, seen, stop_at_classes)) + cls._get_underlying_functions(impl, seen, stop_at_classes) + ) return result @classmethod def _get_underlying_functions_from_type( - cls, kls: type, seen: set[int], - stop_at_classes: bool + cls, kls: type, seen: set[int], stop_at_classes: bool ) -> Sequence[Callable]: result: list[Callable] = [] get_filter = cls._class_scoping_policy.get_filter @@ -318,7 +330,8 @@ def _get_underlying_functions_from_type( for member in vars(kls).values(): try: # Stop at class boundaries to enforce scoping behavior member_funcs = cls._get_underlying_functions( - member, seen, stop_at_classes=True) + member, seen, stop_at_classes=True + ) except TypeError: continue for impl in member_funcs: @@ -326,8 +339,11 @@ def _get_underlying_functions_from_type( # Only descend into nested classes if the policy # says so if cls_check(impl): - result.extend(cls._get_underlying_functions( - impl, seen, stop_at_classes)) + result.extend( + cls._get_underlying_functions( + impl, seen, stop_at_classes + ) + ) else: # For non-class callables, they are already filtered # (and added to `seen`) by the above call to @@ -337,8 +353,9 @@ def _get_underlying_functions_from_type( result.append(impl) return result - def _wrap_callable_wrapper(self, wrapper, impl_attrs, *, - args=None, kwargs=None, name_attr=None): + def _wrap_callable_wrapper( + self, wrapper, impl_attrs, *, args=None, kwargs=None, name_attr=None + ): """ Create a profiled wrapper object around callables based on an existing wrapper. @@ -380,8 +397,9 @@ def _wrap_callable_wrapper(self, wrapper, impl_attrs, *, """ # Wrap implementations impls = [getattr(wrapper, attr) for attr in impl_attrs] - new_impls = [None if impl is None else self.wrap_callable(impl) - for impl in impls] + new_impls = [ + None if impl is None else self.wrap_callable(impl) for impl in impls + ] # Get additional init args for the constructor if args is None: @@ -434,16 +452,18 @@ def wrap_boundmethod(self, func): """ Wrap a :py:class:`types.MethodType` to profile it. """ - return self._wrap_callable_wrapper(func, ('__func__',), - args=('__self__',)) + return self._wrap_callable_wrapper( + func, ('__func__',), args=('__self__',) + ) def _wrap_partial(self, func): """ Wrap a :py:func:`functools.partial` or :py:class:`functools.partialmethod` to profile it. """ - return self._wrap_callable_wrapper(func, ('func',), - args='args', kwargs='keywords') + return self._wrap_callable_wrapper( + func, ('func',), args='args', kwargs='keywords' + ) wrap_partial = wrap_partialmethod = _wrap_partial @@ -451,16 +471,20 @@ def wrap_property(self, func): """ Wrap a :py:class:`property` to profile it. """ - return self._wrap_callable_wrapper(func, ('fget', 'fset', 'fdel'), - kwargs={'doc': '__doc__'}, - name_attr='__name__') + return self._wrap_callable_wrapper( + func, + ('fget', 'fset', 'fdel'), + kwargs={'doc': '__doc__'}, + name_attr='__name__', + ) def wrap_cached_property(self, func): """ Wrap a :py:func:`functools.cached_property` to profile it. """ - return self._wrap_callable_wrapper(func, ('func',), - name_attr='attrname') + return self._wrap_callable_wrapper( + func, ('func',), name_attr='attrname' + ) def wrap_async_generator(self, func): """ @@ -478,12 +502,12 @@ async def wrapper(*args, **kwds): while True: self.enable_by_count() try: - item = (await g.asend(input_)) + item = await g.asend(input_) except StopAsyncIteration: return finally: self.disable_by_count() - input_ = (yield item) + input_ = yield item return self._mark_wrapper(wrapper) @@ -527,7 +551,7 @@ def wrapper(*args, **kwds): return finally: self.disable_by_count() - input_ = (yield item) + input_ = yield item return self._mark_wrapper(wrapper) @@ -573,20 +597,27 @@ def wrap_class(self, func): for name, member in vars(func).items(): try: impls = self._get_underlying_functions( - member, stop_at_classes=True) + member, stop_at_classes=True + ) except TypeError: # Not a callable (wrapper) continue - if any((cls_check(impl) - if isinstance(impl, type) else - func_check(impl)) - for impl in impls): + if any( + ( + cls_check(impl) + if isinstance(impl, type) + else func_check(impl) + ) + for impl in impls + ): members_to_wrap[name] = member - self._wrap_namespace_members(func, members_to_wrap, - warning_stack_level=2) + self._wrap_namespace_members( + func, members_to_wrap, warning_stack_level=2 + ) return func def _wrap_namespace_members( - self, namespace, members, *, warning_stack_level=2): + self, namespace, members, *, warning_stack_level=2 + ): wrap_failures = {} for name, member in members.items(): wrapper = self.wrap_callable(member) @@ -602,8 +633,10 @@ def _wrap_namespace_members( # and we shouldn't be here) wrap_failures[name] = member if wrap_failures: - msg = (f'cannot wrap {len(wrap_failures)} attribute(s) of ' - f'{namespace!r} (`{{attr: value}}`): {wrap_failures!r}') + msg = ( + f'cannot wrap {len(wrap_failures)} attribute(s) of ' + f'{namespace!r} (`{{attr: value}}`): {wrap_failures!r}' + ) warn(msg, stacklevel=warning_stack_level) def _already_a_wrapper(self, func): @@ -614,15 +647,14 @@ def _mark_wrapper(self, wrapper): return wrapper def run(self, cmd): - """ Profile a single executable statment in the main namespace. - """ + """Profile a single executable statment in the main namespace.""" import __main__ + main_dict = __main__.__dict__ return self.runctx(cmd, main_dict, main_dict) def runctx(self, cmd, globals, locals): - """ Profile a single executable statement in the given namespaces. - """ + """Profile a single executable statement in the given namespaces.""" self.enable_by_count() try: exec(cmd, globals, locals) @@ -631,8 +663,7 @@ def runctx(self, cmd, globals, locals): return self def runcall(self, func, /, *args, **kw): - """ Profile a single function call. - """ + """Profile a single function call.""" self.enable_by_count() try: return func(*args, **kw) @@ -648,4 +679,5 @@ def __exit__(self, *_, **__): _profiler_wrapped_marker = '__line_profiler_id__' _class_scoping_policy: ScopingPolicy = cast( - ScopingPolicy, ScopingPolicy.CHILDREN) + ScopingPolicy, ScopingPolicy.CHILDREN + ) diff --git a/line_profiler/scoping_policy.py b/line_profiler/scoping_policy.py index b9c9a579..4c1e4d3a 100644 --- a/line_profiler/scoping_policy.py +++ b/line_profiler/scoping_policy.py @@ -16,12 +16,10 @@ DEFAULT_SCOPING_POLICIES: ScopingPolicyDict = { 'func': 'siblings', 'class': 'siblings', - 'module': 'exact' + 'module': 'exact', } - - class ScopingPolicy(StringEnum): """ :py:class:`StrEnum` for scoping policies, that is, how it is @@ -96,6 +94,7 @@ class ScopingPolicy(StringEnum): methods prefixed with a single underscore are to be considered implementation details. """ + EXACT = auto() CHILDREN = auto() DESCENDANTS = auto() @@ -131,21 +130,18 @@ class MockClass: @overload def get_filter( - self, namespace: type | ModuleType, - obj_type: Literal['func']) -> Callable[[Callable], bool]: - ... + self, namespace: type | ModuleType, obj_type: Literal['func'] + ) -> Callable[[Callable], bool]: ... @overload def get_filter( - self, namespace: type | ModuleType, - obj_type: Literal['class']) -> Callable[[type], bool]: - ... + self, namespace: type | ModuleType, obj_type: Literal['class'] + ) -> Callable[[type], bool]: ... @overload def get_filter( - self, namespace: type | ModuleType, - obj_type: Literal['module']) -> Callable[[ModuleType], bool]: - ... + self, namespace: type | ModuleType, obj_type: Literal['module'] + ) -> Callable[[ModuleType], bool]: ... def get_filter(self, namespace: type | ModuleType, obj_type: str): """ @@ -174,17 +170,20 @@ def get_filter(self, namespace: type | ModuleType, obj_type: str): if obj_type == 'module': if is_class: return self._return_const(False) - return self._get_module_filter_in_module(cast(ModuleType, namespace)) + return self._get_module_filter_in_module( + cast(ModuleType, namespace) + ) if is_class: return self._get_callable_filter_in_class( - cast(type, namespace), is_class=(obj_type == 'class')) + cast(type, namespace), is_class=(obj_type == 'class') + ) return self._get_callable_filter_in_module( - cast(ModuleType, namespace), is_class=(obj_type == 'class')) + cast(ModuleType, namespace), is_class=(obj_type == 'class') + ) @classmethod def to_policies( - cls, - policies: str | ScopingPolicy | ScopingPolicyDict | None = None + cls, policies: str | ScopingPolicy | ScopingPolicyDict | None = None ) -> _ScopingPolicyDict: """ Normalize ``policies`` into a dictionary of policies for various @@ -241,14 +240,20 @@ def to_policies( policies = DEFAULT_SCOPING_POLICIES if isinstance(policies, str): policy = cls(policies) - return _ScopingPolicyDict({ - 'func': policy, - 'class': policy, - 'module': policy, - }) - return _ScopingPolicyDict({'func': cls(policies['func']), - 'class': cls(policies['class']), - 'module': cls(policies['module'])}) + return _ScopingPolicyDict( + { + 'func': policy, + 'class': policy, + 'module': policy, + } + ) + return _ScopingPolicyDict( + { + 'func': cls(policies['func']), + 'class': cls(policies['class']), + 'module': cls(policies['module']), + } + ) @staticmethod def _return_const(value: bool) -> Callable[[object], bool]: @@ -262,7 +267,7 @@ def _match_prefix(s: str, prefix: str, sep: str = '.') -> bool: return s == prefix or s.startswith(prefix + sep) def _get_callable_filter_in_class( - self, cls: type, is_class: bool + self, cls: type, is_class: bool ) -> Callable[[FunctionType | type], bool]: def func_is_child(other: FunctionType | type): if not modules_are_equal(other): @@ -278,9 +283,7 @@ def func_is_descdendant(other: FunctionType | type): return other.__qualname__.startswith(cls.__qualname__ + '.') policies: dict[str, Callable[[FunctionType | type], bool]] = { - 'exact': (self._return_const(False) - if is_class else - func_is_child), + 'exact': (self._return_const(False) if is_class else func_is_child), 'children': func_is_child, 'descendants': func_is_descdendant, 'siblings': modules_are_equal, @@ -289,7 +292,7 @@ def func_is_descdendant(other: FunctionType | type): return policies[self.value] def _get_callable_filter_in_module( - self, mod: ModuleType, is_class: bool + self, mod: ModuleType, is_class: bool ) -> Callable[[FunctionType | type], bool]: def func_is_child(other: FunctionType | type): return other.__module__ == mod.__name__ @@ -304,20 +307,20 @@ def func_is_cousin(other: FunctionType | type): parent, _, basename = mod.__name__.rpartition('.') policies: dict[str, Callable[[FunctionType | type], bool]] = { - 'exact': (self._return_const(False) - if is_class else - func_is_child), + 'exact': (self._return_const(False) if is_class else func_is_child), 'children': func_is_child, 'descendants': func_is_descdendant, - 'siblings': (func_is_cousin # Only if a pkg - if basename else - func_is_descdendant), + 'siblings': ( + func_is_cousin # Only if a pkg + if basename + else func_is_descdendant + ), 'none': self._return_const(True), } return policies[self.value] def _get_module_filter_in_module( - self, mod: ModuleType + self, mod: ModuleType ) -> Callable[[ModuleType], bool]: def module_is_descendant(other: ModuleType): return other.__name__.startswith(mod.__name__ + '.') @@ -333,9 +336,11 @@ def module_is_sibling(other: ModuleType): 'exact': self._return_const(False), 'children': module_is_child, 'descendants': module_is_descendant, - 'siblings': (module_is_sibling # Only if a pkg - if basename else - self._return_const(False)), + 'siblings': ( + module_is_sibling # Only if a pkg + if basename + else self._return_const(False) + ), 'none': self._return_const(True), } return policies[self.value] @@ -348,12 +353,14 @@ def module_is_sibling(other: ModuleType): ScopingPolicyDict = TypedDict( 'ScopingPolicyDict', - {'func': Union[str, ScopingPolicy], - 'class': Union[str, ScopingPolicy], - 'module': Union[str, ScopingPolicy]}) + { + 'func': Union[str, ScopingPolicy], + 'class': Union[str, ScopingPolicy], + 'module': Union[str, ScopingPolicy], + }, +) _ScopingPolicyDict = TypedDict( '_ScopingPolicyDict', - {'func': ScopingPolicy, - 'class': ScopingPolicy, - 'module': ScopingPolicy}) + {'func': ScopingPolicy, 'class': ScopingPolicy, 'module': ScopingPolicy}, +) diff --git a/line_profiler/toml_config.py b/line_profiler/toml_config.py index 61557276..781dc60d 100644 --- a/line_profiler/toml_config.py +++ b/line_profiler/toml_config.py @@ -2,6 +2,7 @@ Read and resolve user-supplied TOML files and combine them with the default to generate configurations. """ + from __future__ import annotations import copy @@ -10,6 +11,7 @@ import itertools import os import pathlib + try: import tomllib except ImportError: # Python < 3.11 @@ -53,6 +55,7 @@ class ConfigSource: :py:attr:`~.ConfigSource.path` :py:attr:`~.ConfigSource.conf_dict` can be found. """ + conf_dict: Mapping[str, Any] path: pathlib.Path subtable: list[str] @@ -63,10 +66,12 @@ def copy(self) -> ConfigSource: Copy of the object. """ return type(self)( - copy.deepcopy(self.conf_dict), self.path, self.subtable.copy()) + copy.deepcopy(self.conf_dict), self.path, self.subtable.copy() + ) - def get_subconfig(self, *headers: str, allow_absence: bool = False, - copy: bool = False) -> ConfigSource: + def get_subconfig( + self, *headers: str, allow_absence: bool = False, copy: bool = False + ) -> ConfigSource: """ Arguments: headers (str): @@ -98,7 +103,8 @@ def get_subconfig(self, *headers: str, allow_absence: bool = False, """ new_dict = cast( Dict[str, Any], - get_subtable(self.conf_dict, headers, allow_absence=allow_absence)) + get_subtable(self.conf_dict, headers, allow_absence=allow_absence), + ) new_subtable = [*self.subtable, *headers] return type(self)(new_dict, self.path, new_subtable) @@ -125,6 +131,7 @@ def from_default(cls, *, copy: bool = True) -> ConfigSource: except AttributeError: # Python < 3.9 find_file = ir.path else: + def find_file(anc, *chunks): return ir_as_file(ir_files(anc).joinpath(*chunks)) @@ -138,19 +145,25 @@ def find_file(anc, *chunks): result = find_and_read_config_file(config=path) if result is None: raise FileNotFoundError( - 'Default configuration file could not be read') + 'Default configuration file could not be read' + ) conf_dict, source = result conf_dict = cast( Dict[str, Any], - get_subtable(conf_dict, NAMESPACE, allow_absence=False)) + get_subtable(conf_dict, NAMESPACE, allow_absence=False), + ) _DEFAULTS = cls(conf_dict, source, list(NAMESPACE)) if not copy: return _DEFAULTS return _DEFAULTS.copy() @classmethod - def from_config(cls, config: str | PathLike | bool | None = None, *, - read_env: bool = True) -> ConfigSource: + def from_config( + cls, + config: str | PathLike | bool | None = None, + *, + read_env: bool = True, + ) -> ConfigSource: """ Create an instance by loading from a config file. @@ -207,8 +220,11 @@ def from_config(cls, config: str | PathLike | bool | None = None, *, configuration (see :py:meth:`~.ConfigSource.from_default`). """ + def merge(template: Mapping[str, Any], supplied: Mapping[str, Any]): - if not (isinstance(template, Mapping) and isinstance(supplied, Mapping)): + if not ( + isinstance(template, Mapping) and isinstance(supplied, Mapping) + ): return supplied result = {} for key, default in template.items(): @@ -266,22 +282,28 @@ def merge(template: Mapping[str, Any], supplied: Mapping[str, Any]): all_headers = {'tool', 'tool.line_profiler'} all_headers.update( '.'.join(('tool.line_profiler', *header)) - for header in get_headers(default_instance.conf_dict, - include_implied=True)) + for header in get_headers( + default_instance.conf_dict, include_implied=True + ) + ) raise ValueError( f'config = {config!r}: expected each of these keys to ' 'either be nonexistent or map to a table: ' - f'{sorted(all_headers)!r}') from None + f'{sorted(all_headers)!r}' + ) from None # Filter the content of `conf` down to just the key-value pairs # pairs present in the default configs return cls( - merge(default_instance.conf_dict, conf), source, list(NAMESPACE)) + merge(default_instance.conf_dict, conf), source, list(NAMESPACE) + ) def find_and_read_config_file( - *, config: str | PathLike | None = None, - env_var: str | None = ENV_VAR, - targets: Sequence[str | PathLike] = TARGETS) -> Config | None: + *, + config: str | PathLike | None = None, + env_var: str | None = ENV_VAR, + targets: Sequence[str | PathLike] = TARGETS, +) -> Config | None: """ Arguments: config (str | os.PathLike[str] | None): @@ -305,6 +327,7 @@ def find_and_read_config_file( Otherwise None """ + def iter_configs(dir_path): for dpath in itertools.chain((dir_path,), dir_path.parents): for target in targets: @@ -316,9 +339,9 @@ def iter_configs(dir_path): pass if config: - configs = pathlib.Path(config).absolute(), + configs = (pathlib.Path(config).absolute(),) elif env_var and os.environ.get(env_var): - configs = pathlib.Path(os.environ[env_var]).absolute(), + configs = (pathlib.Path(os.environ[env_var]).absolute(),) else: pwd = pathlib.Path.cwd().absolute() configs = iter_configs(pwd) @@ -331,8 +354,9 @@ def iter_configs(dir_path): return None -def get_subtable(table: Mapping[K, Mapping], keys: Sequence[K], *, - allow_absence: bool = True) -> Mapping: +def get_subtable( + table: Mapping[K, Mapping], keys: Sequence[K], *, allow_absence: bool = True +) -> Mapping: """ Arguments: table (Mapping): @@ -372,14 +396,17 @@ def get_subtable(table: Mapping[K, Mapping], keys: Sequence[K], *, else: subtable = subtable[key] if not isinstance(subtable, Mapping): - raise TypeError(f'table = {table!r}, keys = {list(keys)!r}: ' - 'expected result to be a mapping, got a ' - f'`{type(subtable).__name__}` ({subtable!r})') + raise TypeError( + f'table = {table!r}, keys = {list(keys)!r}: ' + 'expected result to be a mapping, got a ' + f'`{type(subtable).__name__}` ({subtable!r})' + ) return subtable -def get_headers(table: Mapping[K, Any], *, - include_implied: bool = False) -> set[tuple[K, ...]]: +def get_headers( + table: Mapping[K, Any], *, include_implied: bool = False +) -> set[tuple[K, ...]]: """ Arguments: table (Mapping): diff --git a/pyproject.toml b/pyproject.toml index a63e487d..430249ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,3 +92,23 @@ include = [ "line_profiler/line_profiler_utils.py", ] rules = { unused-type-ignore-comment = "ignore" } + +[tool.ruff] +line-length = 80 +target-version = "py38" + +[tool.ruff.lint] +# Enable Flake8 (E, F) and isort (I) rules. +select = ["E", "F", "I"] +# Ignore specific rules, for example, E501 (line too long) as it's handled by the formatter. +ignore = [ + "E501", # line too long + "E402", # Module level import not at top of file +] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false diff --git a/run_tests.py b/run_tests.py index ae053a04..da2b41d2 100755 --- a/run_tests.py +++ b/run_tests.py @@ -2,6 +2,7 @@ """ Based on template in rc/run_tests.binpy.py.in """ + import os import sqlite3 import sys @@ -28,7 +29,7 @@ def is_cibuildwheel(): def replace_docker_path(path, runner_project_dir): """Update path to a file installed in a temp venv to runner_project_dir.""" - pattern = re.compile(r"\/tmp\/.+?\/site-packages") + pattern = re.compile(r'\/tmp\/.+?\/site-packages') return pattern.sub(runner_project_dir, path) @@ -47,7 +48,10 @@ def update_coverage_file(coverage_path, runner_project_dir): cursor.execute(read_file_query) old_records = cursor.fetchall() - new_records = [(replace_docker_path(path, runner_project_dir), _id) for _id, path in old_records] + new_records = [ + (replace_docker_path(path, runner_project_dir), _id) + for _id, path in old_records + ] print('Updated coverage file paths:\n', new_records) sql_update_query = 'Update file set path = ? where id = ?' @@ -79,6 +83,7 @@ def copy_coverage_cibuildwheel_docker(runner_project_dir): def main(): import pathlib + orig_cwd = os.getcwd() repo_dir = pathlib.Path(__file__).parent.absolute() test_dir = repo_dir / 'tests' @@ -117,12 +122,16 @@ def main(): except IndexError: print('[run_tests] Confirmed repo dir is not in sys.path') else: - print(f'[run_tests] Removing _resolved_repo_dir={_resolved_repo_dir} from search path') + print( + f'[run_tests] Removing _resolved_repo_dir={_resolved_repo_dir} from search path' + ) del temp_path[_idx] if is_cibuildwheel(): # Remove from sys.path to prevent the import mechanism from testing # the source repo rather than the installed wheel. - print(f'[run_tests] Removing _resolved_repo_dir={_resolved_repo_dir} from sys.path to ensure wheels are tested') + print( + f'[run_tests] Removing _resolved_repo_dir={_resolved_repo_dir} from sys.path to ensure wheels are tested' + ) del sys.path[_idx] print(f'[run_tests] sys.path = {ub.urepr(sys.path, nl=1)}') @@ -135,7 +144,9 @@ def main(): print(f'[run_tests] Found installed version of {package_name}') print(f'[run_tests] modpath={modpath}') modpath_contents = list(pathlib.Path(modpath).glob('*')) - print(f'[run_tests] modpath_contents = {ub.urepr(modpath_contents, nl=1)}') + print( + f'[run_tests] modpath_contents = {ub.urepr(modpath_contents, nl=1)}' + ) # module = ub.import_module_from_path(modpath, index=0) # print(f'[run_tests] Installed module = {module!r}') else: @@ -146,20 +157,23 @@ def main(): try: import pytest + pytest_args = [] if use_coverage: pytest_args += [ - '--cov-config', os.fspath(pyproject_fpath), - '--cov-report', 'html', - '--cov-report', 'term', - '--cov-report', 'xml', + '--cov-config', + os.fspath(pyproject_fpath), + '--cov-report', + 'html', + '--cov-report', + 'term', + '--cov-report', + 'xml', '--cov=' + package_name, ] - pytest_args += [ - os.fspath(modpath), os.fspath(test_dir) - ] + pytest_args += [os.fspath(modpath), os.fspath(test_dir)] if is_cibuildwheel() and use_coverage: pytest_args.append('--cov-append') @@ -174,7 +188,9 @@ def main(): os.chdir(orig_cwd) if is_cibuildwheel() and use_coverage: # for CIBW under linux - copy_coverage_cibuildwheel_docker(f'/home/runner/work/{package_name}/{package_name}') + copy_coverage_cibuildwheel_docker( + f'/home/runner/work/{package_name}/{package_name}' + ) print('[run_tests] Restoring cwd = {!r}'.format(orig_cwd)) return retcode diff --git a/setup.py b/setup.py index 1942b3b2..85eabaad 100755 --- a/setup.py +++ b/setup.py @@ -7,10 +7,12 @@ def _choose_build_method(): - DISABLE_C_EXTENSIONS = os.environ.get("DISABLE_C_EXTENSIONS", "").lower() - LINE_PROFILER_BUILD_METHOD = os.environ.get("LINE_PROFILER_BUILD_METHOD", "auto").lower() + DISABLE_C_EXTENSIONS = os.environ.get('DISABLE_C_EXTENSIONS', '').lower() + LINE_PROFILER_BUILD_METHOD = os.environ.get( + 'LINE_PROFILER_BUILD_METHOD', 'auto' + ).lower() - if DISABLE_C_EXTENSIONS in {"true", "on", "yes", "1"}: + if DISABLE_C_EXTENSIONS in {'true', 'on', 'yes', '1'}: LINE_PROFILER_BUILD_METHOD = 'setuptools' if LINE_PROFILER_BUILD_METHOD == 'auto': @@ -38,7 +40,7 @@ def parse_version(fpath): """ Statically parse the version number from a python file """ - value = static_parse("__version__", fpath) + value = static_parse('__version__', fpath) return value @@ -50,18 +52,20 @@ def static_parse(varname, fpath): import ast if not exists(fpath): - raise ValueError("fpath={!r} does not exist".format(fpath)) - with open(fpath, "r") as file_: + raise ValueError('fpath={!r} does not exist'.format(fpath)) + with open(fpath, 'r') as file_: sourcecode = file_.read() pt = ast.parse(sourcecode) class StaticVisitor(ast.NodeVisitor): def visit_Assign(self, node: ast.Assign): for target in node.targets: - if getattr(target, "id", None) == varname: + if getattr(target, 'id', None) == varname: value: ast.expr = node.value if not isinstance(value, ast.Constant): - raise ValueError("variable {!r} is not a constant".format(varname)) + raise ValueError( + 'variable {!r} is not a constant'.format(varname) + ) self.static_value = value.value visitor = StaticVisitor() @@ -69,7 +73,7 @@ def visit_Assign(self, node: ast.Assign): try: value = visitor.static_value except AttributeError: - value = "Unknown {}".format(varname) + value = 'Unknown {}'.format(varname) warnings.warn(value) return value @@ -84,16 +88,16 @@ def parse_description(): """ from os.path import dirname, join, exists - readme_fpath = join(dirname(__file__), "README.rst") + readme_fpath = join(dirname(__file__), 'README.rst') # This breaks on pip install, so check that it exists. if exists(readme_fpath): - with open(readme_fpath, "r") as f: + with open(readme_fpath, 'r') as f: text = f.read() return text - return "" + return '' -def parse_requirements(fname="requirements.txt", versions=False): +def parse_requirements(fname='requirements.txt', versions=False): """ Parse the package dependencies listed in a requirements file but strips specific versioning information. @@ -112,7 +116,7 @@ def parse_requirements(fname="requirements.txt", versions=False): require_fpath = fname - def parse_line(line, dpath=""): + def parse_line(line, dpath=''): """ Parse information from a line in a requirements text file @@ -120,72 +124,74 @@ def parse_line(line, dpath=""): line = '-e git+https://a.com/somedep@sometag#egg=SomeDep' """ # Remove inline comments - comment_pos = line.find(" #") + comment_pos = line.find(' #') if comment_pos > -1: line = line[:comment_pos] - if line.startswith("-r "): + if line.startswith('-r '): # Allow specifying requirements in other files - target = join(dpath, line.split(" ")[1]) + target = join(dpath, line.split(' ')[1]) for info in parse_require_file(target): yield info else: # See: https://www.python.org/dev/peps/pep-0508/ - info = {"line": line} - if line.startswith("-e "): - info["package"] = line.split("#egg=")[1] + info = {'line': line} + if line.startswith('-e '): + info['package'] = line.split('#egg=')[1] else: - if ";" in line: - pkgpart, platpart = line.split(";") + if ';' in line: + pkgpart, platpart = line.split(';') # Handle platform specific dependencies # setuptools.readthedocs.io/en/latest/setuptools.html # #declaring-platform-specific-dependencies plat_deps = platpart.strip() - info["platform_deps"] = plat_deps + info['platform_deps'] = plat_deps else: pkgpart = line platpart = None # Remove versioning from the package - pat = "(" + "|".join([">=", "==", ">"]) + ")" + pat = '(' + '|'.join(['>=', '==', '>']) + ')' parts = re.split(pat, pkgpart, maxsplit=1) parts = [p.strip() for p in parts] - info["package"] = parts[0] + info['package'] = parts[0] if len(parts) > 1: op, rest = parts[1:] version = rest # NOQA - info["version"] = (op, version) + info['version'] = (op, version) yield info def parse_require_file(fpath): dpath = dirname(fpath) - with open(fpath, "r") as f: + with open(fpath, 'r') as f: for line in f.readlines(): line = line.strip() - if line and not line.startswith("#"): + if line and not line.startswith('#'): for info in parse_line(line, dpath=dpath): yield info def gen_packages_items(): if exists(require_fpath): for info in parse_require_file(require_fpath): - parts = [info["package"]] - if versions and "version" in info: - if versions == "strict": + parts = [info['package']] + if versions and 'version' in info: + if versions == 'strict': # In strict mode, we pin to the minimum version - if info["version"]: + if info['version']: # Only replace the first >= instance - verstr = "".join(info["version"]).replace(">=", "==", 1) + verstr = ''.join(info['version']).replace( + '>=', '==', 1 + ) parts.append(verstr) else: - parts.extend(info["version"]) - if not sys.version.startswith("3.4"): + parts.extend(info['version']) + if not sys.version.startswith('3.4'): # apparently package_deps are broken in 3.4 - plat_deps = info.get("platform_deps") + plat_deps = info.get('platform_deps') if plat_deps is not None: - parts.append(";" + plat_deps) - item = "".join(parts) + parts.append(';' + plat_deps) + item = ''.join(parts) yield item packages = list(gen_packages_items()) @@ -203,8 +209,8 @@ def gen_packages_items(): """ -NAME = "line_profiler" -INIT_PATH = "line_profiler/line_profiler.py" +NAME = 'line_profiler' +INIT_PATH = 'line_profiler/line_profiler.py' VERSION = parse_version(INIT_PATH) @@ -216,6 +222,7 @@ def gen_packages_items(): setup = setuptools.setup elif LINE_PROFILER_BUILD_METHOD == 'scikit-build': import skbuild # NOQA + setup = skbuild.setup elif LINE_PROFILER_BUILD_METHOD == 'cython': # no need to try importing cython because an import @@ -227,24 +234,36 @@ def gen_packages_items(): def run_cythonize(force=False): return cythonize( Extension( - name="line_profiler._line_profiler", - sources=["line_profiler/_line_profiler.pyx", - "line_profiler/timers.c", - "line_profiler/c_trace_callbacks.c"], + name='line_profiler._line_profiler', + sources=[ + 'line_profiler/_line_profiler.pyx', + 'line_profiler/timers.c', + 'line_profiler/c_trace_callbacks.c', + ], # force a recompile if this changes depends=[ - "line_profiler/_map_helpers.pxd", + 'line_profiler/_map_helpers.pxd', + ], + language='c++', + define_macros=[ + ( + 'CYTHON_TRACE', + (1 if os.getenv('DEV') == 'true' else 0), + ) ], - language="c++", - define_macros=[("CYTHON_TRACE", (1 if os.getenv("DEV") == "true" else 0))], ), compiler_directives={ - "language_level": 3, - "infer_types": True, - "legacy_implicit_noexcept": True, - "linetrace": (True if os.getenv("DEV") == "true" else False) + 'language_level': 3, + 'infer_types': True, + 'legacy_implicit_noexcept': True, + 'linetrace': ( + True if os.getenv('DEV') == 'true' else False + ), }, - include_path=["line_profiler/python25.pxd", "line_profiler/_map_helpers.pxd"], + include_path=[ + 'line_profiler/python25.pxd', + 'line_profiler/_map_helpers.pxd', + ], force=force, nthreads=multiprocessing.cpu_count(), ) @@ -254,48 +273,62 @@ def run_cythonize(force=False): else: raise Exception('Unknown build method') - setupkw["install_requires"] = parse_requirements( - "requirements/runtime.txt", versions="loose" + setupkw['install_requires'] = parse_requirements( + 'requirements/runtime.txt', versions='loose' ) - setupkw["extras_require"] = { - "all": parse_requirements("requirements.txt", versions="loose"), - "tests": parse_requirements("requirements/tests.txt", versions="loose"), - "optional": parse_requirements("requirements/optional.txt", versions="loose"), - "all-strict": parse_requirements("requirements.txt", versions="strict"), - "runtime-strict": parse_requirements( - "requirements/runtime.txt", versions="strict" + setupkw['extras_require'] = { + 'all': parse_requirements('requirements.txt', versions='loose'), + 'tests': parse_requirements('requirements/tests.txt', versions='loose'), + 'optional': parse_requirements( + 'requirements/optional.txt', versions='loose' + ), + 'all-strict': parse_requirements('requirements.txt', versions='strict'), + 'runtime-strict': parse_requirements( + 'requirements/runtime.txt', versions='strict' ), - "tests-strict": parse_requirements("requirements/tests.txt", versions="strict"), - "optional-strict": parse_requirements( - "requirements/optional.txt", versions="strict" + 'tests-strict': parse_requirements( + 'requirements/tests.txt', versions='strict' + ), + 'optional-strict': parse_requirements( + 'requirements/optional.txt', versions='strict' + ), + 'ipython': parse_requirements( + 'requirements/ipython.txt', versions='loose' + ), + 'ipython-strict': parse_requirements( + 'requirements/ipython.txt', versions='strict' ), - "ipython": parse_requirements('requirements/ipython.txt', versions="loose"), - "ipython-strict": parse_requirements('requirements/ipython.txt', versions="strict"), } setupkw['entry_points'] = { 'console_scripts': [ 'kernprof=kernprof:main', ], } - setupkw["name"] = NAME - setupkw["version"] = VERSION - setupkw["author"] = "Robert Kern" - setupkw["author_email"] = "robert.kern@enthought.com" - setupkw["url"] = "https://github.com/pyutils/line_profiler" - setupkw["description"] = "Line-by-line profiler" - setupkw["long_description"] = parse_description() - setupkw["long_description_content_type"] = "text/x-rst" - setupkw["license"] = "BSD" - setupkw["packages"] = list(setuptools.find_packages()) - setupkw["py_modules"] = ['kernprof', 'line_profiler'] - setupkw["python_requires"] = ">=3.8" + setupkw['name'] = NAME + setupkw['version'] = VERSION + setupkw['author'] = 'Robert Kern' + setupkw['author_email'] = 'robert.kern@enthought.com' + setupkw['url'] = 'https://github.com/pyutils/line_profiler' + setupkw['description'] = 'Line-by-line profiler' + setupkw['long_description'] = parse_description() + setupkw['long_description_content_type'] = 'text/x-rst' + setupkw['license'] = 'BSD' + setupkw['packages'] = list(setuptools.find_packages()) + setupkw['py_modules'] = ['kernprof', 'line_profiler'] + setupkw['python_requires'] = '>=3.8' setupkw['license_files'] = ['LICENSE.txt', 'LICENSE_Python.txt'] - setupkw["package_data"] = {"line_profiler": ["py.typed", "*.pyi", "*.toml"]} + setupkw['package_data'] = {'line_profiler': ['py.typed', '*.pyi', '*.toml']} # `include_package_data` is needed to put `rc/line_profiler.toml` in # the wheel - setupkw["include_package_data"] = True - setupkw['keywords'] = ['timing', 'timer', 'profiling', 'profiler', 'line_profiler'] - setupkw["classifiers"] = [ + setupkw['include_package_data'] = True + setupkw['keywords'] = [ + 'timing', + 'timer', + 'profiling', + 'profiler', + 'line_profiler', + ] + setupkw['classifiers'] = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Operating System :: OS Independent', diff --git a/tests/complex_example.py b/tests/complex_example.py index af66d399..c23d33ec 100644 --- a/tests/complex_example.py +++ b/tests/complex_example.py @@ -47,6 +47,7 @@ PROFILE_TYPE=custom python complex_example.py """ + from __future__ import annotations import os @@ -64,11 +65,13 @@ def profile(func): elif PROFILE_TYPE == 'explicit': # Use the explicit profile decorator import line_profiler + profile = line_profiler.profile elif PROFILE_TYPE == 'custom': # Create a custom profile decorator import line_profiler import atexit + profile = line_profiler.LineProfiler() @atexit.register @@ -106,6 +109,7 @@ def main(): Run a lot of different Fibonacci jobs """ import argparse + parser = argparse.ArgumentParser() parser.add_argument('--serial_size', type=int, default=10) parser.add_argument('--thread_size', type=int, default=10) @@ -114,11 +118,11 @@ def main(): for i in range(args.serial_size): fib(i) - funcy_fib( - i) + funcy_fib(i) fib(i) from concurrent.futures import ThreadPoolExecutor + executor = ThreadPoolExecutor(max_workers=4) with executor: jobs = [] @@ -136,6 +140,7 @@ def main(): job.result() from concurrent.futures import ProcessPoolExecutor + executor = ProcessPoolExecutor(max_workers=4) with executor: jobs = [] @@ -158,14 +163,11 @@ def funcy_fib(n): """ Alternative fib function where code splits out over multiple lines """ - a, b = ( - 0, 1 - ) + a, b = (0, 1) while a < n: # print( # a, end=' ') - a, b = b, \ - a + b + a, b = b, a + b # print( # ) diff --git a/tests/test_autoprofile.py b/tests/test_autoprofile.py index c6f8656b..2de01c8c 100644 --- a/tests/test_autoprofile.py +++ b/tests/test_autoprofile.py @@ -19,26 +19,37 @@ def test_single_function_autoprofile(): temp_dpath = ub.Path(tmp) code = ub.codeblock( - ''' + """ def func1(a): return a + 1 func1(1) - ''') + """ + ) with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(code) - args = [sys.executable, '-m', 'kernprof', '-p', 'script.py', '-l', - os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'script.py', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args) print(proc.stdout) print(proc.stderr) proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args) raw_output = proc.stdout proc.check_returncode() @@ -55,7 +66,7 @@ def test_multi_function_autoprofile(): temp_dpath = ub.Path(tmp) code = ub.codeblock( - ''' + """ def func1(a): return a + 1 @@ -69,21 +80,32 @@ def func4(a): return a % 2 + 4 func1(1) - ''') + """ + ) with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(code) - args = [sys.executable, '-m', 'kernprof', '-p', 'script.py', '-l', - os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'script.py', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args) print(proc.stdout) print(proc.stderr) proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args) raw_output = proc.stdout proc.check_returncode() @@ -103,7 +125,7 @@ def test_duplicate_function_autoprofile(): temp_dpath = ub.Path(tmp) code = ub.codeblock( - ''' + """ def func1(a): return a + 1 @@ -119,21 +141,32 @@ def func4(a): func1(1) func2(1) func3(1) - ''') + """ + ) with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(code) - args = [sys.executable, '-m', 'kernprof', '-p', 'script.py', '-l', - os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'script.py', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args) print(proc.stdout) print(proc.stderr) proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args) raw_output = proc.stdout print(raw_output) @@ -153,7 +186,7 @@ def test_async_func_autoprofile(): temp_dpath = ub.Path(tmp) code = ub.codeblock( - ''' + """ import asyncio @@ -179,21 +212,30 @@ def main(debug=None): if __name__ == '__main__': main(debug=True) - ''') + """ + ) with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(code) - args = [sys.executable, '-m', 'kernprof', - '-p', 'script.py', '-v', '-l', os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'script.py', + '-v', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args) raw_output = proc.stdout print(raw_output) print(proc.stderr) proc.check_returncode() - assert raw_output.startswith('[5, 4, 3, 2, 1, 0] ' - '[0, 1, 2, 3, 4, 5]') + assert raw_output.startswith( + '[5, 4, 3, 2, 1, 0] [0, 1, 2, 3, 4, 5]' + ) assert 'Function: main' in raw_output assert 'Function: foo' in raw_output @@ -208,13 +250,17 @@ def _write_demo_module(temp_dpath): (temp_dpath / 'test_mod/subpkg').ensuredir() (temp_dpath / 'test_mod/__init__.py').touch() - (temp_dpath / 'test_mod/subpkg/__init__.py').write_text(ub.codeblock( - ''' + (temp_dpath / 'test_mod/subpkg/__init__.py').write_text( + ub.codeblock( + """ from .submod3 import add_three - ''')) + """ + ) + ) - (temp_dpath / 'test_mod/__main__.py').write_text(ub.codeblock( - ''' + (temp_dpath / 'test_mod/__main__.py').write_text( + ub.codeblock( + """ import argparse from .submod1 import add_one @@ -228,28 +274,37 @@ def _main(args=None): if __name__ == '__main__': _main() - ''')) + """ + ) + ) - (temp_dpath / 'test_mod/util.py').write_text(ub.codeblock( - ''' + (temp_dpath / 'test_mod/util.py').write_text( + ub.codeblock( + """ def add_operator(a, b): return a + b - ''')) + """ + ) + ) # Note: this can't be profiled because `test_mod.more-utils` is not # a valid dotted path - (temp_dpath / 'test_mod/dev-utils.py').write_text(ub.codeblock( - ''' + (temp_dpath / 'test_mod/dev-utils.py').write_text( + ub.codeblock( + ''' """ Just imagine that the file contains some dev tools. """ def publish_pkg(): pass - ''')) - - (temp_dpath / 'test_mod/submod1.py').write_text(ub.codeblock( ''' + ) + ) + + (temp_dpath / 'test_mod/submod1.py').write_text( + ub.codeblock( + """ from test_mod.util import add_operator def add_one(items): new_items = [] @@ -257,23 +312,32 @@ def add_one(items): new_item = add_operator(item, 1) new_items.append(new_item) return new_items - ''')) - (temp_dpath / 'test_mod/submod2.py').write_text(ub.codeblock( - ''' + """ + ) + ) + (temp_dpath / 'test_mod/submod2.py').write_text( + ub.codeblock( + """ from test_mod.util import add_operator def add_two(items): new_items = [add_operator(item, 2) for item in items] return new_items - ''')) - (temp_dpath / 'test_mod/subpkg/submod3.py').write_text(ub.codeblock( - ''' + """ + ) + ) + (temp_dpath / 'test_mod/subpkg/submod3.py').write_text( + ub.codeblock( + """ from test_mod.util import add_operator def add_three(items): new_items = [add_operator(item, 3) for item in items] return new_items - ''')) - (temp_dpath / 'test_mod/subpkg/submod4.py').write_text(ub.codeblock( - ''' + """ + ) + ) + (temp_dpath / 'test_mod/subpkg/submod4.py').write_text( + ub.codeblock( + """ import argparse from test_mod import submod1 @@ -291,11 +355,14 @@ def _main(args=None): if __name__ == '__main__': _main() - ''')) + """ + ) + ) - script_fpath = (temp_dpath / 'script.py') - script_fpath.write_text(ub.codeblock( - ''' + script_fpath = temp_dpath / 'script.py' + script_fpath.write_text( + ub.codeblock( + """ from test_mod import submod1 from test_mod import submod2 from test_mod.subpkg import submod3 @@ -311,7 +378,9 @@ def main(): print(result) main() - ''')) + """ + ) + ) return script_fpath @@ -327,15 +396,26 @@ def test_autoprofile_script_with_module(): # args = [sys.executable, '-m', 'kernprof', '--prof-imports', # '-p', 'script.py', '-l', os.fspath(script_fpath)] - args = [sys.executable, '-m', 'kernprof', '-p', 'script.py', '-l', - os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'script.py', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args, cwd=temp_dpath, verbose=2) print(proc.stdout) print(proc.stderr) proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args, cwd=temp_dpath) raw_output = proc.stdout print(raw_output) @@ -356,8 +436,15 @@ def test_autoprofile_module(static_resolution): script_fpath = _write_demo_module(temp_dpath) - args = [sys.executable, '-m', 'kernprof', '-p', 'test_mod', '-l', - os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'test_mod', + '-l', + os.fspath(script_fpath), + ] env = dict(os.environ) if static_resolution: env['LINE_PROFILER_STATIC_ANALYSIS'] = '1' @@ -368,8 +455,12 @@ def test_autoprofile_module(static_resolution): print(proc.stderr) proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args, cwd=temp_dpath) raw_output = proc.stdout print(raw_output) @@ -390,16 +481,26 @@ def test_autoprofile_module_list(): # args = [sys.executable, '-m', 'kernprof', '--prof-imports', # '-p', 'script.py', '-l', os.fspath(script_fpath)] - args = [sys.executable, '-m', 'kernprof', - '-p', 'test_mod.submod1,test_mod.subpkg.submod3', '-l', - os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'test_mod.submod1,test_mod.subpkg.submod3', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args, cwd=temp_dpath, verbose=2) print(proc.stdout) print(proc.stderr) proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args, cwd=temp_dpath) raw_output = proc.stdout print(raw_output) @@ -419,15 +520,27 @@ def test_autoprofile_module_with_prof_imports(): temp_dpath = ub.Path(tmp) script_fpath = _write_demo_module(temp_dpath) - args = [sys.executable, '-m', 'kernprof', '--prof-imports', - '-p', 'test_mod.submod1', '-l', os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '--prof-imports', + '-p', + 'test_mod.submod1', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args, cwd=temp_dpath, verbose=2) print(proc.stdout) print(proc.stderr) proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args, cwd=temp_dpath) raw_output = proc.stdout print(raw_output) @@ -452,8 +565,16 @@ def test_autoprofile_script_with_prof_imports(): # import pytest # pytest.skip('Failing due to the noop bug') - args = [sys.executable, '-m', 'kernprof', '--prof-imports', - '-p', 'script.py', '-l', os.fspath(script_fpath)] + args = [ + sys.executable, + '-m', + 'kernprof', + '--prof-imports', + '-p', + 'script.py', + '-l', + os.fspath(script_fpath), + ] proc = ub.cmd(args, cwd=temp_dpath, verbose=0) print('Kernprof Stdout:') print(proc.stdout) @@ -462,8 +583,12 @@ def test_autoprofile_script_with_prof_imports(): print('About to check kernprof return code') proc.check_returncode() - args = [sys.executable, '-m', 'line_profiler', - os.fspath(script_fpath) + '.lprof'] + args = [ + sys.executable, + '-m', + 'line_profiler', + os.fspath(script_fpath) + '.lprof', + ] proc = ub.cmd(args, cwd=temp_dpath, verbose=0) raw_output = proc.stdout print('Line_profile Stdout:') @@ -480,22 +605,38 @@ def test_autoprofile_script_with_prof_imports(): @pytest.mark.parametrize( ('use_kernprof_exec', 'prof_mod', 'flags', 'profiled_funcs'), - [(False, ['test_mod.submod1'], '', {'add_one', 'add_operator'}), - # By not using `--no-preimports`, the entirety of `.submod1` is - # passed to `add_imported_function_or_module()` - (False, ['test_mod.submod1'], '--no-preimports', {'add_one'}), - (False, ['test_mod.submod2'], - '--prof-imports', {'add_two', 'add_operator'}), - (False, ['test_mod'], - '--prof-imports', {'add_one', 'add_two', 'add_operator', '_main'}), - # Explicitly add all the modules via multiple `-p` flags, without - # using the `--prof-imports` flag - (False, ['test_mod', 'test_mod.submod1,test_mod.submod2'], - '', {'add_one', 'add_two', 'add_operator', '_main'}), - (False, [], '--prof-imports', set()), - (True, [], '--prof-imports', set())]) -def test_autoprofile_exec_package(use_kernprof_exec, prof_mod, - flags, profiled_funcs): + [ + (False, ['test_mod.submod1'], '', {'add_one', 'add_operator'}), + # By not using `--no-preimports`, the entirety of `.submod1` is + # passed to `add_imported_function_or_module()` + (False, ['test_mod.submod1'], '--no-preimports', {'add_one'}), + ( + False, + ['test_mod.submod2'], + '--prof-imports', + {'add_two', 'add_operator'}, + ), + ( + False, + ['test_mod'], + '--prof-imports', + {'add_one', 'add_two', 'add_operator', '_main'}, + ), + # Explicitly add all the modules via multiple `-p` flags, without + # using the `--prof-imports` flag + ( + False, + ['test_mod', 'test_mod.submod1,test_mod.submod2'], + '', + {'add_one', 'add_two', 'add_operator', '_main'}, + ), + (False, [], '--prof-imports', set()), + (True, [], '--prof-imports', set()), + ], +) +def test_autoprofile_exec_package( + use_kernprof_exec, prof_mod, flags, profiled_funcs +): """ Test the execution of a package. """ @@ -535,30 +676,55 @@ def test_autoprofile_exec_package(use_kernprof_exec, prof_mod, @pytest.mark.parametrize( ('use_kernprof_exec', 'prof_mod', 'flags', 'profiled_funcs'), - [(False, 'test_mod.submod2,test_mod.subpkg.submod3.add_three', - '--no-preimports', {'add_two'}), - # By not using `--no-preimports`: - # - The entirety of `.submod2` is passed to - # `add_imported_function_or_module()` - # - Despite not having been imported anywhere, `add_three()` is - # still profiled - (False, 'test_mod.submod2,test_mod.subpkg.submod3.add_three', - '', {'add_two', 'add_three', 'add_operator'}), - (False, 'test_mod.submod1', '', {'add_one', 'add_operator'}), - (False, 'test_mod.subpkg.submod4', - '--prof-imports', - {'add_one', 'add_two', 'add_four', 'add_operator', '_main'}), - (False, None, '--prof-imports', {}), - (True, None, '--prof-imports', {}), - # Packages are descended into by default, unless they are specified - # with `.__init__` - (False, 'test_mod', '', - {'add_one', 'add_two', 'add_three', 'add_four', 'add_operator', - '_main'}), - (False, 'test_mod.subpkg', '', {'add_three', 'add_four', '_main'}), - (False, 'test_mod.subpkg.__init__', '', {'add_three'})]) -def test_autoprofile_exec_module(use_kernprof_exec, prof_mod, - flags, profiled_funcs): + [ + ( + False, + 'test_mod.submod2,test_mod.subpkg.submod3.add_three', + '--no-preimports', + {'add_two'}, + ), + # By not using `--no-preimports`: + # - The entirety of `.submod2` is passed to + # `add_imported_function_or_module()` + # - Despite not having been imported anywhere, `add_three()` is + # still profiled + ( + False, + 'test_mod.submod2,test_mod.subpkg.submod3.add_three', + '', + {'add_two', 'add_three', 'add_operator'}, + ), + (False, 'test_mod.submod1', '', {'add_one', 'add_operator'}), + ( + False, + 'test_mod.subpkg.submod4', + '--prof-imports', + {'add_one', 'add_two', 'add_four', 'add_operator', '_main'}, + ), + (False, None, '--prof-imports', {}), + (True, None, '--prof-imports', {}), + # Packages are descended into by default, unless they are specified + # with `.__init__` + ( + False, + 'test_mod', + '', + { + 'add_one', + 'add_two', + 'add_three', + 'add_four', + 'add_operator', + '_main', + }, + ), + (False, 'test_mod.subpkg', '', {'add_three', 'add_four', '_main'}), + (False, 'test_mod.subpkg.__init__', '', {'add_three'}), + ], +) +def test_autoprofile_exec_module( + use_kernprof_exec, prof_mod, flags, profiled_funcs +): """ Test the execution of a module. """ @@ -567,8 +733,14 @@ def test_autoprofile_exec_module(use_kernprof_exec, prof_mod, _write_demo_module(temp_dpath) # Sanity check - all_checked_funcs = {'add_one', 'add_two', 'add_three', 'add_four', - 'add_operator', '_main'} + all_checked_funcs = { + 'add_one', + 'add_two', + 'add_three', + 'add_four', + 'add_operator', + '_main', + } profiled_funcs = set(profiled_funcs) assert profiled_funcs <= all_checked_funcs @@ -601,10 +773,14 @@ def test_autoprofile_exec_module(use_kernprof_exec, prof_mod, @pytest.mark.parametrize('prof_mod', [True, False]) @pytest.mark.parametrize( ('outfile', 'expected_outfile'), - [(None, 'kernprof-stdin-*.lprof'), - ('test-stdin.lprof', 'test-stdin.lprof')]) + [ + (None, 'kernprof-stdin-*.lprof'), + ('test-stdin.lprof', 'test-stdin.lprof'), + ], +) def test_autoprofile_from_stdin( - outfile, expected_outfile, prof_mod, view) -> None: + outfile, expected_outfile, prof_mod, view +) -> None: """ Test the profiling of a script read from stdin. """ @@ -613,7 +789,7 @@ def test_autoprofile_from_stdin( kp_cmd = [sys.executable, '-m', 'kernprof', '-l'] if prof_mod: - kp_cmd += ['-p' 'test_mod.submod1,test_mod.subpkg.submod3'] + kp_cmd += ['-ptest_mod.submod1,test_mod.subpkg.submod3'] if outfile: kp_cmd += ['-o', outfile] if view: @@ -621,15 +797,17 @@ def test_autoprofile_from_stdin( kp_cmd += ['-'] with ub.ChDir(temp_dpath): script_fpath = _write_demo_module(ub.Path()) - proc = subprocess.run(kp_cmd, - input=script_fpath.read_text(), - text=True, - capture_output=True) + proc = subprocess.run( + kp_cmd, + input=script_fpath.read_text(), + text=True, + capture_output=True, + ) print(proc.stdout) print(proc.stderr) proc.check_returncode() - outfile, = temp_dpath.glob(expected_outfile) + (outfile,) = temp_dpath.glob(expected_outfile) lp_cmd = [sys.executable, '-m', 'line_profiler', str(outfile)] lp_proc = ub.cmd(lp_cmd) lp_proc.check_returncode() @@ -652,8 +830,11 @@ def test_autoprofile_from_stdin( @pytest.mark.parametrize( ('outfile', 'expected_outfile'), - [(None, 'kernprof-command-*.lprof'), - ('test-command.lprof', 'test-command.lprof')]) + [ + (None, 'kernprof-command-*.lprof'), + ('test-command.lprof', 'test-command.lprof'), + ], +) def test_autoprofile_from_inlined_script(outfile, expected_outfile) -> None: """ Test the profiling of an inlined script (supplied with the `-c` @@ -664,18 +845,26 @@ def test_autoprofile_from_inlined_script(outfile, expected_outfile) -> None: _write_demo_module(temp_dpath) - inlined_script = ('from test_mod import submod1, submod2; ' - 'from test_mod.subpkg import submod3; ' - 'import statistics; ' - 'data = [1, 2, 3]; ' - 'val = submod1.add_one(data); ' - 'val = submod2.add_two(val); ' - 'val = submod3.add_three(val); ' - 'result = statistics.harmonic_mean(val); ' - 'print(result);') - - kp_cmd = [sys.executable, '-m', 'kernprof', - '-p', 'test_mod.submod1,test_mod.subpkg.submod3', '-l'] + inlined_script = ( + 'from test_mod import submod1, submod2; ' + 'from test_mod.subpkg import submod3; ' + 'import statistics; ' + 'data = [1, 2, 3]; ' + 'val = submod1.add_one(data); ' + 'val = submod2.add_two(val); ' + 'val = submod3.add_three(val); ' + 'result = statistics.harmonic_mean(val); ' + 'print(result);' + ) + + kp_cmd = [ + sys.executable, + '-m', + 'kernprof', + '-p', + 'test_mod.submod1,test_mod.subpkg.submod3', + '-l', + ] if outfile: kp_cmd += ['-o', outfile] kp_cmd += ['-c', inlined_script] @@ -683,7 +872,7 @@ def test_autoprofile_from_inlined_script(outfile, expected_outfile) -> None: print(proc.stdout) print(proc.stderr) proc.check_returncode() - outfile, = temp_dpath.glob(expected_outfile) + (outfile,) = temp_dpath.glob(expected_outfile) lp_cmd = [sys.executable, '-m', 'line_profiler', str(outfile)] proc = ub.cmd(lp_cmd) raw_output = proc.stdout @@ -697,15 +886,26 @@ def test_autoprofile_from_inlined_script(outfile, expected_outfile) -> None: @pytest.mark.parametrize( ('explicit_config', 'prof_mod', 'prof_imports', 'profiled_funcs'), - [(True, 'test_mod.submod2', False, {'add_two', 'add_operator'}), - (False, None, False, {'add_one', 'add_operator'}), - (True, 'test_mod.subpkg.submod4', None, - {'add_one', 'add_two', 'add_four', 'add_operator', '_main'}), - (False, - '', # This negates the `prof-mod` configued in the TOML file - True, {})]) + [ + (True, 'test_mod.submod2', False, {'add_two', 'add_operator'}), + (False, None, False, {'add_one', 'add_operator'}), + ( + True, + 'test_mod.subpkg.submod4', + None, + {'add_one', 'add_two', 'add_four', 'add_operator', '_main'}, + ), + ( + False, + '', # This negates the `prof-mod` configued in the TOML file + True, + {}, + ), + ], +) def test_autoprofile_with_customized_config( - explicit_config, prof_mod, prof_imports, profiled_funcs): + explicit_config, prof_mod, prof_imports, profiled_funcs +): """ Test autoprofiling a module with a customized TOML config file. @@ -724,15 +924,27 @@ def test_autoprofile_with_customized_config( > line = 8 # 2 wider than the default """ docstring = test_autoprofile_with_customized_config.__doc__ - toml_content = ub.codeblock('\n'.join( - line.lstrip('>') - for line in (ub.codeblock(docstring).strip('\n') - .partition('----\n')[-1].splitlines()))) + toml_content = ub.codeblock( + '\n'.join( + line.lstrip('>') + for line in ( + ub.codeblock(docstring) + .strip('\n') + .partition('----\n')[-1] + .splitlines() + ) + ) + ) lineno_col_width = 8 # Sanity check - all_checked_funcs = {'add_one', 'add_two', 'add_four', - 'add_operator', '_main'} + all_checked_funcs = { + 'add_one', + 'add_two', + 'add_four', + 'add_operator', + '_main', + } profiled_funcs = set(profiled_funcs) assert profiled_funcs <= all_checked_funcs @@ -754,8 +966,9 @@ def test_autoprofile_with_customized_config( if prof_mod is not None: kernprof_cmd.extend(['-p', prof_mod]) if prof_imports in (True, False): - kernprof_cmd.append('--{}prof-imports' - .format('' if prof_imports else 'no-')) + kernprof_cmd.append( + '--{}prof-imports'.format('' if prof_imports else 'no-') + ) kernprof_cmd.extend(['-m', 'test_mod.subpkg.submod4', '1', '2', '3']) proc = ub.cmd(kernprof_cmd, cwd=temp_dpath, env=env, verbose=2) print(proc.stdout) @@ -794,10 +1007,10 @@ def test_autoprofile_with_no_config(no_config, view_in_process): """ Test disabling config lookup with the `--no-config` flag. """ - toml_content = ub.codeblock(''' + toml_content = ub.codeblock(""" [tool.line_profiler.show.column_widths] line = 8 # 2 wider than the default - ''') + """) lineno_col_width = 6 if no_config else 8 with tempfile.TemporaryDirectory() as tmp: @@ -807,10 +1020,14 @@ def test_autoprofile_with_no_config(no_config, view_in_process): toml.write_text(toml_content) prof = temp_dpath / 'my_output.lprof' - kernprof_cmd = ['kernprof', - '-p', 'test_mod.subpkg.submod4', - '-o', 'my_output.lprof', - '-l'] + kernprof_cmd = [ + 'kernprof', + '-p', + 'test_mod.subpkg.submod4', + '-o', + 'my_output.lprof', + '-l', + ] lp_cmd = [sys.executable, '-m', 'line_profiler', os.fspath(prof)] if view_in_process: kernprof_cmd.append('--view') @@ -855,12 +1072,31 @@ def test_autoprofile_with_no_config(no_config, view_in_process): @pytest.mark.parametrize( ('prof_mod', 'profiled_funcs'), - [('my_module', - {'function', 'method', 'class_method', 'static_method', 'descriptor'}), - # `function()` included in profiling via `Class.partial_method()` - ('my_module.Class', - {'function', 'method', 'class_method', 'static_method', 'descriptor'}), - ('my_module.Class.descriptor', {'descriptor'})]) + [ + ( + 'my_module', + { + 'function', + 'method', + 'class_method', + 'static_method', + 'descriptor', + }, + ), + # `function()` included in profiling via `Class.partial_method()` + ( + 'my_module.Class', + { + 'function', + 'method', + 'class_method', + 'static_method', + 'descriptor', + }, + ), + ('my_module.Class.descriptor', {'descriptor'}), + ], +) def test_autoprofile_callable_wrapper_objects(prof_mod, profiled_funcs): """ Test that on-import profiling catches various callable-wrapper @@ -872,9 +1108,14 @@ def test_autoprofile_callable_wrapper_objects(prof_mod, profiled_funcs): Like it does regular methods and functions. """ # Sanity check - all_checked_funcs = {'function', 'method', - 'partial_method', 'class_method', 'static_method', - 'descriptor'} + all_checked_funcs = { + 'function', + 'method', + 'partial_method', + 'class_method', + 'static_method', + 'descriptor', + } profiled_funcs = set(profiled_funcs) assert profiled_funcs <= all_checked_funcs # Note: `partial_method()` not to be included as its own item @@ -885,7 +1126,8 @@ def test_autoprofile_callable_wrapper_objects(prof_mod, profiled_funcs): temp_dpath = ub.Path(tmpdir) path = temp_dpath / 'path' path.mkdir() - (path / 'my_module.py').write_text(ub.codeblock(""" + (path / 'my_module.py').write_text( + ub.codeblock(""" import functools @@ -910,26 +1152,36 @@ def static_method(): @property def descriptor(self): return - """)) - (temp_dpath / 'script.py').write_text(ub.codeblock(""" + """) + ) + (temp_dpath / 'script.py').write_text( + ub.codeblock(""" import my_module if __name__ == '__main__': pass - """)) + """) + ) with ub.ChDir(temp_dpath): - args = [sys.executable, '-m', 'kernprof', - '-p', prof_mod, '-lv', 'script.py'] + args = [ + sys.executable, + '-m', + 'kernprof', + '-p', + prof_mod, + '-lv', + 'script.py', + ] python_path = os.environ.get('PYTHONPATH') if python_path: python_path = '{}:{}'.format(path, python_path) else: python_path = str(path) - proc = ub.cmd(args, - env={**os.environ, 'PYTHONPATH': python_path}, - verbose=2) + proc = ub.cmd( + args, env={**os.environ, 'PYTHONPATH': python_path}, verbose=2 + ) raw_output = proc.stdout print(raw_output) print(proc.stderr) @@ -941,7 +1193,7 @@ def descriptor(self): prefix = r'.*\.' else: prefix = '' - in_output = re.search(f'^Function: {prefix}{func}', - raw_output, - re.MULTILINE) + in_output = re.search( + f'^Function: {prefix}{func}', raw_output, re.MULTILINE + ) assert bool(in_output) == (func in profiled_funcs) diff --git a/tests/test_cli.py b/tests/test_cli.py index c36aa899..ed634f42 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -28,9 +28,10 @@ def parser(): -c -> c """ parser = ArgumentParser( - formatter_class=partial(HelpFormatter, - max_help_position=float('inf'), - width=float('inf'))) + formatter_class=partial( + HelpFormatter, max_help_position=float('inf'), width=float('inf') + ) + ) # Normal boolean flag (w/2 short forms) # -> adds 3 actions (long, short, long-negated) add_argument(parser, '-f', '-F', '--foo', action='store_true') @@ -39,19 +40,26 @@ def parser(): add_argument(parser, '-b', '--bar', action='store_true', help='Set `bar`') # Boolean flag w/parenthetical remark in help text # -> adds 3 actions (long, short, long-negated) - add_argument(parser, '-B', '--baz', - action='store_true', help='Set `baz` (BAZ)') + add_argument( + parser, '-B', '--baz', action='store_true', help='Set `baz` (BAZ)' + ) # Negative boolean flag # -> adds 1 action (long-negated) - add_argument(parser, '--no-spam', - action='store_false', dest='spam', help='Set `spam` to false') + add_argument( + parser, + '--no-spam', + action='store_false', + dest='spam', + help='Set `spam` to false', + ) # Boolean flag w/o short form # -> adds 2 actions (long, long-negated) add_argument(parser, '--ham', action='store_true', help='Set `ham`') # Short-form-only boolean flag # -> adds 1 action (short) - add_argument(parser, '-e', - action='store_true', dest='eggs', help='Set `eggs`') + add_argument( + parser, '-e', action='store_true', dest='eggs', help='Set `eggs`' + ) yield parser @@ -65,41 +73,59 @@ def test_boolean_argument_help_text(parser): parser.print_help(sio) help_text = sio.getvalue() matches = partial(re.search, string=help_text, flags=re.MULTILINE) - assert matches(r'^ --foo \[.*\] +' - + re.escape('(Short forms: -f, -F)') - + '$') - assert matches(r'^ --bar \[.*\] +' - + re.escape('Set `bar` (Short form: -b)') - + '$') - assert matches(r'^ --baz \[.*\] +' - + re.escape('Set `baz` (BAZ; short form: -B)') - + '$') - assert matches(r'^ --no-spam \[.*\] +' - + re.escape('Set `spam` to false') - + '$') - assert matches(r'^ --ham \[.*\] +' - + re.escape('Set `ham`') - + '$') - assert matches(r'^ -e +' - + re.escape('Set `eggs`') - + '$') + assert matches( + r'^ --foo \[.*\] +' + re.escape('(Short forms: -f, -F)') + '$' + ) + assert matches( + r'^ --bar \[.*\] +' + re.escape('Set `bar` (Short form: -b)') + '$' + ) + assert matches( + r'^ --baz \[.*\] +' + + re.escape('Set `baz` (BAZ; short form: -B)') + + '$' + ) + assert matches( + r'^ --no-spam \[.*\] +' + re.escape('Set `spam` to false') + '$' + ) + assert matches(r'^ --ham \[.*\] +' + re.escape('Set `ham`') + '$') + assert matches(r'^ -e +' + re.escape('Set `eggs`') + '$') @pytest.mark.parametrize( ('args', 'foo', 'bar', 'baz', 'spam', 'ham', 'eggs', 'expect_error'), - [('--foo q', *((None,) * 6), True), # Can't parse `q` into boolean - ('-fbB' # Test short-flag concatenation - ' --ham=', # Empty string -> set to false - True, True, True, None, False, None, False), - ('--foo' # No-arg -> set to true - ' --bar=0' # Falsy arg -> set to false - ' --no-baz' # No-arg (negated flag) -> set to false - ' --no-spam=no' # Falsy arg (negated flag) -> set to true - ' --ham=on' # Truey arg -> set to true - ' -e', # No-arg -> set to true - True, False, False, True, True, True, False)]) + [ + ('--foo q', *((None,) * 6), True), # Can't parse `q` into boolean + ( + '-fbB' # Test short-flag concatenation + ' --ham=', # Empty string -> set to false + True, + True, + True, + None, + False, + None, + False, + ), + ( + '--foo' # No-arg -> set to true + ' --bar=0' # Falsy arg -> set to false + ' --no-baz' # No-arg (negated flag) -> set to false + ' --no-spam=no' # Falsy arg (negated flag) -> set to true + ' --ham=on' # Truey arg -> set to true + ' -e', # No-arg -> set to true + True, + False, + False, + True, + True, + True, + False, + ), + ], +) def test_boolean_argument_parsing( - parser, capsys, args, foo, bar, baz, spam, ham, eggs, expect_error): + parser, capsys, args, foo, bar, baz, spam, ham, eggs, expect_error +): """ Test the handling of boolean flags. """ @@ -131,7 +157,7 @@ def test_cli(): """ # Create a dummy source file code = ub.codeblock( - ''' + """ @profile def my_inefficient_function(): a = 0 @@ -142,7 +168,8 @@ def my_inefficient_function(): if __name__ == '__main__': my_inefficient_function() - ''') + """ + ) with TemporaryDirectory() as tmp_dpath: tmp_src_fpath = join(tmp_dpath, 'foo.py') with open(tmp_src_fpath, 'w') as file: @@ -155,8 +182,11 @@ def my_inefficient_function(): tmp_lprof_fpath = join(tmp_dpath, 'foo.py.lprof') tmp_lprof_fpath - info = ub.cmd(f'{executable} -m line_profiler {tmp_lprof_fpath}', - cwd=tmp_dpath, verbose=3) + info = ub.cmd( + f'{executable} -m line_profiler {tmp_lprof_fpath}', + cwd=tmp_dpath, + verbose=3, + ) assert info['ret'] == 0 # Check for some patterns that should be in the output assert '% Time' in info['out'] @@ -168,6 +198,7 @@ def test_multiple_lprof_files(capsys): Test that we can aggregate profiling results with ``python -m line_profiler``. """ + def sum_n(n: int) -> int: x = 0 for n in range(1, n + 1): @@ -180,18 +211,23 @@ def sum_nsq(n: int) -> int: x += n * n # Loop: sum_nsq return x # Return: sum_nsq - profs = {0: LineProfiler(sum_n), - 1: LineProfiler(sum_nsq), - 2: LineProfiler(sum_n, sum_nsq)} + profs = { + 0: LineProfiler(sum_n), + 1: LineProfiler(sum_nsq), + 2: LineProfiler(sum_n, sum_nsq), + } with TemporaryDirectory() as tmp_dpath: # Write several profiling output files stats_files = [] nhits = {} - for i, (func, n, expected) in enumerate([ + for i, (func, n, expected) in enumerate( + [ (sum_n, 10, 10 * 11 // 2), (sum_nsq, 20, 20 * 21 * 41 // 6), - (sum_n, 30, 30 * 31 // 2)]): + (sum_n, 30, 30 * 31 // 2), + ] + ): prof = profs[i] with prof: assert func(n) == expected @@ -218,7 +254,7 @@ def sum_nsq(n: int) -> int: checks[f' # {comment}: {func.__name__}'] = n for line in out.splitlines(): try: - suffix, = (suffix for suffix in checks if line.endswith(suffix)) + (suffix,) = (suffix for suffix in checks if line.endswith(suffix)) except ValueError: # No match continue assert int(line.split()[1]) == checks.pop(suffix) diff --git a/tests/test_complex_case.py b/tests/test_complex_case.py index ffc5e88b..63a6d4e7 100644 --- a/tests/test_complex_case.py +++ b/tests/test_complex_case.py @@ -4,6 +4,7 @@ import tempfile from contextlib import ExitStack import ubelt as ub + LINUX = sys.platform.startswith('linux') @@ -22,7 +23,12 @@ def test_complex_example_python_none(): Make sure the complex example script works without any profiling """ complex_fpath = get_complex_example_fpath() - info = ub.cmd(f'{sys.executable} {complex_fpath}', shell=True, verbose=3, env=ub.udict(os.environ) | {'PROFILE_TYPE': 'none'}) + info = ub.cmd( + f'{sys.executable} {complex_fpath}', + shell=True, + verbose=3, + env=ub.udict(os.environ) | {'PROFILE_TYPE': 'none'}, + ) assert info.stdout == '' info.check_returncode() @@ -37,7 +43,7 @@ def test_varied_complex_invocations(): # Enumerate valid cases to test cases = [] - for runner in ['python', 'kernprof']: + for runner in ['python', 'kernprof']: for env_line_profile in ['0', '1']: if runner == 'kernprof': for profile_type in ['explicit', 'implicit']: @@ -47,45 +53,53 @@ def test_varied_complex_invocations(): else: outpath = 'complex_example.py.prof' - cases.append({ - 'runner': runner, - 'kern_flags': kern_flags, - 'env_line_profile': env_line_profile, - 'profile_type': profile_type, - 'outpath': outpath, - }) + cases.append( + { + 'runner': runner, + 'kern_flags': kern_flags, + 'env_line_profile': env_line_profile, + 'profile_type': profile_type, + 'outpath': outpath, + } + ) else: if env_line_profile == '1': outpath = 'profile_output.txt' else: outpath = None - cases.append({ - 'runner': runner, - 'env_line_profile': env_line_profile, - 'profile_type': 'explicit', - 'outpath': outpath, - }) + cases.append( + { + 'runner': runner, + 'env_line_profile': env_line_profile, + 'profile_type': 'explicit', + 'outpath': outpath, + } + ) # Add case for auto-profile # FIXME: this runs, but doesn't quite work. - cases.append({ - 'runner': 'kernprof', - 'kern_flags': '-l --prof-mod complex_example.py', - 'env_line_profile': '0', - 'profile_type': 'none', - 'outpath': 'complex_example.py.lprof', - 'ignore_checks': True, - }) - - if 0: - # FIXME: this does not run with prof-imports - cases.append({ + cases.append( + { 'runner': 'kernprof', - 'kern_flags': '-l --prof-imports --prof-mod complex_example.py', + 'kern_flags': '-l --prof-mod complex_example.py', 'env_line_profile': '0', 'profile_type': 'none', 'outpath': 'complex_example.py.lprof', - }) + 'ignore_checks': True, + } + ) + + if 0: + # FIXME: this does not run with prof-imports + cases.append( + { + 'runner': 'kernprof', + 'kern_flags': '-l --prof-imports --prof-mod complex_example.py', + 'env_line_profile': '0', + 'profile_type': 'none', + 'outpath': 'complex_example.py.lprof', + } + ) complex_fpath = get_complex_example_fpath() @@ -111,17 +125,19 @@ def test_varied_complex_invocations(): prog_flags = ' --process_size=0' runner = f'{sys.executable} -m kernprof {kern_flags}' else: - env['LINE_PROFILE'] = case["env_line_profile"] + env['LINE_PROFILE'] = case['env_line_profile'] runner = f'{sys.executable}' prog_flags = '' - env['PROFILE_TYPE'] = case["profile_type"] + env['PROFILE_TYPE'] = case['profile_type'] command = f'{runner} {complex_fpath}' + prog_flags HAS_SHELL = LINUX if HAS_SHELL: # Use shell because it gives a indication of what is happening environ_prefix = ' '.join([f'{k}={v}' for k, v in env.items()]) - info = ub.cmd(environ_prefix + ' ' + command, shell=True, verbose=3) + info = ub.cmd( + environ_prefix + ' ' + command, shell=True, verbose=3 + ) else: env = ub.udict(os.environ) | env info = ub.cmd(command, env=env, verbose=3) @@ -143,6 +159,7 @@ def test_varied_complex_invocations(): if 0: import pandas as pd import rich + table = pd.DataFrame(results) rich.print(table) diff --git a/tests/test_cython.py b/tests/test_cython.py index 11b402d5..0e948904 100644 --- a/tests/test_cython.py +++ b/tests/test_cython.py @@ -1,6 +1,7 @@ """ Tests for profiling Cython code. """ + from __future__ import annotations import math import os @@ -18,9 +19,13 @@ import pytest from line_profiler._line_profiler import ( - CANNOT_LINE_TRACE_CYTHON, find_cython_source_file) + CANNOT_LINE_TRACE_CYTHON, + find_cython_source_file, +) from line_profiler.line_profiler import ( # type:ignore[attr-defined] - get_code_block, LineProfiler) + get_code_block, + LineProfiler, +) def propose_name(prefix: str) -> Generator[str, None, None]: @@ -29,15 +34,16 @@ def propose_name(prefix: str) -> Generator[str, None, None]: def _install_cython_example( - tmp_path_factory: pytest.TempPathFactory, - editable: bool) -> Generator[Tuple[Path, str], None, None]: + tmp_path_factory: pytest.TempPathFactory, editable: bool +) -> Generator[Tuple[Path, str], None, None]: """ Install the example Cython module in a name-clash-free manner. """ source = Path(__file__).parent / 'cython_example' assert source.is_dir() - module = next(name for name in propose_name('cython_example') - if not find_spec(name)) + module = next( + name for name in propose_name('cython_example') if not find_spec(name) + ) replace = methodcaller('replace', 'cython_example', module) pip = [sys.executable, '-m', 'pip'] tmp_path = tmp_path_factory.mktemp('cython_example') @@ -49,14 +55,15 @@ def _install_cython_example( _, ext = os.path.splitext(fname) if ext not in {'.py', '.pyx', '.pxd', '.toml'}: continue - dir_out = tmp_path.joinpath(*( - replace(part) for part in dir_in.relative_to(source).parts)) + dir_out = tmp_path.joinpath( + *(replace(part) for part in dir_in.relative_to(source).parts) + ) dir_out.mkdir(exist_ok=True) file_in = dir_in / fname file_out = dir_out / replace(fname) file_out.write_text(replace(file_in.read_text())) # There should only be one Cython source file - cython_source, = tmp_path.glob('*.pyx') + (cython_source,) = tmp_path.glob('*.pyx') pip_install = pip + ['install', '--verbose'] if editable: pip_install += ['--editable', str(tmp_path)] @@ -98,15 +105,18 @@ def test_recover_cython_source(cython_example: Tuple[Path, ModuleType]) -> None: assert source assert expected_source.samefile(source) source_lines = get_code_block(source, func.__code__.co_firstlineno) - for line, prefix in [(source_lines[0], '# Start: '), - (source_lines[-1], '# End: ')]: + for line, prefix in [ + (source_lines[0], '# Start: '), + (source_lines[-1], '# End: '), + ]: assert line.rstrip('\n').endswith(prefix + func.__name__) @pytest.mark.skipif( CANNOT_LINE_TRACE_CYTHON, reason='Cannot line-trace Cython code in version ' - + '.'.join(str(v) for v in sys.version_info[:3])) + + '.'.join(str(v) for v in sys.version_info[:3]), +) def test_profile_cython_source(cython_example: Tuple[Path, ModuleType]) -> None: """ Check that calls to Cython functions (built with the appropriate @@ -118,11 +128,13 @@ def test_profile_cython_source(cython_example: Tuple[Path, ModuleType]) -> None: _, module = cython_example cos = prof_cos(module.cos) sin = prof_sin(module.sin) - assert pytest.approx(cos(.125, 10)) == math.cos(.125) - assert pytest.approx(sin(2.5, 3)) == 2.5 - 2.5 ** 3 / 6 + 2.5 ** 5 / 120 + assert pytest.approx(cos(0.125, 10)) == math.cos(0.125) + assert pytest.approx(sin(2.5, 3)) == 2.5 - 2.5**3 / 6 + 2.5**5 / 120 for prof, func, expected_nhits in [ - (prof_cos, 'cos', 10), (prof_sin, 'sin', 3)]: + (prof_cos, 'cos', 10), + (prof_sin, 'sin', 3), + ]: with StringIO() as fobj: prof.print_stats(fobj) result = fobj.getvalue() diff --git a/tests/test_eager_preimports.py b/tests/test_eager_preimports.py index 79ba6035..d0ed0f92 100644 --- a/tests/test_eager_preimports.py +++ b/tests/test_eager_preimports.py @@ -5,6 +5,7 @@ ----- Most of the features are already covered by the doctests. """ + from __future__ import annotations import subprocess import sys @@ -21,6 +22,7 @@ from warnings import catch_warnings import pytest + try: import flake8 # noqa: F401 except ImportError: @@ -29,7 +31,10 @@ HAS_FLAKE8 = True from line_profiler.autoprofile.eager_preimports import ( - split_dotted_path, resolve_profiling_targets, write_eager_import_module) + split_dotted_path, + resolve_profiling_targets, + write_eager_import_module, +) def write(path: Path, content: Optional[str] = None) -> None: @@ -64,19 +69,22 @@ def sample_package(preserve_sys_state: None, tmp_path: Path) -> str: Write a normal package and put it in :py:data:`sys.path`. When we're done, reset :py:data:`sys.path` and `sys.modules`. """ - module_name = next(name for name in gen_names('my_sample_pkg') - if name not in sys.modules) + module_name = next( + name for name in gen_names('my_sample_pkg') if name not in sys.modules + ) new_path = tmp_path / '_modules' write(new_path / module_name / '__init__.py') write(new_path / module_name / 'foo' / '__init__.py') write(new_path / module_name / 'foo' / 'bar.py') - write(new_path / module_name / 'foo' / 'baz.py', - """ + write( + new_path / module_name / 'foo' / 'baz.py', + """ ''' This is a bad module. ''' raise AssertionError - """) + """, + ) write(new_path / module_name / 'foobar.py') # Cleanup managed with `preserve_sys_state()` sys.path.insert(0, str(new_path)) @@ -85,16 +93,20 @@ def sample_package(preserve_sys_state: None, tmp_path: Path) -> str: @pytest.fixture def sample_namespace_package( - preserve_sys_state: None, - tmp_path_factory: pytest.TempPathFactory) -> str: + preserve_sys_state: None, tmp_path_factory: pytest.TempPathFactory +) -> str: """ Write a namespace package and put it in :py:data:`sys.path`. When we're done, reset :py:data:`sys.path` and `sys.modules`. """ - module_name = next(name for name in gen_names('my_sample_namespace_pkg') - if name not in sys.modules) - new_paths = [tmp_path_factory.mktemp('_modules-', numbered=True) - for _ in range(3)] + module_name = next( + name + for name in gen_names('my_sample_namespace_pkg') + if name not in sys.modules + ) + new_paths = [ + tmp_path_factory.mktemp('_modules-', numbered=True) for _ in range(3) + ] for submod, new_path in zip(['one', 'two', 'three'], new_paths): write(new_path / module_name / (submod + '.py')) # Cleanup managed with `preserve_sys_state()` @@ -104,9 +116,11 @@ def sample_namespace_package( @pytest.mark.parametrize( ('adder', 'xc'), - [('foo; bar', ValueError), (1, TypeError), ('(foo\n .bar)', ValueError)]) + [('foo; bar', ValueError), (1, TypeError), ('(foo\n .bar)', ValueError)], +) def test_write_eager_import_module_wrong_adder( - adder: str, xc: Type[Exception]) -> None: + adder: str, xc: Type[Exception] +) -> None: """ Test passing an erroneous ``adder`` to :py:meth:`~.write_eager_import_module()`. @@ -127,45 +141,72 @@ def test_written_module_pep8_compliance(sample_package: str): with module.open(mode='w') as fobj: write_eager_import_module( [sample_package + '.foobar'], - recurse=[sample_package + '.foo'], stream=fobj) + recurse=[sample_package + '.foo'], + stream=fobj, + ) print(module.read_text()) - (subprocess - .run([sys.executable, '-m', 'flake8', - '--extend-ignore=E501', # Allow long lines - module]) - .check_returncode()) + ( + subprocess.run( + [ + sys.executable, + '-m', + 'flake8', + '--extend-ignore=E501', # Allow long lines + module, + ] + ).check_returncode() + ) @pytest.mark.parametrize( ('dotted_paths', 'recurse', 'warnings', 'error'), - [(['__MODULE__.foobar'], ['__MODULE__.foo'], - # `foo.baz` is indirectly included, so its raising an error - # shouldn't cause the script to error out - [{'target cannot', '__MODULE__.foo.baz'}], - None), - # We don't recurse down `__MODULE__.foo`, so that doesn't give a - # warning; but `__MODULE__.baz` cannot be imported because it - # doesn't exist - (['__MODULE__.foo', '__MODULE__.baz'], False, - [{'target cannot', '__MODULE__.baz'}], None), - # If we do recurse however, `__MODULE__.foo.baz` also ends up in - # the warning - # (also there's a `__MODULE___foo` which doesn't exist, about which - # the warning is issued during module generation) - (['__MODULE__' + '_foo', '__MODULE__', '__MODULE__.baz'], True, - [{'target cannot', '__MODULE__' + '_foo'}, # Fails at write - {'targets cannot', # Fails at import - '__MODULE__.foo.baz', '__MODULE__.baz'}], - None), - # And if the problematic module is an explicit target, raise the - # error - (['__MODULE__', '__MODULE__.foo.baz'], False, [], AssertionError)]) + [ + ( + ['__MODULE__.foobar'], + ['__MODULE__.foo'], + # `foo.baz` is indirectly included, so its raising an error + # shouldn't cause the script to error out + [{'target cannot', '__MODULE__.foo.baz'}], + None, + ), + # We don't recurse down `__MODULE__.foo`, so that doesn't give a + # warning; but `__MODULE__.baz` cannot be imported because it + # doesn't exist + ( + ['__MODULE__.foo', '__MODULE__.baz'], + False, + [{'target cannot', '__MODULE__.baz'}], + None, + ), + # If we do recurse however, `__MODULE__.foo.baz` also ends up in + # the warning + # (also there's a `__MODULE___foo` which doesn't exist, about which + # the warning is issued during module generation) + ( + ['__MODULE__' + '_foo', '__MODULE__', '__MODULE__.baz'], + True, + [ + {'target cannot', '__MODULE__' + '_foo'}, # Fails at write + { + 'targets cannot', # Fails at import + '__MODULE__.foo.baz', + '__MODULE__.baz', + }, + ], + None, + ), + # And if the problematic module is an explicit target, raise the + # error + (['__MODULE__', '__MODULE__.foo.baz'], False, [], AssertionError), + ], +) def test_written_module_error_handling( - sample_package: str, - dotted_paths: Collection[str], - recurse: Union[Collection[str], bool], - warnings: Sequence[Collection[str]], - error: Union[Type[Exception], None]): + sample_package: str, + dotted_paths: Collection[str], + recurse: Union[Collection[str], bool], + warnings: Sequence[Collection[str]], + error: Union[Type[Exception], None], +): """ Test that the module written by :py:meth:`~.write_eager_import_module()` gracefully handles errors @@ -175,8 +216,9 @@ def test_written_module_error_handling( dotted_paths = [replace(target) for target in dotted_paths] if recurse not in (True, False): recurse = [replace(target) for target in recurse] - warnings = [{replace(fragment) for fragment in fragments} - for fragments in warnings] + warnings = [ + {replace(fragment) for fragment in fragments} for fragments in warnings + ] with TemporaryDirectory() as tmpdir: module = Path(tmpdir) / 'module.py' with ExitStack() as stack: @@ -186,13 +228,15 @@ def test_written_module_error_handling( captured_warnings = enter(catch_warnings(record=True)) with module.open(mode='w') as fobj: write_eager_import_module( - dotted_paths, recurse=recurse, stream=fobj) + dotted_paths, recurse=recurse, stream=fobj + ) print(module.read_text()) if error is not None: enter(pytest.raises(error)) # Just use a dummy object, no need to instantiate a profiler prof = SimpleNamespace( - add_imported_function_or_module=lambda *_, **__: 0) + add_imported_function_or_module=lambda *_, **__: 0 + ) run_path(str(module), {'profile': prof}, 'module') assert len(captured_warnings) == len(warnings) for warning, fragments in zip(captured_warnings, warnings): @@ -213,17 +257,20 @@ def test_split_dotted_path_staticity() -> None: def test_resolve_profiling_targets_staticity( - sample_namespace_package: str) -> None: + sample_namespace_package: str, +) -> None: """ Test subpackage/-module discovery with `resolve_profiling_targets()` with different values for `static`. """ - all_targets = ({f'{sample_namespace_package}.{submod}' - for submod in ['one', 'two', 'three']} - | {sample_namespace_package}) + all_targets = { + f'{sample_namespace_package}.{submod}' + for submod in ['one', 'two', 'three'] + } | {sample_namespace_package} # Static analysis can't handle namespace packages resolve = partial( - resolve_profiling_targets, [sample_namespace_package], recurse=True) + resolve_profiling_targets, [sample_namespace_package], recurse=True + ) static_result = resolve(static=True) assert set(static_result.targets) < all_targets, static_result # The import system successfully retrieves all submodules diff --git a/tests/test_explicit_profile.py b/tests/test_explicit_profile.py index 73d8a79e..1a5b2a7c 100644 --- a/tests/test_explicit_profile.py +++ b/tests/test_explicit_profile.py @@ -13,6 +13,7 @@ class enter_tmpdir: """ Set up a temporary directory and :cmd:`chdir` into it. """ + def __init__(self): self.stack = ExitStack() @@ -43,6 +44,7 @@ class restore_sys_modules: """ Restore :py:attr:`sys.modules` after exiting the context. """ + def __enter__(self): self.old = sys.modules.copy() @@ -64,6 +66,7 @@ def test_simple_explicit_nonglobal_usage(): python -c "from test_explicit_profile import *; test_simple_explicit_nonglobal_usage()" """ from line_profiler import LineProfiler + profiler = LineProfiler() def func(a): @@ -83,7 +86,7 @@ def func(a): def _demo_explicit_profile_script(): return ub.codeblock( - ''' + """ from line_profiler import profile @profile @@ -93,7 +96,8 @@ def fib(n): a, b = b, a + b return a fib(10) - ''') + """ + ) def test_explicit_profile_with_nothing(): @@ -103,7 +107,6 @@ def test_explicit_profile_with_nothing(): with tempfile.TemporaryDirectory() as tmp: temp_dpath = ub.Path(tmp) with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(_demo_explicit_profile_script()) @@ -128,7 +131,6 @@ def test_explicit_profile_with_environ_on(): env['LINE_PROFILE'] = '1' with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(_demo_explicit_profile_script()) @@ -154,7 +156,6 @@ def test_explicit_profile_ignores_inherited_owner_marker(): env['PYTHONPATH'] = os.getcwd() with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(_demo_explicit_profile_script()) @@ -173,6 +174,7 @@ def test_explicit_profile_process_pool_forkserver(): Ensure explicit profiler works with forkserver ProcessPoolExecutor. """ import multiprocessing as mp + if 'forkserver' not in mp.get_all_start_methods(): pytest.skip('forkserver start method not available') with tempfile.TemporaryDirectory() as tmp: @@ -183,10 +185,10 @@ def test_explicit_profile_process_pool_forkserver(): env['PYTHONPATH'] = os.getcwd() with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') - script_fpath.write_text(ub.codeblock( - ''' + script_fpath.write_text( + ub.codeblock( + """ import multiprocessing as mp from concurrent.futures import ProcessPoolExecutor from line_profiler import profile @@ -210,7 +212,9 @@ def main(): if __name__ == '__main__': main() - ''').strip()) + """ + ).strip() + ) args = [sys.executable, os.fspath(script_fpath)] proc = ub.cmd(args, env=env) @@ -221,7 +225,10 @@ def main(): output_path = temp_dpath / 'profile_output.txt' assert output_path.exists() assert output_path.stat().st_size > 100 - assert proc.stdout.count('Wrote profile results to profile_output.txt') == 1 + assert ( + proc.stdout.count('Wrote profile results to profile_output.txt') + == 1 + ) def test_explicit_profile_with_environ_off(): @@ -234,7 +241,6 @@ def test_explicit_profile_with_environ_off(): env['LINE_PROFILE'] = '0' with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(_demo_explicit_profile_script()) @@ -258,7 +264,6 @@ def test_explicit_profile_with_cmdline(): with tempfile.TemporaryDirectory() as tmp: temp_dpath = ub.Path(tmp) with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(_demo_explicit_profile_script()) @@ -325,7 +330,7 @@ def test_explicit_profile_with_kernprof_m(builtin: bool, package: bool): temp_dpath = ub.Path(tmp) lib_code = ub.codeblock( - ''' + """ @profile def func1(a): return a + 1 @@ -339,11 +344,12 @@ def func3(a): def func4(a): return a + 1 - ''').strip() + """ + ).strip() if not builtin: lib_code = 'from line_profiler import profile\n' + lib_code target_code = ub.codeblock( - ''' + """ from ._lib import func1, func2, func3, func4 if __name__ == '__main__': @@ -351,7 +357,8 @@ def func4(a): func2(1) func3(1) func4(1) - ''').strip() + """ + ).strip() if package: target_module = 'package' @@ -387,10 +394,15 @@ def func4(a): proc.check_returncode() # Note: in non-builtin mode, the entire script is profiled - for func, profiled in [('func1', True), ('func2', True), - ('func3', not builtin), ('func4', not builtin)]: - result = re.search(r'lib\.py:[0-9]+\({}\)'.format(func), - proc.stdout) + for func, profiled in [ + ('func1', True), + ('func2', True), + ('func3', not builtin), + ('func4', not builtin), + ]: + result = re.search( + r'lib\.py:[0-9]+\({}\)'.format(func), proc.stdout + ) assert bool(result) == profiled assert not (temp_dpath / 'profile_output.txt').exists() @@ -409,7 +421,7 @@ def test_explicit_profile_with_in_code_enable(): temp_dpath = ub.Path(tmp) code = ub.codeblock( - ''' + """ from line_profiler import profile import ubelt as ub print('') @@ -454,9 +466,9 @@ def func4(a): func4(1) profile._profile - ''') + """ + ) with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(code) @@ -468,7 +480,7 @@ def func4(a): print('Finished running script') - output_fpath = (temp_dpath / 'custom_output.txt') + output_fpath = temp_dpath / 'custom_output.txt' raw_output = output_fpath.read_text() print(f'Contents of {output_fpath}') print(raw_output) @@ -493,7 +505,7 @@ def test_explicit_profile_with_duplicate_functions(): temp_dpath = ub.Path(tmp) code = ub.codeblock( - ''' + """ from line_profiler import profile @profile @@ -516,9 +528,9 @@ def func4(a): func2(1) func3(1) func4(1) - ''').strip() + """ + ).strip() with ub.ChDir(temp_dpath): - script_fpath = ub.Path('script.py') script_fpath.write_text(code) @@ -528,7 +540,7 @@ def func4(a): print(proc.stderr) proc.check_returncode() - output_fpath = (temp_dpath / 'profile_output.txt') + output_fpath = temp_dpath / 'profile_output.txt' raw_output = output_fpath.read_text() print(raw_output) @@ -556,7 +568,8 @@ def test_explicit_profile_with_customized_config(): script_fpath = ub.Path('script.py') script_fpath.write_text(_demo_explicit_profile_script()) toml = ub.Path('my_config.toml') - toml.write_text(ub.codeblock(''' + toml.write_text( + ub.codeblock(""" [tool.line_profiler.setup] environ_flags = ['PROFILE'] @@ -567,7 +580,8 @@ def test_explicit_profile_with_customized_config(): [tool.line_profiler.show] details = true summarize = false - ''')) + """) + ) env['LINE_PROFILER_RC'] = str(toml) args = [sys.executable, os.fspath(script_fpath)] @@ -577,19 +591,22 @@ def test_explicit_profile_with_customized_config(): proc.check_returncode() # Check the `write` config - assert set(os.listdir(temp_dpath)) == {'script.py', - 'my_config.toml', - 'my_profiling_results.lprof', - 'my_profiling_results.txt'} + assert set(os.listdir(temp_dpath)) == { + 'script.py', + 'my_config.toml', + 'my_profiling_results.lprof', + 'my_profiling_results.txt', + } # Check the `show` config assert '- fib' not in proc.stdout # No summary assert 'Function: fib' in proc.stdout # With details @pytest.mark.parametrize('reset_enable_count', [True, False]) -@pytest.mark.parametrize('wrap_class, wrap_module', - [(None, None), (False, True), - (True, False), (True, True)]) +@pytest.mark.parametrize( + 'wrap_class, wrap_module', + [(None, None), (False, True), (True, False), (True, True)], +) def test_profiler_add_methods(wrap_class, wrap_module, reset_enable_count): """ Test the `wrap` argument for the @@ -598,7 +615,8 @@ def test_profiler_add_methods(wrap_class, wrap_module, reset_enable_count): `line_profiler.autoprofile.autoprofile. _extend_line_profiler_for_profiling_imports()`) methods. """ - script = ub.codeblock(''' + script = ub.codeblock( + """ from line_profiler import LineProfiler from line_profiler.autoprofile.autoprofile import ( _extend_line_profiler_for_profiling_imports as upgrade_profiler) @@ -628,30 +646,38 @@ def test_profiler_add_methods(wrap_class, wrap_module, reset_enable_count): # isn't wrapped and doesn't auto-`.enable()` before being called func3() profiler.print_stats(details=True, summarize=True) - '''.format( - '' if wrap_module is None else f', wrap={wrap_module}', - '' if wrap_class is None else f', wrap={wrap_class}', - reset_enable_count)) + """.format( + '' if wrap_module is None else f', wrap={wrap_module}', + '' if wrap_class is None else f', wrap={wrap_class}', + reset_enable_count, + ) + ) with enter_tmpdir() as curdir: write(curdir / 'script.py', script) - write(curdir / 'my_module_1.py', - ''' + write( + curdir / 'my_module_1.py', + """ def func1(): pass # Marker: func1 - ''') - write(curdir / 'my_module_2.py', - ''' + """, + ) + write( + curdir / 'my_module_2.py', + """ class Class: @classmethod def method2(cls): pass # Marker: method2 - ''') - write(curdir / 'my_module_3.py', - ''' + """, + ) + write( + curdir / 'my_module_3.py', + """ def func3(): pass # Marker: func3 - ''') + """, + ) proc = ub.cmd([sys.executable, str(curdir / 'script.py')]) # Check that the profiler has seen each of the methods @@ -665,10 +691,16 @@ def func3(): assert '# Marker: func3' in raw_output # Check that the timing info (of the lack thereof) are correct - for func, has_timing in [('func1', wrap_module), ('method2', wrap_class), - ('func3', False)]: - line, = (line for line in raw_output.splitlines() - if line.endswith('Marker: ' + func)) + for func, has_timing in [ + ('func1', wrap_module), + ('method2', wrap_class), + ('func3', False), + ]: + (line,) = ( + line + for line in raw_output.splitlines() + if line.endswith('Marker: ' + func) + ) has_timing = has_timing or not reset_enable_count assert line.split()[1] == ('1' if has_timing else 'pass') @@ -730,7 +762,8 @@ def __setattr__(cls, attr, value): if not getattr(cls, '_initialized', None): return super(ProblamticMeta, cls).__setattr__(attr, value) raise AttributeError( - f'cannot set attribute on {type(cls)} instance') + f'cannot set attribute on {type(cls)} instance' + ) class ProblematicClass(metaclass=ProblamticMeta): def method(self): @@ -739,9 +772,11 @@ def method(self): profile = LineProfiler() vanilla_method = ProblematicClass.method - with pytest.warns(match=r"cannot wrap 1 attribute\(s\) of " - r" \(`\{attr: value\}`\): " - r"\{'method': \}"): + with pytest.warns( + match=r'cannot wrap 1 attribute\(s\) of ' + r" \(`\{attr: value\}`\): " + r"\{'method': \}" + ): # The method is added to the profiler, but we can't assign its # wrapper back into the class namespace assert profile.add_class(ProblematicClass, wrap=True) == 1 @@ -751,29 +786,59 @@ def method(self): @pytest.mark.parametrize( ('scoping_policy', 'add_module_targets', 'add_class_targets'), - [('exact', {}, {'class3_method'}), - ('children', - {'class2_method', 'child_class2_method'}, - {'class3_method', 'child_class3_method'}), - ('descendants', - {'class2_method', 'child_class2_method', - 'class3_method', 'child_class3_method'}, - {'class3_method', 'child_class3_method'}), - ('siblings', - {'class1_method', 'child_class1_method', - 'class2_method', 'child_class2_method', - 'class3_method', 'child_class3_method', 'other_class3_method'}, - {'class3_method', 'child_class3_method', 'other_class3_method'}), - ('none', - {'class1_method', 'child_class1_method', - 'class2_method', 'child_class2_method', - 'class3_method', 'child_class3_method', 'other_class3_method'}, - {'child_class1_method', - 'class3_method', 'child_class3_method', 'other_class3_method'})]) -def test_profiler_class_scope_matching(monkeypatch, - scoping_policy, - add_module_targets, - add_class_targets): + [ + ('exact', {}, {'class3_method'}), + ( + 'children', + {'class2_method', 'child_class2_method'}, + {'class3_method', 'child_class3_method'}, + ), + ( + 'descendants', + { + 'class2_method', + 'child_class2_method', + 'class3_method', + 'child_class3_method', + }, + {'class3_method', 'child_class3_method'}, + ), + ( + 'siblings', + { + 'class1_method', + 'child_class1_method', + 'class2_method', + 'child_class2_method', + 'class3_method', + 'child_class3_method', + 'other_class3_method', + }, + {'class3_method', 'child_class3_method', 'other_class3_method'}, + ), + ( + 'none', + { + 'class1_method', + 'child_class1_method', + 'class2_method', + 'child_class2_method', + 'class3_method', + 'child_class3_method', + 'other_class3_method', + }, + { + 'child_class1_method', + 'class3_method', + 'child_class3_method', + 'other_class3_method', + }, + ), + ], +) +def test_profiler_class_scope_matching( + monkeypatch, scoping_policy, add_module_targets, add_class_targets +): """ Test for the class-scope-matching strategies of the `LineProfiler.add_*()` methods. @@ -784,8 +849,9 @@ def test_profiler_class_scope_matching(monkeypatch, pkg_dir = curdir / 'packages' / 'my_pkg' write(pkg_dir / '__init__.py') - write(pkg_dir / 'submod1.py', - """ + write( + pkg_dir / 'submod1.py', + """ class Class1: def class1_method(self): pass @@ -793,9 +859,11 @@ def class1_method(self): class ChildClass1: def child_class1_method(self): pass - """) - write(pkg_dir / 'subpkg2' / '__init__.py', - """ + """, + ) + write( + pkg_dir / 'subpkg2' / '__init__.py', + """ from ..submod1 import Class1 # Import from a sibling from .submod3 import Class3 # Import descendant from a child @@ -809,9 +877,11 @@ def child_class2_method(self): pass BorrowedChildClass = Class1.ChildClass1 # Non-sibling class - """) - write(pkg_dir / 'subpkg2' / 'submod3.py', - """ + """, + ) + write( + pkg_dir / 'subpkg2' / 'submod3.py', + """ from ..submod1 import Class1 @@ -832,14 +902,18 @@ def other_class3_method(self): # Sibling class Class3.BorrowedChildClass3 = OtherClass3 - """) + """, + ) monkeypatch.syspath_prepend(pkg_dir.parent) from my_pkg import subpkg2 from line_profiler import LineProfiler - policies = {'func': 'none', 'class': scoping_policy, - 'module': 'exact'} # Don't descend into submodules + policies = { + 'func': 'none', + 'class': scoping_policy, + 'module': 'exact', + } # Don't descend into submodules # Add a module profile = LineProfiler() profile.add_module(subpkg2, scoping_policy=policies) @@ -856,17 +930,21 @@ def other_class3_method(self): @pytest.mark.parametrize( ('scoping_policy', 'add_module_targets', 'add_subpackage_targets'), - [('exact', {'func4'}, {'class_method'}), - ('children', {'func4'}, {'class_method', 'func2'}), - ('descendants', {'func4'}, {'class_method', 'func2'}), - ('siblings', {'func4'}, {'class_method', 'func2', 'func3'}), - ('none', - {'func4', 'func5'}, - {'class_method', 'func2', 'func3', 'func4', 'func5'})]) -def test_profiler_module_scope_matching(monkeypatch, - scoping_policy, - add_module_targets, - add_subpackage_targets): + [ + ('exact', {'func4'}, {'class_method'}), + ('children', {'func4'}, {'class_method', 'func2'}), + ('descendants', {'func4'}, {'class_method', 'func2'}), + ('siblings', {'func4'}, {'class_method', 'func2', 'func3'}), + ( + 'none', + {'func4', 'func5'}, + {'class_method', 'func2', 'func3', 'func4', 'func5'}, + ), + ], +) +def test_profiler_module_scope_matching( + monkeypatch, scoping_policy, add_module_targets, add_subpackage_targets +): """ Test for the module-scope-matching strategies of the `LineProfiler.add_*()` methods. @@ -877,8 +955,9 @@ def test_profiler_module_scope_matching(monkeypatch, pkg_dir = curdir / 'packages' / 'my_pkg' write(pkg_dir / '__init__.py') - write(pkg_dir / 'subpkg1' / '__init__.py', - """ + write( + pkg_dir / 'subpkg1' / '__init__.py', + """ import my_mod4 # Unrelated from .. import submod3 # Sibling from . import submod2 # Child @@ -891,38 +970,50 @@ def class_method(cls): # We shouldn't descend into this no matter what import my_mod5 as module - """) - write(pkg_dir / 'subpkg1' / 'submod2.py', - """ + """, + ) + write( + pkg_dir / 'subpkg1' / 'submod2.py', + """ def func2(): pass - """) - write(pkg_dir / 'submod3.py', - """ + """, + ) + write( + pkg_dir / 'submod3.py', + """ def func3(): pass - """) - write(curdir / 'packages' / 'my_mod4.py', - """ + """, + ) + write( + curdir / 'packages' / 'my_mod4.py', + """ import my_mod5 # Unrelated def func4(): pass - """) - write(curdir / 'packages' / 'my_mod5.py', - """ + """, + ) + write( + curdir / 'packages' / 'my_mod5.py', + """ def func5(): pass - """) + """, + ) monkeypatch.syspath_prepend(pkg_dir.parent) import my_mod4 from my_pkg import subpkg1 from line_profiler import LineProfiler - policies = {'func': 'none', 'class': 'children', - 'module': scoping_policy} + policies = { + 'func': 'none', + 'class': 'children', + 'module': scoping_policy, + } # Add a module profile = LineProfiler() profile.add_module(my_mod4, scoping_policy=policies) @@ -943,19 +1034,25 @@ def func5(): @pytest.mark.parametrize( ('scoping_policy', 'add_module_targets', 'add_class_targets'), - [('exact', {'func1'}, {'method'}), - ('children', {'func1'}, {'method'}), - ('descendants', {'func1', 'func2'}, {'method', 'child_class_method'}), - ('siblings', - {'func1', 'func2', 'func3'}, - {'method', 'child_class_method', 'func1'}), - ('none', - {'func1', 'func2', 'func3', 'func4'}, - {'method', 'child_class_method', 'func1', 'another_func4'})]) -def test_profiler_func_scope_matching(monkeypatch, - scoping_policy, - add_module_targets, - add_class_targets): + [ + ('exact', {'func1'}, {'method'}), + ('children', {'func1'}, {'method'}), + ('descendants', {'func1', 'func2'}, {'method', 'child_class_method'}), + ( + 'siblings', + {'func1', 'func2', 'func3'}, + {'method', 'child_class_method', 'func1'}, + ), + ( + 'none', + {'func1', 'func2', 'func3', 'func4'}, + {'method', 'child_class_method', 'func1', 'another_func4'}, + ), + ], +) +def test_profiler_func_scope_matching( + monkeypatch, scoping_policy, add_module_targets, add_class_targets +): """ Test for the class-scope-matching strategies of the `LineProfiler.add_*()` methods. @@ -966,8 +1063,9 @@ def test_profiler_func_scope_matching(monkeypatch, pkg_dir = curdir / 'packages' / 'my_pkg' write(pkg_dir / '__init__.py') - write(pkg_dir / 'subpkg1' / '__init__.py', - """ + write( + pkg_dir / 'subpkg1' / '__init__.py', + """ from ..submod3 import func3 # Sibling from .submod2 import func2 # Descendant from my_mod4 import func4 # Unrelated @@ -992,34 +1090,44 @@ def child_class_method(cls): # Unrelated from my_mod4 import another_func4 as imported_method - """) - write(pkg_dir / 'subpkg1' / 'submod2.py', - """ + """, + ) + write( + pkg_dir / 'subpkg1' / 'submod2.py', + """ def func2(): pass - """) - write(pkg_dir / 'submod3.py', - """ + """, + ) + write( + pkg_dir / 'submod3.py', + """ def func3(): pass - """) - write(curdir / 'packages' / 'my_mod4.py', - """ + """, + ) + write( + curdir / 'packages' / 'my_mod4.py', + """ def func4(): pass def another_func4(_): pass - """) + """, + ) monkeypatch.syspath_prepend(pkg_dir.parent) from my_pkg import subpkg1 from line_profiler import LineProfiler - policies = {'func': scoping_policy, - # No descensions - 'class': 'exact', 'module': 'exact'} + policies = { + 'func': scoping_policy, + # No descensions + 'class': 'exact', + 'module': 'exact', + } # Add a module profile = LineProfiler() profile.add_module(subpkg1, scoping_policy=policies) diff --git a/tests/test_import.py b/tests/test_import.py index 4c2184e7..78e62b1a 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -3,6 +3,7 @@ def test_import(): import line_profiler + assert hasattr(line_profiler, 'LineProfiler') assert hasattr(line_profiler, '__version__') @@ -11,8 +12,10 @@ def test_version(): import line_profiler from packaging.version import Version import kernprof + line_profiler_version1 = Version(line_profiler.__version__) line_profiler_version2 = Version(line_profiler.line_profiler.__version__) kernprof_version = Version(kernprof.__version__) - assert line_profiler_version1 == line_profiler_version2 == kernprof_version, ( - 'All 3 places should have the same version') + assert ( + line_profiler_version1 == line_profiler_version2 == kernprof_version + ), 'All 3 places should have the same version' diff --git a/tests/test_ipython.py b/tests/test_ipython.py index 29ba4ddd..336895b1 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -23,6 +23,7 @@ class tempdir: ... >>> assert not td.is_dir() """ + def __init__(self, *args, **kwargs): self._get_tmpdir = partial(TemporaryDirectory, *args, **kwargs) self._stacks = [] @@ -61,10 +62,10 @@ class TestLPRun(_TestIPython): CommandLine: pytest -k "TestLPRun and not TestLPRunAll" -s -v """ + @pytest.mark.parametrize('modules', [None, 'calendar']) def test_lprun_profiling_targets(self, request, modules): - """ Test ``%lprun`` with the ``-m`` flag. - """ + """Test ``%lprun`` with the ``-m`` flag.""" mods = shlex.split(modules or '') if mods: chunks = [] @@ -77,21 +78,26 @@ def test_lprun_profiling_targets(self, request, modules): # Check the profiling of functions # - from the `-f` flag - assert any(getattr(func, '__name__', None) == 'func' - for func in lprof.functions) + assert any( + getattr(func, '__name__', None) == 'func' + for func in lprof.functions + ) # - from the `-m` flag for mod in mods: assert any( - (getattr(func, '__module__', '') == mod - or getattr(func, '__module__', '').startswith(mod + '.')) - for func in lprof.functions) + ( + getattr(func, '__module__', '') == mod + or getattr(func, '__module__', '').startswith(mod + '.') + ) + for func in lprof.functions + ) @pytest.mark.parametrize( ('output', 'text'), - [(None, None), ('myprof.txt', True), ('myprof.lprof', False)]) + [(None, None), ('myprof.txt', True), ('myprof.lprof', False)], + ) def test_lprun_file_io(self, request, output, text): - """ Test ``%lprun`` with the ``-D`` and ``-T`` flags. - """ + """Test ``%lprun`` with the ``-D`` and ``-T`` flags.""" with tempdir() as tmpdir: if output: more_flags = shlex.join(['-' + ('T' if text else 'D'), output]) @@ -111,29 +117,27 @@ def test_lprun_file_io(self, request, output, text): @pytest.mark.parametrize('bad', [True, False]) def test_lprun_timer_unit(self, request, bad): - """ Test ``%lprun`` with the ``-u`` flag. - """ + """Test ``%lprun`` with the ``-u`` flag.""" capsys = request.getfixturevalue('capsys') if bad: # Test invalid value with pytest.raises(TypeError): self._test_lprun(request, '-u not_a_number') return else: - unit = 1E-3 + unit = 1e-3 self._test_lprun(request, f'-u {unit}') out = capsys.readouterr().out # Check the timer (`-u`) pattern = re.compile(r'Timer unit:\s*([^\s]+)\s*s') - match, = ( - m for m in (pattern.match(line) for line in out.splitlines()) - if m) + (match,) = ( + m for m in (pattern.match(line) for line in out.splitlines()) if m + ) assert pytest.approx(float(match.group(1))) == unit @pytest.mark.parametrize('skip_zero', [None, '-s', '-z']) def test_lprun_skip_zero(self, request, skip_zero): - """ Test ``%lprun`` with the ``-s`` and ``-z`` flags. - """ + """Test ``%lprun`` with the ``-s`` and ``-z`` flags.""" capsys = request.getfixturevalue('capsys') # Throw in an unrelated module, whose timings are always zero more_flags = '-m calendar' @@ -147,21 +151,21 @@ def test_lprun_skip_zero(self, request, skip_zero): match = re.search( r'^File:\s*.*{}calendar\.py'.format(re.escape(os.sep)), out, - flags=re.MULTILINE) + flags=re.MULTILINE, + ) assert bool(match) == (not skip_zero) - @pytest.mark.parametrize(('xc', 'raised'), - [(SystemExit(0), False), - (ValueError('foo'), True)]) + @pytest.mark.parametrize( + ('xc', 'raised'), [(SystemExit(0), False), (ValueError('foo'), True)] + ) def test_lprun_exception_handling(self, capsys, xc, raised): - """ Test ``%lprun``-ing a function which raises exceptions. - """ + """Test ``%lprun``-ing a function which raises exceptions.""" ip = self._get_ipython_instance() ip.run_line_magic('load_ext', 'line_profiler') xc_repr = '{}({})'.format( - type(xc).__name__, ', '.join(repr(a) for a in xc.args)) - ip.run_cell( - raw_cell=f'func = lambda: (_ for _ in ()).throw({xc_repr})') + type(xc).__name__, ', '.join(repr(a) for a in xc.args) + ) + ip.run_cell(raw_cell=f'func = lambda: (_ for _ in ()).throw({xc_repr})') if raised: # Normal excepts should be bubbled up with pytest.raises(type(xc)): @@ -186,17 +190,21 @@ def _test_lprun(self, request, more_flags): # Check the recorded timings filtered_timings = { (filename, lineno, funcname): entries - for (filename, lineno, funcname), entries - in lprof.get_stats().timings.items() + for ( + filename, + lineno, + funcname, + ), entries in lprof.get_stats().timings.items() if filename.startswith('')} + if filename.endswith('>') + } assert len(filtered_timings) == 1 # 1 function func_data, lines_data = next(iter(filtered_timings.items())) self._emit(request, f'func_data={func_data}') self._emit(request, f'lines_data={lines_data}', file=stderr) assert func_data[1] == 1 # lineno of the function - assert func_data[2] == "func" # function name + assert func_data[2] == 'func' # function name assert len(lines_data) == 1 # 1 line of code assert lines_data[0][0] == 2 # lineno assert lines_data[0][1] == 1 # hits @@ -209,13 +217,14 @@ class TestLPRunAll(_TestIPython): CommandLine: pytest -k TestLPRunAll -s -v """ + def test_lprun_all_autoprofile(self): - """ Test ``%%lprun_all`` without the ``-p`` flag. - """ + """Test ``%%lprun_all`` without the ``-p`` flag.""" ip = self._get_ipython_instance() ip.run_line_magic('load_ext', 'line_profiler') lprof = ip.run_cell_magic( - 'lprun_all', line='-r', cell=self.lprun_all_cell_body) + 'lprun_all', line='-r', cell=self.lprun_all_cell_body + ) timings = lprof.get_stats().timings # 2 scopes: the module scope and an inner scope (Test.test) @@ -227,29 +236,33 @@ def test_lprun_all_autoprofile(self): print(f'func_1_data={func_1_data}') print(f'lines_1_data={lines_1_data}') assert func_1_data[1] == 1 # lineno of the module - assert len(lines_1_data) == 2 # only 2 lines were executed in this outer scope + assert ( + len(lines_1_data) == 2 + ) # only 2 lines were executed in this outer scope assert lines_1_data[0][0] == 1 # lineno assert lines_1_data[0][1] == 1 # hits print(f'func_2_data={func_2_data}') print(f'lines_2_data={lines_2_data}') assert func_2_data[1] == 2 # lineno of the inner function - assert len(lines_2_data) == 5 # only 5 lines were executed in this inner scope + assert ( + len(lines_2_data) == 5 + ) # only 5 lines were executed in this inner scope assert lines_2_data[1][0] == 4 # lineno assert lines_2_data[1][1] == self.loops - 1 # hits # Check that the code is executed in the right scope and with # the expected side effects - assert isinstance(ip.user_ns.get("Test"), type) + assert isinstance(ip.user_ns.get('Test'), type) assert ip.user_ns['z'] is None def test_lprun_all_autoprofile_toplevel(self): - """ Test ``%%lprun_all`` with the ``-p`` flag. - """ + """Test ``%%lprun_all`` with the ``-p`` flag.""" ip = self._get_ipython_instance() ip.run_line_magic('load_ext', 'line_profiler') lprof = ip.run_cell_magic( - 'lprun_all', line='-r -p', cell=self.lprun_all_cell_body) + 'lprun_all', line='-r -p', cell=self.lprun_all_cell_body + ) timings = lprof.get_stats().timings # 1 scope: the module scope @@ -260,30 +273,32 @@ def test_lprun_all_autoprofile_toplevel(self): print(f'func_data={func_data}') print(f'lines_data={lines_data}') assert func_data[1] == 1 # lineno of the module - assert len(lines_data) == 2 # only 2 lines were executed in this outer scope + assert ( + len(lines_data) == 2 + ) # only 2 lines were executed in this outer scope assert lines_data[0][0] == 1 # lineno assert lines_data[0][1] == 1 # hits # Check that the code is executed in the right scope and with # the expected side effects - assert isinstance(ip.user_ns.get("Test"), type) + assert isinstance(ip.user_ns.get('Test'), type) assert ip.user_ns['z'] is None def test_lprun_all_timetaken(self): - """ Test ``%%lprun_all`` with the ``-t`` flag. - """ + """Test ``%%lprun_all`` with the ``-t`` flag.""" ip = self._get_ipython_instance() ip.run_line_magic('load_ext', 'line_profiler') result = ip.run_cell_magic( - 'lprun_all', line='-t', cell=self.lprun_all_cell_body) + 'lprun_all', line='-t', cell=self.lprun_all_cell_body + ) assert result is None # No `-r` flag -> profiler not returned # Check that the code is executed in the right scope and with # the expected side effects - assert isinstance(ip.user_ns.get("Test"), type) + assert isinstance(ip.user_ns.get('Test'), type) assert ip.user_ns['z'] is None # Check that the elapsed time is written to the right scope - assert ip.user_ns.get("_total_time_taken", None) is not None + assert ip.user_ns.get('_total_time_taken', None) is not None # This example has 2 scopes # - The top level (module) scope, and diff --git a/tests/test_kernprof.py b/tests/test_kernprof.py index 3d6b7e97..19a53d91 100644 --- a/tests/test_kernprof.py +++ b/tests/test_kernprof.py @@ -15,41 +15,54 @@ def f(x): - """ A function. """ + """A function.""" y = x + 10 return y def g(x): - """ A generator. """ + """A generator.""" y = yield x + 10 yield y + 20 @pytest.mark.parametrize( 'use_kernprof_exec, args, expected_output, expect_error', - [([False, ['-m'], '', True]), - # `python -m kernprof` - (False, ['-m', 'mymod'], "[__MYMOD__]", False), - # `kernprof` - (True, ['-m', 'mymod'], "[__MYMOD__]", False), - (False, ['-m', 'mymod', '-p', 'bar'], "[__MYMOD__, '-p', 'bar']", False), - # `-p bar` consumed by `kernprof`, `-p baz` are not - (False, - ['-p', 'bar', '-m', 'mymod', '-p', 'baz'], - "[__MYMOD__, '-p', 'baz']", - False), - # Separator `--` broke off the remainder, so the requisite arg for - # `-m` is not found and we error out - (False, ['-p', 'bar', '-m', '--', 'mymod', '-p', 'baz'], '', True), - # Separator `--` broke off the remainder, so `-m` is passed to the - # script instead of being parsed as the module to execute - (False, - ['-p', 'bar', 'mymod.py', '--', '-m', 'mymod', '-p', 'baz'], - "['mymod.py', '-m', 'mymod', '-p', 'baz']", - False)]) + [ + ([False, ['-m'], '', True]), + # `python -m kernprof` + (False, ['-m', 'mymod'], '[__MYMOD__]', False), + # `kernprof` + (True, ['-m', 'mymod'], '[__MYMOD__]', False), + ( + False, + ['-m', 'mymod', '-p', 'bar'], + "[__MYMOD__, '-p', 'bar']", + False, + ), + # `-p bar` consumed by `kernprof`, `-p baz` are not + ( + False, + ['-p', 'bar', '-m', 'mymod', '-p', 'baz'], + "[__MYMOD__, '-p', 'baz']", + False, + ), + # Separator `--` broke off the remainder, so the requisite arg for + # `-m` is not found and we error out + (False, ['-p', 'bar', '-m', '--', 'mymod', '-p', 'baz'], '', True), + # Separator `--` broke off the remainder, so `-m` is passed to the + # script instead of being parsed as the module to execute + ( + False, + ['-p', 'bar', 'mymod.py', '--', '-m', 'mymod', '-p', 'baz'], + "['mymod.py', '-m', 'mymod', '-p', 'baz']", + False, + ), + ], +) def test_kernprof_m_parsing( - use_kernprof_exec, args, expected_output, expect_error): + use_kernprof_exec, args, expected_output, expect_error +): """ Test that `kernprof -m` behaves like `python -m` in that it requires an argument and cuts off everything after it, passing that along @@ -58,14 +71,17 @@ def test_kernprof_m_parsing( with tempfile.TemporaryDirectory() as tmpdir: temp_dpath = ub.Path(tmpdir) mod = (temp_dpath / 'mymod.py').resolve() - mod.write_text(ub.codeblock( - ''' + mod.write_text( + ub.codeblock( + """ import sys if __name__ == '__main__': print(sys.argv) - ''')) + """ + ) + ) if use_kernprof_exec: cmd = ['kernprof'] else: @@ -77,17 +93,24 @@ def test_kernprof_m_parsing( else: proc.check_returncode() expected_output = re.escape(expected_output).replace( - '__MYMOD__', "'.*{}'".format(re.escape(os.path.sep + mod.name))) + '__MYMOD__', "'.*{}'".format(re.escape(os.path.sep + mod.name)) + ) assert re.match('^' + expected_output, proc.stdout) -@pytest.mark.skipif(sys.version_info[:2] < (3, 11), - reason='no `@enum.global_enum` in Python ' - f'{".".join(str(v) for v in sys.version_info[:3])}') -@pytest.mark.parametrize(('flags', 'profiled_main'), - [('-lv -p mymod', True), # w/autoprofile - ('-lv', True), # w/o autoprofile - ('-b', False)]) # w/o line profiling +@pytest.mark.skipif( + sys.version_info[:2] < (3, 11), + reason='no `@enum.global_enum` in Python ' + f'{".".join(str(v) for v in sys.version_info[:3])}', +) +@pytest.mark.parametrize( + ('flags', 'profiled_main'), + [ + ('-lv -p mymod', True), # w/autoprofile + ('-lv', True), # w/o autoprofile + ('-b', False), + ], +) # w/o line profiling def test_kernprof_m_sys_modules(flags, profiled_main): """ Test that `kernprof -m` is amenable to modules relying on the global @@ -95,8 +118,9 @@ def test_kernprof_m_sys_modules(flags, profiled_main): """ with tempfile.TemporaryDirectory() as tmpdir: temp_dpath = ub.Path(tmpdir) - (temp_dpath / 'mymod.py').write_text(ub.codeblock( - ''' + (temp_dpath / 'mymod.py').write_text( + ub.codeblock( + """ import enum import os import sys @@ -117,9 +141,17 @@ def main(): if __name__ == '__main__': main() - ''')) - cmd = [sys.executable, '-m', 'kernprof', - *shlex.split(flags), '-m', 'mymod'] + """ + ) + ) + cmd = [ + sys.executable, + '-m', + 'kernprof', + *shlex.split(flags), + '-m', + 'mymod', + ] proc = ub.cmd(cmd, cwd=temp_dpath, verbose=2) proc.check_returncode() assert proc.stdout.startswith('3\n') @@ -135,7 +167,7 @@ def test_kernprof_m_import_resolution(static, autoprof): :env:`LINE_PROFILER_STATIC_ANALYSIS`; note that static analysis doesn't handle namespace modules while dynamic resolution does. """ - code = ub.codeblock(''' + code = ub.codeblock(""" import enum import os import sys @@ -148,12 +180,13 @@ def main(): if __name__ == '__main__': main() - ''') + """) cmd = [sys.executable, '-m', 'kernprof', '-lv'] if autoprof: # Remove the explicit decorator, and use the `--prof-mod` flag - code = '\n'.join(line for line in code.splitlines() - if '@profile' not in line) + code = '\n'.join( + line for line in code.splitlines() if '@profile' not in line + ) cmd += ['-p', 'my_namesapce_pkg.mysubmod'] with tempfile.TemporaryDirectory() as tmpdir: temp_dpath = ub.Path(tmpdir) @@ -163,11 +196,13 @@ def main(): python_path = tmpdir if 'PYTHONPATH' in os.environ: python_path += ':' + os.environ['PYTHONPATH'] - env = {**os.environ, - # Toggle use of static analysis - 'LINE_PROFILER_STATIC_ANALYSIS': str(bool(static)), - # Add the tempdir to `sys.path` - 'PYTHONPATH': python_path} + env = { + **os.environ, + # Toggle use of static analysis + 'LINE_PROFILER_STATIC_ANALYSIS': str(bool(static)), + # Add the tempdir to `sys.path` + 'PYTHONPATH': python_path, + } cmd += ['-m', 'my_namesapce_pkg.mysubmod'] proc = ub.cmd(cmd, cwd=temp_dpath, verbose=2, env=env) if static: @@ -200,8 +235,9 @@ def test_kernprof_sys_restoration(capsys, error, args): tmpdir = enter(tempfile.TemporaryDirectory()) assert tmpdir not in sys.path temp_dpath = ub.Path(tmpdir) - (temp_dpath / 'mymod.py').write_text(ub.codeblock( - f''' + (temp_dpath / 'mymod.py').write_text( + ub.codeblock( + f""" import sys @@ -217,7 +253,9 @@ def main(): if __name__ == '__main__': main() - ''')) + """ + ) + ) enter(ub.ChDir(tmpdir)) if error: ctx = pytest.raises(BaseException) @@ -239,47 +277,70 @@ def main(): @pytest.mark.parametrize( ('flags', 'expected_stdout', 'expected_stderr'), - [('', # Neutral verbosity level - {'^Output to stdout': True, - r"^Wrote .* '.*script\.py\.lprof'": True, - r'^Inspect results with:''\n' - r'\S*python\S* -m line_profiler .*script\.py\.lprof': True, - r'line_profiler\.autoprofile\.autoprofile' - r'\.run\(\n(?:.+,\n)*.*\)': False, - r'^\[kernprof .*\]': False, - r'^Function: main': False}, - {'^Output to stderr': True}), - ('--view', # Verbosity level 1 = `--view` - {'^Output to stdout': True, - r"^Wrote .* '.*script\.py\.lprof'": True, - r'^Inspect results with:''\n' - r'\S*python\S* -m line_profiler .*script\.py\.lprof': False, - r'line_profiler\.autoprofile\.autoprofile' - r'\.run\(\n(?:.+,\n)*.*\)': False, - r'^\[kernprof .*\]': False, - r'^Function: main': True}, - {'^Output to stderr': True}), - ('-vv', # Verbosity level 2, show diagnostics - {'^Output to stdout': True, - r"^\[kernprof .*\] Wrote .* '.*script\.py\.lprof'": True, - r'Inspect results with:''\n' - r'\S*python\S* -m line_profiler .*script\.py\.lprof': False, - r'line_profiler\.autoprofile\.autoprofile' - r'\.run\(\n(?:.+,\n)*.*\)': True, - r'^Function: main': True}, - {'^Output to stderr': True}), - # Verbosity level -1, suppress `kernprof` output - ('--quiet', - {'^Output to stdout': True, 'Wrote': False}, - {'^Output to stderr': True}), - # Verbosity level -2, suppress script stdout - # (also test verbosity arithmatics) - ('--quiet --quiet --verbose -q', None, {'^Output to stderr': True}), - # Verbosity level -3, suppress script stderr - ('-qq --quiet', None, - # This should have been `None`, but there's something weird with - # `coverage` in CI which causes a spurious warning... - {'^Output to stderr': False})]) + [ + ( + '', # Neutral verbosity level + { + '^Output to stdout': True, + r"^Wrote .* '.*script\.py\.lprof'": True, + r'^Inspect results with:' + '\n' + r'\S*python\S* -m line_profiler .*script\.py\.lprof': True, + r'line_profiler\.autoprofile\.autoprofile' + r'\.run\(\n(?:.+,\n)*.*\)': False, + r'^\[kernprof .*\]': False, + r'^Function: main': False, + }, + {'^Output to stderr': True}, + ), + ( + '--view', # Verbosity level 1 = `--view` + { + '^Output to stdout': True, + r"^Wrote .* '.*script\.py\.lprof'": True, + r'^Inspect results with:' + '\n' + r'\S*python\S* -m line_profiler .*script\.py\.lprof': False, + r'line_profiler\.autoprofile\.autoprofile' + r'\.run\(\n(?:.+,\n)*.*\)': False, + r'^\[kernprof .*\]': False, + r'^Function: main': True, + }, + {'^Output to stderr': True}, + ), + ( + '-vv', # Verbosity level 2, show diagnostics + { + '^Output to stdout': True, + r"^\[kernprof .*\] Wrote .* '.*script\.py\.lprof'": True, + r'Inspect results with:' + '\n' + r'\S*python\S* -m line_profiler .*script\.py\.lprof': False, + r'line_profiler\.autoprofile\.autoprofile' + r'\.run\(\n(?:.+,\n)*.*\)': True, + r'^Function: main': True, + }, + {'^Output to stderr': True}, + ), + # Verbosity level -1, suppress `kernprof` output + ( + '--quiet', + {'^Output to stdout': True, 'Wrote': False}, + {'^Output to stderr': True}, + ), + # Verbosity level -2, suppress script stdout + # (also test verbosity arithmatics) + ('--quiet --quiet --verbose -q', None, {'^Output to stderr': True}), + # Verbosity level -3, suppress script stderr + ( + '-qq --quiet', + None, + # This should have been `None`, but there's something weird with + # `coverage` in CI which causes a spurious warning... + {'^Output to stderr': False}, + ), + ], +) def test_kernprof_verbosity(flags, expected_stdout, expected_stderr): """ Test the various verbosity levels of `kernprof`. @@ -288,8 +349,9 @@ def test_kernprof_verbosity(flags, expected_stdout, expected_stderr): enter = stack.enter_context tmpdir = enter(tempfile.TemporaryDirectory()) temp_dpath = ub.Path(tmpdir) - (temp_dpath / 'script.py').write_text(ub.codeblock( - ''' + (temp_dpath / 'script.py').write_text( + ub.codeblock( + """ import sys @@ -300,16 +362,30 @@ def main(): if __name__ == '__main__': main() - ''')) + """ + ) + ) enter(ub.ChDir(tmpdir)) - proc = ub.cmd(['kernprof', '-l', - # Add an eager pre-import target - '-p', 'script.py', '-p', 'zipfile', '-z', - *shlex.split(flags), 'script.py']) + proc = ub.cmd( + [ + 'kernprof', + '-l', + # Add an eager pre-import target + '-p', + 'script.py', + '-p', + 'zipfile', + '-z', + *shlex.split(flags), + 'script.py', + ] + ) proc.check_returncode() print(proc.stdout) - for expected_outputs, stream in [(expected_stdout, proc.stdout), - (expected_stderr, proc.stderr)]: + for expected_outputs, stream in [ + (expected_stdout, proc.stdout), + (expected_stderr, proc.stderr), + ]: if expected_outputs is None: assert not stream continue @@ -317,11 +393,12 @@ def main(): found = re.search(pattern, stream, flags=re.MULTILINE) if not bool(found) == expect_match: msg = ub.paragraph( - f''' + f""" Searching for pattern: {pattern!r} in output. Did we expect a match? {expect_match!r}. Did we get a match? {bool(found)!r}. - ''') + """ + ) raise AssertionError(msg) @@ -330,7 +407,7 @@ def test_kernprof_eager_preimport_bad_module(): Test for the preservation of the full traceback when an error occurs in an auto-generated pre-import module. """ - bad_module = '''raise Exception('Boo')''' + bad_module = """raise Exception('Boo')""" with contextlib.ExitStack() as stack: enter = stack.enter_context tmpdir = enter(tempfile.TemporaryDirectory()) @@ -342,10 +419,17 @@ def test_kernprof_eager_preimport_bad_module(): python_path = f'{python_path}:{tmpdir}' else: python_path = tmpdir - proc = ub.cmd(['kernprof', '-l', - # Add an eager pre-import target - '-pmy_bad_module', '-c', 'print(1)'], - env={**os.environ, 'PYTHONPATH': python_path}) + proc = ub.cmd( + [ + 'kernprof', + '-l', + # Add an eager pre-import target + '-pmy_bad_module', + '-c', + 'print(1)', + ], + env={**os.environ, 'PYTHONPATH': python_path}, + ) # Check that the traceback is preserved print(proc.stdout) print(proc.stderr, file=sys.stderr) @@ -369,17 +453,21 @@ def test_kernprof_bad_temp_script(stdin): Test for the preservation of the full traceback when an error occurs in a temporary script supplied via `kernprof -c` or `kernprof -`. """ - bad_script = '''1 / 0''' + bad_script = """1 / 0""" with contextlib.ExitStack() as stack: enter = stack.enter_context enter(ub.ChDir(enter(tempfile.TemporaryDirectory()))) if stdin: proc = subprocess.run( ['kernprof', '-'], - input=bad_script, capture_output=True, text=True) + input=bad_script, + capture_output=True, + text=True, + ) else: - proc = subprocess.run(['kernprof', '-c', bad_script], - capture_output=True, text=True) + proc = subprocess.run( + ['kernprof', '-c', bad_script], capture_output=True, text=True + ) # Check that the traceback is preserved print(proc.stdout) print(proc.stderr, file=sys.stderr) @@ -405,9 +493,17 @@ def test_bad_prof_mod_target(debug): with contextlib.ExitStack() as stack: enter = stack.enter_context enter(ub.ChDir(enter(tempfile.TemporaryDirectory()))) - proc = ub.cmd(['kernprof', '-l', '-p', './nonexistent.py', - '-c', 'print("Output: foo")'], - env={**os.environ, 'LINE_PROFILER_DEBUG': str(debug)}) + proc = ub.cmd( + [ + 'kernprof', + '-l', + '-p', + './nonexistent.py', + '-c', + 'print("Output: foo")', + ], + env={**os.environ, 'LINE_PROFILER_DEBUG': str(debug)}, + ) print(proc.stdout) print(proc.stderr, file=sys.stderr) proc.check_returncode() @@ -429,22 +525,24 @@ def test_call_with_diagnostics(module, builtin): cmd = ['kernprof'] if builtin: cmd += ['-b'] - proc = ub.cmd(cmd + to_run, - env={**os.environ, 'LINE_PROFILER_DEBUG': 'true'}) + proc = ub.cmd( + cmd + to_run, env={**os.environ, 'LINE_PROFILER_DEBUG': 'true'} + ) print(proc.stdout) print(proc.stderr, file=sys.stderr) proc.check_returncode() assert os.listdir() # Profile results has_runctx_call = re.search( - r'Calling: .+\.runctx\(.+\)', proc.stdout, flags=re.DOTALL) + r'Calling: .+\.runctx\(.+\)', proc.stdout, flags=re.DOTALL + ) has_execfile_call = re.search( - r'execfile\(.+\)', proc.stdout, flags=re.DOTALL) + r'execfile\(.+\)', proc.stdout, flags=re.DOTALL + ) assert bool(has_runctx_call) == (not builtin) assert bool(has_execfile_call) == (not module) class TestKernprof(unittest.TestCase): - def test_enable_disable(self): profile = ContextualProfile() self.assertEqual(profile.enable_count, 0) diff --git a/tests/test_line_profiler.py b/tests/test_line_profiler.py index 067cb9bf..6537baff 100644 --- a/tests/test_line_profiler.py +++ b/tests/test_line_profiler.py @@ -61,8 +61,10 @@ class check_timings_and_mem: - We don't leak reference counts for code objects (which are retrieved with a C function in the Cython code). """ - def __init__(self, prof, *, - check_timings=True, check_ref_counts=True, gc=False): + + def __init__( + self, prof, *, check_timings=True, check_ref_counts=True, gc=False + ): self.prof = prof self.check_timings = bool(check_timings) self.check_ref_counts = bool(check_ref_counts) @@ -85,14 +87,16 @@ def _check_timings_enter(self): return timings = self.timings assert not any(timings.values()), ( - f'Expected no timing entries, got {timings!r}') + f'Expected no timing entries, got {timings!r}' + ) def _check_timings_exit(self): if not self.check_timings: return timings = self.timings assert any(timings.values()), ( - f'Expected timing entries, got {timings!r}') + f'Expected timing entries, got {timings!r}' + ) def _check_ref_counts_enter(self): if not self.check_ref_counts: @@ -105,12 +109,20 @@ def _check_ref_counts_exit(self): assert self.ref_counts is not None for key, count in self.get_ref_counts().items(): try: - referrers = repr(gc.get_referrers(*( - code for code in self.prof.code_hash_map - if code.co_name == key))) - msg = (f'{key}(): ' - f'ref count {self.ref_counts[key]} -> {count} ' - f'(referrers: {referrers})') + referrers = repr( + gc.get_referrers( + *( + code + for code in self.prof.code_hash_map + if code.co_name == key + ) + ) + ) + msg = ( + f'{key}(): ' + f'ref count {self.ref_counts[key]} -> {count} ' + f'(referrers: {referrers})' + ) if self.ref_counts[key] == count: print(msg) else: @@ -166,8 +178,7 @@ def get_last_time(prof, *, c=False): @prof def func(): - return (get_last_time(prof, c=True).copy(), - get_last_time(prof).copy()) + return (get_last_time(prof, c=True).copy(), get_last_time(prof).copy()) # These are always empty outside a profiling context # (hence the need of the above function to capture the transient @@ -276,8 +287,9 @@ def test_coroutine_decorator(): """ Test for `LineProfiler.wrap_coroutine()`. """ - async def coro(delay=.015625): - return (await asyncio.sleep(delay, 1)) + + async def coro(delay=0.015625): + return await asyncio.sleep(delay, 1) profile = LineProfiler() coro_wrapped = profile(coro) @@ -295,16 +307,18 @@ def test_async_gen_decorator(gc): """ Test for `LineProfiler.wrap_async_generator()`. """ - delay = .015625 + delay = 0.015625 async def use_agen_complex(*args, delay=delay): results = [] agen = ag_wrapped(delay) results.append(await agen.asend(None)) # Start the generator for send in args: - with (pytest.raises(StopAsyncIteration) - if send is None else - contextlib.nullcontext()): + with ( + pytest.raises(StopAsyncIteration) + if send is None + else contextlib.nullcontext() + ): results.append(await agen.asend(send)) if send is None: break @@ -334,18 +348,22 @@ async def use_agen_simple(*args, delay=delay): xfail_312 = hasattr(sys, 'monitoring') and not gc if xfail_312: # Python 3.12+ excinfo = stack.enter_context( - pytest.raises(AssertionError, match=r'ag\(\): ref count')) + pytest.raises(AssertionError, match=r'ag\(\): ref count') + ) stack.enter_context( - check_timings_and_mem(profile, check_timings=False, gc=gc)) + check_timings_and_mem(profile, check_timings=False, gc=gc) + ) assert profile.enable_count == 0 assert asyncio.run(use_agen_complex(1, 2, 3)) == [0, 1, 3, 6] assert profile.enable_count == 0 assert asyncio.run(use_agen_complex(1, 2, 3, None, 4)) == [0, 1, 3, 6] assert profile.enable_count == 0 if xfail_312: - pytest.xfail('\nsys.version={!r}..., gc={}:\n{}' - .format(sys.version.strip().split()[0], gc, - excinfo.getrepr(style='no'))) + pytest.xfail( + '\nsys.version={!r}..., gc={}:\n{}'.format( + sys.version.strip().split()[0], gc, excinfo.getrepr(style='no') + ) + ) def test_classmethod_decorator(): @@ -373,7 +391,7 @@ def foo(cls) -> str: output = strip(get_prof_stats(profile, name='profile', summarize=True)) # Check that we have profiled `Object.foo()` assert output.endswith('foo') - line, = (line for line in output.splitlines() if line.endswith('* 2')) + (line,) = (line for line in output.splitlines() if line.endswith('* 2')) # Check that it has been run twice assert int(line.split()[1]) == 2 assert profile.enable_count == 0 @@ -404,7 +422,7 @@ def foo(x: int) -> int: output = strip(get_prof_stats(profile, name='profile', summarize=True)) # Check that we have profiled `Object.foo()` assert output.endswith('foo') - line, = (line for line in output.splitlines() if line.endswith('* 2')) + (line,) = (line for line in output.splitlines() if line.endswith('* 2')) # Check that it has been run twice assert int(line.split()[1]) == 2 assert profile.enable_count == 0 @@ -435,14 +453,11 @@ def foo(self, x: int) -> int: assert profile.enable_count == 0 # XXX: should we try do remove duplicates? assert profile.functions == [Object.foo, Object.foo] - assert (profiled_foo_1(2) - == profiled_foo_2(2) - == obj.foo(2) - == id(obj) * 2) + assert profiled_foo_1(2) == profiled_foo_2(2) == obj.foo(2) == id(obj) * 2 output = strip(get_prof_stats(profile, name='profile', summarize=True)) # Check that we have profiled `Object.foo()` assert output.endswith('foo') - line, = (line for line in output.splitlines() if line.endswith('* x')) + (line,) = (line for line in output.splitlines() if line.endswith('* x')) # Check that the wrapped methods has been run twice in total assert int(line.split()[1]) == 2 assert profile.enable_count == 0 @@ -466,8 +481,9 @@ def foo(self, x: int) -> int: bar = profile(functools.partialmethod(foo, 1)) - assert isinstance(inspect.getattr_static(Object, 'bar'), - functools.partialmethod) + assert isinstance( + inspect.getattr_static(Object, 'bar'), functools.partialmethod + ) obj = Object() assert profile.enable_count == 0 assert profile.functions == [Object.foo] @@ -475,7 +491,7 @@ def foo(self, x: int) -> int: output = strip(get_prof_stats(profile, name='profile', summarize=True)) # Check that we have profiled `Object.foo()` (via `.bar()`) assert output.endswith('foo') - line, = (line for line in output.splitlines() if line.endswith('* x')) + (line,) = (line for line in output.splitlines() if line.endswith('* x')) # Check that the wrapped method has been run once assert int(line.split()[1]) == 1 assert profile.enable_count == 0 @@ -504,15 +520,11 @@ def foo(x: int, y: int) -> int: assert profile.enable_count == 0 # XXX: should we try do remove duplicates? assert profile.functions == [foo, foo] - assert (profiled_bar_1(3) - == profiled_bar_2(3) - == bar(3) - == foo(2, 3) - == 5) + assert profiled_bar_1(3) == profiled_bar_2(3) == bar(3) == foo(2, 3) == 5 output = strip(get_prof_stats(profile, name='profile', summarize=True)) # Check that we have profiled `foo()` assert output.endswith('foo') - line, = (line for line in output.splitlines() if line.endswith('x + y')) + (line,) = (line for line in output.splitlines() if line.endswith('x + y')) # Check that the wrapped partials has been run twice in total assert int(line.split()[1]) == 2 assert profile.enable_count == 0 @@ -558,10 +570,12 @@ def foo(self, foo) -> None: output = strip(get_prof_stats(profile, name='profile', summarize=True)) # Check that we have profiled `Object.foo` assert output.endswith('foo') - getter_line, = (line for line in output.splitlines() - if line.endswith('* 2')) - setter_line, = (line for line in output.splitlines() - if line.endswith('// 2')) + (getter_line,) = ( + line for line in output.splitlines() if line.endswith('* 2') + ) + (setter_line,) = ( + line for line in output.splitlines() if line.endswith('// 2') + ) # Check that the getter has been run twice and the setter once assert int(getter_line.split()[1]) == 2 assert int(setter_line.split()[1]) == 1 @@ -598,7 +612,7 @@ def foo(self) -> int: output = strip(get_prof_stats(profile, name='profile', summarize=True)) # Check that we have profiled `Object.foo` assert output.endswith('foo') - line, = (line for line in output.splitlines() if line.endswith('* 2')) + (line,) = (line for line in output.splitlines() if line.endswith('* 2')) # Check that the getter has been run once assert int(line.split()[1]) == 1 assert profile.enable_count == 0 @@ -634,7 +648,8 @@ def class_method(cls, n): assert set(profile.functions) == { Object.__init__.__wrapped__, Object.id.fget.__wrapped__, - vars(Object)['class_method'].__func__.__wrapped__} + vars(Object)['class_method'].__func__.__wrapped__, + } # Make some calls assert not profile.enable_count obj = Object(1) @@ -678,7 +693,8 @@ def __repr__(self): # Check data all_nhits = { func_name.rpartition('.')[-1]: sum(nhits for (_, nhits, _) in entries) - for (*_, func_name), entries in profile.get_stats().timings.items()} + for (*_, func_name), entries in profile.get_stats().timings.items() + } assert all_nhits['__init__'] == all_nhits['__repr__'] == 2 @@ -691,13 +707,14 @@ def test_profiler_c_callable_no_op(decorate): """ profile = LineProfiler() - for (func, Type) in [ - (len, types.BuiltinFunctionType), - ('string'.split, types.BuiltinMethodType), - (vars(int)['from_bytes'], types.ClassMethodDescriptorType), - (str.split, types.MethodDescriptorType), - ((1).__str__, types.MethodWrapperType), - (int.__repr__, types.WrapperDescriptorType)]: + for func, Type in [ + (len, types.BuiltinFunctionType), + ('string'.split, types.BuiltinMethodType), + (vars(int)['from_bytes'], types.ClassMethodDescriptorType), + (str.split, types.MethodDescriptorType), + ((1).__str__, types.MethodWrapperType), + (int.__repr__, types.WrapperDescriptorType), + ]: assert isinstance(func, Type) if decorate: # Decoration is no-op assert profile(func) is func @@ -710,6 +727,7 @@ def test_show_func_column_formatting(): from line_profiler.line_profiler import show_func import line_profiler import io + # Use a function in this module as an example func = line_profiler.line_profiler.show_text start_lineno = func.__code__.co_firstlineno @@ -718,12 +736,23 @@ def test_show_func_column_formatting(): def get_func_linenos(func): import sys + if sys.version_info[0:2] >= (3, 10): - return sorted(set([t[0] if t[2] is None else t[2] - for t in func.__code__.co_lines()])) + return sorted( + set( + [ + t[0] if t[2] is None else t[2] + for t in func.__code__.co_lines() + ] + ) + ) else: import dis - return sorted(set([t[1] for t in dis.findlinestarts(func.__code__)])) + + return sorted( + set([t[1] for t in dis.findlinestarts(func.__code__)]) + ) + line_numbers = get_func_linenos(func) unit = 1.0 @@ -736,8 +765,16 @@ def get_func_linenos(func): for idx, lineno in enumerate(line_numbers, start=1) ] stream = io.StringIO() - show_func(filename, start_lineno, func_name, timings, unit, - output_unit, stream, stripzeros) + show_func( + filename, + start_lineno, + func_name, + timings, + unit, + output_unit, + stream, + stripzeros, + ) text = stream.getvalue() print(text) @@ -746,17 +783,27 @@ def get_func_linenos(func): for idx, lineno in enumerate(line_numbers, start=1) ] stream = io.StringIO() - show_func(filename, start_lineno, func_name, timings, unit, - output_unit, stream, stripzeros) + show_func( + filename, + start_lineno, + func_name, + timings, + unit, + output_unit, + stream, + stripzeros, + ) text = stream.getvalue() print(text) # TODO: write a check to verify columns are aligned nicely -@pytest.mark.skipif(not hasattr(sys, 'monitoring'), - reason='no `sys.monitoring` in version ' - f'{".".join(str(v) for v in sys.version_info[:2])}') +@pytest.mark.skipif( + not hasattr(sys, 'monitoring'), + reason='no `sys.monitoring` in version ' + f'{".".join(str(v) for v in sys.version_info[:2])}', +) def test_sys_monitoring(monkeypatch): """ Test that `LineProfiler` is properly registered with @@ -795,17 +842,24 @@ def test_profile_generated_code(): generated_code_name = "" assert is_generated_code(generated_code_name) - code_lines = [ - "def test_fn():", - " return 42" - ] + code_lines = ['def test_fn():', ' return 42'] - linecache.cache[generated_code_name] = (None, None, [l + "\n" for l in code_lines], None) + linecache.cache[generated_code_name] = ( + None, + None, + [l + '\n' for l in code_lines], + None, + ) # Compile the generated code ns = {} - exec(compile("".join(l + "\n" for l in code_lines), generated_code_name, "exec"), ns) - fn = ns["test_fn"] + exec( + compile( + ''.join(l + '\n' for l in code_lines), generated_code_name, 'exec' + ), + ns, + ) + fn = ns['test_fn'] # Profile the generated function profiler = LineProfiler() @@ -839,14 +893,14 @@ def sum_n(n): def sum_n_sq(n): x = 0 for n in range(1, n + 1): - x += n ** 2 + x += n**2 return x @prof2 def sum_n_cb(n): x = 0 for n in range(1, n + 1): - x += n ** 3 + x += n**3 return x # If we decorate a wrapper, just "register" the profiler with the @@ -859,16 +913,20 @@ def sum_n_cb(n): # Call the functions n = 400 - assert sum_n_wrapper_1(n) == .5 * n * (n + 1) - assert sum_n_wrapper_2(n) == .5 * n * (n + 1) + assert sum_n_wrapper_1(n) == 0.5 * n * (n + 1) + assert sum_n_wrapper_2(n) == 0.5 * n * (n + 1) assert 6 * sum_n_sq(n) == n * (n + 1) * (2 * n + 1) - assert sum_n_cb(n) == .25 * (n * (n + 1)) ** 2 + assert sum_n_cb(n) == 0.25 * (n * (n + 1)) ** 2 # Inspect the timings - t1 = {fname.rpartition('.')[-1]: entries - for (*_, fname), entries in prof1.get_stats().timings.items()} - t2 = {fname.rpartition('.')[-1]: entries - for (*_, fname), entries in prof2.get_stats().timings.items()} + t1 = { + fname.rpartition('.')[-1]: entries + for (*_, fname), entries in prof1.get_stats().timings.items() + } + t2 = { + fname.rpartition('.')[-1]: entries + for (*_, fname), entries in prof2.get_stats().timings.items() + } assert set(t1) == {'sum_n_sq', 'sum_n'} assert set(t2) == {'sum_n_cb', 'sum_n'} # Note: `prof1` active when both wrapper is called, but `prof2` only @@ -909,7 +967,7 @@ def func(n): assert namespace_2['func'](20) == 20 * 21 // 2 # Check that data from both calls are aggregated # (Entries are represented as tuples `(lineno, nhits, time)`) - entries, = profile.get_stats().timings.values() + (entries,) = profile.get_stats().timings.values() assert entries[-2][1] == 10 + 20 @@ -928,18 +986,9 @@ def func(n): '-func3:prof_all' '-func4:prof_all:prof_some', # More profiler stacks - 'func1:p1:p2' - '-func2:p2:p3' - '-func3:p3:p4' - '-func4:p4:p1', - 'func1:p1:p2:p3' - '-func2:p2:p3:p4' - '-func3:p3:p4:p1' - '-func4:p4:p1:p2', - 'func1:p1:p2:p3' - '-func2:p4:p3:p2' - '-func3:p3:p4:p1' - '-func4:p2:p1:p4', + 'func1:p1:p2-func2:p2:p3-func3:p3:p4-func4:p4:p1', + 'func1:p1:p2:p3-func2:p2:p3:p4-func3:p3:p4:p1-func4:p4:p1:p2', + 'func1:p1:p2:p3-func2:p4:p3:p2-func3:p3:p4:p1-func4:p2:p1:p4', # Misc. edge cases # - Naive padding of the following case would cause `func1()` # and `func2()` to end up with the same bytecode, so guard @@ -953,9 +1002,11 @@ def func(n): '-func3:p3:p4' '-func4:p4:p1' '-func1:p1', # Now we're passing `func1()` to `p1` twice - ]) + ], +) def test_multiple_profilers_identical_bytecode( - tmp_path, ops, force_same_line_numbers): + tmp_path, ops, force_same_line_numbers +): """ Test that functions compiling down to the same bytecode are correctly handled between multiple profilers. @@ -969,36 +1020,42 @@ def test_multiple_profilers_identical_bytecode( - `force_same_line_numbers` is used to coerce all functions to compile down to code objects with the same line numbers. """ + def check_seen(name, output, func_id, expected): - lines = [line for line in output.splitlines() - if line.startswith('Function: ')] + lines = [ + line + for line in output.splitlines() + if line.startswith('Function: ') + ] if any(func_id in line for line in lines) == expected: return if expected: - raise AssertionError( - f'profiler `@{name}` didn\'t see `{func_id}()`') - raise AssertionError( - f'profiler `@{name}` saw `{func_id}()`') + raise AssertionError(f"profiler `@{name}` didn't see `{func_id}()`") + raise AssertionError(f'profiler `@{name}` saw `{func_id}()`') def check_has_profiling_data(name, output, func_id, expected): assert func_id.startswith('func') - nloops = func_id[len('func'):] + nloops = func_id[len('func') :] try: - line = next(line for line in output.splitlines() - if line.endswith(f'result.append({nloops})')) + line = next( + line + for line in output.splitlines() + if line.endswith(f'result.append({nloops})') + ) except StopIteration: if expected: raise AssertionError( - f'profiler `@{name}` didn\'t see `{func_id}()`') + f"profiler `@{name}` didn't see `{func_id}()`" + ) else: return if (line.split()[1] == nloops) == expected: return if expected: raise AssertionError( - f'profiler `@{name}` didn\'t get data from `{func_id}()`') - raise AssertionError( - f'profiler `@{name}` got data from `{func_id}()`') + f"profiler `@{name}` didn't get data from `{func_id}()`" + ) + raise AssertionError(f'profiler `@{name}` got data from `{func_id}()`') if force_same_line_numbers: funcs = {} @@ -1015,6 +1072,7 @@ def func{0}(): tempfile.write_text(source) exec(compile(source, str(tempfile), 'exec'), funcs) else: + def func1(): result = [] for _ in range(1): @@ -1039,8 +1097,7 @@ def func4(): result.append(4) return result - funcs = {'func1': func1, 'func2': func2, - 'func3': func3, 'func4': func4} + funcs = {'func1': func1, 'func2': func2, 'func3': func3, 'func4': func4} # Apply the decorators in order all_dec_names = {f'func{i}': set() for i in [1, 2, 3, 4]} @@ -1060,10 +1117,14 @@ def func4(): assert funcs['func3']() == [3, 3, 3] assert funcs['func4']() == [4, 4, 4, 4] # Check that the bytecodes of the profiled functions are distinct - profiled_funcs = {funcs[name].__line_profiler_id__.func - for name, decs in all_dec_names.items() if decs} - assert len({func.__code__.co_code - for func in profiled_funcs}) == len(profiled_funcs) + profiled_funcs = { + funcs[name].__line_profiler_id__.func + for name, decs in all_dec_names.items() + if decs + } + assert len({func.__code__.co_code for func in profiled_funcs}) == len( + profiled_funcs + ) # Check the profiling results for name, prof in sorted(all_profs.items()): output = get_prof_stats(prof, name=name, summarize=True) @@ -1079,6 +1140,7 @@ def test_aggregate_profiling_data_between_code_versions(): are preserved when another profiler causes the code object of a function to be overwritten. """ + def func(n): x = 0 for n in range(1, n + 1): @@ -1101,13 +1163,16 @@ def func(n): # `prof1.get_stats()` for prof, name, count in (prof1, 'prof1', 25), (prof2, 'prof2', 15): result = get_prof_stats(prof, name) - loop_body = next(line for line in result.splitlines() - if line.endswith('x += n')) + loop_body = next( + line for line in result.splitlines() if line.endswith('x += n') + ) assert loop_body.split()[1] == str(count) -@pytest.mark.xfail(condition=sys.version_info[:2] == (3, 9), - reason='Handling of `finally` bugged in Python 3.9') +@pytest.mark.xfail( + condition=sys.version_info[:2] == (3, 9), + reason='Handling of `finally` bugged in Python 3.9', +) def test_profiling_exception(): """ Test that profiling data is reported for: @@ -1150,9 +1215,12 @@ def func_try_except_finally(reraise): l.append(3) # Try-except-finally: finally l = [] - for func in [func_raise, func_try_finally, - functools.partial(func_try_except_finally, True), - functools.partial(func_try_except_finally, False)]: + for func in [ + func_raise, + func_try_finally, + functools.partial(func_try_except_finally, True), + functools.partial(func_try_except_finally, False), + ]: try: func() except MyException: @@ -1160,11 +1228,15 @@ def func_try_except_finally(reraise): result = get_prof_stats(prof) assert l == [1, 2, 3, 2, 3] for stmt, nhits in [ - ('raise', 1), ('try-finally', 1), ('try-except-finally', 2)]: + ('raise', 1), + ('try-finally', 1), + ('try-except-finally', 2), + ]: for step in stmt.split('-'): comment = '# {}: {}'.format(stmt.capitalize(), step) - line = next(line for line in result.splitlines() - if line.endswith(comment)) + line = next( + line for line in result.splitlines() if line.endswith(comment) + ) assert line.split()[1] == str(nhits) @@ -1179,6 +1251,7 @@ def test_load_stats_files(legacy, n): that we ensure that ``'.lprof'`` files written by old versions of :py:mod:`line_profiler` is still properly handled. """ + def write(stats, filename): if legacy: legacy_stats = type(stats).__base__(stats.timings, stats.unit) @@ -1189,10 +1262,14 @@ def write(stats, filename): stats.to_file(filename) return filename - stats1 = LineStats({('foo', 1, 'spam.py'): [(2, 3, 3600)]}, .015625) - stats2 = LineStats({('foo', 1, 'spam.py'): [(2, 4, 700)], - ('bar', 10, 'spam.py'): [(10, 20, 1000)]}, - .0625) + stats1 = LineStats({('foo', 1, 'spam.py'): [(2, 3, 3600)]}, 0.015625) + stats2 = LineStats( + { + ('foo', 1, 'spam.py'): [(2, 4, 700)], + ('bar', 10, 'spam.py'): [(10, 20, 1000)], + }, + 0.0625, + ) with TemporaryDirectory() as tmpdir: fname1 = write(stats1, os.path.join(tmpdir, '1.lprof')) if n == 1: diff --git a/tests/test_sys_monitoring.py b/tests/test_sys_monitoring.py index 98007607..247d945e 100644 --- a/tests/test_sys_monitoring.py +++ b/tests/test_sys_monitoring.py @@ -8,10 +8,18 @@ from io import StringIO from itertools import count from types import CodeType, ModuleType -from typing import (Any, Optional, Union, Literal, - Callable, Generator, - Dict, FrozenSet, Tuple, - ClassVar) +from typing import ( + Any, + Optional, + Union, + Literal, + Callable, + Generator, + Dict, + FrozenSet, + Tuple, + ClassVar, +) import pytest @@ -31,9 +39,11 @@ class SysMonHelper: Helper object which helps with simplifying attribute access on :py:mod:`sys.monitoring`. """ + tool_id: int no_tool_id_callables: ClassVar[FrozenSet[str]] = frozenset( - {'restart_events'}) + {'restart_events'} + ) def __init__(self, tool_id: Optional[int] = None) -> None: if tool_id is None: @@ -68,7 +78,8 @@ def __getattr__(self, attr: str): return result def get_current_callback( - self, event_id: Optional[int] = None) -> Union[Callable, None]: + self, event_id: Optional[int] = None + ) -> Union[Callable, None]: """ Arguments: event_id (int | None) @@ -92,13 +103,14 @@ class restore_events(AbstractContextManager): """ Restore the global or local :py:mod:`sys.monitoring` events. """ + code: Union[CodeType, None] mon: SysMonHelper events: int - def __init__(self, *, - code: Optional[CodeType] = None, - tool_id: Optional[int] = None) -> None: + def __init__( + self, *, code: Optional[CodeType] = None, tool_id: Optional[int] = None + ) -> None: self.code = code self.mon = SysMonHelper(tool_id) self.events = sys.monitoring.events.NO_EVENTS @@ -137,6 +149,7 @@ class LineCallback: Settable boolean determining whether to return :py:data:`sys.monitoring.DISABLE` on a reported line event. """ + nhits: Dict[Tuple[str, int, str], 'Counter[int]'] predicate: Callable[[CodeType, int], bool] disable: bool @@ -146,7 +159,7 @@ def __init__( predicate: Callable[[CodeType, int], bool], *, register: bool = True, - disable: bool = False + disable: bool = False, ) -> None: """ Arguments: @@ -184,8 +197,9 @@ def handle_line_event(self, code: CodeType, lineno: int) -> bool: """ result = self.predicate(code, lineno) if result: - self.nhits.setdefault( - _line_profiler.label(code), Counter())[lineno] += 1 + self.nhits.setdefault(_line_profiler.label(code), Counter())[ + lineno + ] += 1 return result def __call__(self, code: CodeType, lineno: int) -> Any: @@ -237,7 +251,8 @@ def disable_line_events(code: Optional[CodeType] = None) -> None: @pytest.fixture(autouse=True) def sys_mon_cleanup( - monkeypatch: pytest.MonkeyPatch) -> Generator[None, None, None]: + monkeypatch: pytest.MonkeyPatch, +) -> Generator[None, None, None]: """ If :py:mod:`sys.monitoring` is available: * Make sure that we are using the default behavior by overriding @@ -254,6 +269,7 @@ def sys_mon_cleanup( otherwise, automatically :py:func:`pytest.skip` the test. """ + def restore(message): for name, callback in callbacks.items(): prev_callback = MON.register_callback(event_ids[name], callback) @@ -261,16 +277,22 @@ def restore(message): callback_repr = '(UNCHANGED)' else: callback_repr = '-> ' + repr(callback) - print('{} (`sys.monitoring.events.{}`): {!r} {}'.format( - message, name, prev_callback, callback_repr)) + print( + '{} (`sys.monitoring.events.{}`): {!r} {}'.format( + message, name, prev_callback, callback_repr + ) + ) if not USE_SYS_MONITORING: pytest.skip('No `sys.monitoring`') # Remember the callbacks - event_ids = {name: getattr(MON, name) - for name in ('LINE', 'PY_RETURN', 'PY_YIELD')} - callbacks = {name: MON.register_callback(event_id, None) - for name, event_id in event_ids.items()} + event_ids = { + name: getattr(MON, name) for name in ('LINE', 'PY_RETURN', 'PY_YIELD') + } + callbacks = { + name: MON.register_callback(event_id, None) + for name, event_id in event_ids.items() + } # Restore the callbacks since we "popped" them restore('Pre-test: putting the callbacks back') # Set the tool name if it isn't set already @@ -308,25 +330,28 @@ def test_wrapping_trace(wrap_trace: bool) -> None: prof = LineProfiler(wrap_trace=wrap_trace) try: nhits_expected = _test_callback_helper( - 6, 7, 8, 9, prof=prof, callback_called=wrap_trace) + 6, 7, 8, 9, prof=prof, callback_called=wrap_trace + ) finally: with StringIO() as sio: prof.print_stats(sio) output = sio.getvalue() print(output) - line = next(line for line in output.splitlines() - if line.endswith('# Loop body')) + line = next( + line for line in output.splitlines() if line.endswith('# Loop body') + ) nhits = int(line.split()[1]) assert nhits == nhits_expected def _test_callback_helper( - nloop_no_trace: int, - nloop_trace_global: int, - nloop_trace_local: int, - nloop_disabled: int, - prof: Optional[LineProfiler] = None, - callback_called: bool = True) -> int: + nloop_no_trace: int, + nloop_trace_global: int, + nloop_trace_local: int, + nloop_disabled: int, + prof: Optional[LineProfiler] = None, + callback_called: bool = True, +) -> int: cumulative_nhits = 0 def func(n: int) -> int: @@ -337,8 +362,9 @@ def func(n: int) -> int: def get_loop_hits() -> int: nonlocal cumulative_nhits - cumulative_nhits = ( - callback.nhits[_line_profiler.label(code)][lineno_loop]) + cumulative_nhits = callback.nhits[_line_profiler.label(code)][ + lineno_loop + ] return cumulative_nhits def test_running_func(n: int) -> int: @@ -353,9 +379,11 @@ def test_running_func(n: int) -> int: assert MON.get_current_callback() is callback new_ref_count = callback.get_return_ref_count() referrers = repr(gc.get_referrers(callback.return_value)) - msg = (f'ReturnObjectCallback.return_value: ' - f'ref count {old_ref_count} -> {new_ref_count} ' - f'(referrers: {referrers})') + msg = ( + f'ReturnObjectCallback.return_value: ' + f'ref count {old_ref_count} -> {new_ref_count} ' + f'(referrers: {referrers})' + ) if new_ref_count == old_ref_count: print(msg) else: @@ -367,6 +395,7 @@ class ReturnObjectCallback(LineCallback): Callback which returns an arbitrary object in place of :py:data:`None` for reference-count-tracking purposes. """ + return_value: ClassVar[Any] = object() def __call__(self, *args, **kwargs) -> Any: @@ -383,8 +412,10 @@ def get_return_ref_count(cls, use_gc: bool = False) -> int: lines, first_lineno = inspect.getsourcelines(func) lineno_loop = first_lineno + next( - offset for offset, line in enumerate(lines) - if line.rstrip().endswith('# Loop body')) + offset + for offset, line in enumerate(lines) + if line.rstrip().endswith('# Loop body') + ) names = {func.__name__, func.__qualname__} code = func.__code__ if prof is not None: @@ -450,10 +481,12 @@ def get_return_ref_count(cls, use_gc: bool = False) -> int: # Return the total number of loops run # (Note: `nloop_disabled` is used twice) - return (nloop_no_trace - + nloop_trace_global - + nloop_trace_local - + 2 * nloop_disabled) + return ( + nloop_no_trace + + nloop_trace_global + + nloop_trace_local + + 2 * nloop_disabled + ) @pytest.mark.parametrize('standalone', [True, False]) @@ -478,14 +511,16 @@ def test_callback_switching(standalone: bool) -> None: if prof is None: return - line = next(line for line in output.splitlines() - if line.endswith('# Loop body')) + line = next( + line for line in output.splitlines() if line.endswith('# Loop body') + ) nhits = int(line.split()[1]) assert nhits == nhits_expected def _test_callback_switching_helper( - nloop: int, prof: Optional[LineProfiler] = None) -> int: + nloop: int, prof: Optional[LineProfiler] = None +) -> int: cumulative_nhits = 0, 0 def func(n: int) -> int: @@ -497,9 +532,11 @@ def func(n: int) -> int: def get_loop_hits() -> Tuple[int, int]: nonlocal cumulative_nhits cumulative_nhits = tuple( # type: ignore[assignment] - callback.nhits.get( - _line_profiler.label(code), Counter())[lineno_loop] - for callback in (callback_1, callback_2)) + callback.nhits.get(_line_profiler.label(code), Counter())[ + lineno_loop + ] + for callback in (callback_1, callback_2) + ) return cumulative_nhits def predicate(code: CodeType, lineno: int) -> bool: @@ -510,11 +547,12 @@ class SwitchingCallback(LineCallback): Callback which switches to the next one after having been triggered. """ + next: Union['SwitchingCallback', None] - def __init__(self, *args, - next: Optional['SwitchingCallback'] = None, - **kwargs) -> None: + def __init__( + self, *args, next: Optional['SwitchingCallback'] = None, **kwargs + ) -> None: super().__init__(*args, **kwargs) self.next = next @@ -528,8 +566,10 @@ def __call__(self, code: CodeType, lineno: int) -> Any: lines, first_lineno = inspect.getsourcelines(func) lineno_loop = first_lineno + next( - offset for offset, line in enumerate(lines) - if line.rstrip().endswith('# Loop body')) + offset + for offset, line in enumerate(lines) + if line.rstrip().endswith('# Loop body') + ) names = {func.__name__, func.__qualname__} code = func.__code__ if prof is not None: @@ -552,8 +592,10 @@ def __call__(self, code: CodeType, lineno: int) -> Any: if nhits_one == nhits_other: assert get_loop_hits() == (nhits_one, nhits_other) else: # Odd number - assert get_loop_hits() in ((nhits_one, nhits_other), - (nhits_other, nhits_one)) + assert get_loop_hits() in ( + (nhits_one, nhits_other), + (nhits_other, nhits_one), + ) return nloop @@ -563,8 +605,11 @@ def __call__(self, code: CodeType, lineno: int) -> Any: @pytest.mark.parametrize('start_with_events', [True, False]) @pytest.mark.parametrize('standalone', [True, False]) def test_callback_update_events( - standalone: bool, start_with_events: bool, - code_local_events: bool, add_events: bool) -> None: + standalone: bool, + start_with_events: bool, + code_local_events: bool, + add_events: bool, +) -> None: """ Check that a :py:mod:`sys.monitoring` callback which updates the event set (global and code-object-local) after a certain number of @@ -582,8 +627,9 @@ def func(n: int) -> int: def get_loop_hits() -> int: nonlocal cumulative_nhits - cumulative_nhits = ( - callback.nhits[_line_profiler.label(code)][lineno_loop]) + cumulative_nhits = callback.nhits[_line_profiler.label(code)][ + lineno_loop + ] return cumulative_nhits class EventUpdatingCallback(LineCallback): @@ -593,9 +639,14 @@ class EventUpdatingCallback(LineCallback): - Enables :py:attr:`sys.monitoring.CALL` events (if :py:attr:`~.call` is true) """ - def __init__(self, *args, - code: Optional[CodeType] = None, call: bool = False, - **kwargs) -> None: + + def __init__( + self, + *args, + code: Optional[CodeType] = None, + call: bool = False, + **kwargs, + ) -> None: super().__init__(*args, **kwargs) self.count = 0 self.code = code @@ -613,12 +664,15 @@ def __call__(self, code: CodeType, lineno: int) -> None: MON.set_events(MON.get_events() | MON.CALL) else: MON.set_local_events( - self.code, MON.get_local_events(self.code) | MON.CALL) + self.code, MON.get_local_events(self.code) | MON.CALL + ) lines, first_lineno = inspect.getsourcelines(func) lineno_loop = first_lineno + next( - offset for offset, line in enumerate(lines) - if line.rstrip().endswith('# Loop body')) + offset + for offset, line in enumerate(lines) + if line.rstrip().endswith('# Loop body') + ) names = {func.__name__, func.__qualname__} code = func.__code__ @@ -635,9 +689,10 @@ def __call__(self, code: CodeType, lineno: int) -> None: code = orig_func.__code__ callback = EventUpdatingCallback( - lambda code, lineno: (code.co_name in names and lineno == lineno_loop), + lambda code, lineno: code.co_name in names and lineno == lineno_loop, code=code if code_local_events else None, - call=add_events) + call=add_events, + ) local_events = local_events_after = MON.NO_EVENTS global_events_after = global_events @@ -673,8 +728,9 @@ def __call__(self, code: CodeType, lineno: int) -> None: if prof is None: return - line = next(line for line in output.splitlines() - if line.endswith('# Loop body')) + line = next( + line for line in output.splitlines() if line.endswith('# Loop body') + ) nhits = int(line.split()[1]) assert nhits == nloop @@ -694,7 +750,8 @@ def test_callback_toggle_local_events(standalone: bool) -> None: try: nhits_expected = _test_callback_toggle_local_events_helper( - 17, 18, 19, prof) + 17, 18, 19, prof + ) finally: if prof is not None: with StringIO() as sio: @@ -704,17 +761,19 @@ def test_callback_toggle_local_events(standalone: bool) -> None: if prof is None: return - line = next(line for line in output.splitlines() - if line.endswith('# Loop body')) + line = next( + line for line in output.splitlines() if line.endswith('# Loop body') + ) nhits = int(line.split()[1]) assert nhits == nhits_expected def _test_callback_toggle_local_events_helper( - nloop_before_disabling: int, - nloop_when_disabled: int, - nloop_after_reenabling: int, - prof: Optional[LineProfiler] = None) -> int: + nloop_before_disabling: int, + nloop_when_disabled: int, + nloop_after_reenabling: int, + prof: Optional[LineProfiler] = None, +) -> int: cumulative_nhits = 0 def func(*nloops) -> int: @@ -728,8 +787,9 @@ def func(*nloops) -> int: def get_loop_hits() -> int: nonlocal cumulative_nhits - cumulative_nhits = ( - callback.nhits[_line_profiler.label(code)][lineno_loop]) + cumulative_nhits = callback.nhits[_line_profiler.label(code)][ + lineno_loop + ] return cumulative_nhits class LocalDisablingCallback(LineCallback): @@ -737,6 +797,7 @@ class LocalDisablingCallback(LineCallback): Callback which disables LINE events locally after a certain number of hits """ + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.switch_count = 0 @@ -760,11 +821,15 @@ def __call__(self, code: CodeType, lineno: int) -> Any: lines, first_lineno = inspect.getsourcelines(func) lineno_loop = first_lineno + next( - offset for offset, line in enumerate(lines) - if line.rstrip().endswith('# Loop body')) + offset + for offset, line in enumerate(lines) + if line.rstrip().endswith('# Loop body') + ) lineno_switch = first_lineno + next( - offset for offset, line in enumerate(lines) - if line.rstrip().endswith('# Switching location')) + offset + for offset, line in enumerate(lines) + if line.rstrip().endswith('# Switching location') + ) linenos = {lineno_loop, lineno_switch} names = {func.__name__, func.__qualname__} code = func.__code__ @@ -773,14 +838,18 @@ def __call__(self, code: CodeType, lineno: int) -> Any: code = orig_func.__code__ callback = LocalDisablingCallback( - lambda code, lineno: (code.co_name in names and lineno in linenos)) + lambda code, lineno: code.co_name in names and lineno in linenos + ) MON.set_events(MON.get_events() | MON.LINE) n = nloop_before_disabling + nloop_when_disabled + nloop_after_reenabling assert MON.get_current_callback() is callback - assert func(nloop_before_disabling, - nloop_when_disabled, - nloop_after_reenabling) == n * (n + 1) // 2 + assert ( + func( + nloop_before_disabling, nloop_when_disabled, nloop_after_reenabling + ) + == n * (n + 1) // 2 + ) assert MON.get_current_callback() is callback print(callback.nhits) assert get_loop_hits() == nloop_before_disabling + nloop_after_reenabling @@ -790,7 +859,8 @@ def __call__(self, code: CodeType, lineno: int) -> Any: @pytest.mark.parametrize('profile_when', ['before', 'after']) def test_local_event_preservation( - profile_when: Literal['before', 'after']) -> None: + profile_when: Literal['before', 'after'], +) -> None: """ Check that existing :py:mod:`sys.monitoring` code-local events are preserved when a profiler swaps out the callable's code object. @@ -821,12 +891,15 @@ def profile() -> None: orig_func, func = func, prof(func) code = orig_func.__code__ assert code is not orig_code, ( - '`line_profiler` didn\'t overwrite the function\'s code object') + "`line_profiler` didn't overwrite the function's code object" + ) lines, first_lineno = inspect.getsourcelines(func) lineno_loop = first_lineno + next( - offset for offset, line in enumerate(lines) - if line.rstrip().endswith('# Loop body')) + offset + for offset, line in enumerate(lines) + if line.rstrip().endswith('# Loop body') + ) names = {func.__name__, func.__qualname__} code = func.__code__ callback = LineCallback(lambda code, _: code.co_name in names) @@ -855,7 +928,8 @@ def profile() -> None: prof.print_stats(sio) output = sio.getvalue() print(output) - line = next(line for line in output.splitlines() - if line.endswith('# Loop body')) + line = next( + line for line in output.splitlines() if line.endswith('# Loop body') + ) nhits = int(line.split()[1]) assert nhits == n diff --git a/tests/test_sys_trace.py b/tests/test_sys_trace.py index 23432892..ad73d224 100644 --- a/tests/test_sys_trace.py +++ b/tests/test_sys_trace.py @@ -10,6 +10,7 @@ - However, there effects are isolated since each test is run in a separate Python subprocess. """ + from __future__ import annotations import concurrent.futures import functools @@ -46,7 +47,8 @@ def strip(s: str) -> str: def isolate_test_in_subproc( - func: Optional[Callable] = None, debug: bool = DEBUG) -> Callable: + func: Optional[Callable] = None, debug: bool = DEBUG +) -> Callable: """ Run the test function with the supplied arguments in a subprocess so that it doesn't pollute the state of the current interpretor. @@ -63,8 +65,9 @@ def isolate_test_in_subproc( if func is None: return functools.partial(isolate_test_in_subproc, debug=debug) - def message(msg: str, header: str, *, - short: bool = False, **kwargs) -> None: + def message( + msg: str, header: str, *, short: bool = False, **kwargs + ) -> None: header = strip(header) if not header.endswith(':'): header += ':' @@ -85,8 +88,9 @@ def wrapper(*args, **kwargs): assert literal_eval(repr(kwargs)) == kwargs # Write a test script - args_reprs = ([repr(a) for a in args] - + [f'{k}={v!r}' for k, v in kwargs.items()]) + args_reprs = [repr(a) for a in args] + [ + f'{k}={v!r}' for k, v in kwargs.items() + ] if len(args_reprs) > 1: args_repr = '\n' + textwrap.indent(',\n'.join(args_reprs), ' ' * 8) else: @@ -107,8 +111,9 @@ def {test}(): test_dir, test_filename = os.path.split(__file__) test_module_name, dot_py = os.path.splitext(test_filename) assert dot_py == '.py' - code = code_template.format(path=test_dir, mod=test_module_name, - test=test_func, args=args_repr) + code = code_template.format( + path=test_dir, mod=test_module_name, test=test_func, args=args_repr + ) # Run the test script in a subprocess if debug: # Use `pytest` to get perks like assertion rewriting cmd = [sys.executable, '-m', 'pytest', '-s'] @@ -128,7 +133,8 @@ def {test}(): # Make sure that we're testing the "default behavior" env.pop('LINE_PROFILER_CORE', '') proc = subprocess.run( - cmd, capture_output=True, env=env, text=True) + cmd, capture_output=True, env=env, text=True + ) finally: os.chdir(curdir) if proc.stdout: @@ -190,22 +196,29 @@ def __exit__(self, *_, **__): self.callback = None -def get_incr_logger(logs: List[str], func: Literal[foo, bar, baz] = foo, *, - bugged: bool = False, - report_return: bool = False) -> TracingFunc: - ''' +def get_incr_logger( + logs: List[str], + func: Literal[foo, bar, baz] = foo, + *, + bugged: bool = False, + report_return: bool = False, +) -> TracingFunc: + """ Append a ': spam = <...>' message whenever we hit the line in `func()` containing the incrementation of `result`. If it's made `bugged`, it sets the frame's `.f_trace_lines` to false after writing the first log entry, disabling line events. If `report_return` is true, a 'Returning from ()' log entry is written on return. - ''' - def callback( - frame: FrameType, event: Event, _) -> Union[TracingFunc, None]: + """ + + def callback(frame: FrameType, event: Event, _) -> Union[TracingFunc, None]: if DEBUG and callback.emit_debug: - print('{0.co_filename}:{1.f_lineno} - {0.co_name} ({2})' - .format(frame.f_code, frame, event)) + print( + '{0.co_filename}:{1.f_lineno} - {0.co_name} ({2})'.format( + frame.f_code, frame, event + ) + ) if event == 'call': # Set up tracing for nested scopes return callback if event not in events: # Only trace the specified events @@ -229,9 +242,10 @@ def callback( func_name = func.__name__ filename = func.__code__.co_filename lineno = func.__code__.co_firstlineno - block = inspect.getblock(linecache.getlines(__file__)[lineno - 1:]) - (offset, line), = ((i, line) for i, line in enumerate(block) - if 'result +=' in line) + block = inspect.getblock(linecache.getlines(__file__)[lineno - 1 :]) + ((offset, line),) = ( + (i, line) for i, line in enumerate(block) if 'result +=' in line + ) lineno += offset counter = line.split()[-1] @@ -244,17 +258,20 @@ def callback( def get_return_logger(logs: List[str], *, bugged: bool = False) -> TracingFunc: - ''' + """ Append a 'Returning from `()`' message whenever we hit return from a function defined in this file. If it's made `bugged`, it panics and errors out when returning from `bar`, thus unsetting the `sys` trace. - ''' - def callback( - frame: FrameType, event: Event, _) -> Union[TracingFunc, None]: + """ + + def callback(frame: FrameType, event: Event, _) -> Union[TracingFunc, None]: if DEBUG and callback.emit_debug: - print('{0.co_filename}:{1.f_lineno} - {0.co_name} ({2})' - .format(frame.f_code, frame, event)) + print( + '{0.co_filename}:{1.f_lineno} - {0.co_name} ({2})'.format( + frame.f_code, frame, event + ) + ) if event == 'call': # Set up tracing for nested scopes return callback @@ -275,6 +292,7 @@ def callback( class MyException(Exception): """Unique exception raised by some of the tests.""" + pass @@ -282,14 +300,16 @@ class MyException(Exception): def _test_helper_callback_preservation( - callback: Union[TracingFunc, None]) -> None: + callback: Union[TracingFunc, None], +) -> None: sys.settrace(callback) - assert sys.gettrace() is callback, f'can\'t set trace to {callback!r}' + assert sys.gettrace() is callback, f"can't set trace to {callback!r}" profile = LineProfiler(wrap_trace=False) profile.enable_by_count() if not USE_SYS_MONITORING: assert profile in sys.gettrace().active_instances, ( - 'can\'t set trace to the profiler') + "can't set trace to the profiler" + ) profile.disable_by_count() assert sys.gettrace() is callback, f'trace not restored to {callback!r}' sys.settrace(None) @@ -308,13 +328,19 @@ def test_callback_preservation(): @pytest.mark.parametrize('set_frame_local_trace', [True, False]) @pytest.mark.parametrize( ('label', 'use_profiler', 'wrap_trace'), - [('base case', False, False), - ('profiled (trace suspended)', True, False), - ('profiled (trace wrapped)', True, True)]) + [ + ('base case', False, False), + ('profiled (trace suspended)', True, False), + ('profiled (trace wrapped)', True, True), + ], +) @isolate_test_in_subproc def test_callback_wrapping( - label: str, use_profiler: bool, - wrap_trace: bool, set_frame_local_trace: bool) -> None: + label: str, + use_profiler: bool, + wrap_trace: bool, + set_frame_local_trace: bool, +) -> None: """ Test in a subprocess that the profiler can wrap around an existing trace callback such that we both profile the code and do whatever @@ -326,7 +352,8 @@ def test_callback_wrapping( if use_profiler: profile = LineProfiler( - wrap_trace=wrap_trace, set_frame_local_trace=set_frame_local_trace) + wrap_trace=wrap_trace, set_frame_local_trace=set_frame_local_trace + ) foo_like = profile(foo) trace_preserved = wrap_trace else: @@ -337,7 +364,7 @@ def test_callback_wrapping( else: exp_logs = [] - assert sys.gettrace() is my_callback, 'can\'t set custom trace' + assert sys.gettrace() is my_callback, "can't set custom trace" my_callback.emit_debug = True x = foo_like(5) my_callback.emit_debug = False @@ -357,19 +384,23 @@ def test_callback_wrapping( profile.print_stats(stream=sio, summarize=True) out = sio.getvalue() print(out) - line, = (line for line in out.splitlines() if '+=' in line) + (line,) = (line for line in out.splitlines() if '+=' in line) nhits = int(line.split()[1]) assert nhits == 5, f'expected 5 profiler hits, got {nhits!r}' @pytest.mark.parametrize( ('label', 'use_profiler', 'enable_count'), - [('base case', False, 0), - ('profiled (isolated)', True, 0), - ('profiled (continuous)', True, 1)]) + [ + ('base case', False, 0), + ('profiled (isolated)', True, 0), + ('profiled (continuous)', True, 1), + ], +) @isolate_test_in_subproc def test_wrapping_throwing_callback( - label: str, use_profiler: bool, enable_count: int) -> None: + label: str, use_profiler: bool, enable_count: int +) -> None: """ Test in a subprocess that if the profiler wraps around an existing trace callback that errors out: @@ -392,7 +423,7 @@ def test_wrapping_throwing_callback( logs = [] my_callback = get_return_logger(logs, bugged=True) sys.settrace(my_callback) - assert sys.gettrace() is my_callback, 'can\'t set custom trace' + assert sys.gettrace() is my_callback, "can't set custom trace" if use_profiler: profile = LineProfiler(wrap_trace=True) @@ -412,7 +443,7 @@ def test_wrapping_throwing_callback( # disables itself pass else: - assert False, 'tracing function didn\'t error out' + assert False, "tracing function didn't error out" y = baz_like(5) # Not logged because trace disabled itself my_callback.emit_debug = False for _ in range(enable_count): @@ -421,7 +452,8 @@ def test_wrapping_throwing_callback( assert x == 6, f'expected `foo(3) = 6`, got {x!r}' assert y == 15, f'expected `baz(5) = 15`, got {y!r}' assert sys.gettrace() is None, ( - '`sys` trace = {sys.gettrace()!r} not reset afterwards') + '`sys` trace = {sys.gettrace()!r} not reset afterwards' + ) # Check that the existing trace function has been called where # appropriate @@ -437,20 +469,27 @@ def test_wrapping_throwing_callback( profile.print_stats(stream=sio, summarize=True) out = sio.getvalue() print(out) - for func, marker, exp_nhits in [('foo', 'spam', 3), ('bar', 'ham', 4), - ('baz', 'eggs', 5)]: - line, = (line for line in out.splitlines() - if line.endswith('+= ' + marker)) + for func, marker, exp_nhits in [ + ('foo', 'spam', 3), + ('bar', 'ham', 4), + ('baz', 'eggs', 5), + ]: + (line,) = ( + line for line in out.splitlines() if line.endswith('+= ' + marker) + ) nhits = int(line.split()[1]) - assert nhits == exp_nhits, (f'expected {exp_nhits} ' - f'profiler hits, got {nhits!r}') + assert nhits == exp_nhits, ( + f'expected {exp_nhits} profiler hits, got {nhits!r}' + ) -@pytest.mark.parametrize(('label', 'use_profiler'), - [('base case', False), ('profiled', True)]) +@pytest.mark.parametrize( + ('label', 'use_profiler'), [('base case', False), ('profiled', True)] +) @isolate_test_in_subproc -def test_wrapping_line_event_disabling_callback(label: str, - use_profiler: bool) -> None: +def test_wrapping_line_event_disabling_callback( + label: str, use_profiler: bool +) -> None: """ Test in a subprocess that if the profiler wraps around an existing trace callback that disables `.f_trace_lines`: @@ -468,7 +507,7 @@ def test_wrapping_line_event_disabling_callback(label: str, else: foo_like = foo - assert sys.gettrace() is my_callback, 'can\'t set custom trace' + assert sys.gettrace() is my_callback, "can't set custom trace" my_callback.emit_debug = True x = foo_like(5) my_callback.emit_debug = False @@ -489,13 +528,14 @@ def test_wrapping_line_event_disabling_callback(label: str, profile.print_stats(stream=sio, summarize=True) out = sio.getvalue() print(out) - line, = (line for line in out.splitlines() if '+=' in line) + (line,) = (line for line in out.splitlines() if '+=' in line) nhits = int(line.split()[1]) assert nhits == 5, f'expected 5 profiler hits, got {nhits!r}' def _test_helper_wrapping_thread_local_callbacks( - profile: Union[LineProfiler, None], sleep: float = .0625) -> str: + profile: Union[LineProfiler, None], sleep: float = 0.0625 +) -> str: logs = [] if threading.current_thread() == threading.main_thread(): thread_label = 'main' @@ -515,7 +555,7 @@ def _test_helper_wrapping_thread_local_callbacks( # Check result sys.settrace(my_callback) - assert sys.gettrace() is my_callback, 'can\'t set custom trace' + assert sys.gettrace() is my_callback, "can't set custom trace" my_callback.emit_debug = True x = func_like(5) my_callback.emit_debug = False @@ -529,11 +569,13 @@ def _test_helper_wrapping_thread_local_callbacks( return '\n'.join(logs) -@pytest.mark.parametrize(('label', 'use_profiler'), - [('base case', False), ('profiled', True)]) +@pytest.mark.parametrize( + ('label', 'use_profiler'), [('base case', False), ('profiled', True)] +) @isolate_test_in_subproc -def test_wrapping_thread_local_callbacks(label: str, - use_profiler: bool) -> None: +def test_wrapping_thread_local_callbacks( + label: str, use_profiler: bool +) -> None: """ Test in a subprocess that the profiler properly handles thread-local `sys` trace callbacks. @@ -550,15 +592,19 @@ def test_wrapping_thread_local_callbacks(label: str, results = set() with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: tasks = [] - tasks.append(executor.submit( # This is run on a side thread - _test_helper_wrapping_thread_local_callbacks, profile)) + tasks.append( + executor.submit( # This is run on a side thread + _test_helper_wrapping_thread_local_callbacks, profile + ) + ) # This is run on the main thread results.add(_test_helper_wrapping_thread_local_callbacks(profile)) results.update( - future.result() - for future in concurrent.futures.as_completed(tasks)) - assert results == expected_results, (f'expected {expected_results!r}, ' - f'got {results!r}') + future.result() for future in concurrent.futures.as_completed(tasks) + ) + assert results == expected_results, ( + f'expected {expected_results!r}, got {results!r}' + ) # Check profiling if profile is None: @@ -568,30 +614,48 @@ def test_wrapping_thread_local_callbacks(label: str, out = sio.getvalue() print(out) for var in 'spam', 'ham': - line, = (line for line in out.splitlines() - if line.endswith('+= ' + var)) + (line,) = ( + line for line in out.splitlines() if line.endswith('+= ' + var) + ) nhits = int(line.split()[1]) assert nhits == 5, f'expected 5 profiler hits, got {nhits!r}' @pytest.mark.parametrize( ('stay_in_scope', 'set_frame_local_trace', 'n', 'nhits'), - [(True, True, 100, {0: 2, # Both calls are traced - 5: 0, # Tracing suspended - 7: 100}), # Tracing restored (both calls) - # If `set_frame_local_trace` is false: - # - When using legacy tracing, tracing is suspended for the rest of - # the frame - # - Else, tracing is unaffected - (True, False, 100, - {0: 2, 5: 0, 7: 100 if USE_SYS_MONITORING else 0}), - # Calling a function always triggers `.__call__()` - (False, True, 100, {0: 1, # Only one of the calls is traced - 2: 100}), # 100 hits on the line in the loop - (False, False, 100, {0: 1, 2: 100})]) + [ + ( + True, + True, + 100, + { + 0: 2, # Both calls are traced + 5: 0, # Tracing suspended + 7: 100, + }, + ), # Tracing restored (both calls) + # If `set_frame_local_trace` is false: + # - When using legacy tracing, tracing is suspended for the rest of + # the frame + # - Else, tracing is unaffected + (True, False, 100, {0: 2, 5: 0, 7: 100 if USE_SYS_MONITORING else 0}), + # Calling a function always triggers `.__call__()` + ( + False, + True, + 100, + { + 0: 1, # Only one of the calls is traced + 2: 100, + }, + ), # 100 hits on the line in the loop + (False, False, 100, {0: 1, 2: 100}), + ], +) @isolate_test_in_subproc def test_python_level_trace_manipulation( - stay_in_scope, set_frame_local_trace, n, nhits): + stay_in_scope, set_frame_local_trace, n, nhits +): """ Test that: - When Python code retrieves the trace object set by `line_profiler` @@ -640,10 +704,14 @@ def func_break_in_middle(n): timings = prof.get_stats().timings print(timings) prof.print_stats() - entries = next(entries for (*_, func_name), entries in timings.items() - if func_name.endswith(func.__name__)) + entries = next( + entries + for (*_, func_name), entries in timings.items() + if func_name.endswith(func.__name__) + ) body_start_line = min(lineno for (lineno, *_) in entries) - all_nhits = {lineno - body_start_line: _nhits - for (lineno, _nhits, _) in entries} + all_nhits = { + lineno - body_start_line: _nhits for (lineno, _nhits, _) in entries + } all_nhits = {lineno: all_nhits.get(lineno, 0) for lineno in nhits} assert all_nhits == nhits, f'expected {nhits=}, got {all_nhits=}' diff --git a/tests/test_toml_config.py b/tests/test_toml_config.py index 3b1b1602..9b4648a7 100644 --- a/tests/test_toml_config.py +++ b/tests/test_toml_config.py @@ -1,6 +1,7 @@ """ Test the handling of TOML configs. """ + from __future__ import annotations import os import re @@ -22,7 +23,8 @@ def write_text(path: Path, text: str, /, *args, **kwargs) -> int: @pytest.fixture(autouse=True) def fresh_curdir( - monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory, + monkeypatch: pytest.MonkeyPatch, + tmp_path_factory: pytest.TempPathFactory, ) -> Generator[Path, None, None]: """ Ensure that the tests start on a clean slate: they shouldn't see @@ -54,7 +56,8 @@ def test_default_config_deep_copy() -> None: copy of the default config. """ default_1, default_2 = ( - ConfigSource.from_default().conf_dict for _ in (1, 2)) + ConfigSource.from_default().conf_dict for _ in (1, 2) + ) assert default_1 == default_2 assert default_1 is not default_2 # Sublist @@ -77,14 +80,17 @@ def test_table_normalization(fresh_curdir: Path) -> None: """ default_config = ConfigSource.from_default().conf_dict toml = fresh_curdir / 'foo.toml' - write_text(toml, """ + write_text( + toml, + """ [unrelated.table] foo = 'foo' # This should be ignored [tool.line_profiler.write] output_prefix = 'my_prefix' # This is parsed and retained nonexistent_key = 'nonexistent_value' # This should be ignored - """) + """, + ) loaded = ConfigSource.from_config(toml) assert loaded.path.samefile(toml) assert loaded.conf_dict['write']['output_prefix'] == 'my_prefix' @@ -100,18 +106,24 @@ def test_malformed_table(fresh_curdir: Path) -> None: non-subtable value taking the place of a supposed subtable. """ toml = fresh_curdir / 'foo.toml' - write_text(toml, """ + write_text( + toml, + """ [tool.line_profiler] write = [{lprof = true}] # This shouldn't be a list - """) - with pytest.raises(ValueError, - match=r"config = .*: expected .* keys.*:" - r".*'tool\.line_profiler\.write'"): + """, + ) + with pytest.raises( + ValueError, + match=r'config = .*: expected .* keys.*:' + r".*'tool\.line_profiler\.write'", + ): ConfigSource.from_config(toml) -def test_config_lookup_hierarchy(monkeypatch: pytest.MonkeyPatch, - fresh_curdir: Path) -> None: +def test_config_lookup_hierarchy( + monkeypatch: pytest.MonkeyPatch, fresh_curdir: Path +) -> None: """ Test the hierarchy according to which we load config files. """ @@ -147,8 +159,9 @@ def test_config_lookup_hierarchy(monkeypatch: pytest.MonkeyPatch, with pytest.raises(FileNotFoundError): ConfigSource.from_config(highest_priority) highest_priority.touch() - assert (ConfigSource.from_config(highest_priority) - .path.samefile(highest_priority)) + assert ConfigSource.from_config(highest_priority).path.samefile( + highest_priority + ) # Also test that `True` is equivalent to the default behavior # (`None`), and `False` to disabling all lookup assert ConfigSource.from_config(True).path.samefile(high_priority) @@ -167,9 +180,8 @@ def test_importlib_resources_deprecation() -> None: print(ConfigSource.from_default()) """) - command = [sys.executable, - '-W', 'always::DeprecationWarning', - '-c', code] + command = [sys.executable, '-W', 'always::DeprecationWarning', '-c', code] proc = run(command, check=True, capture_output=True, text=True) - assert not re.search('DeprecationWarning.*importlib[-_]?resources', - proc.stderr), proc.stderr + assert not re.search( + 'DeprecationWarning.*importlib[-_]?resources', proc.stderr + ), proc.stderr