Skip to content

Conversation

@TTsangSC
Copy link
Collaborator

This PR addresses the following issues with recent PRs. (Note that there is no actual behavior change on the code-side).

Typing bugs

  • line_profiler.cli_utils.add_argument(ArgumentParser(...), ...) actually fails type check.
  • The stubs for the following types are botched:
    • line_profiler.toml_config.ConfigSource should have a .path field instead of a .source field.
    • line_profiler.scoping_policy.ScopingPolicy is missing an .EXACT item.

These typing bugs went under our nose because we only do stubs, and thus mypy don't really look into how we're using any of these. But mypy flared up the moment I installed the line_profiler in a non-editable way in another project, which got me investigating... and here we are. This PR remedies the above bugs.

While we're at it, I also updated the stubs for line_profiler.profiler_mixin.ByCountProfilerMixin.wrap_*() and line_profiler.line_profiler.LineProfiler.__call__() so that we get better parametrizations for the param specs and return values (e.g. p: partial[int] = ...; p_profiled = reveal_type(profile(p)) gives partial[int] instead of partial[Any]).

Lacking docs

Shortly after finishing #335 I realized that we never explained whence the config file is looked up (i.e. the current directory's and its ancestors' line_profiler.toml and pyproject.toml) in the online docs, because neither line_profiler.toml_config.find_and_read_config_file() nor .TARGETS is in .__all__, and they are thus omitted from the built doc pages. The PR thus rewrites the docstring for line_profiler.toml_config.ConfigSource.from_config() to (1) be more clear, and (2) document the aforementioned lookup targets and mechanism.

TTsangSC added 2 commits July 27, 2025 13:15
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/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
@TTsangSC
Copy link
Collaborator Author

TTsangSC commented Jul 27, 2025

Apropos, I appreciate the rationale behind separating the type annotations into their own stub files:

  • This allow for writing more concise type annotations (e.g. dict[int, str | int] instead of typing.Dict[int, typing.Union[str, int]]) without worrying about the code being valid Python in legacy versions.
  • This prevents complex function signatures and @typing.overloads from cluttering code files.

However, this does prevent mypy from looking into much of the codebase and actually type-checking it. I wonder if --check-untyped-defs would solve this, or if it will cause more trouble than it's worth.

@codecov
Copy link

codecov bot commented Jul 27, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.18%. Comparing base (60e928f) to head (10005a2).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #367      +/-   ##
==========================================
+ Coverage   87.56%   88.18%   +0.61%     
==========================================
  Files          18       20       +2     
  Lines        1641     1862     +221     
  Branches      348      401      +53     
==========================================
+ Hits         1437     1642     +205     
- Misses        149      160      +11     
- Partials       55       60       +5     
Files with missing lines Coverage Δ
line_profiler/cli_utils.py 88.88% <ø> (ø)
line_profiler/toml_config.py 93.44% <ø> (ø)

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9616f7e...10005a2. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Erotemic
Copy link
Member

LGTM.

Would running mypy on an installed wheel help in the CI? Strange that it picks up stubs in non-editable mode, but not in editable mode.

@Erotemic Erotemic merged commit 593d203 into pyutils:main Jul 29, 2025
36 checks passed
@TTsangSC TTsangSC deleted the stub-fix branch July 30, 2025 00:13
@TTsangSC
Copy link
Collaborator Author

TTsangSC commented Jul 30, 2025

Upon closer inspection, this seems to be two separate problems and they are deeper than I suspected:

What's wrong with editable installs?

Strange that it picks up stubs in non-editable mode, but not in editable mode.

The problem was that if line_profiler is installed as editable, when we type-check another project, mypy can't seem to locate ANY of line_profiler files by default, as indicated from the complete lack of line_profiler-related output in mypy --verbose. It seems that the static analysis it uses neglects the __editable___*_finder.py and __editable__.*.pth hooks (see also python/mypy#13392). Nothing we can do on our end it seems – downstream package maintainers will have to install line_profiler in a non-editable manner.

... unless we're willing to write and maintain a mypy plugin to resolve said hooks. Technically we do have the tech for doing so in line_profiler.autoprofile.util_static, but doing that seems to be out of scope for this project, and (unlike pytest plugins) would still require downstream projects to manually configure for said plugin to be used.

Is our code type-checked at all?

Would running mypy on an installed wheel help in the CI?

Unfortunately no. The problem seems to be that if a stub file exists, mypy just doesn't see the Python source file AT ALL:

image

Hence even with --check-untyped, only the errors in _logger.py and _diagnostics.py are exposed (which BTW we should maybe fix in another PR), but not the mock error I deliberately wrote in line_profiler.py (the short_string_path(1), where said function only takes path-likes).

Trying to explicitly include both the *.py and *.pyi files in the type check however causes an error:

(py3.13)  $ find line_profiler \( -name '*.py' -o -name '*.pyi' \) | xargs mypy --check-untyped
line_profiler/line_profiler_utils.py: error: Duplicate module named "line_profiler.line_profiler_utils" (also at "line_profiler/line_profiler_utils.pyi")
line_profiler/line_profiler_utils.py: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules for more info
line_profiler/line_profiler_utils.py: note: Common resolutions include: a) using `--exclude` to avoid checking one of them, b) adding `__init__.py` somewhere, c) using `--explicit-package-bases` or adjusting MYPYPATH
Found 1 error in 1 file (errors prevented further checking)

The only way out seem to be doing something like

find line_profiler -name '*.py' | xargs mypy --check-untyped

but that obviously ignores information present in the *.pyi files. So such type-checking can at most make the Python code self-consistent in terms of typing... but then we still have no ways of guaranteeing consistency of the stubs with the actual code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants