wrapture 1.0.0.dev2__tar.gz → 1.0.0.dev3__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.
Files changed (64) hide show
  1. {wrapture-1.0.0.dev2/src/wrapture.egg-info → wrapture-1.0.0.dev3}/PKG-INFO +20 -22
  2. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/README.md +17 -19
  3. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/pyproject.toml +15 -3
  4. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/__init__.py +76 -2
  5. wrapture-1.0.0.dev3/src/wrapture/__main__.py +148 -0
  6. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/attributes.py +53 -9
  7. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/bindings.py +404 -58
  8. wrapture-1.0.0.dev3/src/wrapture/bootstrap.py +86 -0
  9. wrapture-1.0.0.dev3/src/wrapture/config.py +1113 -0
  10. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/events.py +49 -11
  11. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/exceptions.py +18 -0
  12. wrapture-1.0.0.dev3/src/wrapture/export.py +298 -0
  13. wrapture-1.0.0.dev3/src/wrapture/observed.py +422 -0
  14. wrapture-1.0.0.dev3/src/wrapture/sinks.py +1006 -0
  15. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/stacks.py +49 -7
  16. wrapture-1.0.0.dev3/src/wrapture/timeline.py +566 -0
  17. wrapture-1.0.0.dev3/src/wrapture/tools/__init__.py +8 -0
  18. wrapture-1.0.0.dev3/src/wrapture/tools/__main__.py +51 -0
  19. wrapture-1.0.0.dev3/src/wrapture/tools/convert.py +150 -0
  20. wrapture-1.0.0.dev3/src/wrapture/wsgi.py +633 -0
  21. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3/src/wrapture.egg-info}/PKG-INFO +20 -22
  22. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/SOURCES.txt +23 -1
  23. wrapture-1.0.0.dev3/src/wrapture.egg-info/entry_points.txt +2 -0
  24. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_attribute_events.py +14 -2
  25. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_bindings.py +173 -1
  26. wrapture-1.0.0.dev3/tests/test_bootstrap.py +147 -0
  27. wrapture-1.0.0.dev3/tests/test_config.py +1084 -0
  28. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_events.py +8 -9
  29. wrapture-1.0.0.dev3/tests/test_export.py +253 -0
  30. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_generator_events.py +9 -4
  31. wrapture-1.0.0.dev3/tests/test_jsonlines.py +307 -0
  32. wrapture-1.0.0.dev3/tests/test_observed.py +264 -0
  33. wrapture-1.0.0.dev3/tests/test_predicates.py +231 -0
  34. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_recording.py +44 -7
  35. wrapture-1.0.0.dev3/tests/test_runner.py +213 -0
  36. wrapture-1.0.0.dev3/tests/test_sink_library.py +360 -0
  37. wrapture-1.0.0.dev3/tests/test_sinks.py +454 -0
  38. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_stacks.py +56 -0
  39. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_threads.py +67 -2
  40. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_timeline.py +154 -31
  41. wrapture-1.0.0.dev3/tests/test_tools.py +235 -0
  42. wrapture-1.0.0.dev3/tests/test_wsgi.py +668 -0
  43. wrapture-1.0.0.dev2/src/wrapture/timeline.py +0 -364
  44. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/LICENSE +0 -0
  45. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/setup.cfg +0 -0
  46. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/behaviours.py +0 -0
  47. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/capture.py +0 -0
  48. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/eventlogs.py +0 -0
  49. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/iterators.py +0 -0
  50. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/py.typed +0 -0
  51. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture/pytest_plugin.py +0 -0
  52. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/dependency_links.txt +0 -0
  53. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/not-zip-safe +0 -0
  54. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/requires.txt +0 -0
  55. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/src/wrapture.egg-info/top_level.txt +0 -0
  56. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_attributes.py +0 -0
  57. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_behaviours.py +0 -0
  58. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_capture.py +0 -0
  59. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_eventlogs.py +0 -0
  60. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_iterators.py +0 -0
  61. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_pytest_plugin.py +0 -0
  62. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_targets.py +0 -0
  63. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_testing_patterns.py +0 -0
  64. {wrapture-1.0.0.dev2 → wrapture-1.0.0.dev3}/tests/test_version.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wrapture
