functiontrace 0.5.1__tar.gz → 0.5.2__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,7 +1,7 @@
1
1
  [package]
2
2
  name = "_functiontrace_rs"
3
3
  version = "0.1.0"
4
- edition = "2021"
4
+ edition = "2024"
5
5
 
6
6
  [dependencies]
7
7
  color-eyre = { version = "0.6", default-features = false }
@@ -10,17 +10,17 @@ functiontrace-server = { version = "0.8.4", default-features = false, features =
10
10
  jiff = { version = "0.2", default-features = false, features = ["std", "serde"] }
11
11
  libc = "0.2"
12
12
  owo-colors = "4.2"
13
- pyo3 = { version = "0.25", features = ["eyre"] }
14
- reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
13
+ pyo3 = { version = "0.29", features = ["eyre"] }
14
+ reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
15
15
  rmp-serde = "1.3"
16
16
  serde = "1"
17
- toml = "0.9"
17
+ toml = "1.1"
18
18
  tracing = { version = "0.1", features = ["log"] }
19
19
  tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "env-filter", "fmt", "std"] }
20
20
 
21
21
  [build-dependencies]
22
- toml = { version = "0.9", default-features = false, features = ["parse", "serde"] }
23
- pyo3-build-config = { version = "0.25", features = ["resolve-config"] }
22
+ toml = { version = "1.1", default-features = false, features = ["parse", "serde"] }
23
+ pyo3-build-config = { version = "0.29" }
24
24
 
25
25
  [lib]
26
26
  # See https://github.com/PyO3/pyo3 for details
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: A profiling tool that traces your application's execution
5
5
  Author: Alistair Dobke
6
6
  Author-email: Matt Bryant <functiontrace@programsareproofs.com>
@@ -15,7 +15,7 @@ Classifier: Environment :: Console
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
18
- Requires-Python: >=3.9
18
+ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/x-rst
20
20
  License-File: LICENSE.md
21
21
  Dynamic: license-file
@@ -28,7 +28,7 @@ Dependencies
28
28
  ------------
29
29
 
30
30
  The FunctionTrace server must be installed for this package to work.
31
- See installation instructions at https://functiontrace.com#installation.
31
+ See installation instructions at https://functiontrace.com/installation/.
32
32
 
33
33
  Usage
34
34
  -----
@@ -6,7 +6,7 @@ Dependencies
6
6
  ------------
7
7
 
8
8
  The FunctionTrace server must be installed for this package to work.
9
- See installation instructions at https://functiontrace.com#installation.
9
+ See installation instructions at https://functiontrace.com/installation/.
10
10
 
11
11
  Usage
12
12
  -----
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: A profiling tool that traces your application's execution
5
5
  Author: Alistair Dobke
6
6
  Author-email: Matt Bryant <functiontrace@programsareproofs.com>
@@ -15,7 +15,7 @@ Classifier: Environment :: Console
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
18
- Requires-Python: >=3.9
18
+ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/x-rst
20
20
  License-File: LICENSE.md
21
21
  Dynamic: license-file
@@ -28,7 +28,7 @@ Dependencies
28
28
  ------------
29
29
 
30
30
  The FunctionTrace server must be installed for this package to work.
31
- See installation instructions at https://functiontrace.com#installation.
31
+ See installation instructions at https://functiontrace.com/installation/.
32
32
 
33
33
  Usage
34
34
  -----
@@ -19,7 +19,12 @@ exec $(which {python}) -m functiontrace "$@"
19
19
 
20
20
 
21
21
  def setup_dependencies():
22
- # Generate a temp directory to store our wrappers in. We'll temporarily
22
+ if f"{tempfile.gettempdir()}/py-functiontrace" in os.environ["PATH"]:
23
+ # We've already set up dependencies. This only needs to happen once, since otherwise we'll
24
+ # just make a bunch of useless temp directories.
25
+ return
26
+
27
+ # Generate a temp directory to store our wrappers in. We'll temporarily
23
28
  # add this directory to our path.
