-
Notifications
You must be signed in to change notification settings - Fork 131
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
TTsangSC
wants to merge
25
commits into
pyutils:main
Choose a base branch
from
TTsangSC:toml-config
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
77e6bab
Basic TOML facilities
TTsangSC 4afce8c
Packaging updates
TTsangSC db3e0b0
WIP: `kernprof` refactoring
TTsangSC 0707290
`kernprof` refactoring (reading configs)
TTsangSC 2bb5de0
Feature: read config file from the env
TTsangSC 4c7a368
Reorganized code: `line_profiler.cli_utils`
TTsangSC 71e1932
Made `line_profiler.line_profiler` configurable
TTsangSC e63cbe4
Refactored `line_profiler.toml_config`
TTsangSC 6c27c45
Moved code around
TTsangSC c042e24
Made `.explicit_profiler` configurable
TTsangSC 84649f2
TOML tests
TTsangSC 4ca90af
Config test in `tests/test_explicit_profile.py`
TTsangSC 7bed993
Config test in `tests/test_autoprofile.py`
TTsangSC 0ad634c
Added changelog entry
TTsangSC 35f8918
CI fixes
TTsangSC cf30d3f
`line_profiler_rc.toml` -> `line_profiler.toml`
TTsangSC 39b6b46
Updated comments in TOML file
TTsangSC 4430459
Reduce code run during import-time
TTsangSC bc0bf64
Added the `--no-config` flag
TTsangSC 6f53cff
Centralized code for `config = <bool>`
TTsangSC 0cd8581
Fixed MANIFEST
TTsangSC f005278
Better boolean-option parsing
TTsangSC 9387cec
`line_profiler` minor refactoring
TTsangSC f2fa45d
Refactoring in `kernprof.py`
TTsangSC 3e12ee8
Tests for `line_profiler.cli_utils.add_argument`
TTsangSC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
graft line_profiler/rc | ||
include *.md | ||
include *.rst | ||
include *.py | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
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})' | ||
long_kwargs['help'] = help_text | ||
else: | ||
long_kwargs['help'] = f'({additional_msg})' | ||
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 | ||
if not (const and long_flags): # Negative or short-only flag | ||
return action | ||
|
||
# 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) | ||
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 | ||
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' | ||
else: | ||
return short_string_path(sys.executable) | ||
|
||
|
||
def positive_float(value): | ||
""" | ||
Arguments: | ||
value (str) | ||
|
||
Returns: | ||
x (float > 0) | ||
""" | ||
val = float(value) | ||
if val <= 0: | ||
# 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 | ||
|
||
|
||
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: | ||
pass | ||
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}') | ||
return fallback | ||
|
||
|
||
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: | ||
... |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.