gadget-timer 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.
- gadget_timer-0.1.0/LICENSE +21 -0
- gadget_timer-0.1.0/PKG-INFO +100 -0
- gadget_timer-0.1.0/README.md +72 -0
- gadget_timer-0.1.0/gadget.py +103 -0
- gadget_timer-0.1.0/gadget_timer.egg-info/PKG-INFO +100 -0
- gadget_timer-0.1.0/gadget_timer.egg-info/SOURCES.txt +10 -0
- gadget_timer-0.1.0/gadget_timer.egg-info/dependency_links.txt +1 -0
- gadget_timer-0.1.0/gadget_timer.egg-info/requires.txt +4 -0
- gadget_timer-0.1.0/gadget_timer.egg-info/top_level.txt +1 -0
- gadget_timer-0.1.0/pyproject.toml +53 -0
- gadget_timer-0.1.0/setup.cfg +4 -0
- gadget_timer-0.1.0/tests/test_gadget.py +128 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sam Maddrell-Mander
|
|
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,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gadget-timer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tiny timing helper for printing elapsed times with call-site context
|
|
5
|
+
Author: Sam Maddrell-Mander
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/s-maddrellmander/gadget
|
|
8
|
+
Project-URL: Repository, https://github.com/s-maddrellmander/gadget
|
|
9
|
+
Project-URL: Issues, https://github.com/s-maddrellmander/gadget/issues
|
|
10
|
+
Keywords: timing,debugging,profiling,logging
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# gadget
|
|
30
|
+
|
|
31
|
+
[](https://github.com/s-maddrellmander/gadget/actions/workflows/tests.yml)
|
|
32
|
+
|
|
33
|
+
A tiny helper for quick timing/debug prints with file and line context.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
### From a local checkout
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Directly from GitHub
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install "git+https://github.com/s-maddrellmander/gadget.git"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Editable install (for development)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Functional API
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from gadget import gadget, gadget_reset, gadget_config
|
|
61
|
+
|
|
62
|
+
gadget("start")
|
|
63
|
+
# ... some code ...
|
|
64
|
+
gadget("after step")
|
|
65
|
+
|
|
66
|
+
gadget("phase 1", group="build")
|
|
67
|
+
gadget("phase 2", group="build")
|
|
68
|
+
gadget_reset("build")
|
|
69
|
+
|
|
70
|
+
gadget_config(verbose=False) # disable output globally
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Class-based API
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from gadget import Gadget
|
|
77
|
+
|
|
78
|
+
timer = Gadget(verbose=True)
|
|
79
|
+
timer("start")
|
|
80
|
+
# ... some code ...
|
|
81
|
+
timer("step", group="build")
|
|
82
|
+
timer.reset("build")
|
|
83
|
+
timer.reset() # reset all groups
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Install from PyPI
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install gadget-timer
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- Package name on PyPI is `gadget-timer`.
|
|
95
|
+
- Import path is still `gadget`.
|
|
96
|
+
- For Git installs, the repository URL is:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install "git+https://github.com/s-maddrellmander/gadget.git"
|
|
100
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# gadget
|
|
2
|
+
|
|
3
|
+
[](https://github.com/s-maddrellmander/gadget/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
A tiny helper for quick timing/debug prints with file and line context.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### From a local checkout
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Directly from GitHub
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "git+https://github.com/s-maddrellmander/gadget.git"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Editable install (for development)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Functional API
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from gadget import gadget, gadget_reset, gadget_config
|
|
33
|
+
|
|
34
|
+
gadget("start")
|
|
35
|
+
# ... some code ...
|
|
36
|
+
gadget("after step")
|
|
37
|
+
|
|
38
|
+
gadget("phase 1", group="build")
|
|
39
|
+
gadget("phase 2", group="build")
|
|
40
|
+
gadget_reset("build")
|
|
41
|
+
|
|
42
|
+
gadget_config(verbose=False) # disable output globally
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Class-based API
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from gadget import Gadget
|
|
49
|
+
|
|
50
|
+
timer = Gadget(verbose=True)
|
|
51
|
+
timer("start")
|
|
52
|
+
# ... some code ...
|
|
53
|
+
timer("step", group="build")
|
|
54
|
+
timer.reset("build")
|
|
55
|
+
timer.reset() # reset all groups
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Install from PyPI
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install gadget-timer
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Notes
|
|
65
|
+
|
|
66
|
+
- Package name on PyPI is `gadget-timer`.
|
|
67
|
+
- Import path is still `gadget`.
|
|
68
|
+
- For Git installs, the repository URL is:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install "git+https://github.com/s-maddrellmander/gadget.git"
|
|
72
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import time
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
class Gadget:
|
|
11
|
+
def __init__(self, verbose=True):
|
|
12
|
+
self.verbose = verbose
|
|
13
|
+
self.t0 = None
|
|
14
|
+
self.group_times = {}
|
|
15
|
+
|
|
16
|
+
def __call__(self, s='', group=None, _caller_frame=None):
|
|
17
|
+
"""Make the instance callable like a function."""
|
|
18
|
+
if not self.verbose:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
current_time = time.time()
|
|
22
|
+
if self.t0 is None:
|
|
23
|
+
self.t0 = current_time
|
|
24
|
+
elapsed = current_time - self.t0
|
|
25
|
+
self.t0 = time.time()
|
|
26
|
+
|
|
27
|
+
# Use provided caller frame or get it from stack
|
|
28
|
+
if _caller_frame is None:
|
|
29
|
+
caller_frame = inspect.currentframe().f_back
|
|
30
|
+
else:
|
|
31
|
+
caller_frame = _caller_frame
|
|
32
|
+
frame_info = inspect.getframeinfo(caller_frame)
|
|
33
|
+
|
|
34
|
+
line_number = frame_info.lineno
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with open(frame_info.filename, 'r') as f:
|
|
38
|
+
lines = f.readlines()
|
|
39
|
+
line_content = lines[line_number - 2].rstrip()
|
|
40
|
+
except:
|
|
41
|
+
line_content = ""
|
|
42
|
+
|
|
43
|
+
green_color = "\033[32m"
|
|
44
|
+
reset_color = "\033[0m"
|
|
45
|
+
|
|
46
|
+
# Shorten the file path - make it relative to cwd or just use filename
|
|
47
|
+
try:
|
|
48
|
+
short_path = os.path.relpath(frame_info.filename)
|
|
49
|
+
except:
|
|
50
|
+
short_path = os.path.basename(frame_info.filename)
|
|
51
|
+
|
|
52
|
+
# Clickable file path for VS Code
|
|
53
|
+
file_link = f"{short_path}:{line_number}"
|
|
54
|
+
|
|
55
|
+
# Handle group tracking
|
|
56
|
+
group_info = ""
|
|
57
|
+
if group:
|
|
58
|
+
if group not in self.group_times:
|
|
59
|
+
self.group_times[group] = 0.0
|
|
60
|
+
self.group_times[group] += elapsed
|
|
61
|
+
group_info = f" [{group}: {elapsed:.6f}s (total: {self.group_times[group]:.6f}s)]"
|
|
62
|
+
|
|
63
|
+
# Build the left side of the output
|
|
64
|
+
left_output = f"{green_color}[line={line_number}] {elapsed:.6f}s {line_content}{reset_color} {s}{group_info}"
|
|
65
|
+
|
|
66
|
+
# Get terminal width and right-align the file link
|
|
67
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
68
|
+
# Strip ANSI codes for accurate length calculation
|
|
69
|
+
left_output_plain = re.sub(r'\033\[[0-9;]+m', '', left_output)
|
|
70
|
+
left_length = len(left_output_plain)
|
|
71
|
+
|
|
72
|
+
# Calculate padding needed
|
|
73
|
+
file_link_display = f"→ {file_link}"
|
|
74
|
+
padding_needed = terminal_width - left_length - len(file_link_display) - 1
|
|
75
|
+
padding = " " * max(1, padding_needed) # At least one space
|
|
76
|
+
|
|
77
|
+
output = f"{left_output}{padding}{file_link_display}"
|
|
78
|
+
print(output)
|
|
79
|
+
|
|
80
|
+
def reset(self, group=None):
|
|
81
|
+
"""Reset group timer(s). If group is None, reset all groups."""
|
|
82
|
+
if group is None:
|
|
83
|
+
self.group_times = {}
|
|
84
|
+
elif group in self.group_times:
|
|
85
|
+
del self.group_times[group]
|
|
86
|
+
|
|
87
|
+
# Create a default instance for convenience
|
|
88
|
+
_default_gadget = Gadget()
|
|
89
|
+
|
|
90
|
+
# Convenience functions that use the default instance
|
|
91
|
+
def gadget(s='', group=None):
|
|
92
|
+
"""Convenience function using default Gadget instance."""
|
|
93
|
+
caller_frame = inspect.currentframe().f_back
|
|
94
|
+
_default_gadget(s, group, _caller_frame=caller_frame)
|
|
95
|
+
|
|
96
|
+
def gadget_reset(group=None):
|
|
97
|
+
"""Convenience function using default Gadget instance."""
|
|
98
|
+
_default_gadget.reset(group)
|
|
99
|
+
|
|
100
|
+
def gadget_config(verbose=True):
|
|
101
|
+
"""Configure the default Gadget instance."""
|
|
102
|
+
global _default_gadget
|
|
103
|
+
_default_gadget = Gadget(verbose=verbose)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gadget-timer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tiny timing helper for printing elapsed times with call-site context
|
|
5
|
+
Author: Sam Maddrell-Mander
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/s-maddrellmander/gadget
|
|
8
|
+
Project-URL: Repository, https://github.com/s-maddrellmander/gadget
|
|
9
|
+
Project-URL: Issues, https://github.com/s-maddrellmander/gadget/issues
|
|
10
|
+
Keywords: timing,debugging,profiling,logging
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# gadget
|
|
30
|
+
|
|
31
|
+
[](https://github.com/s-maddrellmander/gadget/actions/workflows/tests.yml)
|
|
32
|
+
|
|
33
|
+
A tiny helper for quick timing/debug prints with file and line context.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
### From a local checkout
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Directly from GitHub
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install "git+https://github.com/s-maddrellmander/gadget.git"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Editable install (for development)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Functional API
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from gadget import gadget, gadget_reset, gadget_config
|
|
61
|
+
|
|
62
|
+
gadget("start")
|
|
63
|
+
# ... some code ...
|
|
64
|
+
gadget("after step")
|
|
65
|
+
|
|
66
|
+
gadget("phase 1", group="build")
|
|
67
|
+
gadget("phase 2", group="build")
|
|
68
|
+
gadget_reset("build")
|
|
69
|
+
|
|
70
|
+
gadget_config(verbose=False) # disable output globally
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Class-based API
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from gadget import Gadget
|
|
77
|
+
|
|
78
|
+
timer = Gadget(verbose=True)
|
|
79
|
+
timer("start")
|
|
80
|
+
# ... some code ...
|
|
81
|
+
timer("step", group="build")
|
|
82
|
+
timer.reset("build")
|
|
83
|
+
timer.reset() # reset all groups
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Install from PyPI
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install gadget-timer
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- Package name on PyPI is `gadget-timer`.
|
|
95
|
+
- Import path is still `gadget`.
|
|
96
|
+
- For Git installs, the repository URL is:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install "git+https://github.com/s-maddrellmander/gadget.git"
|
|
100
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
gadget.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
gadget_timer.egg-info/PKG-INFO
|
|
6
|
+
gadget_timer.egg-info/SOURCES.txt
|
|
7
|
+
gadget_timer.egg-info/dependency_links.txt
|
|
8
|
+
gadget_timer.egg-info/requires.txt
|
|
9
|
+
gadget_timer.egg-info/top_level.txt
|
|
10
|
+
tests/test_gadget.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gadget
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gadget-timer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tiny timing helper for printing elapsed times with call-site context"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Sam Maddrell-Mander" }
|
|
13
|
+
]
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
keywords = ["timing", "debugging", "profiling", "logging"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/s-maddrellmander/gadget"
|
|
31
|
+
Repository = "https://github.com/s-maddrellmander/gadget"
|
|
32
|
+
Issues = "https://github.com/s-maddrellmander/gadget/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"pytest-cov>=5.0"
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
py-modules = ["gadget"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
addopts = "-q --cov=gadget --cov-report=term-missing --cov-fail-under=100"
|
|
46
|
+
|
|
47
|
+
[tool.coverage.run]
|
|
48
|
+
branch = true
|
|
49
|
+
source = ["gadget"]
|
|
50
|
+
|
|
51
|
+
[tool.coverage.report]
|
|
52
|
+
show_missing = true
|
|
53
|
+
fail_under = 100
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import gadget as gadget_module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DummySize:
|
|
7
|
+
columns = 120
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_gadget_call_updates_group_and_prints(monkeypatch, capsys):
|
|
11
|
+
g = gadget_module.Gadget(verbose=True)
|
|
12
|
+
|
|
13
|
+
times = iter([10.0, 10.5, 12.0, 12.2, 13.0, 14.0])
|
|
14
|
+
monkeypatch.setattr(gadget_module.time, "time", lambda: next(times))
|
|
15
|
+
|
|
16
|
+
monkeypatch.setattr(
|
|
17
|
+
gadget_module.inspect,
|
|
18
|
+
"getframeinfo",
|
|
19
|
+
lambda _frame: SimpleNamespace(lineno=2, filename="fake_file.py"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class FakeFile:
|
|
23
|
+
def __enter__(self):
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def __exit__(self, exc_type, exc, tb):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def readlines(self):
|
|
30
|
+
return ["line1\n", "line2\n", "line3\n"]
|
|
31
|
+
|
|
32
|
+
monkeypatch.setattr("builtins.open", lambda *args, **kwargs: FakeFile())
|
|
33
|
+
monkeypatch.setattr(gadget_module.os.path, "relpath", lambda _p: "fake_file.py")
|
|
34
|
+
monkeypatch.setattr(gadget_module.shutil, "get_terminal_size", lambda: DummySize())
|
|
35
|
+
|
|
36
|
+
g("first", group="phase")
|
|
37
|
+
g("second", group="phase")
|
|
38
|
+
g("third", _caller_frame="explicit_frame")
|
|
39
|
+
|
|
40
|
+
out = capsys.readouterr().out
|
|
41
|
+
assert "first" in out
|
|
42
|
+
assert "second" in out
|
|
43
|
+
assert "third" in out
|
|
44
|
+
assert "[phase:" in out
|
|
45
|
+
assert g.group_times["phase"] == 1.5
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_gadget_call_handles_open_and_relpath_errors(monkeypatch, capsys):
|
|
49
|
+
g = gadget_module.Gadget(verbose=True)
|
|
50
|
+
|
|
51
|
+
times = iter([20.0, 20.25])
|
|
52
|
+
monkeypatch.setattr(gadget_module.time, "time", lambda: next(times))
|
|
53
|
+
|
|
54
|
+
monkeypatch.setattr(
|
|
55
|
+
gadget_module.inspect,
|
|
56
|
+
"currentframe",
|
|
57
|
+
lambda: SimpleNamespace(f_back="fake_frame"),
|
|
58
|
+
)
|
|
59
|
+
monkeypatch.setattr(
|
|
60
|
+
gadget_module.inspect,
|
|
61
|
+
"getframeinfo",
|
|
62
|
+
lambda _frame: SimpleNamespace(lineno=1, filename="/tmp/does-not-exist.py"),
|
|
63
|
+
)
|
|
64
|
+
monkeypatch.setattr("builtins.open", lambda *args, **kwargs: (_ for _ in ()).throw(OSError("boom")))
|
|
65
|
+
monkeypatch.setattr(
|
|
66
|
+
gadget_module.os.path,
|
|
67
|
+
"relpath",
|
|
68
|
+
lambda _p: (_ for _ in ()).throw(ValueError("bad path")),
|
|
69
|
+
)
|
|
70
|
+
monkeypatch.setattr(gadget_module.shutil, "get_terminal_size", lambda: DummySize())
|
|
71
|
+
|
|
72
|
+
g("hello")
|
|
73
|
+
out = capsys.readouterr().out
|
|
74
|
+
assert "hello" in out
|
|
75
|
+
assert "does-not-exist.py:1" in out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_gadget_early_return_when_not_verbose(capsys):
|
|
79
|
+
g = gadget_module.Gadget(verbose=False)
|
|
80
|
+
g("silent")
|
|
81
|
+
assert capsys.readouterr().out == ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_reset_branches():
|
|
85
|
+
g = gadget_module.Gadget()
|
|
86
|
+
g.group_times = {"a": 1.0, "b": 2.0}
|
|
87
|
+
|
|
88
|
+
g.reset("a")
|
|
89
|
+
assert g.group_times == {"b": 2.0}
|
|
90
|
+
|
|
91
|
+
g.reset("missing")
|
|
92
|
+
assert g.group_times == {"b": 2.0}
|
|
93
|
+
|
|
94
|
+
g.reset()
|
|
95
|
+
assert g.group_times == {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_convenience_functions_and_config(monkeypatch):
|
|
99
|
+
calls = []
|
|
100
|
+
|
|
101
|
+
class StubDefault:
|
|
102
|
+
def __call__(self, s, group, _caller_frame=None):
|
|
103
|
+
calls.append(("call", s, group, _caller_frame))
|
|
104
|
+
|
|
105
|
+
def reset(self, group=None):
|
|
106
|
+
calls.append(("reset", group))
|
|
107
|
+
|
|
108
|
+
stub = StubDefault()
|
|
109
|
+
monkeypatch.setattr(gadget_module, "_default_gadget", stub)
|
|
110
|
+
monkeypatch.setattr(
|
|
111
|
+
gadget_module.inspect,
|
|
112
|
+
"currentframe",
|
|
113
|
+
lambda: SimpleNamespace(f_back="callsite_frame"),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
gadget_module.gadget("msg", group="grp")
|
|
117
|
+
gadget_module.gadget_reset("grp")
|
|
118
|
+
|
|
119
|
+
assert calls[0] == ("call", "msg", "grp", "callsite_frame")
|
|
120
|
+
assert calls[1] == ("reset", "grp")
|
|
121
|
+
|
|
122
|
+
gadget_module.gadget_config(verbose=False)
|
|
123
|
+
assert isinstance(gadget_module._default_gadget, gadget_module.Gadget)
|
|
124
|
+
assert gadget_module._default_gadget.verbose is False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_version_export():
|
|
128
|
+
assert gadget_module.__version__ == "0.1.0"
|