24
29
  tempdir = tempfile.mkdtemp(prefix="py-functiontrace")
25
30
  os.environ["PATH"] = tempdir + os.pathsep + os.environ["PATH"]
@@ -89,10 +94,19 @@ def main():
89
94
  # below, which needs to operate in the context of our new module.
90
95
  # NOTE: We use SourceFileLoader, rather than allowing importlib to infer
91
96
  # the proper loader, in order to support files without a .py extension.
92
- spec = util.spec_from_loader(
93
- "__main__", machinery.SourceFileLoader("__main__", target_file)
97
+ mod = util.module_from_spec(
98
+ util.spec_from_loader(
99
+ "__main__", machinery.SourceFileLoader("__main__", target_file)
100
+ )
94
101
  )
95
- mod = util.module_from_spec(spec)
102
+
103
+ # Clear __spec__ to match Python's standard `python foo.py` loading
104
+ mod.__spec__ = None
105
+ # Record the file we're using so `multiprocessing` can load it (in a non-forked context).
106
+ # Without this, `multiprocessing` in `forkserver` contexts is asked to load functions that
107
+ # it can't find.
108
+ mod.__file__ = target_file
109
+
96
110
  sys.modules.update({"__main__": mod})
97
111
 
98
112
  # Ensure we're setup to be able to run.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.5.1"
3
+ version = "0.5.2"
4
4
  description = "A profiling tool that traces your application's execution"
5
5
  authors = [
6
6
  { name = "Matt Bryant", email = "functiontrace@programsareproofs.com"},
@@ -9,13 +9,13 @@ authors = [
9
9
  license = "LicenseRef-Prosperity-Public-License-3.0.0"
10
10
  license-files = ["LICENSE.md"]
11
11
  readme = "README.rst"
12
- # NOTE: Actively tested on Python 3.9 - 3.13 and may plausibly break on greater
13
- # versions due to C extension hacks. However, setting a maximum version causes
14
- # new Python versions to simply install very old versions of Functiontrace that
15
- # didn't have the `requires-python` annotation on them and are also unlikely to
16
- # work. Best to only set the minimum and let bug reports notify us of issues
17
- # with new Python releases.
18
- requires-python = ">=3.9"
12
+ # NOTE: Actively tested on Python 3.10 - 3.14 and may plausibly break on
13
+ # greater versions due to C extension hacks. However, setting a maximum version
14
+ # causes new Python versions to simply install very old versions of
15
+ # Functiontrace that didn't have the `requires-python` annotation on them and
16
+ # are also unlikely to work. Best to only set the minimum and let bug reports
17
+ # notify us of issues with new Python releases.
18
+ requires-python = ">=3.10"
19
19
  keywords = [
20
20
  "profiler",
21
21
  "debugger"
@@ -7,7 +7,7 @@ mod _functiontrace_rs {
7
7
  use serde::{Deserialize, Serialize};
8
8
  use std::borrow::Cow;
9
9
  use std::cell::Cell;
10
- use std::ffi::{CStr, OsString, c_void};
10
+ use std::ffi::{CStr, OsString};
11
11
  use std::io::Write;
12
12
  use std::os::unix::net::UnixStream;
13
13
  use std::sync::atomic::{AtomicBool, Ordering};
@@ -148,7 +148,7 @@ mod _functiontrace_rs {
148
148
  let state = THREAD_STATE.get();
149
149
 
150
150
  if state.is_null() {
151
- // We shouldn't be asked to reset at thread that hasn't yet been registered.
151
+ // We shouldn't be asked to reset a thread that hasn't yet been registered.
152
152
  return None;
153
153
  }
154
154
 
@@ -206,7 +206,7 @@ mod _functiontrace_rs {
206
206
  .wrap_err("Failed to convert MIN_PYTHON_VERSION to int")?,
207
207
  )
208
208
  .wrap_err("Failed to expose _min_python_version")?;
209
- m.add("_max_python_version", 13)
209
+ m.add("_max_python_version", 14)
210
210
  .wrap_err("Failed to expose _max_python_version")?;
211
211
 
212
212
  color_eyre::install().wrap_err("Failed to install color_eyre")?;
@@ -259,20 +259,20 @@ mod _functiontrace_rs {
259
259
  // Parse args out of `sys.argv`, since it excludes the Python executable and shows the
260
260
  // script's path first. This is much more complicated than using `std::env::args`, but
261
261
  // is more practical.
262
- Python::with_gil(|py| -> Result<String> {
262
+ Python::attach(|py| -> Result<String> {
263
263
  let argv = py
264
264
  .import("sys")
265
265
  .and_then(|sys| sys.getattr("argv"))
266
266
  .wrap_err("Failed to access sys.argv")?;
267
267
 
268
268
  let args = argv
269
- .downcast::<pyo3::types::PyList>()
269
+ .cast::<pyo3::types::PyList>()
270
270
  .map_err(|e| eyre!("Failed to read sys.argv as list: {}", e))?;
271
271
 
272
272
  let mut full_args = args.iter().fold(String::new(), |s, arg| {
273
273
  // Fetch the next arg as a string if possible.
274
274
  let arg = arg
275
- .downcast::<pyo3::types::PyString>()
275
+ .cast::<pyo3::types::PyString>()
276
276
  .map_err(|e| eyre!("Failed to read sys.argv[x] as string: {}", e))
277
277
  .and_then(|arg| {
278
278
  arg.to_str()
@@ -327,7 +327,7 @@ mod _functiontrace_rs {
327
327
  Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
328
328
  eprintln!("Unable to find `functiontrace-server` in the current $PATH.");
329
329
  eprintln!(
330
- "See https://functiontrace.com#installation for installation instructions."
330
+ "See https://functiontrace.com/installation/ for installation instructions."
331
331
  );
332
332
 
333
333
  // Exit here rather than returning an error, since that'll emit a confusing
@@ -340,12 +340,12 @@ mod _functiontrace_rs {
340
340
  format!("/tmp/functiontrace-server.sock.{}", server.id()).into()
341
341
  };
342
342
 
343
- Python::with_gil(|_| {
343
+ Python::attach(|_| {
344
344
  // Register an env var so subprocesses know how to connect to our
345
345
  // profile.
346
346
  unsafe {
347
347
  // SAFETY: `set_var` can only be done while no one else is reading or writing to the
348
- // environment. We're holding the GIL, so this _should_ be true, but there's no real
348
+ // environment. We're holding the GIL, so this _should_ be true, but there's no real
349
349
  // way to enforce this.
350
350
  //
351
351
  // This doesn't seem to blow up in C even when we don't have the GIL, so
@@ -362,7 +362,7 @@ mod _functiontrace_rs {
362
362
  // Send an initialization message to the server with some information about us. This will
363
363
  // only be sent once, and all future threads/subprocesses will be associated underneath us.
364
364
  let init = {
365
- let python_version = Python::with_gil(|py| {
365
+ let python_version = Python::attach(|py| {
366
366
  let version = py.version_info();
367
367
 
368
368
  format!(
@@ -371,14 +371,14 @@ mod _functiontrace_rs {
371
371
  )
372
372
  });
373
373
 
374
- let platform = Python::with_gil(|py| -> Result<String> {
374
+ let platform = Python::attach(|py| -> Result<String> {
375
375
  let platform = py
376
376
  .import("sys")
377
377
  .and_then(|sys| sys.getattr("platform"))
378
378
  .wrap_err("Failed to access sys.platform")?;
379
379
 
380
380
  Ok(platform
381
- .downcast::<pyo3::types::PyString>()
381
+ .cast::<pyo3::types::PyString>()
382
382
  .map_err(|e| eyre!("Failed to read sys.platform as string: {}", e))?
383
383
  .to_str()
384
384
  .wrap_err("Failed to read Rust string out of PyString")?
@@ -433,7 +433,7 @@ mod _functiontrace_rs {
433
433
  //
434
434
  // NOTE: We specifically do this rather than `Py_AtExit` since we want to run before all
435
435
  // the Python teardown occurs.
436
- Python::with_gil(|py| -> Result<()> {
436
+ Python::attach(|py| -> Result<()> {
437
437
  py.import("atexit")
438
438
  .and_then(|atexit| atexit.getattr("register"))
439
439
  .wrap_err("Failed to access atexit.register")?
@@ -460,12 +460,13 @@ mod _functiontrace_rs {
460
460
 
461
461
  // Register our tracing functions with Python - both the normal one and the multithreaded
462
462
  // setup trampoline.
463
+ // TODO: PyEval_SetProfileAllThreads in >=3.12 should simplify this.
463
464
  unsafe {
464
465
  // SAFETY: both parameters are of the proper type.
465
466
  pyo3::ffi::PyEval_SetProfile(Some(functiontrace), std::ptr::null_mut())
466
467
  };
467
468
 
468
- Python::with_gil(|py| -> Result<()> {
469
+ Python::attach(|py| -> Result<()> {
469
470
  py.import("threading")
470
471
  .and_then(|threading| threading.getattr("setprofile"))
471
472
  .wrap_err("Failed to access threading.setprofile")?
@@ -561,23 +562,6 @@ mod _functiontrace_rs {
561
562
  Ok(())
562
563
  }
563
564
 
564
- #[unsafe(no_mangle)]
565
- extern "C" fn c_thread_teardown(state: *mut c_void) {
566
- if state.is_null() {
567
- // We never fully initialized this thread, so skip teardown.
568
- return;
569
- }
570
-
571
- let writer = unsafe {
572
- // SAFETY: writer is a valid pointer to an initialized `ThreadState`.
573
- &mut (state as *mut ThreadState)
574
- .as_mut()
575
- .expect("state is non-null")
576
- };
577
-
578
- thread_teardown(writer).expect("Failed to tear down FunctionTrace thread")
579
- }
580
-
581
565
  /// Tear down the given thread by flushing any outstanding messages.
582
566
  ///
583
567
  /// This is called when the thread has shut down, including in multithread/process situations.
@@ -636,7 +620,11 @@ mod _functiontrace_rs {
636
620
  /// thread state, before replacing itself with the normal Fprofile_FunctionTrace handler.
637
621
  #[pyfunction]
638
622
  #[pyo3(name = "_thread_trace_trampoline")]
639
- fn thread_trace_trampoline(_frame: PyObject, _event: PyObject, _arg: PyObject) -> Result<()> {
623
+ fn thread_trace_trampoline(
624
+ _frame: Py<PyAny>,
625
+ _event: Py<PyAny>,
626
+ _arg: Py<PyAny>,
627
+ ) -> Result<()> {
640
628
  // Register the current thread for tracing
641
629
  thread_register().wrap_err("Failed to register new FunctionTrace thread")?;
642
630
 
@@ -787,7 +775,7 @@ mod _functiontrace_rs {
787
775
  pyo3::ffi::PyFrame_GetCode(frame) as *mut pyo3::ffi::PyObject
788
776
  };
789
777
 
790
- Python::with_gil(|py| {
778
+ Python::attach(|py| {
791
779
  let code = unsafe {
792
780
  // SAFETY: code is guaranteed to be a valid PyCodeObject since it
793
781
  // came from `PyFrame_GetCode`.
@@ -799,7 +787,7 @@ mod _functiontrace_rs {
799
787
  let unicode_converter = |pyobj: PyResult<Bound<'_, PyAny>>| {
800
788
  pyobj
801
789
  .map(|obj| {
802
- if let Ok(filename) = obj.downcast::<pyo3::types::PyString>() {
790
+ if let Ok(filename) = obj.cast::<pyo3::types::PyString>() {
803
791
  Cow::from(filename.to_string_lossy().into_owned())
804
792
  } else {
805
793
  Cow::Borrowed("<UNKNOWN>")
@@ -856,13 +844,13 @@ mod _functiontrace_rs {
856
844
  if !func.m_module.is_null() {
857
845
  // NOTE: _Typically_ `m_module` is a PyModule, but there are no requirements on
858
846
  // what type it needs to be! In practice, we only see PyModules and PyStrings.
859
- Python::with_gil(|py| {
847
+ Python::attach(|py| {
860
848
  let m_module = unsafe {
861
849
  // SAFETY: m_module is a non-NULL pointer to a valid Python object.
862
850
  Bound::from_borrowed_ptr(py, func.m_module)
863
851
  };
864
852
 
865
- if let Ok(module) = m_module.downcast::<pyo3::types::PyModule>() {
853
+ if let Ok(module) = m_module.cast::<pyo3::types::PyModule>() {
866
854
  // This is a PyModule, so get its name if possible.
867
855
  let name = module.name();
868
856
  let module_name = if let Ok(name) = &name {
@@ -879,7 +867,7 @@ mod _functiontrace_rs {
879
867
  .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
880
868
  } else {
881
869
  let module_name =
882
- if let Ok(module) = m_module.downcast::<pyo3::types::PyString>() {
870
+ if let Ok(module) = m_module.cast::<pyo3::types::PyString>() {
883
871
  module.to_cow().unwrap_or(Cow::Borrowed("<invalid string>"))
884
872
  } else {
885
873
  if let Ok(typ) = m_module.get_type().qualname().map(|x| x.to_string()) {
@@ -1125,7 +1113,7 @@ mod _functiontrace_rs {
1125
1113
  if let Some(result) = parser(&input) {
1126
1114
  return Ok(result);
1127
1115
  }
1128
- Python::with_gil(|py| {
1116
+ Python::attach(|py| {
1129
1117
  // We need to check with the Python interpreter during loops, otherwise we
1130
1118
  // might not receive ctrl-c events.
1131
1119
  py.check_signals()
@@ -1507,7 +1495,7 @@ mod hooks {
1507
1495
  /// function, then attaching enough information about the original function to call it.
1508
1496
  pub fn install() -> Result<()> {
1509
1497
  for hook in HOOKS.iter() {
1510
- Python::with_gil(|py| -> Result<()> {
1498
+ Python::attach(|py| -> Result<()> {
1511
1499
  let (module_name, method_name) = hook
1512
1500
  .target
1513
1501
  .rsplit_once(".")
@@ -1649,14 +1637,14 @@ mod hooks {
1649
1637
  .get()
1650
1638
  .expect("Wrapper functions can't be called until hooked");
1651
1639
 
1652
- let proxied = Python::with_gil(|py| -> Result<()> {
1640
+ let proxied = Python::attach(|py| -> Result<()> {
1653
1641
  if let Some(mut writer) = crate::_functiontrace_rs::thread_state() {
1654
1642
  let args = unsafe {
1655
1643
  // SAFETY: `args` represents a valid argument tuple
1656
1644
  Bound::from_borrowed_ptr(py, args)
1657
1645
  };
1658
1646
  let args = args
1659
- .downcast()
1647
+ .cast()
1660
1648
  .map_err(Into::<PyErr>::into)
1661
1649
  .wrap_err("Python args must be a tuple")?;
1662
1650
 
@@ -1733,7 +1721,7 @@ mod hooks {
1733
1721
  // `import` takes rather complicated arguments, but in practice we only care about
1734
1722
  // the first argument (name)
1735
1723
  let module = if let Ok(arg) = args.get_item(0) {
1736
- arg.downcast()
1724
+ arg.cast()
1737
1725
  .map_err(Into::into)
1738
1726
  .and_then(|module| module.to_str())
1739
1727
  .map(|module| Cow::Owned(module.to_string()))
File without changes
File without changes
File without changes
File without changes
File without changes