From 4ac5308b2e3fe7d666e1027c794c06cdc43d1655 Mon Sep 17 00:00:00 2001 From: "Terence S.-C. Tsang" Date: Sun, 27 Jul 2025 13:15:59 +0200 Subject: [PATCH 1/2] Doc fixes line_profiler/toml_config.py::ConfigSource.from_config() Updated docstring to: - Clarify the behavior of the arguments `config` and `read_env` - Explain the path-based lookup mechanism (because the looked-up filenames has not been mentioned anywhere else in the user-facing docs) --- line_profiler/cli_utils.py | 6 +-- line_profiler/toml_config.py | 73 +++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/line_profiler/cli_utils.py b/line_profiler/cli_utils.py index cbfae0d6..6344b6fa 100644 --- a/line_profiler/cli_utils.py +++ b/line_profiler/cli_utils.py @@ -219,7 +219,7 @@ def boolean(value, *, fallback=None, invert=False): Arguments: value (str) Value to be parsed into a boolean (case insensitive) - fallback (Union[bool, None]) + fallback (bool | None) Optional value to fall back to in case ``value`` doesn't match any of the specified invert (bool) @@ -278,12 +278,12 @@ def boolean(value, *, fallback=None, invert=False): def short_string_path(path): """ Arguments: - path (Union[str, PurePath]): + path (str | os.PathLike[str]): Path-like Returns: str: short_path - The shortest formatted ``path`` among the provided path, the + The shortest formatted path among the provided ``path``, the corresponding absolute path, and its relative path to the current directory. """ diff --git a/line_profiler/toml_config.py b/line_profiler/toml_config.py index 740540fb..039e80ec 100644 --- a/line_profiler/toml_config.py +++ b/line_profiler/toml_config.py @@ -123,38 +123,57 @@ def from_config(cls, config=None, *, read_env=True): Create an instance by loading from a config file. Arguments: - config (Union[str, PurePath, bool, None]): + config (str | os.PathLike[str] | bool | None): Optional path to a specific TOML file; - if a (string) path, skip lookup and just try to read - from that file; - if :py:data:`None` or :py:data:`True`, use lookup to + if a (string) path, try to read from that file; + if :py:data:`None` or :py:data:`True`, look up and resolve to the correct file; - if :py:data:`False`, skip lookup and just use the - default configs + if :py:data:`False`, just return a copy of the default + config (see :py:meth:`~.ConfigSource.from_default`). read_env (bool): - Whether to read the environment variable - :envvar:`!LINE_PROFILER_RC` for a config file (instead - of moving straight onto environment-based lookup) if - ``config`` is not provided. + How to look up the config file if not provided (i.e. + ``config = None`` or equivalently :py:data:`True`): + + :py:data:`True` + Try to read the environment variable + :envvar:`!LINE_PROFILER_RC` as the path to a config + file; + if that fails, fall back to the default configuation + (see :py:meth:`~.ConfigSource.from_default`). + + :py:data:`False` + Use path-based lookup (see Note) to resolve to a + config file. Returns: New instance Note: - For the config TOML file, it is required that each of the - following keys either is absent or maps to a table: - - * ``tool`` and ``tool.line_profiler`` - * ``tool.line_profiler.kernprof``, ``.cli``, ``.setup``, - ``.write``, and ``.show`` - * ``tool.line_profiler.show.column_widths`` - - If this is not the case: - - * If ``config`` is provided, a :py:class:`ValueError` is - raised. - * Otherwise, the looked-up file is considered invalid and - ignored. + * For the config TOML file, it is required that each of the + following keys either is absent or maps to a table: + + * ``tool`` and ``tool.line_profiler`` + * ``tool.line_profiler.kernprof``, ``.cli``, ``.setup``, + ``.write``, and ``.show`` + * ``tool.line_profiler.show.column_widths`` + + If this is not the case: + + * If ``config`` is provided, a :py:class:`ValueError` is + raised. + * Otherwise, the looked-up file is considered invalid and + ignored. + * When performing path-based lookup: + + * The current directory is checked first to see if it has + a valid, readable TOML file named ``line_profiler.toml``. + * If not, check if there is a valid, readable TOML file + named ``pyproject.toml``. + * If not, check the parent directory, and so on. + * If we reached the file-system root without finding a + valid, readable TOML file, fall back to the default + configuration (see + :py:meth:`~.ConfigSource.from_default`). """ def merge(template, supplied): if not (isinstance(template, dict) and isinstance(supplied, dict)): @@ -230,15 +249,15 @@ def find_and_read_config_file( *, config=None, env_var=ENV_VAR, targets=TARGETS): """ Arguments: - config (Union[str, PurePath, None]): + config (str | os.PathLike[str] | None): Optional path to a specific TOML file; if provided, skip lookup and just try to read from that file - env_var (Union[str, None]): + env_var (str | None): Name of the of the environment variable containing the path to a TOML file; if true-y and if ``config`` isn't provided, skip lookup and just try to read from that file - targets (Sequence[str | PurePath]): + targets (Sequence[str | os.PathLike[str]]): Filenames among which TOML files are looked up (if neither ``config`` or ``env_var`` is given) From 10005a22cd092c5246684d8c15cb36c4e8f78e34 Mon Sep 17 00:00:00 2001 From: "Terence S.-C. Tsang" Date: Sun, 27 Jul 2025 13:25:51 +0200 Subject: [PATCH 2/2] Stub fixes line_profiler/autoprofile/cli_utils.pyi ActionLike Added parametrization because `add_argument()` fails type check otherwise short_string_path() Changed `pathlib.PurePath` in parameter annotation to `os.PathLike[str]` line_profiler/line_profiler.pyi get_column_widths(), LineProfiler.print_stats() show_func(), show_text() Changed `pathlib.PurePath` in parameter annotation to `os.PathLike[str]` LineProfiler.__call__() Added more specific overloads to ensure that parametrization of parametrizable types (e.g. `staticmethod[PS, T]`) are inherited by the return value line_profiler/profiler_mixin.pyi::ByCountProfilerMixin wrap_callable() Added more specific overloads to ensure that parametrization of parametrizable types (e.g. `staticmethod[PS, T]`) are inherited by the return value wrap_classmethod(), wrap_staticmethod(), wrap_partialmethod() wrap_partial(), wrap_cached_property() Added type parameters to the argument and the return type line_profiler/scoping_policy.pyi::ScopingPolicy.EXACT Added missing enumeration item line_profiler/toml_config.pyi::ConfigSource path Added missing field source Removed nonexistent field --- .../autoprofile/line_profiler_utils.pyi | 22 +++-- line_profiler/cli_utils.pyi | 16 ++-- line_profiler/line_profiler.pyi | 65 ++++++++++--- line_profiler/profiler_mixin.pyi | 93 ++++++++++++++----- line_profiler/scoping_policy.pyi | 1 + line_profiler/toml_config.pyi | 27 +++--- 6 files changed, 157 insertions(+), 67 deletions(-) diff --git a/line_profiler/autoprofile/line_profiler_utils.pyi b/line_profiler/autoprofile/line_profiler_utils.pyi index b710b577..2d114b34 100644 --- a/line_profiler/autoprofile/line_profiler_utils.pyi +++ b/line_profiler/autoprofile/line_profiler_utils.pyi @@ -1,25 +1,29 @@ -from types import ModuleType -from typing import overload, Any, Literal, TYPE_CHECKING +from functools import partial, partialmethod, cached_property +from types import FunctionType, MethodType, ModuleType +from typing import overload, Any, Literal, TypeVar, TYPE_CHECKING if TYPE_CHECKING: # Stub-only annotations - from ..line_profiler import CallableLike - from ..profiler_mixin import CLevelCallable + from ..profiler_mixin import CLevelCallable, CythonCallable from ..scoping_policy import ScopingPolicy, ScopingPolicyDict + + @overload def add_imported_function_or_module( self, item: CLevelCallable | Any, - scoping_policy: ( - ScopingPolicy | str | ScopingPolicyDict | None) = None, + scoping_policy: ScopingPolicy | str | ScopingPolicyDict | None = None, wrap: bool = False) -> Literal[0]: ... @overload def add_imported_function_or_module( - self, item: CallableLike | ModuleType, - scoping_policy: ( - ScopingPolicy | str | ScopingPolicyDict | None) = None, + 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]: ... diff --git a/line_profiler/cli_utils.pyi b/line_profiler/cli_utils.pyi index 46a5fb4c..182efe98 100644 --- a/line_profiler/cli_utils.pyi +++ b/line_profiler/cli_utils.pyi @@ -4,19 +4,21 @@ Shared utilities between the :command:`python -m line_profiler` and """ import argparse import pathlib -from typing import Protocol, Sequence, Tuple, TypeVar, Union +from os import PathLike +from typing import Protocol, Sequence, Tuple, TypeVar from line_profiler.toml_config import ConfigSource +P_con = TypeVar('P_con', bound='ParserLike', contravariant=True) A_co = TypeVar('A_co', bound='ActionLike', covariant=True) -class ActionLike(Protocol): - def __call__(self, parser: 'ParserLike', +class ActionLike(Protocol[P_con]): + def __call__(self, parser: P_con, namespace: argparse.Namespace, - values: Sequence, - option_string: Union[str, None] = None) -> None: + values: str | Sequence | None, + option_string: str | None = None) -> None: ... def format_usage(self) -> str: @@ -50,9 +52,9 @@ def positive_float(value: str) -> float: def boolean(value: str, *, - fallback: Union[bool, None] = None, invert: bool = False) -> bool: + fallback: bool | None = None, invert: bool = False) -> bool: ... -def short_string_path(path: Union[str, pathlib.PurePath]) -> str: +def short_string_path(path: str | PathLike[str]) -> str: ... diff --git a/line_profiler/line_profiler.pyi b/line_profiler/line_profiler.pyi index 833996a6..0c2e655f 100644 --- a/line_profiler/line_profiler.pyi +++ b/line_profiler/line_profiler.pyi @@ -1,24 +1,30 @@ import io import pathlib from functools import cached_property, partial, partialmethod -from types import FunctionType, MethodType, ModuleType -from typing import (overload, - Callable, List, Literal, Mapping, Tuple, - TypeVar, Union) +from os import PathLike +from types import FunctionType, ModuleType +from typing import TYPE_CHECKING, overload, Callable, Literal, Mapping, TypeVar +try: + from typing import ( # type: ignore[attr-defined] # noqa: F401 + ParamSpec) +except ImportError: + from typing_extensions import ParamSpec # noqa: F401 from _typeshed import Incomplete from ._line_profiler import LineProfiler as CLineProfiler from .profiler_mixin import ByCountProfilerMixin, CLevelCallable from .scoping_policy import ScopingPolicy, ScopingPolicyDict +if TYPE_CHECKING: + from .profiler_mixin import UnparametrizedCallableLike -CallableLike = TypeVar('CallableLike', - FunctionType, partial, property, cached_property, - MethodType, staticmethod, classmethod, partialmethod, - type) + +T = TypeVar('T') +T_co = TypeVar('T_co', covariant=True) +PS = ParamSpec('PS') def get_column_widths( - config: Union[bool, str, pathlib.PurePath, None] = False) -> Mapping[ + config: bool | str | PathLike[str] | None = False) -> Mapping[ Literal['line', 'hits', 'time', 'perhit', 'percent'], int]: ... @@ -33,9 +39,39 @@ class LineProfiler(CLineProfiler, ByCountProfilerMixin): func: CLevelCallable) -> CLevelCallable: ... + @overload + def __call__( # type: ignore[overload-overlap] + self, func: UnparametrizedCallableLike, + ) -> UnparametrizedCallableLike: + ... + @overload def __call__(self, # type: ignore[overload-overlap] - func: CallableLike) -> CallableLike: + func: type[T]) -> type[T]: + ... + + @overload + def __call__(self, # type: ignore[overload-overlap] + func: partial[T]) -> partial[T]: + ... + + @overload + def __call__(self, func: partialmethod[T]) -> partialmethod[T]: + ... + + @overload + def __call__(self, func: cached_property[T_co]) -> cached_property[T_co]: + ... + + @overload + def __call__(self, # type: ignore[overload-overlap] + func: staticmethod[PS, T_co]) -> staticmethod[PS, T_co]: + ... + + @overload + def __call__( + self, func: classmethod[type[T], PS, T_co], + ) -> classmethod[type[T], PS, T_co]: ... # Fallback: just wrap the `.__call__()` of a generic callable @@ -62,8 +98,7 @@ class LineProfiler(CLineProfiler, ByCountProfilerMixin): sort: bool = ..., rich: bool = ..., *, - config: Union[str, pathlib.PurePath, - bool, None] = None) -> None: + config: str | PathLike[str] | bool | None = None) -> None: ... def add_module( @@ -88,14 +123,14 @@ def is_generated_code(filename): def show_func(filename: str, start_lineno: int, func_name: str, - timings: List[Tuple[int, int, float]], + timings: list[tuple[int, int, float]], unit: float, output_unit: float | None = None, stream: io.TextIOBase | None = None, stripzeros: bool = False, rich: bool = False, *, - config: Union[str, pathlib.PurePath, bool, None] = None) -> None: + config: str | PathLike[str] | bool | None = None) -> None: ... @@ -109,7 +144,7 @@ def show_text(stats, sort: bool = ..., rich: bool = ..., *, - config: Union[str, pathlib.PurePath, bool, None] = None) -> None: + config: str | PathLike[str] | bool | None = None) -> None: ... diff --git a/line_profiler/profiler_mixin.pyi b/line_profiler/profiler_mixin.pyi index bd32a47a..ba7a9d3a 100644 --- a/line_profiler/profiler_mixin.pyi +++ b/line_profiler/profiler_mixin.pyi @@ -3,8 +3,8 @@ from types import (CodeType, FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, ClassMethodDescriptorType, MethodDescriptorType, MethodWrapperType, WrapperDescriptorType) -from typing import (TYPE_CHECKING, - Any, Callable, Dict, List, Mapping, Protocol, TypeVar) +from typing import (TYPE_CHECKING, overload, + Any, Callable, Mapping, Protocol, TypeVar) try: from typing import ( # type: ignore[attr-defined] # noqa: F401 ParamSpec) @@ -23,9 +23,10 @@ except ImportError: # Python < 3.13 from ._line_profiler import label -T = TypeVar('T', bound=type) +UnparametrizedCallableLike = TypeVar('UnparametrizedCallableLike', + FunctionType, property, MethodType) +T = TypeVar('T') T_co = TypeVar('T_co', covariant=True) -R = TypeVar('R') PS = ParamSpec('PS') if TYPE_CHECKING: @@ -66,31 +67,31 @@ if TYPE_CHECKING: ... @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 @@ -160,31 +161,79 @@ def is_cached_property(f: Any) -> TypeIs[cached_property]: class ByCountProfilerMixin: - def get_underlying_functions(self, func) -> List[FunctionType]: + def get_underlying_functions(self, func) -> list[FunctionType]: ... - def wrap_callable(self, func): + @overload + def wrap_callable(self, # type: ignore[overload-overlap] + func: CLevelCallable) -> CLevelCallable: ... - def wrap_classmethod(self, func: classmethod) -> classmethod: + @overload + def wrap_callable( # type: ignore[overload-overlap] + self, func: UnparametrizedCallableLike, + ) -> UnparametrizedCallableLike: ... - def wrap_staticmethod(self, func: staticmethod) -> staticmethod: + @overload + def wrap_callable(self, # type: ignore[overload-overlap] + func: type[T]) -> type[T]: + ... + + @overload + def wrap_callable(self, # type: ignore[overload-overlap] + func: partial[T]) -> partial[T]: + ... + + @overload + def wrap_callable(self, func: partialmethod[T]) -> partialmethod[T]: + ... + + @overload + def wrap_callable(self, + func: cached_property[T_co]) -> cached_property[T_co]: + ... + + @overload + def wrap_callable(self, # type: ignore[overload-overlap] + func: staticmethod[PS, T_co]) -> staticmethod[PS, T_co]: + ... + + @overload + def wrap_callable( + self, func: classmethod[type[T], PS, T_co], + ) -> classmethod[type[T], PS, T_co]: + ... + + # Fallback: just return a wrapper function around a generic callable + + @overload + def wrap_callable(self, func: Callable) -> FunctionType: + ... + + def wrap_classmethod( + self, func: classmethod[type[T], PS, T_co], + ) -> classmethod[type[T], PS, T_co]: + ... + + def wrap_staticmethod( + self, func: staticmethod[PS, T_co]) -> staticmethod[PS, T_co]: ... def wrap_boundmethod(self, func: MethodType) -> MethodType: ... - def wrap_partialmethod(self, func: partialmethod) -> partialmethod: + def wrap_partialmethod(self, func: partialmethod[T]) -> partialmethod[T]: ... - def wrap_partial(self, func: partial) -> partial: + def wrap_partial(self, func: partial[T]) -> partial[T]: ... def wrap_property(self, func: property) -> property: ... - def wrap_cached_property(self, func: cached_property) -> cached_property: + def wrap_cached_property( + self, func: cached_property[T_co]) -> cached_property[T_co]: ... def wrap_async_generator(self, func: FunctionType) -> FunctionType: @@ -199,7 +248,7 @@ class ByCountProfilerMixin: def wrap_function(self, func: Callable) -> FunctionType: ... - def wrap_class(self, func: T) -> T: + def wrap_class(self, func: type[T]) -> type[T]: ... def run(self, cmd: str) -> Self: @@ -207,12 +256,12 @@ class ByCountProfilerMixin: def runctx(self, cmd: str, - globals: Dict[str, Any] | None, + globals: dict[str, Any] | None, locals: Mapping[str, Any] | None) -> Self: ... - def runcall(self, func: Callable[PS, R], /, - *args: PS.args, **kw: PS.kwargs) -> R: + def runcall(self, func: Callable[PS, T], /, + *args: PS.args, **kw: PS.kwargs) -> T: ... def __enter__(self) -> Self: diff --git a/line_profiler/scoping_policy.pyi b/line_profiler/scoping_policy.pyi index 0a45a09b..e6987289 100644 --- a/line_profiler/scoping_policy.pyi +++ b/line_profiler/scoping_policy.pyi @@ -5,6 +5,7 @@ from .line_profiler_utils import StringEnum class ScopingPolicy(StringEnum): + EXACT = auto() CHILDREN = auto() DESCENDANTS = auto() SIBLINGS = auto() diff --git a/line_profiler/toml_config.pyi b/line_profiler/toml_config.pyi index 724b9a35..93409341 100644 --- a/line_profiler/toml_config.pyi +++ b/line_profiler/toml_config.pyi @@ -1,8 +1,7 @@ from dataclasses import dataclass -from pathlib import Path, PurePath -from typing import (List, Dict, Set, Tuple, - Mapping, Sequence, - Any, Self, TypeVar, Union) +from os import PathLike +from pathlib import Path +from typing import Mapping, Sequence, Any, Self, TypeVar TARGETS = 'line_profiler.toml', 'pyproject.toml' @@ -10,15 +9,15 @@ ENV_VAR = 'LINE_PROFILER_RC' K = TypeVar('K') V = TypeVar('V') -Config = Tuple[Dict[str, Dict[str, Any]], Path] -NestedTable = Mapping[K, Union['NestedTable[K, V]', V]] +Config = tuple[dict[str, dict[str, Any]], Path] +NestedTable = Mapping[K, 'NestedTable[K, V]' | V] @dataclass class ConfigSource: - conf_dict: Dict[str, Any] - source: Path - subtable: List[str] + conf_dict: dict[str, Any] + path: Path + subtable: list[str] def copy(self) -> Self: ... @@ -32,16 +31,16 @@ class ConfigSource: ... @classmethod - def from_config(cls, config: Union[str, PurePath, bool, None] = None, *, + def from_config(cls, config: str | PathLike | bool | None = None, *, read_env: bool = True) -> Self: ... def find_and_read_config_file( *, - config: Union[str, PurePath, None] = None, - env_var: Union[str, None] = ENV_VAR, - targets: Sequence[Union[str, PurePath]] = TARGETS) -> Config: + config: str | PathLike | None = None, + env_var: str | None = ENV_VAR, + targets: Sequence[str | PathLike] = TARGETS) -> Config: ... @@ -51,5 +50,5 @@ def get_subtable(table: NestedTable[K, V], keys: Sequence[K], *, def get_headers(table: NestedTable[K, Any], *, - include_implied: bool = False) -> Set[Tuple[K, ...]]: + include_implied: bool = False) -> set[tuple[K, ...]]: ...