cs-trace 20260912__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,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs-trace
3
+ Version: 20260912
4
+ Summary: Utilities for tracing operations.
5
+ Keywords: python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Requires-Dist: cs.fs>=20260610
16
+ Requires-Dist: cs.lex>=20260912
17
+ Requires-Dist: cs.py.stack>=20250724
18
+ Requires-Dist: cs.threads>=20260912
19
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
20
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
21
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
22
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/trace.py
23
+
24
+ Utilities for tracing operations.
25
+
26
+ *Latest release 20260912*:
27
+ Initial PyPI release: Trace class for recording salient decisions and actions for later debugging.
28
+
29
+
30
+
31
+ Short summary:
32
+
33
+
34
+ * `Trace`: A class/decorator to trace control flow and decisions. This makes it possible to record function calls and their inner decision chains, and to show these in a nice printout after the fact.
35
+
36
+ # Classes
37
+
38
+ ## class Trace(cs.threads.HasThreadState)
39
+
40
+ A class/decorator to trace control flow and decisions.
41
+ This makes it possible to record function calls and their
42
+ inner decision chains, and to show these in a nice printout
43
+ after the fact.
44
+
45
+ A new trace object adds itself to the records of the ambient trace object.
46
+
47
+ A trace object supports the context manager protocol, making
48
+ it the ambient object, so that it accrues any new trace objects
49
+ make inside the context.
50
+
51
+ with Trace("name') as T:
52
+ ... add records via T ...
53
+
54
+ Calling a trace object adds a new record to the trace
55
+
56
+ if T("test x==2", x==2):
57
+ T("acting on x==2")
58
+ else:
59
+ T("x != 2")
60
+
61
+ As a trace object:
62
+
63
+ >>> from builtins import print
64
+ >>> with Trace("decide!") as T:
65
+ ... print("start")
66
+ ... if T("test 1 for never", 1==2):
67
+ ... print("never")
68
+ ... elif T("test 2 for always", 1==1):
69
+ ... print("always")
70
+ ... with Trace("inside test 2", T) as T2:
71
+ ... assert T2 in T.tests
72
+ ... if T2("inside1",1==1):
73
+ ... print("true")
74
+ ... else:
75
+ ... print("false")
76
+ ...
77
+ start
78
+ always
79
+ true
80
+ >>> T.printt()
81
+ decide!
82
+ ├─test 1 for never -> bool False
83
+ ├─test 2 for always -> bool True
84
+ ╰─inside test 2
85
+ ╰─inside1 -> bool True
86
+
87
+ As a decorator it calls the function with an additional named
88
+ argument `T` which is the `Trace` instance for that call of
89
+ the function:
90
+
91
+ >>> @Trace
92
+ ... def func(x, T):
93
+ ... x2 = T(f'{x=} + 2', x+2)
94
+ ... return x2
95
+ ...
96
+ >>> with Trace("func trace") as T:
97
+ ... x2 = T("call func with 3", func(3))
98
+ ... print("x2", x2)
99
+ ...
100
+ x2 5
101
+ >>> T.printt() # doctest: +ELLIPSIS
102
+ func trace
103
+ ├─func(....)
104
+ │ │ from <module>() <doctest cs.trace.Trace[4]>:2
105
+ │ │ x2 = T("call func with 3", func(3))
106
+ │ ├─x=3 + 2 -> int 5
107
+ │ ╰─return -> int 5
108
+ ╰─call func with 3 -> int 5
109
+
110
+ ### `Trace.__call__(self, label: str, result='', print=False)`
111
+
112
+ Calling the trace object records `(abel,result)` and
113
+ optionally `print`s.
114
+
115
+ ### `Trace.__firstlineno__`
116
+
117
+ int([x]) -> integer
118
+ int(x, base=10) -> integer
119
+
120
+ Convert a number or string to an integer, or return 0 if no arguments
121
+ are given. If x is a number, return x.__int__(). For floating-point
122
+ numbers, this truncates towards zero.
123
+
124
+ If x is not a number or if base is given, then x must be a string,
125
+ bytes, or bytearray instance representing an integer literal in the
126
+ given base. The literal can be preceded by '+' or '-' and be surrounded
127
+ by whitespace. The base defaults to 10. Valid bases are 0 and 2-36.
128
+ Base 0 means to interpret the base from the string as an integer literal.
129
+ >>> int('0b100', base=0)
130
+ 4
131
+
132
+ ### `Trace.__static_attributes__`
133
+
134
+ Built-in immutable sequence.
135
+
136
+ If no argument is given, the constructor returns an empty tuple.
137
+ If iterable is specified the tuple is initialized from iterable's items.
138
+
139
+ If the argument is a tuple, the return value is the same object.
140
+
141
+ ### `Trace.perthread_state`
142
+
143
+ A `Thread` local object with attributes
144
+ which can be used as a context manager to stack attribute values.
145
+
146
+ Example:
147
+
148
+ from cs.threads import ThreadState
149
+
150
+ S = ThreadState(verbose=False)
151
+
152
+ with S(verbose=True) as prev_attrs:
153
+ if S.verbose:
154
+ print("verbose! (formerly verbose=%s)" % prev_attrs['verbose'])
155
+
156
+ ### `Trace.printt(self, **printt_kw)`
157
+
158
+ Use `cs.lex.printt()` to print this trace object.
159
+ Keyword arguments are passed through.
160
+
161
+ ### `Trace.tabulate(self)`
162
+
163
+ Tabulate this trace object for use with `cs.lex.printt()`.
164
+
165
+ # Release Log
166
+
167
+
168
+
169
+ *Release 20260912*:
170
+ Initial PyPI release: Trace class for recording salient decisions and actions for later debugging.
@@ -0,0 +1,194 @@
1
+ [project]
2
+ name = "cs-trace"
3
+ description = "Utilities for tracing operations."
4
+ authors = [
5
+ { name = "Cameron Simpson", email = "cs@cskk.id.au" },
6
+ ]
7
+ keywords = [
8
+ "python3",
9
+ ]
10
+ dependencies = [
11
+ "cs.fs>=20260610",
12
+ "cs.lex>=20260912",
13
+ "cs.py.stack>=20250724",
14
+ "cs.threads>=20260912",
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3",
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
24
+ ]
25
+ version = "20260912"
26
+
27
+ [project.license]
28
+ text = "GNU General Public License v3 or later (GPLv3+)"
29
+
30
+ [project.urls]
31
+ "Monorepo Hg/Mercurial Mirror" = "https://hg.sr.ht/~cameron-simpson/css"
32
+ "Monorepo Git Mirror" = "https://github.com/cameron-simpson/css"
33
+ "MonoRepo Commits" = "https://bitbucket.org/cameron_simpson/css/commits/branch/main"
34
+ Source = "https://github.com/cameron-simpson/css/blob/main/lib/python/cs/trace.py"
35
+
36
+ [project.readme]
37
+ text = """
38
+ Utilities for tracing operations.
39
+
40
+ *Latest release 20260912*:
41
+ Initial PyPI release: Trace class for recording salient decisions and actions for later debugging.
42
+
43
+
44
+
45
+ Short summary:
46
+
47
+
48
+ * `Trace`: A class/decorator to trace control flow and decisions. This makes it possible to record function calls and their inner decision chains, and to show these in a nice printout after the fact.
49
+
50
+ # Classes
51
+
52
+ ## class Trace(cs.threads.HasThreadState)
53
+
54
+ A class/decorator to trace control flow and decisions.
55
+ This makes it possible to record function calls and their
56
+ inner decision chains, and to show these in a nice printout
57
+ after the fact.
58
+
59
+ A new trace object adds itself to the records of the ambient trace object.
60
+
61
+ A trace object supports the context manager protocol, making
62
+ it the ambient object, so that it accrues any new trace objects
63
+ make inside the context.
64
+
65
+ with Trace(\"name') as T:
66
+ ... add records via T ...
67
+
68
+ Calling a trace object adds a new record to the trace
69
+
70
+ if T(\"test x==2\", x==2):
71
+ T(\"acting on x==2\")
72
+ else:
73
+ T(\"x != 2\")
74
+
75
+ As a trace object:
76
+
77
+ >>> from builtins import print
78
+ >>> with Trace(\"decide!\") as T:
79
+ ... print(\"start\")
80
+ ... if T(\"test 1 for never\", 1==2):
81
+ ... print(\"never\")
82
+ ... elif T(\"test 2 for always\", 1==1):
83
+ ... print(\"always\")
84
+ ... with Trace(\"inside test 2\", T) as T2:
85
+ ... assert T2 in T.tests
86
+ ... if T2(\"inside1\",1==1):
87
+ ... print(\"true\")
88
+ ... else:
89
+ ... print(\"false\")
90
+ ...
91
+ start
92
+ always
93
+ true
94
+ >>> T.printt()
95
+ decide!
96
+ ├─test 1 for never -> bool False
97
+ ├─test 2 for always -> bool True
98
+ ╰─inside test 2
99
+ ╰─inside1 -> bool True
100
+
101
+ As a decorator it calls the function with an additional named
102
+ argument `T` which is the `Trace` instance for that call of
103
+ the function:
104
+
105
+ >>> @Trace
106
+ ... def func(x, T):
107
+ ... x2 = T(f'{x=} + 2', x+2)
108
+ ... return x2
109
+ ...
110
+ >>> with Trace(\"func trace\") as T:
111
+ ... x2 = T(\"call func with 3\", func(3))
112
+ ... print(\"x2\", x2)
113
+ ...
114
+ x2 5
115
+ >>> T.printt() # doctest: +ELLIPSIS
116
+ func trace
117
+ ├─func(....)
118
+ │ │ from <module>() <doctest cs.trace.Trace[4]>:2
119
+ │ │ x2 = T(\"call func with 3\", func(3))
120
+ │ ├─x=3 + 2 -> int 5
121
+ │ ╰─return -> int 5
122
+ ╰─call func with 3 -> int 5
123
+
124
+ ### `Trace.__call__(self, label: str, result='', print=False)`
125
+
126
+ Calling the trace object records `(abel,result)` and
127
+ optionally `print`s.
128
+
129
+ ### `Trace.__firstlineno__`
130
+
131
+ int([x]) -> integer
132
+ int(x, base=10) -> integer
133
+
134
+ Convert a number or string to an integer, or return 0 if no arguments
135
+ are given. If x is a number, return x.__int__(). For floating-point
136
+ numbers, this truncates towards zero.
137
+
138
+ If x is not a number or if base is given, then x must be a string,
139
+ bytes, or bytearray instance representing an integer literal in the
140
+ given base. The literal can be preceded by '+' or '-' and be surrounded
141
+ by whitespace. The base defaults to 10. Valid bases are 0 and 2-36.
142
+ Base 0 means to interpret the base from the string as an integer literal.
143
+ >>> int('0b100', base=0)
144
+ 4
145
+
146
+ ### `Trace.__static_attributes__`
147
+
148
+ Built-in immutable sequence.
149
+
150
+ If no argument is given, the constructor returns an empty tuple.
151
+ If iterable is specified the tuple is initialized from iterable's items.
152
+
153
+ If the argument is a tuple, the return value is the same object.
154
+
155
+ ### `Trace.perthread_state`
156
+
157
+ A `Thread` local object with attributes
158
+ which can be used as a context manager to stack attribute values.
159
+
160
+ Example:
161
+
162
+ from cs.threads import ThreadState
163
+
164
+ S = ThreadState(verbose=False)
165
+
166
+ with S(verbose=True) as prev_attrs:
167
+ if S.verbose:
168
+ print(\"verbose! (formerly verbose=%s)\" % prev_attrs['verbose'])
169
+
170
+ ### `Trace.printt(self, **printt_kw)`
171
+
172
+ Use `cs.lex.printt()` to print this trace object.
173
+ Keyword arguments are passed through.
174
+
175
+ ### `Trace.tabulate(self)`
176
+
177
+ Tabulate this trace object for use with `cs.lex.printt()`.
178
+
179
+ # Release Log
180
+
181
+
182
+
183
+ *Release 20260912*:
184
+ Initial PyPI release: Trace class for recording salient decisions and actions for later debugging."""
185
+ content-type = "text/markdown"
186
+
187
+ [build-system]
188
+ build-backend = "flit_core.buildapi"
189
+ requires = [
190
+ "flit_core >=3.2,<4",
191
+ ]
192
+
193
+ [tool.flit.module]
194
+ name = "cs.trace"
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env python3
2
+
3
+ ''' Utilities for tracing operations.
4
+ '''
5
+
6
+ import builtins
7
+ from os.path import relpath
8
+
9
+ from cs.fs import shortpath
10
+ from cs.lex import printt
11
+ from cs.py.stack import caller
12
+ from cs.threads import HasThreadState, ThreadState
13
+
14
+ __version__ = '20260912'
15
+
16
+ DISTINFO = {
17
+ 'keywords': ["python3"],
18
+ 'classifiers': [
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3",
21
+ ],
22
+ 'install_requires': [
23
+ 'cs.fs',
24
+ 'cs.lex',
25
+ 'cs.py.stack',
26
+ 'cs.threads',
27
+ ],
28
+ }
29
+
30
+ class Trace(HasThreadState):
31
+ ''' A class/decorator to trace control flow and decisions.
32
+ This makes it possible to record function calls and their
33
+ inner decision chains, and to show these in a nice printout
34
+ after the fact.
35
+
36
+ A new trace object adds itself to the records of the ambient trace object.
37
+
38
+ A trace object supports the context manager protocol, making
39
+ it the ambient object, so that it accrues any new trace objects
40
+ make inside the context.
41
+
42
+ with Trace("name') as T:
43
+ ... add records via T ...
44
+
45
+ Calling a trace object adds a new record to the trace
46
+
47
+ if T("test x==2", x==2):
48
+ T("acting on x==2")
49
+ else:
50
+ T("x != 2")
51
+
52
+ As a trace object:
53
+
54
+ >>> from builtins import print
55
+ >>> with Trace("decide!") as T:
56
+ ... print("start")
57
+ ... if T("test 1 for never", 1==2):
58
+ ... print("never")
59
+ ... elif T("test 2 for always", 1==1):
60
+ ... print("always")
61
+ ... with Trace("inside test 2", T) as T2:
62
+ ... assert T2 in T.tests
63
+ ... if T2("inside1",1==1):
64
+ ... print("true")
65
+ ... else:
66
+ ... print("false")
67
+ ...
68
+ start
69
+ always
70
+ true
71
+ >>> T.printt()
72
+ decide!
73
+ ├─test 1 for never -> bool False
74
+ ├─test 2 for always -> bool True
75
+ ╰─inside test 2
76
+ ╰─inside1 -> bool True
77
+
78
+ As a decorator it calls the function with an additional named
79
+ argument `T` which is the `Trace` instance for that call of
80
+ the function:
81
+
82
+ >>> @Trace
83
+ ... def func(x, T):
84
+ ... x2 = T(f'{x=} + 2', x+2)
85
+ ... return x2
86
+ ...
87
+ >>> with Trace("func trace") as T:
88
+ ... x2 = T("call func with 3", func(3))
89
+ ... print("x2", x2)
90
+ ...
91
+ x2 5
92
+ >>> T.printt() # doctest: +ELLIPSIS
93
+ func trace
94
+ ├─func(....)
95
+ │ │ from <module>() <doctest cs.trace.Trace[4]>:2
96
+ │ │ x2 = T("call func with 3", func(3))
97
+ │ ├─x=3 + 2 -> int 5
98
+ │ ╰─return -> int 5
99
+ ╰─call func with 3 -> int 5
100
+
101
+ '''
102
+
103
+ # class attribute holding the per-thread state stack
104
+ perthread_state = ThreadState()
105
+
106
+ def __new__(cls, func, *_, print=False): # noqa: A002
107
+ ''' Intercept object creation for use as a decorator.
108
+ If `func` is a callable, decorate it.
109
+ otherwise fall through to normal class instantiation.
110
+
111
+ The decorated function is passed an additional named `T`
112
+ keyword parameter being the `Trace` instance created for
113
+ the function call and return, ready for additional records.
114
+ '''
115
+ if callable(func):
116
+ # class being used as a decorator
117
+
118
+ def traced_func(*func_a, **func_kw):
119
+ c = caller(-4)
120
+ path = relpath(c.filename)
121
+ if path.startswith('../'):
122
+ path = shortpath(c.filename)
123
+ with cls(f'{func.__name__}(....)'
124
+ f'\n from {c.name}() {path}:{c.lineno}'
125
+ f'\n {c.line}') as T:
126
+ try:
127
+ result = func(*func_a, T=T, **func_kw)
128
+ except Exception as e:
129
+ T('RAISE', e, print=print)
130
+ raise
131
+ else:
132
+ T('return', result, print=print)
133
+ return result
134
+
135
+ return traced_func
136
+ assert print is False
137
+ return super().__new__(cls)
138
+
139
+ def __init__(self, name: str, upT=None):
140
+ self.name = name
141
+ self.tests = []
142
+ if upT is None:
143
+ upT = type(self).default()
144
+ if upT is not None:
145
+ upT.tests.append(self)
146
+
147
+ def tabulate(self):
148
+ ''' Tabulate this trace object for use with `cs.lex.printt()`.
149
+ '''
150
+ table = [self.name]
151
+ subtable = []
152
+ for subtest in self.tests:
153
+ if isinstance(subtest, tuple):
154
+ label, result = subtest
155
+ subtable.append([f' {label}', result])
156
+ else:
157
+ subtable.extend(subtest.tabulate())
158
+ if subtable:
159
+ table.append(tuple(subtable))
160
+ return table
161
+
162
+ def printt(self, **printt_kw):
163
+ ''' Use `cs.lex.printt()` to print this trace object.
164
+ Keyword arguments are passed through.
165
+ '''
166
+ printt(*self.tabulate(), **printt_kw)
167
+
168
+ def __call__(self, label: str, result='', print=False): # noqa: A002
169
+ ''' Calling the trace object records `(abel,result)` and
170
+ optionally `print`s.
171
+ '''
172
+ if print is False:
173
+ print = lambda *_, **__: None
174
+ elif print is True:
175
+ print = builtins.print
176
+ elif not callable(print):
177
+ raise TypeError(
178
+ f'print should be False, True or a print()-compatible callable, got {print=}'
179
+ )
180
+ assert isinstance(label, str)
181
+ print(
182
+ f'{type(self).__name__}: {label}: {type(result).__name__}:{result!r}'
183
+ )
184
+ self.tests.append((f'{label} -> {type(result).__name__}', result))
185
+ return result