pytest-codspeed 3.3.0b0__tar.gz → 4.0.0b0__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 (34) hide show
  1. {pytest_codspeed-3.3.0b0/src/pytest_codspeed.egg-info → pytest_codspeed-4.0.0b0}/PKG-INFO +2 -2
  2. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/pyproject.toml +2 -2
  3. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/__init__.py +2 -2
  4. pytest_codspeed-4.0.0b0/src/pytest_codspeed/config.py +119 -0
  5. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/__init__.py +15 -5
  6. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/hooks/__init__.py +10 -11
  7. pytest_codspeed-4.0.0b0/src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi +27 -0
  8. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/valgrind.py +55 -6
  9. pytest_codspeed-4.0.0b0/src/pytest_codspeed/instruments/walltime.py +363 -0
  10. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/plugin.py +91 -47
  11. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0/src/pytest_codspeed.egg-info}/PKG-INFO +2 -2
  12. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed.egg-info/SOURCES.txt +2 -0
  13. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed.egg-info/requires.txt +1 -1
  14. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/tests/test_pytest_plugin.py +140 -0
  15. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/tests/test_pytest_plugin_cpu_instrumentation.py +87 -6
  16. pytest_codspeed-4.0.0b0/tests/test_pytest_plugin_walltime.py +88 -0
  17. pytest_codspeed-3.3.0b0/src/pytest_codspeed/instruments/walltime.py +0 -297
  18. pytest_codspeed-3.3.0b0/tests/test_pytest_plugin_walltime.py +0 -37
  19. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/LICENSE +0 -0
  20. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/README.md +0 -0
  21. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/setup.cfg +0 -0
  22. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/setup.py +0 -0
  23. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/hooks/build.py +0 -0
  24. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/dist/core.c +0 -0
  25. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/callgrind.h +0 -0
  26. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/core.h +0 -0
  27. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/valgrind.h +0 -0
  28. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/zig.h +0 -0
  29. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/py.typed +0 -0
  30. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed/utils.py +0 -0
  31. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed.egg-info/dependency_links.txt +0 -0
  32. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed.egg-info/entry_points.txt +0 -0
  33. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/src/pytest_codspeed.egg-info/top_level.txt +0 -0
  34. {pytest_codspeed-3.3.0b0 → pytest_codspeed-4.0.0b0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-codspeed
3
- Version: 3.3.0b0
3
+ Version: 4.0.0b0
4
4
  Summary: Pytest plugin to create CodSpeed benchmarks
5
5
  Author-email: Arthur Pastel <arthur@codspeed.io>
6
6
  License: The MIT License (MIT)
@@ -52,7 +52,7 @@ Requires-Dist: rich>=13.8.1
52
52
  Requires-Dist: importlib-metadata>=8.5.0; python_version < "3.10"
53
53
  Provides-Extra: lint
54
54
  Requires-Dist: mypy~=1.11.2; extra == "lint"
55
- Requires-Dist: ruff~=0.6.5; extra == "lint"
55
+ Requires-Dist: ruff~=0.11.12; extra == "lint"
56
56
  Provides-Extra: compat
57
57
  Requires-Dist: pytest-benchmark~=5.0.0; extra == "compat"
58
58
  Requires-Dist: pytest-xdist~=3.6.1; extra == "compat"
@@ -37,7 +37,7 @@ dependencies = [
37
37
  ]
38
38
 
39
39
  [project.optional-dependencies]
40
- lint = ["mypy ~= 1.11.2", "ruff ~= 0.6.5"]
40
+ lint = ["mypy ~= 1.11.2", "ruff ~= 0.11.12"]
41
41
  compat = [
42
42
  "pytest-benchmark ~= 5.0.0",
43
43
  "pytest-xdist ~= 3.6.1",
@@ -66,7 +66,7 @@ version = { attr = "pytest_codspeed.__version__" }
66
66
 
67
67
 
68
68
  [tool.bumpver]
69
- current_version = "3.3.0-beta"
69
+ current_version = "4.0.0-beta"
70
70
  version_pattern = "MAJOR.MINOR.PATCH[-TAG[NUM]]"
71
71
  commit_message = "Release v{new_version} 🚀"
72
72
  tag_message = "Release v{new_version} 🚀"
@@ -1,6 +1,6 @@
1
- __version__ = "3.3.0b0"
1
+ __version__ = "4.0.0b0"
2
2
  # We also have the semver version since __version__ is not semver compliant
3
- __semver_version__ = "3.3.0-beta"
3
+ __semver_version__ = "4.0.0-beta"
4
4
 
5
5
  from .plugin import BenchmarkFixture
6
6
 
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Generic, TypeVar
6
+
7
+ T = TypeVar("T")
8
+
9
+ if TYPE_CHECKING:
10
+ from typing import Any, Callable
11
+
12
+ import pytest
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class CodSpeedConfig:
17
+ """
18
+ The configuration for the codspeed plugin.
19
+ Usually created from the command line arguments.
20
+ """
21
+
22
+ warmup_time_ns: int | None = None
23
+ max_time_ns: int | None = None
24
+ max_rounds: int | None = None
25
+
26
+ @classmethod
27
+ def from_pytest_config(cls, config: pytest.Config) -> CodSpeedConfig:
28
+ warmup_time = config.getoption("--codspeed-warmup-time", None)
29
+ warmup_time_ns = (
30
+ int(warmup_time * 1_000_000_000) if warmup_time is not None else None
31
+ )
32
+ max_time = config.getoption("--codspeed-max-time", None)
33
+ max_time_ns = int(max_time * 1_000_000_000) if max_time is not None else None
34
+ return cls(
35
+ warmup_time_ns=warmup_time_ns,
36
+ max_rounds=config.getoption("--codspeed-max-rounds", None),
37
+ max_time_ns=max_time_ns,
38
+ )
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class BenchmarkMarkerOptions:
43
+ group: str | None = None
44
+ """The group name to use for the benchmark."""
45
+ min_time: int | None = None
46
+ """
47
+ The minimum time of a round (in seconds).
48
+ Only available in walltime mode.
49
+ """
50
+ max_time: int | None = None
51
+ """
52
+ The maximum time to run the benchmark for (in seconds).
53
+ Only available in walltime mode.
54
+ """
55
+ max_rounds: int | None = None
56
+ """
57
+ The maximum number of rounds to run the benchmark for.
58
+ Takes precedence over max_time. Only available in walltime mode.
59
+ """
60
+
61
+ @classmethod
62
+ def from_pytest_item(cls, item: pytest.Item) -> BenchmarkMarkerOptions:
63
+ marker = item.get_closest_marker(
64
+ "codspeed_benchmark"
65
+ ) or item.get_closest_marker("benchmark")
66
+ if marker is None:
67
+ return cls()
68
+ if len(marker.args) > 0:
69
+ raise ValueError(
70
+ "Positional arguments are not allowed in the benchmark marker"
71
+ )
72
+ kwargs = marker.kwargs
73
+
74
+ unknown_kwargs = set(kwargs.keys()) - {
75
+ field.name for field in dataclasses.fields(cls)
76
+ }
77
+ if unknown_kwargs:
78
+ raise ValueError(
79
+ "Unknown kwargs passed to benchmark marker: "
80
+ + ", ".join(sorted(unknown_kwargs))
81
+ )
82
+
83
+ return cls(**kwargs)
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class PedanticOptions(Generic[T]):
88
+ """Parameters for running a benchmark using the pedantic fixture API."""
89
+
90
+ target: Callable[..., T]
91
+ setup: Callable[[], Any | None] | None
92
+ teardown: Callable[..., Any | None] | None
93
+ rounds: int
94
+ warmup_rounds: int
95
+ iterations: int
96
+ args: tuple[Any, ...] = field(default_factory=tuple)
97
+ kwargs: dict[str, Any] = field(default_factory=dict)
98
+
99
+ def __post_init__(self) -> None:
100
+ if self.rounds < 0:
101
+ raise ValueError("rounds must be positive")
102
+ if self.warmup_rounds < 0:
103
+ raise ValueError("warmup_rounds must be non-negative")
104
+ if self.iterations <= 0:
105
+ raise ValueError("iterations must be positive")
106
+ if self.iterations > 1 and self.setup is not None:
107
+ raise ValueError(
108
+ "setup cannot be used with multiple iterations, use multiple rounds"
109
+ )
110
+
111
+ def setup_and_get_args_kwargs(self) -> tuple[tuple[Any, ...], dict[str, Any]]:
112
+ if self.setup is None:
113
+ return self.args, self.kwargs
114
+ maybe_result = self.setup(*self.args, **self.kwargs)
115
+ if maybe_result is not None:
116
+ if len(self.args) > 0 or len(self.kwargs) > 0:
117
+ raise ValueError("setup cannot return a value when args are provided")
118
+ return maybe_result
119
+ return self.args, self.kwargs
@@ -5,14 +5,14 @@ from enum import Enum
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  if TYPE_CHECKING:
8
- from typing import Any, Callable, ClassVar, ParamSpec, TypeVar
8
+ from typing import Any, Callable, ClassVar, TypeVar
9
9
 
10
10
  import pytest
11
11
 
12
+ from pytest_codspeed.config import BenchmarkMarkerOptions, PedanticOptions
12
13
  from pytest_codspeed.plugin import CodSpeedConfig
13
14
 
14
15
  T = TypeVar("T")
15
- P = ParamSpec("P")
16
16
 
17
17
 
18
18
  class Instrument(metaclass=ABCMeta):
@@ -27,11 +27,21 @@ class Instrument(metaclass=ABCMeta):
27
27
  @abstractmethod
28
28
  def measure(
29
29
  self,
30
+ marker_options: BenchmarkMarkerOptions,
31
+ name: str,
32
+ uri: str,
33
+ fn: Callable[..., T],
34
+ *args: tuple,
35
+ **kwargs: dict[str, Any],
36
+ ) -> T: ...
37
+
38
+ @abstractmethod
39
+ def measure_pedantic(
40
+ self,
41
+ marker_options: BenchmarkMarkerOptions,
42
+ pedantic_options: PedanticOptions[T],
30
43
  name: str,
31
44
  uri: str,
32
- fn: Callable[P, T],
33
- *args: P.args,
34
- **kwargs: P.kwargs,
35
45
  ) -> T: ...
36
46
 
37
47
  @abstractmethod
@@ -2,10 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import sys
5
+ import warnings
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  if TYPE_CHECKING:
8
- from .dist_instrument_hooks import lib as LibType
9
+ from .dist_instrument_hooks import InstrumentHooksPointer, LibType
9
10
 
10
11
  SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)
11
12
 
@@ -14,7 +15,7 @@ class InstrumentHooks:
14
15
  """Zig library wrapper class providing benchmark measurement functionality."""
15
16
 
16
17
  lib: LibType
17
- instance: int
18
+ instance: InstrumentHooksPointer
18
19
 
19
20
  def __init__(self) -> None:
20
21
  if os.environ.get("CODSPEED_ENV") is None:
@@ -27,17 +28,15 @@ class InstrumentHooks:
27
28
  from .dist_instrument_hooks import lib # type: ignore
28
29
  except ImportError as e:
29
30
  raise RuntimeError(f"Failed to load instrument hooks library: {e}") from e
31
+ self.lib = lib
30
32
 
31
- instance = lib.instrument_hooks_init()
32
- if instance == 0:
33
+ self.instance = self.lib.instrument_hooks_init()
34
+ if self.instance == 0:
33
35
  raise RuntimeError("Failed to initialize CodSpeed instrumentation library.")
34
36
 
35
37
  if SUPPORTS_PERF_TRAMPOLINE:
36
38
  sys.activate_stack_trampoline("perf") # type: ignore
37
39
 
38
- self.lib = lib
39
- self.instance = instance
40
-
41
40
  def __del__(self):
42
41
  if hasattr(self, "lib") and hasattr(self, "instance"):
43
42
  self.lib.instrument_hooks_deinit(self.instance)
@@ -46,13 +45,13 @@ class InstrumentHooks:
46
45
  """Start a new benchmark measurement."""
47
46
  ret = self.lib.instrument_hooks_start_benchmark(self.instance)
48
47
  if ret != 0:
49
- raise RuntimeError("Failed to start benchmark measurement")
48
+ warnings.warn("Failed to start benchmark measurement", RuntimeWarning)
50
49
 
51
50
  def stop_benchmark(self) -> None:
52
51
  """Stop the current benchmark measurement."""
53
52
  ret = self.lib.instrument_hooks_stop_benchmark(self.instance)
54
53
  if ret != 0:
55
- raise RuntimeError("Failed to stop benchmark measurement")
54
+ warnings.warn("Failed to stop benchmark measurement", RuntimeWarning)
56
55
 
57
56
  def set_executed_benchmark(self, uri: str, pid: int | None = None) -> None:
58
57
  """Set the executed benchmark URI and process ID.
@@ -68,7 +67,7 @@ class InstrumentHooks:
68
67
  self.instance, pid, uri.encode("ascii")
69
68
  )
70
69
  if ret != 0:
71
- raise RuntimeError("Failed to set executed benchmark")
70
+ warnings.warn("Failed to set executed benchmark", RuntimeWarning)
72
71
 
73
72
  def set_integration(self, name: str, version: str) -> None:
74
73
  """Set the integration name and version."""
@@ -76,7 +75,7 @@ class InstrumentHooks:
76
75
  self.instance, name.encode("ascii"), version.encode("ascii")
77
76
  )
78
77
  if ret != 0:
79
- raise RuntimeError("Failed to set integration name and version")
78
+ warnings.warn("Failed to set integration name and version", RuntimeWarning)
80
79
 
81
80
  def is_instrumented(self) -> bool:
82
81
  """Check if instrumentation is active."""
@@ -0,0 +1,27 @@
1
+ InstrumentHooksPointer = object
2
+
3
+ class lib:
4
+ @staticmethod
5
+ def instrument_hooks_init() -> InstrumentHooksPointer: ...
6
+ @staticmethod
7
+ def instrument_hooks_deinit(hooks: InstrumentHooksPointer) -> None: ...
8
+ @staticmethod
9
+ def instrument_hooks_is_instrumented(hooks: InstrumentHooksPointer) -> bool: ...
10
+ @staticmethod
11
+ def instrument_hooks_start_benchmark(hooks: InstrumentHooksPointer) -> int: ...
12
+ @staticmethod
13
+ def instrument_hooks_stop_benchmark(hooks: InstrumentHooksPointer) -> int: ...
14
+ @staticmethod
15
+ def instrument_hooks_executed_benchmark(
16
+ hooks: InstrumentHooksPointer, pid: int, uri: bytes
17
+ ) -> int: ...
18
+ @staticmethod
19
+ def instrument_hooks_set_integration(
20
+ hooks: InstrumentHooksPointer, name: bytes, version: bytes
21
+ ) -> int: ...
22
+ @staticmethod
23
+ def callgrind_start_instrumentation() -> int: ...
24
+ @staticmethod
25
+ def callgrind_stop_instrumentation() -> int: ...
26
+
27
+ LibType = type[lib]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+ import warnings
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from pytest_codspeed import __semver_version__
@@ -12,8 +13,9 @@ if TYPE_CHECKING:
12
13
 
13
14
  from pytest import Session
14
15
 
15
- from pytest_codspeed.instruments import P, T
16
- from pytest_codspeed.plugin import CodSpeedConfig
16
+ from pytest_codspeed.config import PedanticOptions
17
+ from pytest_codspeed.instruments import T
18
+ from pytest_codspeed.plugin import BenchmarkMarkerOptions, CodSpeedConfig
17
19
 
18
20
  SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)
19
21
 
@@ -35,7 +37,7 @@ class ValgrindInstrument(Instrument):
35
37
  def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
36
38
  config = (
37
39
  f"mode: instrumentation, "
38
- f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}"
40
+ f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}"
39
41
  )
40
42
  warnings = []
41
43
  if not self.should_measure:
@@ -49,11 +51,12 @@ class ValgrindInstrument(Instrument):
49
51
 
50
52
  def measure(
51
53
  self,
54
+ marker_options: BenchmarkMarkerOptions,
52
55
  name: str,
53
56
  uri: str,
54
- fn: Callable[P, T],
55
- *args: P.args,
56
- **kwargs: P.kwargs,
57
+ fn: Callable[..., T],
58
+ *args: tuple,
59
+ **kwargs: dict[str, Any],
57
60
  ) -> T:
58
61
  self.benchmark_count += 1
59
62
 
@@ -77,8 +80,54 @@ class ValgrindInstrument(Instrument):
77
80
  self.instrument_hooks.lib.callgrind_stop_instrumentation()
78
81
  self.instrument_hooks.set_executed_benchmark(uri)
79
82
 
83
+ def measure_pedantic(
84
+ self,
85
+ marker_options: BenchmarkMarkerOptions,
86
+ pedantic_options: PedanticOptions[T],
87
+ name: str,
88
+ uri: str,
89
+ ) -> T:
90
+ if pedantic_options.rounds != 1 or pedantic_options.iterations != 1:
91
+ warnings.warn(
92
+ "Valgrind instrument ignores rounds and iterations settings "
93
+ "in pedantic mode"
94
+ )
95
+ if not self.instrument_hooks:
96
+ args, kwargs = pedantic_options.setup_and_get_args_kwargs()
97
+ out = pedantic_options.target(*args, **kwargs)
98
+ if pedantic_options.teardown is not None:
99
+ pedantic_options.teardown(*args, **kwargs)
100
+ return out
101
+
102
+ def __codspeed_root_frame__(*args, **kwargs) -> T:
103
+ return pedantic_options.target(*args, **kwargs)
104
+
105
+ # Warmup
106
+ warmup_rounds = max(
107
+ pedantic_options.warmup_rounds, 1 if SUPPORTS_PERF_TRAMPOLINE else 0
108
+ )
109
+ for _ in range(warmup_rounds):
110
+ args, kwargs = pedantic_options.setup_and_get_args_kwargs()
111
+ __codspeed_root_frame__(*args, **kwargs)
112
+ if pedantic_options.teardown is not None:
113
+ pedantic_options.teardown(*args, **kwargs)
114
+
115
+ # Compute the actual result of the function
116
+ args, kwargs = pedantic_options.setup_and_get_args_kwargs()
117
+ self.instrument_hooks.lib.callgrind_start_instrumentation()
118
+ try:
119
+ out = __codspeed_root_frame__(*args, **kwargs)
120
+ finally:
121
+ self.instrument_hooks.lib.callgrind_stop_instrumentation()
122
+ self.instrument_hooks.set_executed_benchmark(uri)
123
+ if pedantic_options.teardown is not None:
124
+ pedantic_options.teardown(*args, **kwargs)
125
+
126
+ return out
127
+
80
128
  def report(self, session: Session) -> None:
81
129
  reporter = session.config.pluginmanager.get_plugin("terminalreporter")
130
+ assert reporter is not None, "terminalreporter not found"
82
131
  count_suffix = "benchmarked" if self.should_measure else "benchmark tested"
83
132
  reporter.write_sep(
84
133
  "=",