Skip to content

Commit 92aae77

Browse files
authored
Merge pull request #62 from MoseleyBioinformaticsLab/granular
Enables use of SubTracker as a function decorator
2 parents 2ce124b + a64d442 commit 92aae77

11 files changed

+96
-51
lines changed

src/gpu_tracker/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111

1212
from .tracker import Tracker
1313
from .sub_tracker import SubTracker
14+
from .sub_tracker import sub_track

src/gpu_tracker/sub_tracker.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@
22
import inspect
33
import os
44
import time
5+
import functools
56
from ._helper_classes import _TrackingFile, _SubTrackerLog
67

78

89
class SubTracker:
910
"""
1011
Context manager that logs to a file for the purposes of sub tracking a code block using the timestamps at which the codeblock begins and ends.
1112
Entering the context manager marks the beginning of the code block and exiting the context manager marks the end of the code block.
12-
At the beginning of the codeblock, the ``SubTracker`` logs a row to a tablular file (".csv" or ".sqlite") that includes the timestamp along with a name for the code block and an indication of whether it is the start or end of the code bock.
13+
At the beginning of the codeblock, the ``SubTracker`` logs a row to a tabular file (".csv" or ".sqlite") that includes the timestamp along with a name for the code block and an indication of whether it is the start or end of the code bock.
1314
This resulting file can be used alongside a tracking file created by a ``Tracker`` object for more granular analysis of specific code blocks.
1415
1516
:ivar str code_block_name: The name of the code block being sub-tracked.
1617
:ivar str sub_tracking_file: The path to the file where the sub-tracking info is logged.
1718
"""
18-
def __init__(self, code_block_name: str | None = None, sub_tracking_file: str | None = None):
19+
def __init__(
20+
self, code_block_name: str | None = None, code_block_attribute: str | None = None, sub_tracking_file: str | None = None):
1921
"""
20-
:param code_block_name: The name of the code block within a ``Tracker`` context that is being sub-tracked. Defaults to the file path and line number where the SubTracker context is started.
22+
:param code_block_name: The name of the code block within a ``Tracker`` context that is being sub-tracked. Defaults to the file path followed by a colon followed by the ``code_block_attribute``.
23+
:param code_block_attribute: Only used if ``code_block_name`` is ``None``. Defaults to the line number where the SubTracker context is started.
2124
:param sub_tracking_file: The path to the file to log the time stamps of the code block being sub-tracked Defaults to the ID of the process where the SubTracker context is created and in CSV format.
2225
"""
2326
if code_block_name is not None:
@@ -26,8 +29,8 @@ def __init__(self, code_block_name: str | None = None, sub_tracking_file: str |
2629
stack = inspect.stack()
2730
caller_frame = stack[1]
2831
file_path = os.path.abspath(caller_frame.filename)
29-
line_number = caller_frame.lineno
30-
self.code_block_name = f'{file_path}:{line_number}'
32+
code_block_attribute = caller_frame.lineno if code_block_attribute is None else code_block_attribute
33+
self.code_block_name = f'{file_path}:{code_block_attribute}'
3134
if sub_tracking_file is None:
3235
sub_tracking_file = f'{os.getpid()}.csv'
3336
self.sub_tracking_file = sub_tracking_file
@@ -44,3 +47,32 @@ def __enter__(self):
4447

4548
def __exit__(self, *_):
4649
self._log(_SubTrackerLog.CodeBlockPosition.END)
50+
51+
52+
def sub_track(code_block_name: str | None = None, code_block_attribute: str | None = None, sub_tracking_file: str | None = None):
53+
"""
54+
Decorator for sub tracking calls to a specified function.
55+
56+
:param code_block_name: The name of the code block within a ``Tracker`` context that is being sub-tracked. Defaults to the file path followed by a colon followed by the ``code_block_attribute``.
57+
:param code_block_attribute: Only used if ``code_block_name`` is ``None``. Defaults to the name of the decorated function i.e. the function being sub-tracked.
58+
:param sub_tracking_file: The path to the file to log the time stamps of the code block being sub-tracked. Defaults to the ID of the process where the SubTracker context is created and in CSV format.
59+
"""
60+
def decorator(func):
61+
nonlocal code_block_name, code_block_attribute, sub_tracking_file
62+
if code_block_name is None:
63+
stack = inspect.stack()
64+
caller_frame = stack[1]
65+
file_path = os.path.abspath(caller_frame.filename)
66+
code_block_attribute = func.__name__ if code_block_attribute is None else code_block_attribute
67+
code_block_name = f'{file_path}:{code_block_attribute}'
68+
69+
@functools.wraps(func)
70+
def wrapper(*args, **kwargs):
71+
nonlocal sub_tracking_file
72+
with SubTracker(
73+
code_block_name=code_block_name, code_block_attribute=code_block_attribute, sub_tracking_file=sub_tracking_file
74+
):
75+
return_value = func(*args, **kwargs)
76+
return return_value
77+
return wrapper
78+
return decorator

tests/data/None_sub-tracking-file.sqlite.csv

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
position,timestamp
2+
START,12
3+
END,13
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ START,6
99
END,7
1010
START,8
1111
END,9
12+
START,10
13+
END,11

tests/data/my-code-block_None.csv

Lines changed: 0 additions & 11 deletions
This file was deleted.

tests/data/my-code-block_sub-tracking-file.csv.csv

Lines changed: 0 additions & 11 deletions
This file was deleted.

tests/data/my-code-block_sub-tracking-file.sqlite.csv

Lines changed: 0 additions & 11 deletions
This file was deleted.

tests/test_sub_tracker.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,53 @@ def code_block_name_test(val: str):
3434
assert val.endswith(default_code_block_end)
3535
else:
3636
assert val == code_block_name
37-
expected_tracking_file = f'tests/data/{code_block_name}_{sub_tracking_file}.csv'
3837
utils.test_tracking_file(
39-
actual_tracking_file=sub_tracker.sub_tracking_file, expected_tracking_file=expected_tracking_file,
38+
actual_tracking_file=sub_tracker.sub_tracking_file, expected_tracking_file='tests/data/sub-tracker.csv',
4039
excluded_col='code_block_name', excluded_col_test=code_block_name_test
4140
)
41+
42+
43+
@pt.fixture(name='code_block_attribute', params=['my-attribute', None])
44+
def get_code_block_attribute(request):
45+
yield request.param
46+
47+
48+
def test_decorator(mocker, code_block_name: str | None, code_block_attribute: str | None):
49+
@gput.sub_track(code_block_name=code_block_name, code_block_attribute=code_block_attribute)
50+
def decorated_function(arg1: int, arg2: int, kwarg1: int = 1, kwarg2: int = 2) -> int:
51+
return arg1 + arg2 - (kwarg1 + kwarg2)
52+
getpid_mock = mocker.patch('gpu_tracker.sub_tracker.os.getpid', return_value=1234)
53+
n_iterations = 3
54+
time_mock = mocker.patch('gpu_tracker.sub_tracker.time', time=mocker.MagicMock(side_effect=range(n_iterations * 2 * 2 + 2)))
55+
for _ in range(n_iterations):
56+
return_val = decorated_function(2, 5)
57+
assert return_val == 4
58+
return_val = decorated_function(3, 2, kwarg1=2, kwarg2=1)
59+
assert return_val == 2
60+
assert len(getpid_mock.call_args_list) == n_iterations * 2
61+
assert len(time_mock.time.call_args_list) == n_iterations * 2 * 2
62+
63+
def code_block_name_test(val):
64+
if code_block_name is None:
65+
if code_block_attribute is None:
66+
assert val.endswith('test_sub_tracker.py:decorated_function')
67+
else:
68+
assert val.endswith('test_sub_tracker.py:my-attribute')
69+
else:
70+
assert val == code_block_name
71+
utils.test_tracking_file(
72+
actual_tracking_file='1234.csv', expected_tracking_file=f'tests/data/decorated-function.csv',
73+
excluded_col='code_block_name', excluded_col_test=code_block_name_test
74+
)
75+
if code_block_name is None and code_block_attribute is None:
76+
return_val = utils.function_in_other_file(1, 2, 3, kw1=4, kw2=5)
77+
assert return_val == ((1, 2, 3), {'kw1': 4, 'kw2': 5})
78+
assert len(getpid_mock.call_args_list) == n_iterations * 2 + 1
79+
assert len(time_mock.time.call_args_list) == n_iterations * 2 * 2 + 2
80+
81+
def code_block_name_test(val):
82+
assert val.endswith('utils.py:function_in_other_file')
83+
utils.test_tracking_file(
84+
actual_tracking_file='1234.csv', expected_tracking_file='tests/data/decorated-function-other-file.csv',
85+
excluded_col='code_block_name', excluded_col_test=code_block_name_test
86+
)

0 commit comments

Comments
 (0)