with-line-profiler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominik Köhler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: with-line-profiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Context-manager based line-by-line profiler for Python functions
|
|
5
|
+
Project-URL: Homepage, https://github.com/mathematiger/lineprofiler
|
|
6
|
+
Project-URL: Repository, https://github.com/mathematiger/lineprofiler
|
|
7
|
+
Project-URL: Issues, https://github.com/mathematiger/lineprofiler/issues
|
|
8
|
+
Author-email: mathematiger <mcop.dkoehler@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: debugging,performance,profiler,profiling,timing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# lineprofiler
|
|
28
|
+
Statistical profiler to find lines that take a long time to compute. One can specify a folder, wherein the profiler traces lines.
|
|
29
|
+
The profiler can be bound using `with`.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
- **Zero configuration** – just wrap code in a `with` block
|
|
33
|
+
- **Line-level timing** – see exactly which lines are slow
|
|
34
|
+
- **Auto-filtering** – only profiles code in your project (auto-detects git repo root)
|
|
35
|
+
- **Flexible output** – sort by time, hits, or line number; filter by threshold
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
`pip install lineprofiler`
|
|
39
|
+
|
|
40
|
+
## Workflow
|
|
41
|
+
```python
|
|
42
|
+
from lineprofiler import LineProfiler
|
|
43
|
+
profiler = LineProfiler(project_folder="path/to/your/project")
|
|
44
|
+
profiler.clear()
|
|
45
|
+
with profiler:
|
|
46
|
+
your_function()
|
|
47
|
+
profiler.print_global_top_stats(min_time_us=0.01, top_n=40)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Method | Description |
|
|
51
|
+
|--------|-------------|
|
|
52
|
+
| `print_stats(min_time_us, top_n_lines, sort_by)` | Print per-function statistics |
|
|
53
|
+
| `print_global_top_stats(top_n, min_time_us, sort_by)` | Print top N lines across all functions |
|
|
54
|
+
| `get_stats()` | Get raw `FunctionStats` dictionary |
|
|
55
|
+
| `clear()` / `reset()` | Clear all collected data |
|
|
56
|
+
|
|
57
|
+
## Licence
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# lineprofiler
|
|
2
|
+
Statistical profiler to find lines that take a long time to compute. One can specify a folder, wherein the profiler traces lines.
|
|
3
|
+
The profiler can be bound using `with`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Zero configuration** – just wrap code in a `with` block
|
|
7
|
+
- **Line-level timing** – see exactly which lines are slow
|
|
8
|
+
- **Auto-filtering** – only profiles code in your project (auto-detects git repo root)
|
|
9
|
+
- **Flexible output** – sort by time, hits, or line number; filter by threshold
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
`pip install lineprofiler`
|
|
13
|
+
|
|
14
|
+
## Workflow
|
|
15
|
+
```python
|
|
16
|
+
from lineprofiler import LineProfiler
|
|
17
|
+
profiler = LineProfiler(project_folder="path/to/your/project")
|
|
18
|
+
profiler.clear()
|
|
19
|
+
with profiler:
|
|
20
|
+
your_function()
|
|
21
|
+
profiler.print_global_top_stats(min_time_us=0.01, top_n=40)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Method | Description |
|
|
25
|
+
|--------|-------------|
|
|
26
|
+
| `print_stats(min_time_us, top_n_lines, sort_by)` | Print per-function statistics |
|
|
27
|
+
| `print_global_top_stats(top_n, min_time_us, sort_by)` | Print top N lines across all functions |
|
|
28
|
+
| `get_stats()` | Get raw `FunctionStats` dictionary |
|
|
29
|
+
| `clear()` / `reset()` | Clear all collected data |
|
|
30
|
+
|
|
31
|
+
## Licence
|
|
32
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""Context-manager based line-by-line profiler for Python functions.
|
|
2
|
+
|
|
3
|
+
This module provides a simple context manager interface for profiling code blocks
|
|
4
|
+
and functions with detailed line-by-line timing information.
|
|
5
|
+
|
|
6
|
+
Test components:
|
|
7
|
+
- Context manager protocol (__enter__/__exit__)
|
|
8
|
+
- Accurate timing measurements with sys.settrace
|
|
9
|
+
- Proper trace function cleanup and restoration
|
|
10
|
+
- Thread-safe profiler state management
|
|
11
|
+
- Correct line timing calculations
|
|
12
|
+
"""
|
|
13
|
+
import inspect
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import FrameType, TracebackType
|
|
19
|
+
|
|
20
|
+
from typing_extensions import Self
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class LineStats:
|
|
25
|
+
"""Statistics for a single line of code.
|
|
26
|
+
|
|
27
|
+
Test components:
|
|
28
|
+
- Correct accumulation of hits and total_time
|
|
29
|
+
- Accurate average_time calculation
|
|
30
|
+
- Proper handling of zero hits
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
line_number: int
|
|
34
|
+
hits: int = 0
|
|
35
|
+
total_time: float = 0.0
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def average_time(self) -> float:
|
|
39
|
+
"""Calculate average time per execution.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
Average time in seconds, or 0.0 if no hits
|
|
44
|
+
|
|
45
|
+
Test components:
|
|
46
|
+
- Division by zero handling
|
|
47
|
+
- Accurate time averaging
|
|
48
|
+
"""
|
|
49
|
+
return self.total_time / self.hits if self.hits > 0 else 0.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class FunctionStats:
|
|
54
|
+
"""Statistics for an entire function.
|
|
55
|
+
|
|
56
|
+
Test components:
|
|
57
|
+
- Correct line_stats dictionary management
|
|
58
|
+
- Accurate source code storage
|
|
59
|
+
- Proper total_time accumulation
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
filename: str
|
|
63
|
+
function_name: str
|
|
64
|
+
first_line: int
|
|
65
|
+
line_stats: dict[int, LineStats] = field(default_factory=dict)
|
|
66
|
+
source_lines: dict[int, str] = field(default_factory=dict)
|
|
67
|
+
total_time: float = 0.0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LineProfiler:
|
|
71
|
+
"""Context manager for line-by-line profiling of code blocks.
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
profiler = LineProfiler()
|
|
75
|
+
with profiler:
|
|
76
|
+
# Code to profile
|
|
77
|
+
result = some_function()
|
|
78
|
+
|
|
79
|
+
profiler.print_stats()
|
|
80
|
+
|
|
81
|
+
Test components:
|
|
82
|
+
- Context manager __enter__ and __exit__ implementation
|
|
83
|
+
- Correct trace function registration and cleanup
|
|
84
|
+
- Accurate timing of line executions
|
|
85
|
+
- Proper handling of nested function calls
|
|
86
|
+
- Thread-safe state management
|
|
87
|
+
- File and line number tracking
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, project_folder: str | Path | None = None) -> None:
|
|
91
|
+
"""Initialize the profiler.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
project_folder: Optional folder path to filter results (e.g., "pandapower_env")
|
|
95
|
+
|
|
96
|
+
Test components:
|
|
97
|
+
- Proper initialization of all tracking dictionaries
|
|
98
|
+
- Correct default state setup
|
|
99
|
+
- Path resolution for project_folder
|
|
100
|
+
"""
|
|
101
|
+
self._function_stats: dict[tuple[str, str, int], FunctionStats] = {}
|
|
102
|
+
self._enabled: bool = False
|
|
103
|
+
self._last_time: float = 0.0
|
|
104
|
+
self._last_line: int | None = None
|
|
105
|
+
self._current_function_key: tuple[str, str, int] | None = None
|
|
106
|
+
self._old_trace = sys.gettrace()
|
|
107
|
+
|
|
108
|
+
# Store project folder for filtering
|
|
109
|
+
if project_folder is not None:
|
|
110
|
+
self._project_folder = Path(project_folder).resolve()
|
|
111
|
+
else:
|
|
112
|
+
# Auto-detect vom Caller
|
|
113
|
+
|
|
114
|
+
caller_frame = inspect.currentframe()
|
|
115
|
+
if caller_frame and caller_frame.f_back:
|
|
116
|
+
caller_file = caller_frame.f_back.f_code.co_filename
|
|
117
|
+
self._project_folder = self._find_repo_root(caller_file)
|
|
118
|
+
|
|
119
|
+
def __enter__(self) -> Self:
|
|
120
|
+
"""Enable profiling when entering context.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
Self for context manager protocol
|
|
125
|
+
|
|
126
|
+
Test components:
|
|
127
|
+
- Correct storage of previous trace function
|
|
128
|
+
- Proper settrace registration
|
|
129
|
+
- Accurate initial timestamp
|
|
130
|
+
- State flag updates
|
|
131
|
+
"""
|
|
132
|
+
self._enabled = True
|
|
133
|
+
self._old_trace = sys.gettrace()
|
|
134
|
+
sys.settrace(self._trace_callback)
|
|
135
|
+
self._last_time = time.perf_counter()
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def __exit__(
|
|
139
|
+
self,
|
|
140
|
+
exc_type: type[BaseException] | None,
|
|
141
|
+
exc_val: BaseException | None,
|
|
142
|
+
exc_tb: TracebackType | None,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Disable profiling when exiting context.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
exc_type: Exception type if raised
|
|
148
|
+
exc_val: Exception value if raised
|
|
149
|
+
exc_tb: Exception traceback if raised
|
|
150
|
+
|
|
151
|
+
Test components:
|
|
152
|
+
- Proper trace function restoration
|
|
153
|
+
- State cleanup on exit
|
|
154
|
+
- Correct handling of exceptions during profiling
|
|
155
|
+
- No interference with exception propagation
|
|
156
|
+
"""
|
|
157
|
+
self._enabled = False
|
|
158
|
+
sys.settrace(self._old_trace)
|
|
159
|
+
|
|
160
|
+
def _trace_callback( # noqa: ANN202, C901
|
|
161
|
+
self,
|
|
162
|
+
frame: FrameType,
|
|
163
|
+
event: str,
|
|
164
|
+
arg, # noqa: ANN001, ARG002; needed for compliance
|
|
165
|
+
):
|
|
166
|
+
"""Trace function called by Python interpreter for each line.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
frame: Current execution frame
|
|
170
|
+
event: Event type ('call', 'line', 'return', etc.)
|
|
171
|
+
arg: Event-specific argument
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
Self to continue tracing, or None to stop
|
|
176
|
+
|
|
177
|
+
Test components:
|
|
178
|
+
- Correct event type handling ('call', 'line', 'return')
|
|
179
|
+
- Accurate time delta calculations
|
|
180
|
+
- Proper frame inspection (filename, function name, line number)
|
|
181
|
+
- FunctionStats creation and updates
|
|
182
|
+
- Source code extraction and caching
|
|
183
|
+
"""
|
|
184
|
+
if not self._enabled:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
current_time = time.perf_counter()
|
|
188
|
+
|
|
189
|
+
if event == "call":
|
|
190
|
+
# New function called
|
|
191
|
+
filename = frame.f_code.co_filename
|
|
192
|
+
if not self._is_in_project_folder(filename):
|
|
193
|
+
return None
|
|
194
|
+
function_name = frame.f_code.co_name
|
|
195
|
+
first_line = frame.f_code.co_firstlineno
|
|
196
|
+
|
|
197
|
+
key = (filename, function_name, first_line)
|
|
198
|
+
|
|
199
|
+
if key not in self._function_stats:
|
|
200
|
+
self._function_stats[key] = FunctionStats(
|
|
201
|
+
filename=filename,
|
|
202
|
+
function_name=function_name,
|
|
203
|
+
first_line=first_line,
|
|
204
|
+
)
|
|
205
|
+
# Load source lines
|
|
206
|
+
self._load_source_lines(key)
|
|
207
|
+
|
|
208
|
+
self._current_function_key = key
|
|
209
|
+
self._last_time = current_time
|
|
210
|
+
self._last_line = None
|
|
211
|
+
|
|
212
|
+
elif event == "line":
|
|
213
|
+
# Line executed
|
|
214
|
+
if self._current_function_key is not None and self._last_line is not None:
|
|
215
|
+
time_delta = current_time - self._last_time
|
|
216
|
+
|
|
217
|
+
func_stats = self._function_stats[self._current_function_key]
|
|
218
|
+
|
|
219
|
+
if self._last_line not in func_stats.line_stats:
|
|
220
|
+
func_stats.line_stats[self._last_line] = LineStats(
|
|
221
|
+
line_number=self._last_line,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
line_stats = func_stats.line_stats[self._last_line]
|
|
225
|
+
line_stats.hits += 1
|
|
226
|
+
line_stats.total_time += time_delta
|
|
227
|
+
func_stats.total_time += time_delta
|
|
228
|
+
|
|
229
|
+
self._last_line = frame.f_lineno
|
|
230
|
+
self._last_time = current_time
|
|
231
|
+
|
|
232
|
+
elif event == "return":
|
|
233
|
+
# Function returning
|
|
234
|
+
if self._current_function_key is not None and self._last_line is not None:
|
|
235
|
+
time_delta = current_time - self._last_time
|
|
236
|
+
|
|
237
|
+
func_stats = self._function_stats[self._current_function_key]
|
|
238
|
+
|
|
239
|
+
if self._last_line not in func_stats.line_stats:
|
|
240
|
+
func_stats.line_stats[self._last_line] = LineStats(
|
|
241
|
+
line_number=self._last_line,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
line_stats = func_stats.line_stats[self._last_line]
|
|
245
|
+
line_stats.hits += 1
|
|
246
|
+
line_stats.total_time += time_delta
|
|
247
|
+
func_stats.total_time += time_delta
|
|
248
|
+
|
|
249
|
+
self._current_function_key = None
|
|
250
|
+
self._last_line = None
|
|
251
|
+
|
|
252
|
+
return self._trace_callback
|
|
253
|
+
|
|
254
|
+
def _load_source_lines(self, key: tuple[str, str, int]) -> None:
|
|
255
|
+
"""Load source code lines for a function.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
key: Tuple of (filename, function_name, first_line)
|
|
259
|
+
|
|
260
|
+
Test components:
|
|
261
|
+
- Correct file reading and line extraction
|
|
262
|
+
- Proper handling of missing files
|
|
263
|
+
- UTF-8 encoding support
|
|
264
|
+
- Line number indexing
|
|
265
|
+
"""
|
|
266
|
+
filename, _, _ = key
|
|
267
|
+
func_stats = self._function_stats[key]
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
path = Path(filename)
|
|
271
|
+
if path.exists():
|
|
272
|
+
with Path.open(path, encoding="utf-8") as f:
|
|
273
|
+
lines = f.readlines()
|
|
274
|
+
for i, line in enumerate(lines, start=1):
|
|
275
|
+
func_stats.source_lines[i] = line.rstrip()
|
|
276
|
+
except (OSError, UnicodeDecodeError):
|
|
277
|
+
# If we can't read the file, just continue
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
def _find_repo_root(self, start_path: str) -> Path:
|
|
281
|
+
"""Return the git repo root (directory containing .git)."""
|
|
282
|
+
p = Path(start_path).resolve()
|
|
283
|
+
|
|
284
|
+
for parent in [p] + list(p.parents): # noqa: RUF005
|
|
285
|
+
if (parent / ".git").exists():
|
|
286
|
+
return parent
|
|
287
|
+
|
|
288
|
+
# No git repo found → fallback to start_path
|
|
289
|
+
return p
|
|
290
|
+
|
|
291
|
+
def _is_in_project_folder(self, filename: str) -> bool:
|
|
292
|
+
if self._project_folder is None:
|
|
293
|
+
return True # type: ignore[unreachable]
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
file_path = Path(filename).resolve()
|
|
297
|
+
# Prüfe ob file_path relativ zu project_folder ist
|
|
298
|
+
try:
|
|
299
|
+
file_path.relative_to(self._project_folder)
|
|
300
|
+
except ValueError:
|
|
301
|
+
return False
|
|
302
|
+
else:
|
|
303
|
+
return True
|
|
304
|
+
except (OSError, ValueError):
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def print_stats( # noqa: C901
|
|
309
|
+
self,
|
|
310
|
+
min_time_us: float = 0.0,
|
|
311
|
+
top_n_lines: int | None = None,
|
|
312
|
+
sort_by: str = "time",
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Print detailed profiling statistics per function.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
min_time_us: Minimum time in microseconds to display a line
|
|
318
|
+
top_n_lines: If set, only show the top N lines per function
|
|
319
|
+
sort_by: How to sort lines - "time" (total time), "hits" (call count),
|
|
320
|
+
or "line" (line number). Default is "time".
|
|
321
|
+
|
|
322
|
+
Test components:
|
|
323
|
+
- Correct formatting of time values (seconds to microseconds)
|
|
324
|
+
- Proper sorting by line number, time, or hits
|
|
325
|
+
- Accurate percentage calculations
|
|
326
|
+
- Column alignment and formatting
|
|
327
|
+
- Filtering based on min_time_us threshold
|
|
328
|
+
- Correct limiting to top_n_lines
|
|
329
|
+
- Project folder filtering
|
|
330
|
+
"""
|
|
331
|
+
if not self._function_stats:
|
|
332
|
+
print("No profiling data collected.")# noqa: T201
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
for key, func_stats in sorted(self._function_stats.items()):
|
|
336
|
+
filename, function_name, first_line = key
|
|
337
|
+
|
|
338
|
+
# Filter by project folder if set
|
|
339
|
+
if not self._is_in_project_folder(filename):
|
|
340
|
+
print(f"filename not in folde: {filename}")# noqa: T201
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
if not func_stats.line_stats:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
print("=" * 100)# noqa: T201
|
|
347
|
+
print(f"File: {filename}")# noqa: T201
|
|
348
|
+
print(f"Function: {function_name} at line {first_line}")# noqa: T201
|
|
349
|
+
print(f"Total time: {func_stats.total_time * 1e6:.1f} µs")# noqa: T201
|
|
350
|
+
print("=" * 100) # noqa: T201
|
|
351
|
+
print(f"{'Line #':<8} {'Hits':<10} {'Time (µs)':<15} {'Per Hit (µs)':<15} {'% Time':<10} {'Line Content'}") # noqa: T201
|
|
352
|
+
print("-" * 100) # noqa: T201
|
|
353
|
+
|
|
354
|
+
# Collect all lines with stats
|
|
355
|
+
line_data: list[tuple[int, LineStats]] = []
|
|
356
|
+
for line_num in func_stats.line_stats:
|
|
357
|
+
line_stats = func_stats.line_stats[line_num]
|
|
358
|
+
time_us = line_stats.total_time * 1e6
|
|
359
|
+
|
|
360
|
+
if time_us >= min_time_us:
|
|
361
|
+
line_data.append((line_num, line_stats))
|
|
362
|
+
|
|
363
|
+
# Sort based on sort_by parameter
|
|
364
|
+
if sort_by == "time":
|
|
365
|
+
line_data.sort(key=lambda x: x[1].total_time, reverse=True)
|
|
366
|
+
elif sort_by == "hits":
|
|
367
|
+
line_data.sort(key=lambda x: x[1].hits, reverse=True)
|
|
368
|
+
else: # sort_by == "line"
|
|
369
|
+
line_data.sort(key=lambda x: x[0])
|
|
370
|
+
|
|
371
|
+
# Limit to top N if requested
|
|
372
|
+
if top_n_lines is not None:
|
|
373
|
+
line_data = line_data[:top_n_lines]
|
|
374
|
+
|
|
375
|
+
# Print the lines
|
|
376
|
+
for line_num, line_stats in line_data:
|
|
377
|
+
time_us = line_stats.total_time * 1e6
|
|
378
|
+
avg_time_us = line_stats.average_time * 1e6
|
|
379
|
+
percent = (line_stats.total_time / func_stats.total_time * 100
|
|
380
|
+
if func_stats.total_time > 0 else 0.0)
|
|
381
|
+
|
|
382
|
+
source_line = func_stats.source_lines.get(line_num, "")
|
|
383
|
+
# Truncate long lines
|
|
384
|
+
if len(source_line) > 50: # noqa: PLR2004
|
|
385
|
+
source_line = source_line[:47] + "..."
|
|
386
|
+
|
|
387
|
+
print(f"{line_num:<8} {line_stats.hits:<10} {time_us:<15.1f} " # noqa: T201
|
|
388
|
+
f"{avg_time_us:<15.1f} {percent:<10.1f} {source_line}")
|
|
389
|
+
|
|
390
|
+
print() # noqa: T201
|
|
391
|
+
|
|
392
|
+
def print_global_top_stats( # noqa: C901, PLR0912
|
|
393
|
+
self,
|
|
394
|
+
top_n: int = 10,
|
|
395
|
+
min_time_us: float = 0.0,
|
|
396
|
+
sort_by: str = "time",
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Print a global summary of the top lines across all functions.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
top_n: Number of top lines to display
|
|
402
|
+
min_time_us: Minimum time in microseconds to include a line
|
|
403
|
+
sort_by: How to sort - "time" (total time) or "hits" (call count)
|
|
404
|
+
|
|
405
|
+
Test components:
|
|
406
|
+
- Correct aggregation across all functions
|
|
407
|
+
- Proper sorting by time or hits
|
|
408
|
+
- Project folder filtering
|
|
409
|
+
- Accurate time and percentage calculations
|
|
410
|
+
- Proper table formatting
|
|
411
|
+
"""
|
|
412
|
+
all_lines: list[dict] = []
|
|
413
|
+
|
|
414
|
+
for key, func_stats in self._function_stats.items():
|
|
415
|
+
filename, function_name, first_line = key
|
|
416
|
+
|
|
417
|
+
# Filter by project folder if set
|
|
418
|
+
if not self._is_in_project_folder(filename):
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
if not func_stats.line_stats:
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
# Use relative path if possible, otherwise basename
|
|
425
|
+
if self._project_folder is not None:
|
|
426
|
+
try:
|
|
427
|
+
display_path = Path(filename).resolve().relative_to(self._project_folder)
|
|
428
|
+
short_filename = str(display_path)
|
|
429
|
+
except (ValueError, OSError):
|
|
430
|
+
short_filename = Path(filename).name
|
|
431
|
+
else:
|
|
432
|
+
short_filename = Path(filename).name # type: ignore[unreachable]
|
|
433
|
+
|
|
434
|
+
for line_num, line_stats in func_stats.line_stats.items():
|
|
435
|
+
time_us = line_stats.total_time * 1e6
|
|
436
|
+
|
|
437
|
+
if time_us < min_time_us:
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
all_lines.append({
|
|
441
|
+
"file": short_filename,
|
|
442
|
+
"function": function_name,
|
|
443
|
+
"line_num": line_num,
|
|
444
|
+
"hits": line_stats.hits,
|
|
445
|
+
"time_us": time_us,
|
|
446
|
+
"avg_time_us": line_stats.average_time * 1e6,
|
|
447
|
+
"percent": (line_stats.total_time / func_stats.total_time * 100
|
|
448
|
+
if func_stats.total_time > 0 else 0.0),
|
|
449
|
+
"source_line": func_stats.source_lines.get(line_num, ""),
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
if not all_lines:
|
|
453
|
+
print("No profiling data above the threshold.") # noqa: T201
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Sort by descending total time or hits
|
|
457
|
+
if sort_by == "hits":
|
|
458
|
+
all_lines.sort(key=lambda x: x["hits"], reverse=True)
|
|
459
|
+
else: # sort_by == "time"
|
|
460
|
+
all_lines.sort(key=lambda x: x["time_us"], reverse=True)
|
|
461
|
+
|
|
462
|
+
# Print header
|
|
463
|
+
print("=" * 130) # noqa: T201
|
|
464
|
+
print(f"Top {top_n} lines across all functions (sorted by {sort_by})") # noqa: T201
|
|
465
|
+
print("=" * 130) # noqa: T201
|
|
466
|
+
print(f"{'File::Function':<50} {'Line':<6} {'Hits':<10} {'Time (µs)':<13} " # noqa: T201
|
|
467
|
+
f"{'Per Hit (µs)':<14} {'% Time':<8} {'Line Content'}")
|
|
468
|
+
print("-" * 130) # noqa: T201
|
|
469
|
+
|
|
470
|
+
# Print top lines
|
|
471
|
+
for line in all_lines[:top_n]:
|
|
472
|
+
source_line = line["source_line"]
|
|
473
|
+
if len(source_line) > 40: # noqa: PLR2004
|
|
474
|
+
source_line = source_line[:37] + "..."
|
|
475
|
+
|
|
476
|
+
file_func = f"{line['file']}::{line['function']}"
|
|
477
|
+
if len(file_func) > 50: # noqa: PLR2004
|
|
478
|
+
file_func = file_func[:47] + "..."
|
|
479
|
+
|
|
480
|
+
print(f"{file_func:<50} {line['line_num']:<6} {line['hits']:<10} " # noqa: T201
|
|
481
|
+
f"{line['time_us']:<13.1f} {line['avg_time_us']:<14.1f} "
|
|
482
|
+
f"{line['percent']:<8.1f} {source_line}")
|
|
483
|
+
|
|
484
|
+
print("=" * 130) # noqa: T201
|
|
485
|
+
print() # noqa: T201
|
|
486
|
+
|
|
487
|
+
def get_stats(self) -> dict[tuple[str, str, int], FunctionStats]:
|
|
488
|
+
"""Get raw profiling statistics.
|
|
489
|
+
|
|
490
|
+
Returns
|
|
491
|
+
-------
|
|
492
|
+
Dictionary mapping function keys to FunctionStats
|
|
493
|
+
|
|
494
|
+
Test components:
|
|
495
|
+
- Correct dictionary structure
|
|
496
|
+
- Immutability considerations (returns reference, not copy)
|
|
497
|
+
"""
|
|
498
|
+
return self._function_stats
|
|
499
|
+
|
|
500
|
+
def clear(self) -> None:
|
|
501
|
+
"""Clear all profiling data.
|
|
502
|
+
|
|
503
|
+
Test components:
|
|
504
|
+
- Complete state reset
|
|
505
|
+
- Proper dictionary clearing
|
|
506
|
+
"""
|
|
507
|
+
self._function_stats.clear()
|
|
508
|
+
self._last_time = 0.0
|
|
509
|
+
self._last_line = None
|
|
510
|
+
self._current_function_key = None
|
|
511
|
+
|
|
512
|
+
def reset(self) -> None:
|
|
513
|
+
"""Reset the profiler to initial state (alias for clear).
|
|
514
|
+
|
|
515
|
+
This method is an alias for clear() to provide a more intuitive
|
|
516
|
+
interface for users who think of "resetting" rather than "clearing".
|
|
517
|
+
|
|
518
|
+
Test components:
|
|
519
|
+
- Verify it calls clear() correctly
|
|
520
|
+
- Ensure all state is reset
|
|
521
|
+
- Check it's safe to call multiple times
|
|
522
|
+
"""
|
|
523
|
+
self.clear()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "with-line-profiler"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Context-manager based line-by-line profiler for Python functions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "mathematiger", email = "mcop.dkoehler@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["profiler", "profiling", "performance", "timing", "debugging"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Debuggers",
|
|
26
|
+
"Topic :: Software Development :: Testing",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"typing_extensions>=4.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/mathematiger/lineprofiler"
|
|
35
|
+
Repository = "https://github.com/mathematiger/lineprofiler"
|
|
36
|
+
Issues = "https://github.com/mathematiger/lineprofiler/issues"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["lineprofiler"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 100
|
|
43
|
+
target-version = "py39"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "W", "I", "N", "UP", "ANN", "B", "C4", "SIM"]
|
|
47
|
+
ignore = ["ANN101", "ANN102"]
|
|
48
|
+
|
|
49
|
+
[tool.mypy]
|
|
50
|
+
python_version = "3.9"
|
|
51
|
+
strict = true
|
|
52
|
+
warn_return_any = true
|
|
53
|
+
warn_unused_ignores = true
|