Skip to content

ENH: read TOML files for configurations #335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
77e6bab
Basic TOML facilities
TTsangSC Apr 19, 2025
4afce8c
Packaging updates
TTsangSC Apr 19, 2025
db3e0b0
WIP: `kernprof` refactoring
TTsangSC Apr 19, 2025
0707290
`kernprof` refactoring (reading configs)
TTsangSC Apr 19, 2025
2bb5de0
Feature: read config file from the env
TTsangSC Apr 19, 2025
4c7a368
Reorganized code: `line_profiler.cli_utils`
TTsangSC Apr 19, 2025
71e1932
Made `line_profiler.line_profiler` configurable
TTsangSC Apr 19, 2025
e63cbe4
Refactored `line_profiler.toml_config`
TTsangSC Apr 19, 2025
6c27c45
Moved code around
TTsangSC Apr 19, 2025
c042e24
Made `.explicit_profiler` configurable
TTsangSC Apr 19, 2025
84649f2
TOML tests
TTsangSC Apr 19, 2025
4ca90af
Config test in `tests/test_explicit_profile.py`
TTsangSC Apr 19, 2025
7bed993
Config test in `tests/test_autoprofile.py`
TTsangSC Apr 19, 2025
0ad634c
Added changelog entry
TTsangSC Apr 19, 2025
35f8918
CI fixes
TTsangSC Apr 20, 2025
cf30d3f
`line_profiler_rc.toml` -> `line_profiler.toml`
TTsangSC May 3, 2025
39b6b46
Updated comments in TOML file
TTsangSC May 3, 2025
4430459
Reduce code run during import-time
TTsangSC May 3, 2025
bc0bf64
Added the `--no-config` flag
TTsangSC May 3, 2025
6f53cff
Centralized code for `config = <bool>`
TTsangSC May 4, 2025
0cd8581
Fixed MANIFEST
TTsangSC May 4, 2025
f005278
Better boolean-option parsing
TTsangSC May 7, 2025
9387cec
`line_profiler` minor refactoring
TTsangSC May 7, 2025
f2fa45d
Refactoring in `kernprof.py`
TTsangSC May 7, 2025
3e12ee8
Tests for `line_profiler.cli_utils.add_argument`
TTsangSC May 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Changes
* FIX: Fixed namespace bug when running ``kernprof -m`` on certain modules (e.g. ``calendar`` on Python 3.12+).
* FIX: Fixed ``@contextlib.contextmanager`` bug where the cleanup code (e.g. restoration of ``sys`` attributes) is not run if exceptions occurred inside the context
* ENH: Added CLI arguments ``-c`` to ``kernprof`` for (auto-)profiling module/package/inline-script execution instead of that of script files; passing ``'-'`` as the script-file name now also reads from and profiles ``stdin``
* ENH: Added capability to parse TOML config files for defaults for ``kernprof`` and ``python -m line_profiler`` CLI options, ``GlobalProfiler`` configurations, and profiler output (e.g. ``LineProfiler.print_stats()``) formatting #335

4.2.0
~~~~~
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
graft line_profiler/rc
include *.md
include *.rst
include *.py
Expand Down
372 changes: 245 additions & 127 deletions kernprof.py

Large diffs are not rendered by default.

279 changes: 279 additions & 0 deletions line_profiler/cli_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
"""
Shared utilities between the `python -m line_profiler` and `kernprof`
CLI tools.
"""
import argparse
import functools
import os
import pathlib
import shutil
import sys
from .toml_config import get_config


_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, /, *args,
hide_complementary_options=True, **kwargs):
"""
Override the 'store_true' and 'store_false' actions so that they
are turned into options which:
- Don't set the default to the opposite boolean, thus allowing us to
later distinguish between cases where the flag has been passed or
not, and
- Set the destination value to the corresponding value in the no-arg
form, but also allow (for long options) for a single arg which is
parsed by :py:func:`.boolean()`.
Also automatically generates complementary boolean options for
``action='store_true'`` options.
If ``hide_complementary_options`` is
true, the auto-generated option (all the long flags prefixed
with 'no-', e.g. '--foo' is negated by '--no-foo') is hidden
from the help text.

Arguments:
parser_like (Any):
Object having a method ``add_argument()``, which has the
same semantics and call signature as
``ArgumentParser.add_argument()``.
hide_complementary_options (bool):
Whether to hide the auto-generated complementary options to
``action='store_true'`` options from the help text for
brevity.
arg, *args, **kwargs
Passed to ``parser_like.add_argument()``

Returns:
action_like (Any):
Return value of ``parser_like.add_argument()``

Notes:
* Short and long flags for 'store_true' and 'store_false'
actions are implemented in separate actions so as to allow for
short-flag concatenation.
* If an option has both short and long flags, the short-flag
action is hidden from the help text, but the long-flag
action's help text is updated to mention the corresponding
short flag(s).
"""
def negate_result(func):
@functools.wraps(func)
def negated(*args, **kwargs):
return not func(*args, **kwargs)

Check warning on line 66 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L66

Added line #L66 was not covered by tests

negated.__name__ = 'negated_' + negated.__name__
return negated

# Make sure there's at least one positional argument
args = [arg, *args]

if kwargs.get('action') not in ('store_true', 'store_false'):
return parser_like.add_argument(*args, **kwargs)

# Long and short boolean flags should be handled separately: short
# flags should remain 0-arg to permit flag concatenation, while long
# flag should be able to take an optional arg parsable into a bool
prefix_chars = tuple(parser_like.prefix_chars)
short_flags = []
long_flags = []
for arg in args:
assert arg.startswith(prefix_chars)
if arg.startswith(tuple(char * 2 for char in prefix_chars)):
long_flags.append(arg)
else:
short_flags.append(arg)

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():
kwargs.setdefault(key, value)
long_kwargs = kwargs.copy()
short_kwargs = {**kwargs, 'action': 'store_const'}
for key, value in dict(
nargs='?',
type=functools.partial(boolean, invert=not const)).items():
long_kwargs.setdefault(key, value)

# 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):
additional_msg = 'Short {}: {}'.format(
'form' if len(short_flags) == 1 else 'forms',
', '.join(short_flags))
if long_kwargs.get('help'):
help_text = long_kwargs['help'].strip()
if help_text.endswith((')', ']')):
# Interpolate into existing parenthetical
help_text = '{}; {}{}{}'.format(
help_text[:-1],
additional_msg[0].lower(),
additional_msg[1:],
help_text[-1])
else:
help_text = f'{help_text} ({additional_msg})'

Check warning on line 122 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L122

Added line #L122 was not covered by tests
long_kwargs['help'] = help_text
else:
long_kwargs['help'] = f'({additional_msg})'

Check warning on line 125 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L125

Added line #L125 was not covered by tests
short_kwargs['help'] = argparse.SUPPRESS

long_action = short_action = None
if long_flags:
long_action = parser_like.add_argument(*long_flags, **long_kwargs)
short_kwargs['dest'] = long_action.dest
if short_flags:
short_action = parser_like.add_argument(*short_flags, **short_kwargs)
if long_action:
action = long_action
else:
assert short_action
action = short_action

Check warning on line 138 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L137-L138

Added lines #L137 - L138 were not covered by tests
if not (const and long_flags): # Negative or short-only flag
return action

Check warning on line 140 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L140

Added line #L140 was not covered by tests

# Automatically generate a complementary option for a long boolean
# option
# (in Python 3.9+ one can use `argparse.BooleanOptionalAction`,
# but we want to maintain compatibility with Python 3.8)
if hide_complementary_options:
falsy_help_text = argparse.SUPPRESS
else:
falsy_help_text = 'Negate these flags: ' + ', '.join(args)

Check warning on line 149 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L149

Added line #L149 was not covered by tests
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})
return action


def get_cli_config(subtable, /, *args, **kwargs):
"""
Get the ``tool.line_profiler.<subtable>`` configs and normalize
its keys (``some-key`` -> ``some_key``).

Arguments:
subtable (str):
Name of the subtable the CLI app should refer to (e.g.
'kernprof')
*args, **kwargs
Passed to ``line_profiler.toml_config.get_config()``

Returns:
subconf_dict, path (tuple[dict, Path])
- ``subconf_dict``: the combination of the
``tool.line_profiler.<subtable>`` subtables of the
provided/looked-up config file (if any) and the default as
a dictionary
- ``path``: absolute path to the config file whence the
config options are loaded
"""
conf, source = get_config(*args, **kwargs)
cli_conf = {key.replace('-', '_'): value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also tackled this in scriptconfig, although the code for doing so was complicated by internal changes to argparse in python/cpython@c02b7ae

Code is here:
https://gitlab.kitware.com/utils/scriptconfig/-/blob/main/scriptconfig/argparse_ext.py?ref_type=heads#L423

Might not be worth the extra complexity, but I figured I would point it out.

for key, value in conf[subtable].items()}
return cli_conf, source