3
- Version: 1.0.0.dev2
4
- Summary: Library for attaching bindings to arbitrary Python call sites for monkey patching, testing, tracing and profiling.
3
+ Version: 1.0.0.dev3
4
+ Summary: Monkey patch, test, and trace Python by attaching bindings to call sites, without modifying the code being observed. Built on wrapt.
5
5
  Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
6
6
  License-Expression: BSD-2-Clause
7
7
  Project-URL: Homepage, https://github.com/GrahamDumpleton/wrapture
8
8
  Project-URL: Documentation, https://wrapture.readthedocs.io
9
9
  Project-URL: Bug Tracker, https://github.com/GrahamDumpleton/wrapture/issues/
10
- Keywords: wrapper,monkey patching,tracing,testing,profiling
10
+ Keywords: wrapper,monkey patching,tracing,testing
11
11
  Classifier: Development Status :: 2 - Pre-Alpha
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.12
@@ -44,10 +44,10 @@ It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
44
44
  and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
45
45
  safe monkey-patching machinery wrapt provides.
46
46
 
47
- > **Status: early development.** The monkey patching and unit testing layers
48
- > are implemented; the tracing and profiling layers are designed but not
49
- > built. Development previews are published to PyPI; the API may still
50
- > shift before 1.0.0.
47
+ > **Status: early development.** The monkey patching, unit testing and
48
+ > ad-hoc tracing layers are implemented, including WSGI request tracing.
49
+ > Development previews are published to PyPI; the API may still shift
50
+ > before 1.0.0.
51
51
 
52
52
  ## Installation
53
53
 
@@ -98,7 +98,7 @@ failure, or transform one argument while the real code keeps running.
98
98
 
99
99
  ## What it does
100
100
 
101
- One mechanism, four uses, in increasing order of machinery:
101
+ One mechanism, three uses, in increasing order of machinery:
102
102
 
103
103
  1. **Monkey patching.** A clean lifecycle and behaviour vocabulary over
104
104
  wrapt's `wrap_object()`. Point at a method by name and stub it, fail it,
@@ -119,16 +119,17 @@ One mechanism, four uses, in increasing order of machinery:
119
119
  3. **Ad-hoc tracing.** Attach bindings to a running application, including
120
120
  one you cannot modify or redeploy, and emit a structured, nested trace to
121
121
  process or chart elsewhere. Name a handful of methods and a call tree
122
- appears; no code changes required.
123
-
124
- 4. **Targeted profiling.** Use a binding as a *scope* within which CPython's
125
- own profiling machinery is active, so you can profile one subsystem of a
126
- live process instead of everything.
127
-
128
- The distinction that matters: most tracing and profiling tools either need
129
- the code to have been written with them in mind, or can only be switched on
130
- for the whole program at once. wrapture needs neither: you point at a method
131
- by name and a trace appears.
122
+ appears; no code changes required: with a `wrapture.toml` naming the
123
+ methods and a sink, `python -m wrapture manage.py runserver` traces the
124
+ application untouched. With [autowrapt](https://github.com/GrahamDumpleton/autowrapt)
125
+ installed, not even the launcher is needed: `AUTOWRAPT_BOOTSTRAP=wrapture`
126
+ in the environment applies the same config at interpreter startup, so the
127
+ program starts with plain `python`.
128
+
129
+ The distinction that matters: most tracing tools either need the code to
130
+ have been written with them in mind, or can only be switched on for the
131
+ whole program at once. wrapture needs neither: you point at a method by
132
+ name and a trace appears.
132
133
 
133
134
  ## Why
134
135
 
@@ -142,8 +143,7 @@ nested trace, assert on it or export it, in tests or in production":
142
143
  `InMemorySpanExporter`) require the code to already be instrumented.
143
144
  - `sys.settrace` tools (`hunter`, `snoop`) give a firehose with no assertion
144
145
  API.
145
- - `cProfile` cannot scope to a subsystem in a live process, and APM agents
146
- are all-or-nothing products rather than a toolkit.
146
+ - APM agents are all-or-nothing products rather than a toolkit.
147
147
 
148
148
  wrapture fills that gap: a targeted call tree with normalized arguments and
149
149
  return values, produced by naming the methods you care about, usable as a
@@ -153,8 +153,6 @@ testing assertion library, a tracing tool, or both at once.
153
153
 
154
154
  - **Not a replacement for `unittest.mock`.** It complements mocking where
155
155
  code has seams; it exists for the code that doesn't.
