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.
@@ -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
+ [![Tests](https://github.com/s-maddrellmander/gadget/actions/workflows/tests.yml/badge.svg)](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
+ [![Tests](https://github.com/s-maddrellmander/gadget/actions/workflows/tests.yml/badge.svg)](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
+ [![Tests](https://github.com/s-maddrellmander/gadget/actions/workflows/tests.yml/badge.svg)](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,4 @@
1
+
2
+ [dev]
3
+ pytest>=8.0
4
+ pytest-cov>=5.0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"