#!/usr/bin/env python
"""
This module defines the core :class:`LineProfiler` class as well as methods to
inspect its output. This depends on the :py:mod:`line_profiler._line_profiler`
Cython backend.
"""
import functools
import inspect
import linecache
import os
import pickle
import sys
import tempfile
import types
import tokenize
from argparse import ArgumentParser
from datetime import datetime
try:
from ._line_profiler import LineProfiler as CLineProfiler
except ImportError as ex:
raise ImportError(
'The line_profiler._line_profiler c-extension is not importable. '
f'Has it been compiled? Underlying error is ex={ex!r}'
)
from . import _diagnostics as diagnostics
from .cli_utils import (
add_argument, get_cli_config, positive_float, short_string_path)
from .profiler_mixin import ByCountProfilerMixin, is_c_level_callable
from .scoping_policy import ScopingPolicy
from .toml_config import ConfigSource
# NOTE: This needs to be in sync with ../kernprof.py and __init__.py
__version__ = '5.0.0'
[docs]
@functools.lru_cache()
def get_column_widths(config=False):
"""
Arguments
config (bool | str | pathlib.PurePath | None)
Passed to :py:meth:`.ConfigSource.from_config`.
Note:
* Results are cached.
* 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'))
return types.MappingProxyType(subconf.conf_dict)
[docs]
def load_ipython_extension(ip):
""" API for IPython to recognize this module as an IPython extension.
"""
from .ipython_extension import LineProfilerMagics
ip.register_magics(LineProfilerMagics)
[docs]
def get_code_block(filename, lineno):
"""
Get the lines in the code block in a file starting from required
line number; understands Cython code.
Args:
filename (Union[os.PathLike, str])
Path to the source file.
lineno (int)
1-indexed line number of the first line in the block.
Returns:
lines (list[str])
Newline-terminated string lines.
Note:
This function makes use of :py:func:`inspect.getblock`, which is
public but undocumented API. That said, it has been in use in
this repo since 2008 (`fb60664`_), so we will continue using it
until we can't.
.. _fb60664: https://github.com/pyutils/line_profiler/commit/\
fb60664135296ba6061cfaa2bb66d4ba77964c53
Example:
>>> from os.path import join
>>> from tempfile import TemporaryDirectory
>>> from textwrap import dedent
>>>
>>>
>>> def get_last_line(*args, **kwargs):
... lines = get_code_block(*args, **kwargs)
... return lines[-1].rstrip('\\n')
...
>>>
>>> with TemporaryDirectory() as tmpdir:
... fname = join(tmpdir, 'cython_source.pyx')
... with open(fname, mode='w') as fobj:
... print(dedent('''
... class NormalClass: # 1
... def __init__(self): # 2
... pass # 3
...
... def normal_method(self, *args): # 5
... pass # 6
...
... cdef class CythonClass: # 8
... cpdef cython_method(self): # 9
... pass # 10
...
... property legacy_cython_prop: # 12
... def __get__(self): # 13
... return None # 14
... def __set__(self, value): # 15
... pass # 16
...
... def normal_func(x, y, z): # 18
... with some_ctx(): # 19
... ... # 20
...
... cdef cython_function( # 22
... int x, int y, int z): # 23
... ... # 24
... ''').strip('\\n'),
... file=fobj)
... # Vanilla Python code blocks:
... # - `NormalClass`
... assert get_last_line(fname, 1).endswith('# 6')
... # - `NormalClass.__init__()`
... assert get_last_line(fname, 2).endswith('# 3')
... # - `normal_func()`
... assert get_last_line(fname, 18).endswith('# 20')
... # Cython code blocks:
... # - `CythonClass`
... assert get_last_line(fname, 8).endswith('# 16')
... # - `CythonClass.cython_method()`
... assert get_last_line(fname, 9).endswith('# 10')
... # - `CythonClass.legacy_cython_prop`
... assert get_last_line(fname, 12).endswith('# 16')
... # - `cython_function()`
... assert get_last_line(fname, 22).endswith('# 24')
"""
BlockFinder = inspect.BlockFinder
namespace = inspect.getblock.__globals__
namespace['BlockFinder'] = _CythonBlockFinder
try:
return inspect.getblock(linecache.getlines(filename)[lineno - 1:])
finally:
namespace['BlockFinder'] = BlockFinder
class _CythonBlockFinder(inspect.BlockFinder):
"""
Compatibility layer turning Cython-specific code blocks (``cdef``,
``cpdef``, and legacy ``property`` declaration) into something that
is understood by :py:class:`inspect.BlockFinder`.
Note:
This function makes use of :py:func:`inspect.BlockFinder`, which
is public but undocumented API. See similar caveat in
:py:func:`~.get_code_block`.
"""
def tokeneater(self, type, token, *args, **kwargs):
if (
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, *args, **kwargs)
class _WrapperInfo:
"""
Helper object for holding the state of a wrapper function.
Attributes:
func (types.FunctionType):
The function it wraps.
profiler_id (int)
ID of the `LineProfiler`.
"""
def __init__(self, func, profiler_id):
self.func = func
self.profiler_id = profiler_id
[docs]
class LineProfiler(CLineProfiler, ByCountProfilerMixin):
"""
A profiler that records the execution times of individual lines.
This provides the core line-profiler functionality.
Example:
>>> import line_profiler
>>> profile = line_profiler.LineProfiler()
>>> @profile
... def func():
... x1 = list(range(10))
... x2 = list(range(100))
... x3 = list(range(1000))
>>> func()
>>> profile.print_stats()
"""
[docs]
def __call__(self, func):
"""
Decorate a function, method, :py:class:`property`,
:py:func:`~functools.partial` object etc. to start the profiler
on function entry and stop it on function exit.
"""
# The same object is returned when:
# - `func` is a `types.FunctionType` which is already
# decorated by the profiler,
# - `func` is a class, or
# - `func` is any of the C-level callables that can't be
# profiled
# otherwise, wrapper objects are always returned.
self.add_callable(func)
return self.wrap_callable(func)
[docs]
def wrap_callable(self, func):
if is_c_level_callable(func): # Non-profilable
return func
return super().wrap_callable(func)
[docs]
def add_callable(self, func, guard=None, name=None):
"""
Register a function, method, :py:class:`property`,
:py:func:`~functools.partial` object, etc. with the underlying
Cython profiler.
Args:
func (...):
Function, class/static/bound method, property, etc.
guard (Optional[Callable[[types.FunctionType], bool]])
Optional checker callable, which takes a function object
and returns true(-y) if it *should not* be passed to
:py:meth:`.add_function()`. Defaults to checking
whether the function is already a profiling wrapper.
name (Optional[str])
Optional name for ``func``, to be used in log messages.
Returns:
1 if any function is added to the profiler, 0 otherwise.
Note:
This method should in general be called instead of the more
low-level :py:meth:`.add_function()`.
"""
if guard is None:
guard = self._already_a_wrapper
nadded = 0
func_repr = self._repr_for_log(func, name)
for impl in self.get_underlying_functions(func):
info, wrapped_by_this_prof = self._get_wrapper_info(impl)
if wrapped_by_this_prof if guard is None else guard(impl):
continue
if info:
# It's still a profiling wrapper, just wrapped by
# someone else -> extract the inner function
impl = info.func
self.add_function(impl)
nadded += 1
if impl is func:
self._debug(f'added {func_repr}')
else:
self._debug(f'added {func_repr} -> {self._repr_for_log(impl)}')
return 1 if nadded else 0
@staticmethod
def _repr_for_log(obj, name=None):
try:
real_name = '{0.__module__}.{0.__qualname__}'.format(obj)
except AttributeError:
try:
real_name = obj.__name__
except AttributeError:
real_name = '???'
return '{} `{}{}` {}@ {:#x}'.format(
type(obj).__name__,
real_name,
'()' if callable(obj) and not isinstance(obj, type) else '',
f'(=`{name}`) ' if name and name != real_name else '',
id(obj))
def _debug(self, msg):
self_repr = f'{type(self).__name__} @ {id(self):#x}'
logger = diagnostics.log
if logger.backend == 'print':
now = datetime.now().isoformat(sep=' ', timespec='seconds')
msg = f'[{self_repr} {now}] {msg}'
else:
msg = f'{self_repr}: {msg}'
logger.debug(msg)
[docs]
def dump_stats(self, filename):
""" Dump a representation of the data to a file as a pickled
:py:class:`~.LineStats` object from :py:meth:`~.get_stats()`.
"""
lstats = self.get_stats()
with open(filename, 'wb') as f:
pickle.dump(lstats, f, pickle.HIGHEST_PROTOCOL)
[docs]
def print_stats(self, stream=None, output_unit=None, stripzeros=False,
details=True, summarize=False, sort=False, rich=False, *,
config=None):
""" Show the gathered statistics.
"""
lstats = self.get_stats()
show_text(lstats.timings, lstats.unit, output_unit=output_unit,
stream=stream, stripzeros=stripzeros,
details=details, summarize=summarize, sort=sort, rich=rich,
config=config)
def _add_namespace(
self, namespace, *,
seen=None,
func_scoping_policy=ScopingPolicy.NONE,
class_scoping_policy=ScopingPolicy.NONE,
module_scoping_policy=ScopingPolicy.NONE,
wrap=False,
name=None):
def func_guard(func):
return self._already_a_wrapper(func) or not func_check(func)
if seen is None:
seen = set()
count = 0
add_namespace = functools.partial(
self._add_namespace,
seen=seen,
func_scoping_policy=func_scoping_policy,
class_scoping_policy=class_scoping_policy,
module_scoping_policy=module_scoping_policy,
wrap=wrap)
members_to_wrap = {}
func_check = func_scoping_policy.get_filter(namespace, 'func')
cls_check = class_scoping_policy.get_filter(namespace, 'class')
mod_check = module_scoping_policy.get_filter(namespace, 'module')
# Logging stuff
if not name:
try: # Class
name = '{0.__module__}.{0.__qualname__}'.format(namespace)
except AttributeError: # Module
name = namespace.__name__
for attr, value in vars(namespace).items():
if id(value) in seen:
continue
seen.add(id(value))
if isinstance(value, type):
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}')):
continue
else:
try:
if not self.add_callable(
value, guard=func_guard, name=f'{name}.{attr}'):
continue
except TypeError: # Not a callable (wrapper)
continue
if wrap:
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)
if count:
self._debug(
'added {} member{} in {}'.format(
count,
'' if count == 1 else 's',
self._repr_for_log(namespace, name)))
return count
[docs]
def add_class(self, cls, *, scoping_policy=None, wrap=False):
"""
Add the members (callables (wrappers), methods, classes, ...) in
a class' local namespace and profile them.
Args:
cls (type):
Class to be profiled.
scoping_policy (Union[str, ScopingPolicy, \
ScopingPolicyDict, None]):
Whether (and how) to match the scope of members and
decide on whether to add them:
:py:class:`str` (incl. :py:class:`~.ScopingPolicy`):
Strings are converted to :py:class:`~.ScopingPolicy`
instances in a case-insensitive manner, and the same
policy applies to all members.
``{'func': ..., 'class': ..., 'module': ...}``
Mapping specifying individual policies to be enacted
for the corresponding member types.
:py:const:`None`
The default, equivalent to
:py:data:\
`~.scoping_policy.DEFAULT_SCOPING_POLICIES`.
See :py:class:`~.ScopingPolicy` and
:py:meth:`.ScopingPolicy.to_policies` for details.
wrap (bool):
Whether to replace the wrapped members with wrappers
which automatically enable/disable the profiler when
called.
Returns:
n (int):
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)
[docs]
def add_module(self, mod, *, scoping_policy=None, wrap=False):
"""
Add the members (callables (wrappers), methods, classes, ...) in
a module's local namespace and profile them.
Args:
mod (ModuleType):
Module to be profiled.
scoping_policy (Union[str, ScopingPolicy, \
ScopingPolicyDict, None]):
Whether (and how) to match the scope of members and
decide on whether to add them:
:py:class:`str` (incl. :py:class:`~.ScopingPolicy`):
Strings are converted to :py:class:`~.ScopingPolicy`
instances in a case-insensitive manner, and the same
policy applies to all members.
``{'func': ..., 'class': ..., 'module': ...}``
Mapping specifying individual policies to be enacted
for the corresponding member types.
:py:const:`None`
The default, equivalent to
:py:data:\
`~.scoping_policy.DEFAULT_SCOPING_POLICIES`.
See :py:class:`~.ScopingPolicy` and
:py:meth:`.ScopingPolicy.to_policies` for details.
wrap (bool):
Whether to replace the wrapped members with wrappers
which automatically enable/disable the profiler when
called.
Returns:
n (int):
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)
def _get_wrapper_info(self, func):
info = getattr(func, self._profiler_wrapped_marker, None)
return info, bool(info and id(self) == info.profiler_id)
# Override these mixed-in bookkeeping methods to take care of
# potential multiple profiler sequences
def _already_a_wrapper(self, func):
return self._get_wrapper_info(func)[1]
def _mark_wrapper(self, wrapper):
# Are re-wrapping an existing wrapper (e.g. created by another
# profiler?)
wrapped = wrapper.__wrapped__
info = getattr(wrapped, self._profiler_wrapped_marker, None)
new_info = _WrapperInfo(info.func if info else wrapped, id(self))
setattr(wrapper, self._profiler_wrapped_marker, new_info)
return 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.
[docs]
def is_generated_code(filename):
""" 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('<generated') or
filename.startswith('<ipython-input-') or
filename.startswith(os.path.join(temp_dir, 'ipykernel_')) or
filename.startswith(os.path.join(temp_dir, 'xpython_'))
)
[docs]
def show_func(filename, start_lineno, func_name, timings, unit,
output_unit=None, stream=None, stripzeros=False, rich=False,
*,
config=None):
"""
Show results for a single function.
Args:
filename (str):
Path to the profiled file
start_lineno (int):
First line number of profiled function
func_name (str): name of profiled function
timings (List[Tuple[int, int, float]]):
Measurements for each line (lineno, nhits, time).
unit (float):
The number of seconds used as the cython LineProfiler's unit.
output_unit (float | None):
Output unit (in seconds) in which the timing info is displayed.
stream (io.TextIOBase | None):
Defaults to sys.stdout
stripzeros (bool):
If True, prints nothing if the function was not run
rich (bool):
If True, attempt to use rich highlighting.
config (Union[str, PurePath, bool, None]):
Optional filename from which to load configurations (e.g.
output column widths);
default (= `True` or `None`) is to look for a config file
based on the environment variable `${LINE_PROFILER_RC}` and
path-based lookup;
passing `False` disables all lookup and falls back to the
default configuration
Example:
>>> from line_profiler.line_profiler import show_func
>>> import line_profiler
>>> # Use a function in this file as an example
>>> func = line_profiler.line_profiler.show_text
>>> start_lineno = func.__code__.co_firstlineno
>>> filename = func.__code__.co_filename
>>> func_name = func.__name__
>>> # Build fake timeings for each line in the example function
>>> import inspect
>>> num_lines = len(inspect.getsourcelines(func)[0])
>>> line_numbers = list(range(start_lineno + 3,
... start_lineno + num_lines))
>>> timings = [(lineno, idx * 1e13, idx * (2e10 ** (idx % 3)))
... for idx, lineno
... in enumerate(line_numbers, start=1)]
>>> unit = 1.0
>>> output_unit = 1.0
>>> stream = None
>>> stripzeros = False
>>> rich = 1
>>> show_func(filename, start_lineno, func_name, timings, unit,
... output_unit, stream, stripzeros, rich)
"""
if stream is None:
stream = sys.stdout
total_hits = sum(t[1] for t in timings)
total_time = sum(t[2] for t in timings)
if stripzeros and total_hits == 0:
return
if rich:
# References:
# https://github.com/Textualize/rich/discussions/3076
try:
from rich.syntax import Syntax
from rich.highlighter import ReprHighlighter
from rich.text import Text
from rich.console import Console
from rich.table import Table
except ImportError:
rich = 0
if output_unit is None:
output_unit = unit
scalar = unit / output_unit
linenos = [t[0] for t in timings]
stream.write('Total time: %g s\n' % (total_time * unit))
if os.path.exists(filename) or is_generated_code(filename):
stream.write(f'File: {filename}\n')
stream.write(f'Function: {func_name} at line {start_lineno}\n')
if os.path.exists(filename):
# Clear the cache to ensure that we get up-to-date results.
linecache.clearcache()
sublines = get_code_block(filename, start_lineno)
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('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
sublines = [''] * nlines
# Define minimum column sizes so text fits and usually looks consistent
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()}
display = {}
# Loop over each line to determine better column formatting.
# Fallback to scientific notation if columns are larger than a threshold.
for lineno, nhits, time in timings:
if total_time == 0: # Happens rarely on empty function
percent = ''
else:
percent = '%5.1f' % (100 * time / total_time)
time_disp = '%5.1f' % (time * scalar)
if len(time_disp) > default_column_sizes['time']:
time_disp = '%5.3g' % (time * scalar)
perhit_disp = '%5.1f' % (float(time) * scalar / nhits)
if len(perhit_disp) > default_column_sizes['perhit']:
perhit_disp = '%5.3g' % (float(time) * scalar / nhits)
nhits_disp = "%d" % nhits
if len(nhits_disp) > default_column_sizes['hits']:
nhits_disp = '%g' % nhits
display[lineno] = (nhits_disp, time_disp, perhit_disp, percent)
# Expand column sizes if the numbers are large.
column_sizes = default_column_sizes.copy()
if len(display):
max_hitlen = max(len(t[0]) for t in display.values())
max_timelen = max(len(t[1]) for t in display.values())
max_perhitlen = max(len(t[2]) for t in display.values())
column_sizes['hits'] = max(column_sizes['hits'], max_hitlen)
column_sizes['time'] = max(column_sizes['time'], max_timelen)
column_sizes['perhit'] = max(column_sizes['perhit'], max_perhitlen)
col_order = ['line', 'hits', 'time', 'perhit', 'percent']
lhs_template = ' '.join(['%' + str(column_sizes[k]) + 's' for k in col_order])
template = lhs_template + ' %-s'
linenos = range(start_lineno, start_lineno + len(sublines))
empty = ('', '', '', '')
header = ('Line #', 'Hits', 'Time', 'Per Hit', '% Time', 'Line Contents')
header = template % header
stream.write('\n')
stream.write(header)
stream.write('\n')
stream.write('=' * len(header))
stream.write('\n')
if rich:
# Build the RHS and LHS of the table separately
lhs_lines = []
rhs_lines = []
for lineno, line in zip(linenos, sublines):
nhits, time, per_hit, percent = display.get(lineno, empty)
txt = lhs_template % (lineno, nhits, time, per_hit, percent)
rhs_lines.append(line.rstrip('\n').rstrip('\r'))
lhs_lines.append(txt)
rhs_text = '\n'.join(rhs_lines)
lhs_text = '\n'.join(lhs_lines)
# Highlight the RHS with Python syntax
rhs = Syntax(rhs_text, 'python', background_color='default')
# Use default highlights for the LHS
# TODO: could use colors to draw the eye to longer running lines.
lhs = Text(lhs_text)
ReprHighlighter().highlight(lhs)
# 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.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=stream, soft_wrap=True, color_system='standard')
write_console.print(table)
stream.write('\n')
else:
for lineno, line in zip(linenos, sublines):
nhits, time, per_hit, percent = display.get(lineno, empty)
line_ = line.rstrip('\n').rstrip('\r')
txt = template % (lineno, nhits, time, per_hit, percent, 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, time, per_hit, percent, line_)
stream.write(txt)
stream.write('\n')
stream.write('\n')
[docs]
def show_text(stats, unit, output_unit=None, stream=None, stripzeros=False,
details=True, summarize=False, sort=False, rich=False, *,
config=None):
"""
Show text for the given timings.
Ignore:
# For developer testing, generate some profile output
python -m kernprof -l -p uuid -m uuid
# Use this function to view it with rich
python -m line_profiler -rmtz "uuid.lprof"
# Use this function to view it without rich
python -m line_profiler -mtz "uuid.lprof"
"""
if stream is None:
stream = sys.stdout
if output_unit is not None:
stream.write('Timer unit: %g s\n\n' % output_unit)
else:
stream.write('Timer unit: %g s\n\n' % unit)
if sort:
# Order by ascending duration
stats_order = sorted(stats.items(), key=lambda kv: sum(t[2] for t in kv[1]))
else:
# Default ordering
stats_order = sorted(stats.items())
# Pre-lookup the appropriate config file
config = ConfigSource.from_config(config).path
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)
if summarize:
# Summarize the total time for each function
if rich:
try:
from rich.console import Console
from rich.markup import escape
except ImportError:
rich = 0
line_template = '%6.2f seconds - %s:%s - %s'
if rich:
write_console = Console(file=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))
write_console.print(line)
else:
for (fn, lineno, name), timings in stats_order:
total_time = sum(t[2] for t in timings) * unit
if not stripzeros or total_time:
line = line_template % (total_time, fn, lineno, name)
stream.write(line + '\n')
[docs]
def load_stats(filename):
""" Utility function to load a pickled :py:class:`~.LineStats`
object from a given filename.
"""
with open(filename, 'rb') as f:
return pickle.load(f)
[docs]
def main():
"""
The line profiler CLI to view output from :command:`kernprof -l`.
"""
parser = ArgumentParser(
description='Read and show line profiling results (`.lprof` files) '
'as generated by the CLI application `kernprof` or by '
'`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',
help="'*.lprof' file created by `kernprof`")
args = parser.parse_args()
if args.config:
config = get_main_config(args.config)
args.config = config.path
for key, default in config.conf_dict.items():
if getattr(args, key, None) is None:
setattr(args, key, default)
lstats = load_stats(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)
if __name__ == '__main__':
main()