156
- - **Not a sampling profiler.** `py-spy` and `austin` do that better and
157
- without distortion.
158
156
  - **Not a production APM.** It is a toolkit that APM-like things could be
159
157
  built on.
160
158
  - **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
@@ -13,10 +13,10 @@ It is a sibling project to [wrapt](https://github.com/GrahamDumpleton/wrapt)
13
13
  and [autowrapt](https://github.com/GrahamDumpleton/autowrapt), building on the
14
14
  safe monkey-patching machinery wrapt provides.
15
15
 
16
- > **Status: early development.** The monkey patching and unit testing layers
17
- > are implemented; the tracing and profiling layers are designed but not
18
- > built. Development previews are published to PyPI; the API may still
19
- > shift before 1.0.0.
16
+ > **Status: early development.** The monkey patching, unit testing and
17
+ > ad-hoc tracing layers are implemented, including WSGI request tracing.
18
+ > Development previews are published to PyPI; the API may still shift
19
+ > before 1.0.0.
20
20
 
21
21
  ## Installation
22
22
 
@@ -67,7 +67,7 @@ failure, or transform one argument while the real code keeps running.
67
67
 
68
68
  ## What it does
69
69
 
70
- One mechanism, four uses, in increasing order of machinery:
70
+ One mechanism, three uses, in increasing order of machinery:
71
71
 
72
72
  1. **Monkey patching.** A clean lifecycle and behaviour vocabulary over
73
73
  wrapt's `wrap_object()`. Point at a method by name and stub it, fail it,
@@ -88,16 +88,17 @@ One mechanism, four uses, in increasing order of machinery:
88
88
  3. **Ad-hoc tracing.** Attach bindings to a running application, including
89
89
  one you cannot modify or redeploy, and emit a structured, nested trace to
90
90
  process or chart elsewhere. Name a handful of methods and a call tree
91
- appears; no code changes required.
92
-
93
- 4. **Targeted profiling.** Use a binding as a *scope* within which CPython's
94
- own profiling machinery is active, so you can profile one subsystem of a
95
- live process instead of everything.
96
-
97
- The distinction that matters: most tracing and profiling tools either need
98
- the code to have been written with them in mind, or can only be switched on
99
- for the whole program at once. wrapture needs neither: you point at a method
100
- by name and a trace appears.
91
+ appears; no code changes required: with a `wrapture.toml` naming the
92
+ methods and a sink, `python -m wrapture manage.py runserver` traces the
93
+ application untouched. With [autowrapt](https://github.com/GrahamDumpleton/autowrapt)
94
+ installed, not even the launcher is needed: `AUTOWRAPT_BOOTSTRAP=wrapture`
95
+ in the environment applies the same config at interpreter startup, so the
96
+ program starts with plain `python`.
97
+
98
+ The distinction that matters: most tracing tools either need the code to
99
+ have been written with them in mind, or can only be switched on for the
100
+ whole program at once. wrapture needs neither: you point at a method by
101
+ name and a trace appears.
101
102
 
102
103
  ## Why
103
104
 
@@ -111,8 +112,7 @@ nested trace, assert on it or export it, in tests or in production":
111
112
  `InMemorySpanExporter`) require the code to already be instrumented.
112
113
  - `sys.settrace` tools (`hunter`, `snoop`) give a firehose with no assertion
113
114
  API.
114
- - `cProfile` cannot scope to a subsystem in a live process, and APM agents
115
- are all-or-nothing products rather than a toolkit.
115
+ - APM agents are all-or-nothing products rather than a toolkit.
116
116
 
117
117
  wrapture fills that gap: a targeted call tree with normalized arguments and
118
118
  return values, produced by naming the methods you care about, usable as a
@@ -122,8 +122,6 @@ testing assertion library, a tracing tool, or both at once.
122
122
 
123
123
  - **Not a replacement for `unittest.mock`.** It complements mocking where
124
124
  code has seams; it exists for the code that doesn't.
125
- - **Not a sampling profiler.** `py-spy` and `austin` do that better and
126
- without distortion.
127
125
  - **Not a production APM.** It is a toolkit that APM-like things could be
128
126
  built on.
129
127
  - **Not an OpenTelemetry competitor.** It should emit to OTel, not replace
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  [project]
8
8
  name = "wrapture"
9
9
  dynamic = ["version"]
10
- description = "Library for attaching bindings to arbitrary Python call sites for monkey patching, testing, tracing and profiling."
10
+ description = "Monkey patch, test, and trace Python by attaching bindings to call sites, without modifying the code being observed. Built on wrapt."
11
11
  readme = "README.md"
12
12
  license = "BSD-2-Clause"
13
13
  license-files = ["LICENSE"]
@@ -15,7 +15,7 @@ authors = [
15
15
  {name = "Graham Dumpleton", email = "Graham.Dumpleton@gmail.com"}
16
16
  ]
17
17
  requires-python = ">=3.12"
18
- keywords = ["wrapper", "monkey patching", "tracing", "testing", "profiling"]
18
+ keywords = ["wrapper", "monkey patching", "tracing", "testing"]
19
19
  classifiers = [
20
20
  "Development Status :: 2 - Pre-Alpha",
21
21
  "Programming Language :: Python :: 3",
@@ -33,6 +33,18 @@ Homepage = "https://github.com/GrahamDumpleton/wrapture"
33
33
  Documentation = "https://wrapture.readthedocs.io"
34
34
  "Bug Tracker" = "https://github.com/GrahamDumpleton/wrapture/issues/"
35
35
 
36
+ # The zero-code injection trigger, in the shape autowrapt consumes:
37
+ # AUTOWRAPT_BOOTSTRAP=wrapture names this entry point group, which
38
+ # autowrapt hands to wrapt.discover_post_import_hooks() at interpreter
39
+ # startup. Each entry maps a trigger module to a callback; hooking os,
40
+ # which is always already imported by then, makes the callback fire
41
+ # immediately at startup. Inert metadata unless autowrapt is installed
42
+ # and the environment variable is set; wrapture itself has no
43
+ # dependency on autowrapt and never imports it.
44
+
45
+ [project.entry-points.wrapture]
46
+ os = "wrapture.bootstrap:bootstrap"
47
+
36
48
  [project.optional-dependencies]
37
49
  dev = [
38
50
  "mypy",
@@ -49,7 +61,7 @@ docs = [
49
61
  [tool.setuptools]
50
62
  package-dir = {"" = "src"}
51
63
  zip-safe = false
52
- packages = ["wrapture"]
64
+ packages = ["wrapture", "wrapture.tools"]
53
65
 
54
66
  [tool.setuptools.package-data]
55
67
  wrapture = ["py.typed"]
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Wrapture is a library for attaching bindings to arbitrary Python call sites,
3
3
  without modifying the code being observed, for use in monkey patching,
4
- testing, tracing and profiling.
4
+ testing and tracing.
5
5
  """
6
6
 
7
7
 
@@ -17,7 +17,7 @@ def _format_version(parts: tuple[str, ...]) -> str:
17
17
  )
18
18
 
19
19
 
20
- __version_info__ = ("1", "0", "0", "dev2")
20
+ __version_info__ = ("1", "0", "0", "dev3")
21
21
  __version__ = _format_version(__version_info__)
22
22
 
23
23
  from wrapt import MISSING
@@ -33,10 +33,19 @@ from .bindings import (
33
33
  BindingGroup,
34
34
  binding,
35
35
  bindings,
36
+ discover,
36
37
  )
37
38
  from .capture import (
38
39
  redact,
39
40
  )
41
+ from .config import (
42
+ AppliedConfig,
43
+ Config,
44
+ ObserveEntry,
45
+ SetupEntry,
46
+ find_config,
47
+ load_config,
48
+ )
40
49
  from .eventlogs import (
41
50
  EventLog,
42
51
  )
@@ -45,13 +54,22 @@ from .events import (
45
54
  )
46
55
  from .exceptions import (
47
56
  AlreadyAppliedError,
57
+ ConfigError,
58
+ ConfigWarning,
48
59
  DeferredTargetError,
49
60
  ExpectationNotMetError,
50
61
  NeverAppliedError,
51
62
  NotImplementedYetError,
52
63
  RecordingGapWarning,
64
+ SinkErrorWarning,
53
65
  WrongModeError,
54
66
  )
67
+ from .export import (
68
+ canonical,
69
+ chrome_trace,
70
+ load_events,
71
+ mermaid,
72
+ )
55
73
  from .iterators import (
56
74
  IteratorAbandonBehaviour,
57
75
  IteratorErrorBehaviour,
@@ -60,8 +78,28 @@ from .iterators import (
60
78
  IteratorProxy,
61
79
  iterator,
62
80
  )
81
+ from .observed import (
82
+ ObservedCallable,
83
+ observed,
84
+ )
85
+ from .sinks import (
86
+ Aggregate,
87
+ Counter,
88
+ Depth,
89
+ Fanout,
90
+ Filter,
91
+ JSONLines,
92
+ PathStats,
93
+ Printer,
94
+ Sample,
95
+ Sink,
96
+ add_sink,
97
+ flush_sinks,
98
+ remove_sink,
99
+ )
63
100
  from .stacks import (
64
101
  StackFrame,
102
+ clear_stacks,
65
103
  stack_frames,
66
104
  )
67
105
  from .timeline import (
@@ -69,40 +107,76 @@ from .timeline import (
69
107
  Timeline,
70
108
  annotate,
71
109
  current_event,
110
+ propagate,
72
111
  timeline,
73
112
  )
113
+ from .wsgi import (
114
+ WSGIMiddleware,
115
+ )
74
116
 
75
117
  __all__ = [
76
118
  "MISSING",
119
+ "Aggregate",
77
120
  "AlreadyAppliedError",
121
+ "AppliedConfig",
78
122
  "Binding",
79
123
  "BindingGroup",
80
124
  "CallBehaviour",
125
+ "Config",
126
+ "ConfigError",
127
+ "ConfigWarning",
128
+ "Counter",
81
129
  "DeferredTargetError",
82
130
  "DeleteBehaviour",
131
+ "Depth",
83
132
  "Event",
84
133
  "EventLog",
85
134
  "ExpectationNotMetError",
135
+ "Fanout",
136
+ "Filter",
86
137
  "GetBehaviour",
87
138
  "IteratorAbandonBehaviour",
88
139
  "IteratorErrorBehaviour",
89
140
  "IteratorFinishBehaviour",
90
141
  "IteratorItemBehaviour",
91
142
  "IteratorProxy",
143
+ "JSONLines",
92
144
  "NeverAppliedError",
93
145
  "NotImplementedYetError",
146
+ "ObserveEntry",
147
+ "ObservedCallable",
148
+ "PathStats",
149
+ "Printer",
94
150
  "RecordingGapWarning",
151
+ "Sample",
95
152
  "SetBehaviour",
153
+ "SetupEntry",
154
+ "Sink",
155
+ "SinkErrorWarning",
96
156
  "StackFrame",
97
157
  "Tape",
98
158
  "Timeline",
159
+ "WSGIMiddleware",
99
160
  "WrongModeError",
161
+ "add_sink",
100
162
  "annotate",
101
163
  "binding",
102
164
  "bindings",
165
+ "canonical",
166
+ "chrome_trace",
167
+ "clear_stacks",
103
168
  "current_event",
169
+ "discover",
170
+ "find_config",
171
+ "flush_sinks",
104
172
  "iterator",
173
+ "load_config",
174
+ "load_events",
175
+ "mermaid",
176
+ "observed",
177
+ "propagate",
105
178
  "redact",
179
+ "remove_sink",
106
180
  "stack_frames",
107
181
  "timeline",
108
182
  ]
@@ -0,0 +1,148 @@
1
+ """Run a Python program with a wrapture config applied first.
2
+
3
+ python -m wrapture [--config PATH] (-m MODULE | SCRIPT) [ARGS...]
4
+
5
+ The config is resolved, loaded and applied before the target runs, so
6
+ patches land before the target module imports anything: the same
7
+ ordering guarantee the injection path gives. The target then runs as
8
+ __main__ with sys.argv rebuilt to the target and its arguments,
9
+ exactly as python itself would have run it.
10
+
11
+ This module is the runner's entry point only, the same private -m
12
+ convention as pdb, cProfile and coverage: run it, do not import it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import runpy
19
+ import sys
20
+ from collections.abc import Sequence
21
+ from dataclasses import dataclass
22
+ from typing import NoReturn
23
+
24
+ from .config import find_config, load_config
25
+ from .exceptions import ConfigError
26
+
27
+ _USAGE = """\
28
+ usage: python -m wrapture [--config PATH] (-m MODULE | SCRIPT) [ARGS...]
29
+
30
+ Apply a wrapture config, then run the target as __main__ with ARGS as
31
+ its arguments. With --config the named TOML file is used; without it
32
+ the standard precedence chain locates one: a path in WRAPTURE_CONFIG,
33
+ wrapture.toml in the current directory, then a [tool.wrapture] table
34
+ in pyproject.toml. Everything after the target belongs to the target,
35
+ so wrapture's own options must come before it.
36
+
37
+ Trace files a run produces can be rendered in other formats with
38
+ python -m wrapture.tools convert.
39
+ """
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class _Invocation:
44
+ config: str | None
45
+ module: str | None
46
+ script: str | None
47
+ arguments: tuple[str, ...]
48
+
49
+
50
+ def _usage_error(message: str) -> NoReturn:
51
+ print(f"wrapture: {message}", file=sys.stderr)
52
+ print(_USAGE, file=sys.stderr, end="")
53
+ raise SystemExit(2)
54
+
55
+
56
+ def _parse(argv: Sequence[str]) -> _Invocation:
57
+ """Split the command line into wrapture's own options and the
58
+ target with its arguments.
59
+
60
+ Parsing stops at the first target: everything after -m MODULE or
61
+ the script path is the target's own, however option-like it
62
+ looks, matching how python itself treats a command line.
63
+ """
64
+
65
+ config: str | None = None
66
+ pending = list(argv)
67
+
68
+ while pending:
69
+ argument = pending[0]
70
+
71
+ if argument in ("-h", "--help"):
72
+ print(_USAGE, end="")
73
+ raise SystemExit(0)
74
+
75
+ if argument == "--config":
76
+ if len(pending) < 2:
77
+ _usage_error("--config requires a path")
78
+ config = pending[1]
79
+ del pending[:2]
80
+ continue
81
+
82
+ if argument.startswith("--config="):
83
+ config = argument.partition("=")[2]
84
+ if not config:
85
+ _usage_error("--config requires a path")
86
+ del pending[0]
87
+ continue
88
+
89
+ if argument == "-m":
90
+ if len(pending) < 2:
91
+ _usage_error("-m requires a module name")
92
+ return _Invocation(config, pending[1], None, tuple(pending[2:]))
93
+
94
+ if argument.startswith("-"):
95
+ _usage_error(f"unknown option {argument!r}")
96
+
97
+ return _Invocation(config, None, argument, tuple(pending[1:]))
98
+
99
+ _usage_error("a target is required: -m MODULE or a script path")
100
+
101
+
102
+ def main(argv: Sequence[str] | None = None) -> None:
103
+ """Parse the command line, apply the config, and run the target."""
104
+
105
+ invocation = _parse(sys.argv[1:] if argv is None else argv)
106
+
107
+ # Resolve and apply the config before anything else happens, so
108
+ # patches and setup hooks are in place before the target module
109
+ # imports anything. Finding no config is an error rather than a
110
+ # silent untraced run.
111
+
112
+ source = invocation.config if invocation.config is not None else find_config()
113
+
114
+ if source is None:
115
+ print(
116
+ "wrapture: no config found: pass --config PATH, set"
117
+ " WRAPTURE_CONFIG, or provide wrapture.toml or a"
118
+ " [tool.wrapture] table in pyproject.toml in the current"
119
+ " directory",
120
+ file=sys.stderr,
121
+ )
122
+ raise SystemExit(1)
123
+
124
+ try:
125
+ load_config(source).apply()
126
+ except ConfigError as exc:
127
+ print(f"wrapture: {exc}", file=sys.stderr)
128
+ raise SystemExit(1) from None
129
+
130
+ # Hand over to the target as python itself would have run it. For
131
+ # a module target, alter_sys replaces sys.argv[0] with the
132
+ # module's own file, matching python -m; for a script, its
133
+ # directory joins the front of sys.path, matching python script.py.
134
+
135
+ if invocation.module is not None:
136
+ sys.argv[:] = [invocation.module, *invocation.arguments]
137
+ runpy.run_module(invocation.module, run_name="__main__", alter_sys=True)
138
+ else:
139
+ script = invocation.script
140
+ assert script is not None
141
+
142
+ sys.argv[:] = [script, *invocation.arguments]
143
+ sys.path.insert(0, os.path.dirname(os.path.abspath(script)))
144
+ runpy.run_path(script, run_name="__main__")
145
+
146
+
147
+ if __name__ == "__main__":
148
+ main()
@@ -22,22 +22,29 @@ from __future__ import annotations
22
22
 
23
23
  import inspect
24
24
  import sys
25
+ import time
25
26
  from collections.abc import Callable
26
27
  from typing import TYPE_CHECKING, Any
27
28
 
28
29
  import wrapt
29
30
  from wrapt import MISSING, BaseObjectProxy, apply_patch
30
31
 
31
- from .capture import REFERENCE, _capture_value, _level_of
32
+ from .capture import _capture_value, _level_of
32
33
  from .events import Event, EventKind
33
34
  from .exceptions import NotImplementedYetError
35
+ from .sinks import (
36
+ _active_sinks,
37
+ _in_recorder,
38
+ _notify_error,
39
+ _notify_exit,
40
+ _record_event,
41
+ _required_policy,
42
+ )
34
43
  from .stacks import _capture as _capture_stack
35
44
  from .timeline import (
36
45
  _capture_result,
37
- _in_recorder,
38
46
  _pop,
39
47
  _push,
40
- _tape,
41
48
  _timelines_active,
42
49
  )
43
50
 
@@ -121,20 +128,43 @@ def _record(
121
128
  on the in-progress stack so anything it triggers nests under it.
122
129
  """
123
130
 
124
- tape = _tape.get()
125
- if tape is None or _in_recorder.get():
126
- if tape is None and not _in_recorder.get() and _timelines_active():
131
+ # when=False is a behaviour-only binding: it never records, counts
132
+ # nothing, and takes no part in gap detection.
133
+
134
+ if binding._when is False:
135
+ return operate()
136
+
137
+ active = _active_sinks()
138
+ if not active or _in_recorder.get():
139
+ if not active and not _in_recorder.get() and _timelines_active():
127
140
  binding._note_missed_call()
128
141
 
129
142
  return operate()
130
143
 
144
+ # The per-access predicate, mapped onto call shape the same way
145
+ # behaviour stages are: a set passes the written value as the one
146
+ # positional argument, a get or delete passes empty args.
147
+
148
+ if binding._when is not None:
149
+ call_args = (value,) if value is not MISSING else ()
150
+
151
+ guard = _in_recorder.set(True)
152
+ try:
153
+ wanted = binding._when(instance, call_args, {})
154
+ finally:
155
+ _in_recorder.reset(guard)
156
+
157
+ if not wanted:
158
+ binding._filtered_calls += 1
159
+ return operate()
160
+
131
161
  # The written value and the prior value are inbound data, so they
132
162
  # capture on the arguments axis, under the attribute's name so a
133
163
  # by-name policy such as redact() applies to writes too.
134
164
 
135
165
  policy = binding._capture_args
136
166
  if policy is None:
137
- policy = getattr(tape, "capture_args", REFERENCE)
167
+ policy = _required_policy(active, "capture_args")
138
168
 
139
169
  guard = _in_recorder.set(True)
140
170
  try:
@@ -163,28 +193,42 @@ def _record(
163
193
  if previous is not MISSING:
164
194
  event.previous = _capture_value(policy, attribute, previous)
165
195
 
166
- tape.record(event)
167
196
  finally:
168
197
  _in_recorder.reset(guard)
169
198
 
199
+ # Position before delivery: pushed first, so sinks hearing
200
+ # on_enter see the event's final depth and parent link. Timing
201
+ # starts after the bookkeeping, so its overhead is not charged to
202
+ # the observed operation.
203
+
170
204
  token = _push(event)
205
+ _record_event(event, active)
206
+
207
+ started = time.perf_counter()
208
+ event.started = started
209
+
171
210
  try:
172
211
  outcome = operate()
173
212
  except BaseException as exc:
213
+ event.duration = time.perf_counter() - started
174
214
  event.exception = exc
215
+ _notify_error(event, active)
175
216
  raise
176
217
  finally:
177
218
  _pop(token)
178
219
 
220
+ event.duration = time.perf_counter() - started
221
+
179
222
  # The value a read produced is its outcome, so it captures on the
180
223
  # result axis, exactly as a call's return value does.
181
224
 
182
225
  if kind == "get":
183
226
  result_policy = binding._capture_result
184
227
  if result_policy is None:
185
- result_policy = getattr(tape, "capture_result", REFERENCE)
228
+ result_policy = _required_policy(active, "capture_result")
186
229
  _capture_result(event, outcome, result_policy)
187
230
 
231
+ _notify_exit(event, active)
188
232
  return outcome
189
233
 
190
234