paramtrack 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramtrack
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Remembers the arguments of each method's last call, defaults included.
5
5
  Author: Alessio Popovic
6
6
  Author-email: Alessio Popovic <alessiopopovic@gmail.com>
@@ -8,10 +8,15 @@ License-Expression: MIT
8
8
  License-File: LICENSE
9
9
  Requires-Python: >=3.9
10
10
  Project-URL: Repository, https://github.com/AlessioPop/paramtrack
11
+ Project-URL: Documentation, https://paramtrack.readthedocs.io
11
12
  Description-Content-Type: text/markdown
12
13
 
13
14
  # paramtrack
14
15
 
16
+ [![PyPI](https://img.shields.io/pypi/v/paramtrack)](https://pypi.org/project/paramtrack/)
17
+ [![Tests](https://github.com/AlessioPop/paramtrack/actions/workflows/tests.yml/badge.svg)](https://github.com/AlessioPop/paramtrack/actions/workflows/tests.yml)
18
+ [![Documentation](https://readthedocs.org/projects/paramtrack/badge/?version=latest)](https://paramtrack.readthedocs.io/en/latest/)
19
+
15
20
  Remembers the arguments of each method's last call, defaults included.
16
21
 
17
22
  ## Install
@@ -74,7 +79,7 @@ This works through chains of `**kwargs` and for calls like `self.method(**kwargs
74
79
 
75
80
  ### Functions
76
81
 
77
- `track_function` does the same for plain functions and stores the arguments as `function.parameters`:
82
+ `track_function` does the same for plain functions and stores the arguments as `function.parameters`. Before the first call, it shows the defaults, with `?` for arguments that have none:
78
83
 
79
84
  ```python
80
85
  from paramtrack import track_function
@@ -83,7 +88,28 @@ from paramtrack import track_function
83
88
  def observe(target, exposure=30, filter="V"):
84
89
  ...
85
90
 
86
- observe("Vega")
87
-
88
91
  print(observe.parameters)
89
92
  ```
93
+
94
+ ```
95
+ observe
96
+ ├─ target = ?
97
+ ├─ exposure = 30
98
+ └─ filter = 'V'
99
+ ```
100
+
101
+ After a call such as `observe("Vega")`, the values of that call are shown instead.
102
+
103
+ ## Development
104
+
105
+ Run the tests:
106
+
107
+ ```bash
108
+ uv run pytest
109
+ ```
110
+
111
+ Build the docs into `docs/_build/html`:
112
+
113
+ ```bash
114
+ uv run --with-requirements docs/requirements.txt sphinx-build docs docs/_build/html
115
+ ```
@@ -1,5 +1,9 @@
1
1
  # paramtrack
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/paramtrack)](https://pypi.org/project/paramtrack/)
4
+ [![Tests](https://github.com/AlessioPop/paramtrack/actions/workflows/tests.yml/badge.svg)](https://github.com/AlessioPop/paramtrack/actions/workflows/tests.yml)
5
+ [![Documentation](https://readthedocs.org/projects/paramtrack/badge/?version=latest)](https://paramtrack.readthedocs.io/en/latest/)
6
+
3
7
  Remembers the arguments of each method's last call, defaults included.
4
8
 
5
9
  ## Install
@@ -62,7 +66,7 @@ This works through chains of `**kwargs` and for calls like `self.method(**kwargs
62
66
 
63
67
  ### Functions
64
68
 
65
- `track_function` does the same for plain functions and stores the arguments as `function.parameters`:
69
+ `track_function` does the same for plain functions and stores the arguments as `function.parameters`. Before the first call, it shows the defaults, with `?` for arguments that have none:
66
70
 
67
71
  ```python
68
72
  from paramtrack import track_function
@@ -71,7 +75,28 @@ from paramtrack import track_function
71
75
  def observe(target, exposure=30, filter="V"):
72
76
  ...
73
77
 
74
- observe("Vega")
75
-
76
78
  print(observe.parameters)
77
79
  ```
80
+
81
+ ```
82
+ observe
83
+ ├─ target = ?
84
+ ├─ exposure = 30
85
+ └─ filter = 'V'
86
+ ```
87
+
88
+ After a call such as `observe("Vega")`, the values of that call are shown instead.
89
+
90
+ ## Development
91
+
92
+ Run the tests:
93
+
94
+ ```bash
95
+ uv run pytest
96
+ ```
97
+
98
+ Build the docs into `docs/_build/html`:
99
+
100
+ ```bash
101
+ uv run --with-requirements docs/requirements.txt sphinx-build docs docs/_build/html
102
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "paramtrack"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Remembers the arguments of each method's last call, defaults included."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -13,6 +13,10 @@ dependencies = []
13
13
 
14
14
  [project.urls]
15
15
  Repository = "https://github.com/AlessioPop/paramtrack"
16
+ Documentation = "https://paramtrack.readthedocs.io"
17
+
18
+ [dependency-groups]
19
+ dev = ["pytest"]
16
20
 
17
21
  [build-system]
18
22
  requires = ["uv_build>=0.11.28,<0.12.0"]
@@ -17,9 +17,29 @@ def tree(arguments):
17
17
  return [f"├─ {line}" for line in lines[:-1]] + [f"└─ {line}" for line in lines[-1:]]
18
18
 
19
19
 
20
+ # Stands in for an argument without a default until a call gives it a value.
21
+ class Unset:
22
+ def __repr__(self):
23
+ return "?"
24
+
25
+
26
+ UNSET = Unset()
27
+
28
+
20
29
  class Parameters(dict):
30
+ def __init__(self):
31
+ super().__init__()
32
+ self.uncalled = {} # functions not called yet, as name -> function giving their defaults
33
+
34
+ # Recompute the entries of functions not called yet: what their **kwargs are passed on to
35
+ # may have been defined after them.
36
+ def refresh(self):
37
+ for name, defaults in self.uncalled.items():
38
+ self[name] = defaults()
39
+
21
40
  # Text version, used by print() and the terminal.
22
41
  def __repr__(self):
42
+ self.refresh()
23
43
  blocks = []
24
44
  for name, arguments in self.items():
25
45
  blocks.append("\n".join([f"\033[1m{name}\033[0m", *tree(arguments)])) # \033[1m = bold
@@ -27,6 +47,7 @@ class Parameters(dict):
27
47
 
28
48
  # Clickable version, used by Jupyter; click a method name to unfold it.
29
49
  def _repr_html_(self):
50
+ self.refresh()
30
51
  html = ""
31
52
  for name, arguments in self.items():
32
53
  lines = escape("\n".join(tree(arguments)))
@@ -64,7 +85,8 @@ def resolve(node, namespace):
64
85
  raise LookupError(ast.dump(node))
65
86
 
66
87
 
67
- # The parameters that func's **kwargs end up in: the passed value where given, the default otherwise.
88
+ # The parameters that func's **kwargs end up in: the passed value where given, the default otherwise,
89
+ # and UNSET for those without one.
68
90
  # local holds func's own arguments so that self.method(**kwargs) resolves. Follows chains of **kwargs.
69
91
  def forwarded(func, local, kwargs, seen=()):
70
92
  seen = (*seen, func)
@@ -84,8 +106,8 @@ def forwarded(func, local, kwargs, seen=()):
84
106
  continue
85
107
  if p.name in rest:
86
108
  result.setdefault(p.name, rest.pop(p.name))
87
- elif p.default is not p.empty:
88
- result.setdefault(p.name, p.default)
109
+ else:
110
+ result.setdefault(p.name, UNSET if p.default is p.empty else p.default)
89
111
  inner = inspect.unwrap(getattr(callee, "__func__", callee))
90
112
  if parameters and parameters[-1].kind is VAR_KEYWORD and inspect.isfunction(inner) and inner not in seen:
91
113
  inner_local = {next(iter(inspect.signature(inner).parameters)): callee.__self__} if inspect.ismethod(callee) else {}
@@ -96,6 +118,19 @@ def forwarded(func, local, kwargs, seen=()):
96
118
  return result
97
119
 
98
120
 
121
+ # What is stored for one call: {name: repr(value)}, without self.
122
+ def describe(function, signature, arguments):
123
+ result = {}
124
+ for name, value in arguments.items():
125
+ # Instead of one dict, list what **kwargs is passed on to, defaults included.
126
+ if signature.parameters[name].kind is VAR_KEYWORD:
127
+ for key, val in forwarded(inspect.unwrap(function), arguments, value).items():
128
+ result.setdefault(key, val)
129
+ elif name != "self":
130
+ result[name] = value
131
+ return {k: repr(v) for k, v in result.items()}
132
+
133
+
99
134
  def track(function, parameters):
100
135
  signature = inspect.signature(function)
101
136
 
@@ -103,21 +138,41 @@ def track(function, parameters):
103
138
  def wrapper(*args, **kwargs):
104
139
  bound = signature.bind(*args, **kwargs)
105
140
  bound.apply_defaults()
106
- arguments = {}
107
- for name, value in bound.arguments.items():
108
- # Instead of one dict, list what **kwargs is passed on to, defaults included.
109
- if signature.parameters[name].kind is VAR_KEYWORD:
110
- for key, val in forwarded(inspect.unwrap(function), bound.arguments, value).items():
111
- arguments.setdefault(key, val)
112
- elif name != "self":
113
- arguments[name] = value
114
- parameters[function.__name__] = {k: repr(v) for k, v in arguments.items()}
141
+ parameters.uncalled.pop(function.__name__, None)
142
+ parameters[function.__name__] = describe(function, signature, bound.arguments)
115
143
  return function(*args, **kwargs)
116
144
  return wrapper
117
145
 
118
146
 
119
- # Class decorator: track every method and store the arguments as cls.parameters.
120
147
  def track_class(cls):
148
+ """Track the arguments of every method of a class.
149
+
150
+ Each call stores the method's arguments, defaults included, in ``cls.parameters``
151
+ under the method's name, replacing those of the previous call. Keywords that a
152
+ method passes on with ``**kwargs`` are listed together with the parameters and
153
+ defaults of the function receiving them.
154
+
155
+ Parameters
156
+ ----------
157
+ cls : type
158
+ The class to track.
159
+
160
+ Returns
161
+ -------
162
+ type
163
+ The same class, with its methods wrapped and a ``parameters`` attribute added.
164
+
165
+ Examples
166
+ --------
167
+ >>> @track_class
168
+ ... class Telescope:
169
+ ... def observe(self, target, exposure=30):
170
+ ... ...
171
+ >>> telescope = Telescope()
172
+ >>> telescope.observe("Vega")
173
+ >>> telescope.parameters["observe"]
174
+ {'target': "'Vega'", 'exposure': '30'}
175
+ """
121
176
  cls.parameters = Parameters()
122
177
  for name, method in list(vars(cls).items()):
123
178
  if inspect.isfunction(method):
@@ -125,9 +180,43 @@ def track_class(cls):
125
180
  return cls
126
181
 
127
182
 
128
- # Function decorator: track the function and store its arguments as function.parameters.
129
183
  def track_function(function):
184
+ """Track the arguments of a function.
185
+
186
+ Each call stores the arguments, defaults included, in ``function.parameters``,
187
+ replacing those of the previous call. Before the first call, ``function.parameters``
188
+ shows the defaults, with ``?`` for arguments that have none. Keywords passed on with
189
+ ``**kwargs`` are listed as in :func:`track_class`.
190
+
191
+ Parameters
192
+ ----------
193
+ function : callable
194
+ The function to track.
195
+
196
+ Returns
197
+ -------
198
+ callable
199
+ A wrapper around the function with a ``parameters`` attribute.
200
+
201
+ Examples
202
+ --------
203
+ >>> @track_function
204
+ ... def observe(target, exposure=30):
205
+ ... ...
206
+ >>> observe.parameters["observe"]
207
+ {'target': '?', 'exposure': '30'}
208
+ >>> observe("Vega")
209
+ >>> observe.parameters["observe"]
210
+ {'target': "'Vega'", 'exposure': '30'}
211
+ """
130
212
  parameters = Parameters()
131
213
  wrapper = track(function, parameters)
132
214
  wrapper.parameters = parameters
215
+ # Before the first call: the defaults, and UNSET for arguments without one.
216
+ signature = inspect.signature(function)
217
+ bound = signature.bind_partial()
218
+ bound.apply_defaults()
219
+ arguments = {name: bound.arguments.get(name, UNSET) for name in signature.parameters}
220
+ parameters.uncalled[function.__name__] = lambda: describe(function, signature, arguments)
221
+ parameters.refresh()
133
222
  return wrapper
File without changes