def get_python_executable():
"""
Returns:
command (str)
Command or path thereto corresponding to ``sys.executable``.
"""
if os.path.samefile(shutil.which('python'), sys.executable):
return 'python'
elif os.path.samefile(shutil.which('python3'), sys.executable):
return 'python3'

Check warning on line 196 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L196

Added line #L196 was not covered by tests
else:
return short_string_path(sys.executable)

Check warning on line 198 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L198

Added line #L198 was not covered by tests


def positive_float(value):
"""
Arguments:
value (str)

Returns:
x (float > 0)
"""
val = float(value)
if val <= 0:

Check warning on line 210 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L209-L210

Added lines #L209 - L210 were not covered by tests
# Note: parsing functions should raise either a `ValueError` or
# a `TypeError` instead of an `argparse.ArgumentError`, which
# expects extra context and in general should be raised by the
# parser object
raise ValueError
return val

Check warning on line 216 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L216

Added line #L216 was not covered by tests


def boolean(value, *, fallback=None, invert=False):
"""
Arguments:
value (str)
Value to be parsed into a boolean (case insensitive)
fallback (Union[bool, None])
Optional value to fall back to in case ``value`` doesn't
match any of the specified
invert (bool)
If ``True``, invert the result of parsing ``value`` (but not
``fallback``)

Returns:
b (bool)

Notes:
These values are parsed into ``False``:

* The empty string
* ``'0'``, ``'F'``, ``'N'``
* ``'off'``, ``'False'``, ``'no'``

And these into ``True``:

* ``'1'``, ``'T'``, ``'Y'``
* ``'on'``, ``'True'``, ``'yes'``
"""
try:
result = _BOOLEAN_VALUES[value.casefold()]
except KeyError:

Check warning on line 248 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L248

Added line #L248 was not covered by tests
pass
else:
return (not result) if invert else result
if fallback is None:

Check warning on line 252 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L252

Added line #L252 was not covered by tests
raise ValueError(f'value = {value!r}: '
'cannot be parsed into a boolean; valid values are'
f'({{string: bool}}): {_BOOLEAN_VALUES!r}')
return fallback

Check warning on line 256 in line_profiler/cli_utils.py

View check run for this annotation

Codecov / codecov/patch

line_profiler/cli_utils.py#L256

Added line #L256 was not covered by tests


def short_string_path(path):
"""
Arguments:
path (Union[str, PurePath]):
Path-like

Returns:
short_path (str):
The shortest formatted ``path`` among the provided path, the
corresponding absolute path, and its relative path to the
current directory.
"""
path = pathlib.Path(path)
paths = {str(path)}
abspath = path.absolute()
paths.add(str(abspath))
try:
paths.add(str(abspath.relative_to(path.cwd().absolute())))
except ValueError: # Not relative to the curdir
pass
return min(paths, key=len)
57 changes: 57 additions & 0 deletions line_profiler/cli_utils.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Shared utilities between the `python -m line_profiler` and `kernprof`
CLI tools.
"""
import argparse
import pathlib
from typing import Protocol, Sequence, Tuple, TypeVar, Union


A_co = TypeVar('A_co', bound='ActionLike', covariant=True)


class ActionLike(Protocol):
def __call__(self, parser: 'ParserLike',
namespace: argparse.Namespace,
values: Sequence,
option_string: Union[str, None] = None) -> None:
...

def format_usage(self) -> str:
...


class ParserLike(Protocol[A_co]):
def add_argument(self, arg: str, /, *args: str, **kwargs) -> A_co:
...

@property
def prefix_chars(self) -> str:
...


def add_argument(parser_like: ParserLike[A_co], arg: str, /, *args: str,
hide_complementary_options: bool = True, **kwargs) -> A_co:
...


def get_cli_config(subtable: str, /,
*args, **kwargs) -> Tuple[dict, pathlib.Path]:
...


def get_python_executable() -> str:
...


def positive_float(value: str) -> float:
...


def boolean(value: str, *,
fallback: Union[bool, None] = None, invert: bool = False) -> bool:
...


def short_string_path(path: Union[str, pathlib.PurePath]) -> str:
...
Loading
Loading