lazyline 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,461 @@
1
+ Metadata-Version: 2.4
2
+ Name: lazyline
3
+ Version: 0.1.0
4
+ Summary: Zero-config line-level profiler for Python packages — no decorators, no code changes.
5
+ Keywords: profiler,profiling,line-profiler,performance,benchmark,optimization,bottleneck,zero-config
6
+ Author: Tomáš Venkrbec
7
+ Author-email: Tomáš Venkrbec <venkrbec.tomas@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Debuggers
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Classifier: Topic :: System :: Benchmark
22
+ Classifier: Typing :: Typed
23
+ Requires-Dist: typer>=0.15
24
+ Requires-Dist: line-profiler>=4.1.0
25
+ Requires-Dist: pygments>=2.14.0 ; extra == 'color'
26
+ Requires-Python: >=3.10
27
+ Project-URL: Homepage, https://github.com/TomasVenkrbec/lazyline
28
+ Project-URL: Repository, https://github.com/TomasVenkrbec/lazyline
29
+ Project-URL: Documentation, https://github.com/TomasVenkrbec/lazyline#readme
30
+ Project-URL: Changelog, https://github.com/TomasVenkrbec/lazyline/blob/main/CHANGELOG.md
31
+ Project-URL: Issues, https://github.com/TomasVenkrbec/lazyline/issues
32
+ Provides-Extra: color
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Lazyline
36
+
37
+ [![PyPI version](https://img.shields.io/pypi/v/lazyline)](https://pypi.org/project/lazyline/)
38
+ [![Python versions](https://img.shields.io/pypi/pyversions/lazyline)](https://pypi.org/project/lazyline/)
39
+ [![Tests](https://github.com/TomasVenkrbec/lazyline/actions/workflows/ci.yml/badge.svg)](https://github.com/TomasVenkrbec/lazyline/actions/workflows/ci.yml)
40
+ [![codecov](https://codecov.io/gh/TomasVenkrbec/lazyline/graph/badge.svg)](https://codecov.io/gh/TomasVenkrbec/lazyline)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
42
+
43
+ **Zero-config line-level profiler for Python packages.**
44
+ Point it at a package, give it a command, get a ranked line-by-line breakdown.
45
+ No `@profile` decorators. No code changes. No guessing.
46
+
47
+ ## Why Lazyline?
48
+
49
+ ### The problem
50
+
51
+ Finding line-level bottlenecks in a Python package typically means
52
+ decorating suspect functions with `@profile`, running `kernprof`,
53
+ reading the output, removing the decorators, and repeating until
54
+ you find the real culprit. If you guess wrong, you waste a cycle.
55
+
56
+ ```bash
57
+ # Without lazyline — manual, iterative workflow:
58
+ # 1. Guess which functions might be slow
59
+ # 2. Add @profile decorators to each one
60
+ # 3. Run: LINE_PROFILE=1 python script.py or kernprof -lv script.py.
61
+ # 4. Read output, realize the bottleneck is elsewhere
62
+ # 5. Remove decorators, add new ones, go to step 3
63
+ # 6. Clean up all decorators when done
64
+ ```
65
+
66
+ Lazyline eliminates this loop. Point it at a package, give it a
67
+ command, and every function is profiled automatically:
68
+
69
+ ```bash
70
+ # With lazyline — one command, done:
71
+ lazyline run my_package -- pytest tests/
72
+ ```
73
+
74
+ No decorators. No code changes. No guessing.
75
+
76
+ ### What lazyline adds over raw line_profiler
77
+
78
+ Lazyline wraps `line_profiler` and adds everything needed to go
79
+ from "I want to profile this package" to "here are the bottlenecks"
80
+ in a single command:
81
+
82
+ #### Zero-config profiling
83
+
84
+ - No `@profile` decorators — every function in the target scope
85
+ is discovered and instrumented automatically
86
+ - Automatic module and namespace package discovery (point at a
87
+ package name, directory, or `.py` file)
88
+ - `lru_cache` and other C-extension wrappers auto-unwrapped
89
+
90
+ #### Subprocess and worker profiling
91
+
92
+ - Child Python processes (e.g., Celery workers, Airflow tasks)
93
+ profiled via `sitecustomize.py` injection — no configuration needed
94
+ - `concurrent.futures.ProcessPoolExecutor` and `multiprocessing.Pool`
95
+ workers profiled with per-worker instances and merged results
96
+
97
+ #### Rich terminal output
98
+
99
+ - Syntax-highlighted source code (Pygments, monokai theme)
100
+ - Adaptive column widths that fit your terminal
101
+ - Compact mode (default) collapses un-hit lines
102
+ - Auto-scaling time units (s/ms/us/ns)
103
+
104
+ #### Analysis workflow
105
+
106
+ - `--top N`, `--filter`, `--summary` to focus on what matters
107
+ - JSON export/import for sharing and later analysis
108
+ - Optional `tracemalloc` memory tracking (`--memory`)
109
+
110
+ ### When to use lazyline
111
+
112
+ Use lazyline when you need **exact, line-level timing** and want to
113
+ find bottlenecks without modifying code. It is especially useful
114
+ for profiling packages you don't own or can't easily change.
115
+
116
+ If you need **low-overhead production profiling**, a sampling
117
+ profiler like Scalene or py-spy is a better fit — they trade
118
+ line-level precision for significantly lower overhead.
119
+
120
+ ## Quick Start
121
+
122
+ ```bash
123
+ pip install lazyline
124
+
125
+ # Profile a package while running a script:
126
+ lazyline run my_package -- python evaluate.py --dataset "some_dataset"
127
+
128
+ # Profile a package while running its CLI tool:
129
+ lazyline run my_package -- my_package_cli run --verbose
130
+
131
+ # Profile a package while running its test suite:
132
+ lazyline run my_package -- pytest tests/
133
+
134
+ # Profile any importable package — no code changes needed:
135
+ lazyline run json -- python -c "import json; json.dumps([1, 2, 3])"
136
+ ```
137
+
138
+ Lazyline discovers all modules in the given scope, instruments
139
+ (attaches timing to) every Python function, runs the command,
140
+ and prints a ranked breakdown:
141
+
142
+ ```text
143
+ Discovered 12 module(s) in scope 'my_package'.
144
+ Registered 89 function(s) for profiling.
145
+
146
+ =================================================
147
+ Lazyline results for my_package
148
+ 3 of 89 functions called | Total: 12.4451s | Wall time: 10.2300s | Unit: s
149
+
150
+ Summary
151
+
152
+ Function Total (s) % Total Calls Time/Call (s)
153
+ -----------------------------------------------------------------------------------------------------------------
154
+ my_package.process.transform 8.3172 66.8% 500 0.016634
155
+ my_package.io.load_data 3.1245 25.1% 1 3.124500
156
+ my_package.utils.normalize 1.0034 8.1% 50000 0.000020
157
+ ...
158
+ -----------------------------------------------------------------------------------------------------------------
159
+ Total 12.4451
160
+
161
+ Functions
162
+
163
+ my_package.process.transform (.../process.py:42)
164
+ 8.3172s total | 500 calls | 0.016634s/call
165
+
166
+ Line Hits Time (s) Time/Hit (s) % Func Source
167
+ ----------------------------------------------------------------------------------------
168
+ 42 def transform(data):
169
+ 43 500 0.031200 0.000062 0.4% result = []
170
+ 44 500000 7.982100 0.000016 96.0% for row in data:
171
+ 45 500000 0.301200 0.000001 3.6% result.append(row)
172
+ 46 500 0.002700 0.000005 0.0% return result
173
+ ```
174
+
175
+ ## Installation
176
+
177
+ ```bash
178
+ pip install lazyline
179
+
180
+ # With syntax highlighting (recommended):
181
+ pip install lazyline[color]
182
+ ```
183
+
184
+ Requires Python 3.10+. The target package must be importable
185
+ (installed or on `sys.path`) in the same environment. The `[color]`
186
+ extra installs Pygments for syntax-highlighted source in terminal
187
+ output (falls back to plain text if not installed).
188
+
189
+ ## Comparison with Alternatives
190
+
191
+ | Tool | Granularity | Method | Code changes? | Subprocess profiling |
192
+ |------|-------------|--------|---------------|----------------------|
193
+ | **lazyline** | Per-line | Deterministic | None | Automatic |
194
+ | `kernprof` / `line_profiler` | Per-line | Deterministic | `@profile` decorators | No |
195
+ | `cProfile` | Per-function | Deterministic | None | No |
196
+ | `Scalene` | Per-line | Sampling | None | Yes |
197
+ | `py-spy` | Per-line (sampled) | Sampling | None (attach) | Yes (follow-children) |
198
+
199
+ **Unique:** Lazyline is the only line-level profiler that
200
+ automatically profiles `ProcessPoolExecutor`, `multiprocessing.Pool`,
201
+ and child Python processes (e.g., Celery workers, Airflow tasks)
202
+ without any configuration.
203
+
204
+ **Deterministic vs sampling:** Deterministic tracing (lazyline,
205
+ line_profiler, cProfile) fires a callback on every line or function
206
+ call, measuring exact execution counts and times. Sampling profilers
207
+ (Scalene, py-spy) periodically interrupt the program and record
208
+ where it is — much lower overhead, but statistical approximations
209
+ rather than exact counts.
210
+
211
+ Lazyline's deterministic approach means **relative rankings are
212
+ reliable** (if function A appears 10x slower than B, that ratio
213
+ holds), but **absolute times are inflated** by tracing overhead.
214
+ See [Overhead and Limitations](#overhead-and-limitations) for
215
+ details.
216
+
217
+ ## Usage
218
+
219
+ ### `lazyline run`
220
+
221
+ ```text
222
+ lazyline run [OPTIONS] SCOPE [SCOPE...] [--] COMMAND [ARGS...]
223
+ ```
224
+
225
+ Profile a command, instrumenting all functions in the given scope(s).
226
+
227
+ | Option | Description |
228
+ |--------|-------------|
229
+ | `--top N` / `-n N` | Show only the N slowest functions |
230
+ | `--memory` | Enable tracemalloc memory tracking |
231
+ | `--output FILE` / `-o FILE` | Export results to JSON (`-` for stdout) |
232
+ | `--compact/--full` | Collapse un-hit lines (default) or show all |
233
+ | `--summary` | Print only the summary table, no per-line detail |
234
+ | `--filter PATTERN` / `-f PATTERN` | Only show functions matching fnmatch pattern(s) (comma-separated) |
235
+ | `--quiet` / `-q` | Suppress discovery/registration stderr messages |
236
+ | `--unit UNIT` | Time display unit: `auto` (default), `s`, `ms`, `us`, or `ns` |
237
+
238
+ Options can appear before or after SCOPE. When placed after SCOPE,
239
+ a `--` separator before COMMAND is required:
240
+
241
+ ```bash
242
+ lazyline run --top 5 my_package -- pytest tests/ # options before scope
243
+ lazyline run my_package --top 5 -- pytest tests/ # options after scope
244
+ ```
245
+
246
+ Multiple scopes can be profiled in a single run (requires `--`):
247
+
248
+ ```bash
249
+ lazyline run utils.py my_package -- python script.py
250
+ ```
251
+
252
+ ### `lazyline show`
253
+
254
+ ```text
255
+ lazyline show FILE [--top N] [--compact/--full] [--summary] [--filter PATTERN] [--unit UNIT]
256
+ ```
257
+
258
+ Display profiling results from a previously saved JSON file.
259
+
260
+ ### `lazyline --version`
261
+
262
+ Print the installed version and exit.
263
+
264
+ ## Scope
265
+
266
+ Unlike tools like cProfile that profile everything, lazyline
267
+ focuses on specific code you choose — this keeps output clean
268
+ and overhead low.
269
+
270
+ SCOPE tells lazyline which package or module to profile. It
271
+ accepts three formats:
272
+
273
+ | Format | Example | What it does |
274
+ |--------|---------|--------------|
275
+ | Dotted module path | `my_package` | Imports and walks all submodules |
276
+ | Directory path | `my_package/utils` | Converted to dotted path, then walked |
277
+ | Single module | `json` | Imports that module only (+ submodules) |
278
+ | Single `.py` file | `utils.py` | Imports the file directly (no `sys.path` needed) |
279
+
280
+ Lazyline discovers modules via `pkgutil.walk_packages()` for
281
+ regular packages and filesystem scanning for implicit namespace
282
+ packages (directories without `__init__.py`). Every Python
283
+ function found is registered. C extension functions are silently
284
+ skipped.
285
+
286
+ ## Commands
287
+
288
+ COMMAND is what lazyline executes under profiling. Supported forms:
289
+
290
+ | Form | Example |
291
+ |------|---------|
292
+ | Bare module name | `pytest tests/` |
293
+ | Console script (incl. hyphens) | `my-tool run-all` |
294
+ | `python -m module` | `python -m pytest -q` |
295
+ | Script file | `python script.py` |
296
+ | Inline code | `python -c "import json; json.dumps(1)"` |
297
+
298
+ Hyphenated console scripts (e.g., `my-tool`) are resolved
299
+ via `importlib.metadata` entry points automatically.
300
+
301
+ ## Examples
302
+
303
+ Profile a package while running its test suite:
304
+
305
+ ```bash
306
+ lazyline run --top 10 my_package -- pytest tests/
307
+ ```
308
+
309
+ Profile a CLI tool (console script with hyphens works too):
310
+
311
+ ```bash
312
+ lazyline run my_package.cli -- my-tool run-all
313
+ ```
314
+
315
+ Profile with memory tracking:
316
+
317
+ ```bash
318
+ lazyline run --memory my_package -- pytest tests/ -q
319
+ ```
320
+
321
+ Export results for later analysis:
322
+
323
+ ```bash
324
+ lazyline run --output results.json my_package -- pytest -q
325
+ lazyline show results.json --top 10
326
+ ```
327
+
328
+ Filter to specific functions (supports comma-separated patterns):
329
+
330
+ ```bash
331
+ lazyline run --filter "*extract*,*match*" my_package -- pytest tests/
332
+ ```
333
+
334
+ Profile a single `.py` file or multiple scopes in one run:
335
+
336
+ ```bash
337
+ lazyline run utils.py -- python script.py
338
+ lazyline run utils.py my_package -- python evaluate.py
339
+ ```
340
+
341
+ ## Output Format
342
+
343
+ Lazyline prints a results header, a summary table, and per-line detail:
344
+
345
+ **Results header** — scope, coverage, total time, wall-clock time, and display unit:
346
+
347
+ ```text
348
+ =================================================
349
+ Lazyline results for pkg
350
+ 2 of 15 functions called | Total: 1.4690s | Wall time: 1.2300s | Unit: s
351
+ ```
352
+
353
+ **Summary table** — all profiled functions ranked by total time:
354
+
355
+ ```text
356
+ Function Total (s) % Total Calls Time/Call (s)
357
+ -----------------------------------------------------------------------------------------------------------------
358
+ pkg.module.slow_func 1.2345 82.1% 100 0.012345
359
+ pkg.module.helper 0.2345 15.6% 1000 0.000235
360
+ ...
361
+ ```
362
+
363
+ **Per-line detail** — source code with timing for each function:
364
+
365
+ ```text
366
+ pkg.module.slow_func (pkg/module.py:42)
367
+ 1.2345s total | 100 calls | 0.012345s/call
368
+
369
+ Line Hits Time (s) Time/Hit (s) % Func Source
370
+ ----------------------------------------------------------------------------------------
371
+ 42 def slow_func(data):
372
+ 43 100 0.000100 0.000001 0.0% result = []
373
+ 44 100000 1.234000 0.000012 99.9% for item in data:
374
+ 45 100000 0.000400 0.000000 0.0% result.append(item)
375
+ 46 100 0.000000 0.000000 0.0% return result
376
+ ```
377
+
378
+ On terminals, source code is syntax-highlighted (monokai theme) and
379
+ faint `│` column separators appear between numeric columns for easier
380
+ visual tracking across wide tables. Piped/file output uses plain text
381
+ with no ANSI formatting.
382
+
383
+ With `--memory`, an additional `Net Mem` column shows per-line
384
+ net memory allocation delta (bytes allocated minus freed).
385
+
386
+ ## How It Works
387
+
388
+ 1. **Discovery** — imports the target scope and walks all
389
+ submodules via `pkgutil.walk_packages()`, supplemented with
390
+ filesystem scanning for implicit namespace packages.
391
+ 2. **Registration** — registers every Python function with
392
+ `line_profiler`'s `LineProfiler.add_module()`. C extensions
393
+ are skipped; `lru_cache` wrappers are auto-unwrapped.
394
+ 3. **Execution** — runs the user's command with profiling
395
+ enabled. `line_profiler` uses `sys.monitoring` (Python 3.12+)
396
+ or `sys.settrace` for deterministic per-line tracing.
397
+ 4. **Collection** — extracts per-line timing data, filters
398
+ out stdlib wrapper leaks via scope path matching.
399
+ 5. **Memory** (optional) — `tracemalloc` takes before/after
400
+ snapshots and computes per-line net allocation deltas.
401
+ 6. **Multiprocessing** — `concurrent.futures.ProcessPoolExecutor`
402
+ and `multiprocessing.Pool` worker processes are automatically
403
+ profiled with per-worker `LineProfiler` instances, stats
404
+ merged after execution.
405
+ 7. **Subprocesses** — if the command spawns child Python
406
+ processes (e.g., Celery workers, Airflow tasks), lazyline injects
407
+ a `sitecustomize.py` bootstrap via `PYTHONPATH` so child
408
+ interpreters profile the same scope automatically.
409
+
410
+ ## Overhead and Limitations
411
+
412
+ Lazyline uses **deterministic tracing** (exact measurement of every
413
+ line, as opposed to sampling which checks periodically). This fires
414
+ a callback on every line execution, which has important implications:
415
+
416
+ **Overhead inflates both wall-clock runtime and reported times.**
417
+ The callback cost is included in each line's measured time. For
418
+ functions with meaningful per-call work (>0.1ms), overhead is
419
+ negligible (~1.2x). For tight loops calling tiny functions millions
420
+ of times, overhead can be ~7x.
421
+
422
+ **Relative rankings are reliable.** If function A appears 10x
423
+ slower than function B, that ratio holds regardless of overhead.
424
+ Use lazyline to find *which* functions are slowest, not to measure
425
+ *absolute* execution time.
426
+
427
+ **Memory measurements are not inflated** by line-profiler tracing.
428
+ `tracemalloc` hooks the memory allocator separately. However,
429
+ allocation-heavy code (JSON serialization, string formatting) may
430
+ see significant wall-clock slowdown from tracemalloc's per-alloc
431
+ hooks. Use `--memory` only when you need allocation data.
432
+
433
+ Lazyline warns when a function exceeds 1M total line hits, as
434
+ reported times for such functions are unreliable.
435
+
436
+ Other limitations:
437
+
438
+ - `concurrent.futures.ProcessPoolExecutor` and `multiprocessing.Pool`
439
+ workers are profiled automatically (requires `fork` start method —
440
+ default on Linux; `spawn`/`forkserver` are not supported). Direct
441
+ `multiprocessing.Process` usage is not covered.
442
+ - Child processes started with `python -S` skip `sitecustomize.py`
443
+ loading, so subprocess profiling is silently disabled.
444
+ - C extension functions are skipped (no Python bytecode to trace).
445
+ - `tracemalloc` shows net allocation delta only — transient
446
+ allocations (alloc + free within the run) appear as ~0.
447
+ - `tracemalloc` adds ~30% memory overhead for its own bookkeeping.
448
+
449
+ See [`benchmarks/README.md`](benchmarks/README.md) for detailed
450
+ overhead measurements and methodology.
451
+
452
+ ## Development
453
+
454
+ ```bash
455
+ ./docker/build.sh
456
+ ./docker/run.sh
457
+ ```
458
+
459
+ See [`CLAUDE.md`](CLAUDE.md) for project structure and
460
+ conventions, [`DESIGN.md`](DESIGN.md) for architecture and
461
+ plan.