Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-asv-runner for openSUSE:Factory checked in at 2026-08-05 17:49:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-asv-runner (Old) and /work/SRC/openSUSE:Factory/.python-asv-runner.new.16738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asv-runner" Wed Aug 5 17:49:35 2026 rev:2 rq:1369597 version:0.3.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-asv-runner/python-asv-runner.changes 2024-10-17 18:41:28.519621944 +0200 +++ /work/SRC/openSUSE:Factory/.python-asv-runner.new.16738/python-asv-runner.changes 2026-08-05 17:50:30.366282482 +0200 @@ -1,0 +2,32 @@ +Tue Aug 4 21:22:22 UTC 2026 - Dirk Müller <[email protected]> + +- update to 0.3.0: + * `asv_runner.__version__` is now defined for source-tree and + editable imports, resolving the installed distribution version + with a `0.0.0.dev0` fallback; built artifacts keep the static + build-time string. + * Releases publish the sdist alongside wheels again: the wheel and + sdist jobs uploaded artifacts under the same default name and the + publish step picked up only the wheel, which is why 0.2.5 reached + PyPI wheel-only and downstream rebuilds from GitHub tarballs + shipped wrong version metadata. + * Timing benchmarks with `setup` hooks now observe freshly set-up + state on every timed call + re-runs `setup` between calls, auto-calibrated `number` resolves + to 1, and an explicitly set `number > 1` batches with `setup` + interleaved between individually timed calls (only the calls are + timed, at the cost of two clock reads per call). + Benchmarks without `setup` hooks are unchanged, `setup_cache` + remains the batching path for input-building setups, and + `timeraw_*` benchmarks (one fresh subprocess per sample) are + exempt. Timings for fast benchmarks that define `setup` will + shift relative to earlier releases because samples are no longer + amortized over auto-calibrated batches. + * Benchmark `version` remains SHA-256 of source text for full + backwards compatibility with asv and historical results. + Token-stable `code_fingerprint()` is exposed via `version_alts` + (and still used where helpful) so tooling can backfill/accept + both identities across releases. Timeraw `env` identity also + uses SHA-256 of code+env payload on the wire. + +------------------------------------------------------------------- Old: ---- asv_runner-0.2.1.tar.gz New: ---- asv_runner-0.3.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-asv-runner.spec ++++++ --- /var/tmp/diff_new_pack.nLLx2e/_old 2026-08-05 17:50:32.706364470 +0200 +++ /var/tmp/diff_new_pack.nLLx2e/_new 2026-08-05 17:50:32.718364890 +0200 @@ -1,7 +1,7 @@ # -# spec file for package python-asv_runner +# spec file for package python-asv-runner # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -15,18 +15,19 @@ # Please submit bugfixes or comments via https://bugs.opensuse.org/ # + %{?sle15_python_module_pythons} Name: python-asv-runner -Version: 0.2.1 +Version: 0.3.0 Release: 0 Summary: Core Python benchmark code for ASV License: BSD-3-Clause -URL: https://asv.readthedocs.io/projects/asv-runner/en/latest/ +URL: https://github.com/airspeed-velocity/asv_runner Source: https://files.pythonhosted.org/packages/source/a/asv_runner/asv_runner-%{version}.tar.gz -BuildRequires: python-rpm-macros BuildRequires: %{python_module pdm-backend} BuildRequires: %{python_module pip} BuildRequires: %{python_module wheel} +BuildRequires: python-rpm-macros # SECTION test requirements BuildRequires: %{python_module importlib-metadata} # /SECTION ++++++ asv_runner-0.2.1.tar.gz -> asv_runner-0.3.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/PKG-INFO new/asv_runner-0.3.0/PKG-INFO --- old/asv_runner-0.2.1/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/asv_runner-0.3.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,8 +1,7 @@ Metadata-Version: 2.1 Name: asv_runner -Version: 0.2.1 +Version: 0.3.0 Summary: Core Python benchmark code for ASV -Home-page: https://asv.readthedocs.io/projects/asv-runner/en/latest/ Author-Email: Rohit Goswami <[email protected]>, Michael Droettboom <[email protected]> Maintainer-Email: Rohit Goswami <[email protected]> License: BSD-3-Clause @@ -18,11 +17,12 @@ Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: System :: Benchmark -Project-URL: Homepage, https://asv.readthedocs.io/projects/asv-runner/en/latest/ -Project-URL: Repository, https://github.com/airspeed-velocity/asv_runner -Project-URL: Documentation, https://asv.readthedocs.io/projects/asv-runner/en/latest/ +Project-URL: homepage, https://asv.readthedocs.io/projects/asv-runner/en/latest/ +Project-URL: repository, https://github.com/airspeed-velocity/asv_runner +Project-URL: documentation, https://asv.readthedocs.io/projects/asv-runner/en/latest/ Requires-Python: >=3.7 -Requires-Dist: importlib-metadata +Requires-Dist: importlib-metadata; python_version < "3.8" +Provides-Extra: docs Requires-Dist: furo; extra == "docs" Requires-Dist: sphinx; extra == "docs" Requires-Dist: sphinx-contributors; extra == "docs" @@ -32,7 +32,6 @@ Requires-Dist: sphinxcontrib-spelling; extra == "docs" Requires-Dist: myst-parser>=2; extra == "docs" Requires-Dist: sphinx-autodoc2>=0.4.2; extra == "docs" -Provides-Extra: docs Description-Content-Type: text/markdown # About [](https://asv.readthedocs.io/projects/asv-runner/en/latest/) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/__init__.py new/asv_runner-0.3.0/asv_runner/__init__.py --- old/asv_runner-0.2.1/asv_runner/__init__.py 2024-02-17 15:11:32.140207300 +0100 +++ new/asv_runner-0.3.0/asv_runner/__init__.py 2026-08-04 13:53:21.864131500 +0200 @@ -1 +1 @@ -__version__ = '0.2.1' \ No newline at end of file +__version__ = '0.3.0' \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/_aux.py new/asv_runner-0.3.0/asv_runner/_aux.py --- old/asv_runner-0.2.1/asv_runner/_aux.py 2024-02-11 21:37:44.847475300 +0100 +++ new/asv_runner-0.3.0/asv_runner/_aux.py 2026-08-04 13:53:18.022216300 +0200 @@ -178,7 +178,7 @@ data += s if not s: raise RuntimeError( - "did not receive data from socket " f"(size {size}, got only {data!r})" + f"did not receive data from socket (size {size}, got only {data!r})" ) return data @@ -215,4 +215,4 @@ try: set_cpu_affinity(affinity_list) except BaseException as exc: - print(f"asv: setting cpu affinity {affinity_list !r} failed: {exc !r}") + print(f"asv: setting cpu affinity {affinity_list!r} failed: {exc!r}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/benchmarks/__init__.py new/asv_runner-0.3.0/asv_runner/benchmarks/__init__.py --- old/asv_runner-0.2.1/asv_runner/benchmarks/__init__.py 2024-02-11 22:48:09.428048400 +0100 +++ new/asv_runner-0.3.0/asv_runner/benchmarks/__init__.py 2026-08-04 13:53:18.022216300 +0200 @@ -32,13 +32,15 @@ adding a new submodule with an "export_as_benchmark" attribute. """ - import importlib import pkgutil from pathlib import Path # py37 doesn't have importlib.metadata -from importlib_metadata import distributions +try: + from importlib.metadata import distributions +except ImportError: + from importlib_metadata import distributions from ._exceptions import NotRequired @@ -48,11 +50,11 @@ submodule_names = [ name for _, name, _ in pkgutil.iter_modules(pkgpath) if "_" not in name ] -asv_modules = [ - dist.metadata["Name"] - for dist in distributions() - if dist.metadata["Name"].startswith("asv_bench") -] +asv_modules = [] +for dist in distributions(): + name = dist.metadata.get("Name", None) + if isinstance(name, str) and name.startswith("asv_bench"): + asv_modules.append(name) benchmark_types = [] # Builtin modules diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/benchmarks/_base.py new/asv_runner-0.3.0/asv_runner/benchmarks/_base.py --- old/asv_runner-0.2.1/asv_runner/benchmarks/_base.py 2023-08-20 14:25:18.051434300 +0200 +++ new/asv_runner-0.3.0/asv_runner/benchmarks/_base.py 2026-08-04 13:53:18.022365000 +0200 @@ -1,14 +1,53 @@ import cProfile as profile import inspect +import io import itertools import math import os import re import textwrap +import tokenize from collections import Counter from hashlib import sha256 +def _token_fingerprint(code): + """ + SHA-256 of Python token stream, ignoring comments and non-semantic whitespace. + + Falls back to hashing the raw UTF-8 bytes if tokenization fails (e.g. incomplete + snippets). Used for default benchmark ``version`` so cosmetic edits do not + invalidate results (asv_runner#43). + """ + if not code: + return sha256(b"").hexdigest() + try: + tokens = [] + readline = io.StringIO(code).readline + for tok in tokenize.generate_tokens(readline): + toknum, tokval = tok[0], tok[1] + if toknum in ( + tokenize.COMMENT, + tokenize.NL, + tokenize.NEWLINE, + tokenize.INDENT, + tokenize.DEDENT, + tokenize.ENDMARKER, + tokenize.ENCODING, + ): + continue + tokens.append((toknum, tokval)) + payload = repr(tokens).encode("utf-8") + except (tokenize.TokenError, IndentationError): + payload = code.encode("utf-8") + return sha256(payload).hexdigest() + + +def code_fingerprint(code): + """Public alias for token-stable hashing of benchmark source (asv_runner#43).""" + return _token_fingerprint(code) + + def _get_attr(source, name, ignore_case=False): """ Retrieves an attribute from a source by its name. @@ -214,7 +253,7 @@ fn = inspect.getsourcefile(obj) fn = os.path.relpath(fn, basedir) _, lineno = inspect.getsourcelines(obj) - return f" in {fn !s}:{lineno !s}" + return f" in {fn!s}:{lineno!s}" except Exception: return "" @@ -260,8 +299,8 @@ info = inspect.getfullargspec(func) except Exception as exc: print( - f"{benchmark_name !s}: failed to check " - f"({func !r}{_get_sourceline_info(func, root) !s}): {exc !s}" + f"{benchmark_name!s}: failed to check " + f"({func!r}{_get_sourceline_info(func, root)!s}): {exc!s}" ) return True @@ -282,9 +321,9 @@ else: num_args_str = f"{min_num_args}-{max_num_args}" print( - f"{benchmark_name !s}: wrong number of arguments " - f"(for {func !r}{_get_sourceline_info(func, root) !s}):", - f"expected {num_args_str}, " f"has {args_str}", + f"{benchmark_name!s}: wrong number of arguments " + f"(for {func!r}{_get_sourceline_info(func, root)!s}):", + f"expected {num_args_str}, has {args_str}", ) return ok @@ -372,9 +411,7 @@ if len(param_names) != len(params): param_names = param_names[: len(params)] - param_names += [ - "param%d" % (k + 1,) for k in range(len(param_names), len(params)) - ] + param_names += [f"param{k + 1}" for k in range(len(param_names), len(params))] return params, param_names @@ -504,9 +541,20 @@ self.setup_cache_timeout = _get_first_attr([self._setup_cache], "timeout", None) self.timeout = _get_first_attr(attr_sources, "timeout", None) self.code = get_source_code([self.func] + self._setups + [self._setup_cache]) + # Primary version: SHA-256 of source text (backwards compatible with asv + # tests and historical result keys). Token-stable fingerprint is an + # alternate identity for tooling/backfill (cosmetic edits share token hash). code_text = self.code.encode("utf-8") code_hash = sha256(code_text).hexdigest() - self.version = str(_get_first_attr(attr_sources, "version", code_hash)) + token_hash = code_fingerprint(self.code) + explicit_version = _get_first_attr(attr_sources, "version", None) + if explicit_version is not None: + self.version = str(explicit_version) + self.version_alts = () + else: + self.version = code_hash + # Prefer unique alts only (token form when it differs from wire version) + self.version_alts = (token_hash,) if token_hash != code_hash else () self.type = "base" self.unit = "unit" @@ -515,6 +563,7 @@ self._params = _get_first_attr(attr_sources, "params", []) self.param_names = _get_first_attr(attr_sources, "param_names", []) self._current_params = () + self._current_cache = None self._params, self.param_names = _validate_params( self._params, self.param_names, self.name @@ -557,12 +606,29 @@ f"Invalid benchmark parameter permutation index: {param_idx!r}" ) + def set_cache(self, cache): + """ + Set the setup_cache result for the benchmark. + + Kept separate from ``_current_params`` so ``skip_params`` / ``@skip_for_params`` + still match the user-declared parameter tuples (asv_runner#49). + + #### Parameters + **cache** (`Any`) + : Value returned by ``setup_cache``, or ``None`` if unused. A value of + ``None`` means "no cache argument" (same as the historical + ``insert_param`` path that only ran when the pickle was not ``None``). + Falsy but non-``None`` caches (``0``, ``[]``, ``{}``) are still passed + through to setup/run/teardown. + """ + self._current_cache = cache + def insert_param(self, param): """ Inserts a parameter at the beginning of the current parameter list. - This method modifies the `_current_params` attribute, inserting the provided - parameter value at the front of the parameter tuple. + Deprecated for setup_cache; use :meth:`set_cache` instead so skip lists + are not polluted with the cache object. #### Parameters **param** (`Any`) @@ -570,6 +636,27 @@ """ self._current_params = tuple([param] + list(self._current_params)) + def _build_params(self): + """Benchmark call args: optional cache first, then declared params.""" + # Use ``is not None`` so falsy caches (0, [], {}) still prepend. + if self._current_cache is not None: + return (self._current_cache,) + tuple(self._current_params) + return tuple(self._current_params) + + @staticmethod + def _is_parameter_free_setup(setup): + """ + True if ``setup`` can be called with no positional arguments. + + Used so module-level ``setup(*args, **kwargs)`` runs before + ``setup_cache`` while class ``setup(self, n)`` waits for ``do_setup``. + """ + try: + inspect.signature(setup).bind() + except TypeError: + return False + return True + def check(self, root): """ Checks call syntax (argument count) for benchmark's setup, call, and teardown. @@ -629,10 +716,10 @@ return True try: for setup in self._setups: - setup(*self._current_params) + setup(*self._build_params()) except NotImplementedError as e: # allow skipping test - print(f"asv: skipped: {e !r} ") + print(f"asv: skipped: {e!r} ") return True return False @@ -648,17 +735,23 @@ # Skip return for teardown in self._teardowns: - teardown(*self._current_params) + teardown(*self._build_params()) def do_setup_cache(self): + # Run parameter-free setup hooks before setup_cache (asv#1592). + # Module-level ``setup(*args, **kwargs)`` (e.g. pandas random seed) is + # included; class setups that require benchmark params run in do_setup. if self._setup_cache is not None: + for setup in self._setups: + if self._is_parameter_free_setup(setup): + setup() return self._setup_cache() def do_run(self): if tuple(self._current_params) in self._skip_tuples: # Skip return - return self.run(*self._current_params) + return self.run(*self._build_params()) def do_profile(self, filename=None): """ @@ -701,5 +794,5 @@ self.redo_setup() profile.runctx( - code, {"run": self.func, "params": self._current_params}, {}, filename + code, {"run": self.func, "params": self._build_params()}, {}, filename ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/benchmarks/mark.py new/asv_runner-0.3.0/asv_runner/benchmarks/mark.py --- old/asv_runner-0.2.1/asv_runner/benchmarks/mark.py 2023-08-13 23:55:04.284114000 +0200 +++ new/asv_runner-0.3.0/asv_runner/benchmarks/mark.py 2026-08-04 13:53:18.022365000 +0200 @@ -420,7 +420,96 @@ return decorator +# Attributes ASV / asv_runner already honor on benchmark callables (asv#1469). +# Keep aligned with readers in benchmarks/_base.py and asv's attr discovery. +_BENCHMARK_ATTRS = frozenset( + { + "pretty_name", + "timeout", + "version", + "warmup_time", + "sample_time", + "number", + "repeat", + "rounds", + "min_run_count", + "processes", + "timer", + "params", + "param_names", + "skip_params", + "skip_benchmark", + "setup_cache_timeout", + "goal_time", + "max_time", + # timeraw: injected into timed subprocess (asv#1471) + "env", + } +) + + +def benchmark(**attrs): + """ + Decorator to set benchmark metadata attributes on a function or class. + + Equivalent to assigning attributes such as ``pretty_name`` or ``timeout`` + on the callable, with optional validation of known names. + + #### Parameters + **attrs** + : Keyword arguments stored as attributes on the decorated object. + Common keys: ``pretty_name``, ``timeout``, ``version``, ``warmup_time``, + ``sample_time``, ``number``, ``repeat``, ``rounds``, ``min_run_count``, + ``processes``, ``timer``, ``params``, ``param_names``. + + #### Returns + **decorator** (`function`) + : Decorator that applies the attributes and returns the original object + (classes) or a thin wrapper (functions) so attributes attach reliably. + + #### Example + ```{code-block} python + from asv_runner.benchmarks.mark import benchmark + + @benchmark(pretty_name="keys", timeout=60) + def time_keys(self): + for key in self.d.keys(): + pass + ``` + """ + unknown = set(attrs) - _BENCHMARK_ATTRS + if unknown: + raise TypeError( + "Unknown benchmark attribute(s): " + + ", ".join(sorted(unknown)) + + ". Known: " + + ", ".join(sorted(_BENCHMARK_ATTRS)) + ) + + def decorator(obj): + if inspect.isclass(obj): + for key, value in attrs.items(): + setattr(obj, key, value) + return obj + + if not callable(obj): + raise TypeError( + "The benchmark decorator can only be used with functions or classes" + ) + + @functools.wraps(obj) + def wrapper(*args, **kwargs): + return obj(*args, **kwargs) + + for key, value in attrs.items(): + setattr(wrapper, key, value) + return wrapper + + return decorator + + __all__ = [ + "benchmark", "parameterize", "skip_benchmark", "skip_benchmark_if", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/benchmarks/time.py new/asv_runner-0.3.0/asv_runner/benchmarks/time.py --- old/asv_runner-0.2.1/asv_runner/benchmarks/time.py 2023-08-20 15:38:25.433409000 +0200 +++ new/asv_runner-0.3.0/asv_runner/benchmarks/time.py 2026-08-04 13:53:18.022497000 +0200 @@ -1,3 +1,4 @@ +import gc import re import sys import timeit @@ -7,6 +8,39 @@ wall_timer = timeit.default_timer +class _SetupInterleavedTimer: + """ + ``timeit.Timer``-compatible batching for explicit ``number > 1`` with + setup hooks (asv#966): setup re-runs before every call via + ``redo_setup`` and only the calls accumulate into the sample, at the + cost of two clock reads per call. Garbage collection is disabled for + the duration of a sample, matching ``timeit.Timer.timeit``. + """ + + def __init__(self, func, redo_setup, timer): + self._func = func + self._redo_setup = redo_setup + self._timer = timer + + def timeit(self, number): + func = self._func + redo_setup = self._redo_setup + timer = self._timer + total = 0.0 + gcold = gc.isenabled() + gc.disable() + try: + for _ in range(number): + redo_setup() + start = timer() + func() + total += timer() - start + finally: + if gcold: + gc.enable() + return total + + class TimeBenchmark(Benchmark): """ Represents a single benchmark for timing. @@ -42,6 +76,13 @@ name_regex = re.compile("^(Time[A-Z_].+)|(time_.+)$") + # With setup hooks present, every timed call observes freshly + # set-up state (asv#966): auto-calibrated number resolves to 1, and + # an explicit number > 1 batches through _SetupInterleavedTimer. + # Subclasses whose samples do not share in-process state (timeraw: + # one subprocess per sample) opt out. + _setup_isolates_timed_calls = True + def __init__(self, name, func, attr_sources): """ Initialize a new instance of `TimeBenchmark`. @@ -82,18 +123,26 @@ self._load_vars() return result - def _get_timer(self, *param): - """Get a `timeit.Timer` for the current benchmark.""" + def _param_bound_func(self, *param): if param: def func(): self.func(*param) - else: - func = self.func + return func + return self.func + + def _get_timer(self, *param): + """Get a `timeit.Timer` for the current benchmark.""" + func = self._param_bound_func(*param) timer = timeit.Timer(stmt=func, setup=self.redo_setup, timer=self.timer) return timer + def _get_interleaved_timer(self, *param): + """Get a per-call-timed batch timer that re-runs setup between calls.""" + func = self._param_bound_func(*param) + return _SetupInterleavedTimer(func, self.redo_setup, self.timer) + def run(self, *param): """ Run the benchmark with the given parameters. @@ -137,7 +186,6 @@ # Transient effects exist also on CPython, e.g. from # OS scheduling warmup_time = 0.1 - timer = self._get_timer(*param) try: min_repeat, max_repeat, max_time = self.repeat @@ -162,13 +210,30 @@ max_repeat = int(max_repeat) max_time = float(max_time) + number = self.number + # timeit(number=N) runs setup once, then the stmt N times: setup + # (via redo_setup) is per sample, never between the inner calls. + # With setup hooks present every timed call must observe freshly + # set-up state (asv#966): auto number resolves to 1 under plain + # timeit, and an explicit number > 1 batches through + # _SetupInterleavedTimer (setup between calls, calls timed + # individually). Without setup hooks, plain timeit batching is + # unchanged. + isolate = self._setups and self._setup_isolates_timed_calls + if isolate and number == 0: + number = 1 + if isolate and number > 1: + timer = self._get_interleaved_timer(*param) + else: + timer = self._get_timer(*param) + samples, number = self.benchmark_timing( timer, min_repeat, max_repeat, max_time=max_time, warmup_time=warmup_time, - number=self.number, + number=number, min_run_count=self.min_run_count, ) @@ -205,7 +270,10 @@ : The time spent warming up the benchmark. **number** (`int`) - : The number of executions of the setup statement. + : The number of executions of the timed statement per sample + (``timeit``'s ``number``). Without setup hooks, setup runs once + per sample; with setup hooks the timer re-runs setup before + every execution and only the executions are timed. **min_run_count** (`int`) : The minimum number of runs required for the benchmark. @@ -275,7 +343,12 @@ elif warmup_time > 0: # Warmup while True: - self._redo_setup_next = False + if not self._setups: + # Skipping redo_setup keeps warmup cheap; with setup + # hooks present, warmup must observe the same + # freshly-set-up state as the measurement loop below + # (asv#966). + self._redo_setup_next = False timing = timer.timeit(number) run_count += number if wall_timer() >= start_time + warmup_time: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/benchmarks/timeraw.py new/asv_runner-0.3.0/asv_runner/benchmarks/timeraw.py --- old/asv_runner-0.2.1/asv_runner/benchmarks/timeraw.py 2023-08-06 19:20:14.069301400 +0200 +++ new/asv_runner-0.3.0/asv_runner/benchmarks/timeraw.py 2026-08-04 13:53:18.022497000 +0200 @@ -1,35 +1,68 @@ +import os import re import subprocess import sys import textwrap +from hashlib import sha256 -from ._base import _get_first_attr +from ._base import _get_first_attr, code_fingerprint from .time import TimeBenchmark +def _normalize_timeraw_env(env): + """ + Normalize a timeraw ``env`` mapping once (load time). + + #### Parameters + **env** (`dict` or `None`) + : Extra environment variables for the timed subprocess, or ``None``. + + #### Returns + **out** (`dict` or `None`) + : Mapping of ``str`` keys to ``str`` values, or ``None`` if unused/empty. + + #### Raises + **TypeError** + : If ``env`` is not a ``dict``, or any value is ``None`` (cannot be a real + env entry; refusing to coerce to the string ``"None"``). + """ + if env is None: + return None + if not isinstance(env, dict): + raise TypeError( + f"timeraw benchmark attribute 'env' must be a dict, got {type(env)!r}" + ) + if not env: + return None + out = {} + for key, value in env.items(): + if value is None: + raise TypeError( + f"timeraw env values must not be None (key {key!r}); " + "omit the key instead" + ) + out[str(key)] = str(value) + return out + + +def _env_fingerprint(env): + """ + Stable, collision-resistant string for ``env`` in benchmark version identity. + + Uses ``repr(sorted(items))`` so values containing newlines or ``=`` cannot + alias another mapping (e.g. ``{"A": "1\\nB=2"}`` vs ``{"A": "1", "B": "2"}``). + """ + if not env: + return "" + # Sorted (key, value) pairs; repr is unambiguous for str pairs on 3.7+. + return repr(sorted(env.items())) + + class _SeparateProcessTimer: """ - This class provides a timer that runs a given function in a separate Python - process. + Timer that runs a statement in a separate Python process via ``timeit``. - The function should return the statement to be timed. This statement is - executed using the Python timeit module in a new Python process. The - execution time is then returned. - - #### Attributes - **subprocess_tmpl** (`str`) - : The template Python code to be run in the subprocess. It imports necessary - modules and prints the execution time of the statement. - - **func** (`callable`) - : The function to be timed. This function should return a string of Python - code to be executed, or a tuple of two strings: the code to be executed and - the setup code to be run before timing. - - #### Methods - **timeit(number)** - : Run the function's code `number` times in a separate Python process, and - return the execution time. + **env** must already be normalized (or ``None``); not re-validated here. """ subprocess_tmpl = textwrap.dedent( @@ -41,28 +74,18 @@ ''' ).strip() - def __init__(self, func): + def __init__(self, func, env=None): self.func = func + self.env = env + + def _child_environ(self): + """Explicit child environ: parent mapping plus optional overrides.""" + child = dict(os.environ) + if self.env: + child.update(self.env) + return child def timeit(self, number): - """ - Run the function's code `number` times in a separate Python process, and - return the execution time. - - #### Parameters - **number** (`int`) - : The number of times to execute the function's code. - - #### Returns - **time** (`float`) - : The time it took to execute the function's code `number` times. - - #### Notes - The function's code is executed in a separate Python process to avoid - interference from the parent process. The function can return either a - single string of code to be executed, or a tuple of two strings: the - code to be executed and the setup code to be run before timing. - """ stmt = self.func() if isinstance(stmt, tuple): stmt, setup = stmt @@ -75,85 +98,83 @@ code = self.subprocess_tmpl.format(stmt=stmt, setup=setup, number=number) - res = subprocess.check_output([sys.executable, "-c", code]) - return float(res.strip()) + evaler = textwrap.dedent( + """ + import sys + code = sys.stdin.read() + exec(code) + """ + ) + + proc = subprocess.Popen( + [sys.executable, "-c", evaler], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=self._child_environ(), + ) + stdout, stderr = proc.communicate(input=code.encode("utf-8")) + # Forward timed-process stderr to this process so asv can record it + # (e.g. timeraw_count writes markers for number*repeat checks). + if stderr: + sys.stderr.write(stderr.decode("utf-8", errors="replace")) + sys.stderr.flush() + if proc.returncode != 0: + raise RuntimeError(f"Subprocess failed: {stderr.decode()}") + + return float(stdout.decode("utf-8").strip()) class TimerawBenchmark(TimeBenchmark): """ - Represents a benchmark for tracking timing benchmarks run once in - a separate process. - - This class inherits from `TimeBenchmark` and modifies it to run the - benchmark function in a separate process. This is useful for isolating the - benchmark from any potential side effects caused by other Python code - running in the same process. - - #### Attributes - **name_regex** (`re.Pattern`) - : The regular expression used to match the names of functions that should be - considered as raw timing benchmarks. - - **number** (`int`) - : The number of times to execute the function's code. By default, the - function's code is executed once. - - #### Methods - **_load_vars()** - : Loads variables for the benchmark from the function's attributes or from - default values. + Timing benchmark executed once per sample in a separate process. - **_get_timer(*param)** - : Returns a timer that runs the benchmark function in a separate process. - - **do_profile(filename=None)** - : Raises a ValueError. Raw timing benchmarks cannot be profiled. + Set ``env = {"KEY": "value"}`` on the function or class (or use + ``@benchmark(env={...})``) to inject variables into the **timed** child + (airspeed-velocity/asv#1471). Default ``version`` includes a fingerprint of + that mapping so env-only changes invalidate results. """ name_regex = re.compile("^(Timeraw[A-Z_].+)|(timeraw_.+)$") + # Each sample is a fresh subprocess, so in-process setup hooks never + # leak state between samples; the number applied inside the child to + # the returned (stmt, setup) sources is out of scope for the + # setup-hook check in TimeBenchmark.run. + _setup_isolates_timed_calls = False + + def __init__(self, name, func, attr_sources): + TimeBenchmark.__init__(self, name, func, attr_sources) + explicit_version = _get_first_attr(attr_sources, "version", None) + if explicit_version is None: + env_fp = _env_fingerprint( + _normalize_timeraw_env(_get_first_attr(attr_sources, "env", None)) + ) + if env_fp: + payload = self.code + "\n# timeraw_env\n" + env_fp + self.version = sha256(payload.encode("utf-8")).hexdigest() + token = code_fingerprint(payload) + self.version_alts = (token,) if token != self.version else () + def _load_vars(self): - """ - Loads variables for the benchmark from the function's attributes or from - default values. - """ TimeBenchmark._load_vars(self) self.number = int(_get_first_attr(self._attr_sources, "number", 1)) + self._timeraw_env = _normalize_timeraw_env( + _get_first_attr(self._attr_sources, "env", None) + ) del self.timer def _get_timer(self, *param): - """ - Returns a timer that runs the benchmark function in a separate process. - - #### Parameters - **param** (`tuple`) - : The parameters to pass to the benchmark function. - - #### Returns - **timer** (`_SeparateProcessTimer`) - : A timer that runs the function in a separate process. - """ if param: def func(): - self.func(*param) + return self.func(*param) else: func = self.func - return _SeparateProcessTimer(func) + return _SeparateProcessTimer(func, env=self._timeraw_env) def do_profile(self, filename=None): - """ - Raises a ValueError. Raw timing benchmarks cannot be profiled. - - #### Parameters - **filename** (`str`, optional) - : The name of the file to which to save the profile. Default is None. - - #### Raises - **ValueError** - : Always. Raw timing benchmarks cannot be profiled. - """ raise ValueError("Raw timing benchmarks cannot be profiled") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/console.py new/asv_runner-0.3.0/asv_runner/console.py --- old/asv_runner-0.2.1/asv_runner/console.py 2023-09-11 05:19:52.171772500 +0200 +++ new/asv_runner-0.3.0/asv_runner/console.py 2026-08-04 13:53:18.022497000 +0200 @@ -233,7 +233,7 @@ def truncate_left(s, l): - return f"...{s[-(l - 3):]}" if len(s) > l else s + return f"...{s[-(l - 3) :]}" if len(s) > l else s class Log: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/run.py new/asv_runner-0.3.0/asv_runner/run.py --- old/asv_runner-0.2.1/asv_runner/run.py 2023-08-20 04:12:14.535963500 +0200 +++ new/asv_runner-0.3.0/asv_runner/run.py 2026-08-04 13:53:18.022497000 +0200 @@ -21,7 +21,7 @@ CPU affinity based on them. It then creates a benchmark from the `benchmark_id`. If the benchmark has a setup cache key, it loads the cache from a file and inserts it - into the benchmark parameters. + via ``set_cache`` (kept separate from declared params for skip lists). Then, the function runs the setup for the benchmark. If the setup indicates that the benchmark should be skipped, @@ -59,8 +59,9 @@ if benchmark.setup_cache_key is not None: with open("cache.pickle", "rb") as fd: cache = pickle.load(fd) - if cache is not None: - benchmark.insert_param(cache) + benchmark.set_cache(cache) + else: + benchmark.set_cache(None) skip = benchmark.do_setup() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/server.py new/asv_runner-0.3.0/asv_runner/server.py --- old/asv_runner-0.2.1/asv_runner/server.py 2023-08-06 19:20:14.069301400 +0200 +++ new/asv_runner-0.3.0/asv_runner/server.py 2026-08-04 13:53:18.022497000 +0200 @@ -47,7 +47,7 @@ data += s if not s: raise RuntimeError( - "did not receive data from socket " f"(size {size}, got only {data !r})" + f"did not receive data from socket (size {size}, got only {data!r})" ) return data @@ -157,7 +157,7 @@ cwd = command.pop("cwd") if command: - raise RuntimeError(f"Command contained unknown data: {command_text !r}") + raise RuntimeError(f"Command contained unknown data: {command_text!r}") # Spawn benchmark run_args = ( @@ -230,6 +230,14 @@ except KeyboardInterrupt: break finally: - conn.close() + # Parent may deliver SIGINT during teardown (asv#1511); do not let + # cleanup raise KeyboardInterrupt and spam the benchmark log. + try: + conn.close() + except KeyboardInterrupt: + pass if stdout_file is not None: - os.unlink(stdout_file) + try: + os.unlink(stdout_file) + except (KeyboardInterrupt, OSError): + pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/asv_runner/util.py new/asv_runner-0.3.0/asv_runner/util.py --- old/asv_runner-0.2.1/asv_runner/util.py 2023-08-06 19:20:14.072634700 +0200 +++ new/asv_runner-0.3.0/asv_runner/util.py 2026-08-04 13:53:18.022497000 +0200 @@ -107,6 +107,8 @@ ("h", 60 * 60), ("d", 60 * 60 * 24), ("w", 60 * 60 * 24 * 7), + ("M", int(60 * 60 * 24 * 7 * 52 / 12)), + ("q", int(60 * 60 * 24 * 7 * 52 / 4)), ("y", 60 * 60 * 24 * 7 * 52), ("C", 60 * 60 * 24 * 7 * 52 * 100), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/pyproject.toml new/asv_runner-0.3.0/pyproject.toml --- old/asv_runner-0.2.1/pyproject.toml 2024-02-17 15:11:32.146874000 +0100 +++ new/asv_runner-0.3.0/pyproject.toml 2026-08-04 13:53:21.868299200 +0200 @@ -23,12 +23,12 @@ "Topic :: System :: Benchmark", ] dependencies = [ - "importlib-metadata", + "importlib-metadata; python_version < '3.8'", ] requires-python = ">=3.7" readme = "README.md" dynamic = [] -version = "0.2.1" +version = "0.3.0" [project.license] text = "BSD-3-Clause" @@ -59,6 +59,8 @@ [tool.ruff] line-length = 88 + +[tool.ruff.lint] extend-ignore = [ "E741", ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/tests/test_cache_skip_setup.py new/asv_runner-0.3.0/tests/test_cache_skip_setup.py --- old/asv_runner-0.2.1/tests/test_cache_skip_setup.py 1970-01-01 01:00:00.000000000 +0100 +++ new/asv_runner-0.3.0/tests/test_cache_skip_setup.py 2026-08-04 13:53:18.024187600 +0200 @@ -0,0 +1,126 @@ +# Regression tests for asv_runner#49 and airspeed-velocity/asv#1592. +# Drive shipped Benchmark / mark APIs only (no re-implementation). + + +import os +import sys +import unittest + +# Allow running without install: repo root on path +_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if _ROOT not in sys.path: + sys.path.insert(0, _ROOT) + +from asv_runner.benchmarks._base import Benchmark # noqa: E402 +from asv_runner.benchmarks.mark import benchmark, skip_for_params # noqa: E402 +from asv_runner.benchmarks.track import TrackBenchmark # noqa: E402 + + +def _make_track_benchmark(func, name="mod.track_x"): + """Build a real TrackBenchmark from a plain function (shipped ctor).""" + # attr_sources: function first so decorators / attrs on func win. + return TrackBenchmark(name, func, [func]) + + +class TestCacheSkipAndSetup(unittest.TestCase): + def test_skip_params_ignores_cache_object(self): + # Declared skip is (2,); cache must not be part of the skip key. + @skip_for_params([(2,)]) + def track_fn(cache, n): + return n + + track_fn.params = [1, 2] + track_fn.param_names = ["n"] + + b = _make_track_benchmark(track_fn, "mod.track_fn") + self.assertEqual(b._skip_tuples, [(2,)]) + + b.set_param_idx(1) # n == 2 + cache_obj = {"payload": 99} + b.set_cache(cache_obj) + + # Skip key is declared params only (asv_runner#49). + self.assertTrue(b.do_setup()) + self.assertIsNone(b.do_run()) + self.assertEqual(b._build_params(), (cache_obj, 2)) + + b.set_param_idx(0) # n == 1 — not skipped + b.set_cache(cache_obj) + self.assertFalse(b.do_setup()) + self.assertEqual(b.do_run(), 1) + + def test_falsy_cache_still_prepended(self): + def track_fn(cache, n): + return (cache, n) + + track_fn.params = [3] + b = _make_track_benchmark(track_fn, "mod.track_falsy") + b.set_param_idx(0) + for falsy in (0, [], {}, ""): + b.set_cache(falsy) + self.assertEqual(b._build_params(), (falsy, 3)) + self.assertEqual(b.do_run(), (falsy, 3)) + + def test_none_cache_does_not_prepend(self): + def track_fn(n): + return n + + track_fn.params = [4] + b = _make_track_benchmark(track_fn, "mod.track_none_cache") + b.set_param_idx(0) + b.set_cache(None) + self.assertEqual(b._build_params(), (4,)) + self.assertEqual(b.do_run(), 4) + + def test_parameter_free_setup_runs_before_setup_cache(self): + order = [] + + def module_setup(*args, **kwargs): + order.append("module_setup") + + def class_setup(n): + order.append("class_setup") + raise AssertionError("class setup must not run in do_setup_cache") + + def setup_cache(): + order.append("setup_cache") + return {"ok": True} + + def track_fn(cache, n): + return n + + track_fn.params = [1] + track_fn.setup = class_setup + track_fn.setup_cache = setup_cache + + b = _make_track_benchmark(track_fn, "mod.track_seed") + # Inject module-level setup at front of MRO-style list (as discovery does). + b._setups = [module_setup, class_setup] + b._setup_cache = setup_cache + + out = b.do_setup_cache() + self.assertEqual(out, {"ok": True}) + self.assertEqual(order, ["module_setup", "setup_cache"]) + self.assertTrue(Benchmark._is_parameter_free_setup(module_setup)) + self.assertFalse(Benchmark._is_parameter_free_setup(class_setup)) + + def test_benchmark_decorator_attrs_and_unknown(self): + @benchmark(pretty_name="Pretty", timeout=12.5, max_time=30.0) + def track_fn(): + return 0 + + self.assertEqual(track_fn.pretty_name, "Pretty") + self.assertEqual(track_fn.timeout, 12.5) + self.assertEqual(track_fn.max_time, 30.0) + + with self.assertRaises(TypeError) as ctx: + + @benchmark(not_a_real_attr=1) + def bad(): + pass + + self.assertIn("Unknown benchmark attribute", str(ctx.exception)) + + +if __name__ == "__main__": + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/tests/test_hash_and_timing.py new/asv_runner-0.3.0/tests/test_hash_and_timing.py --- old/asv_runner-0.2.1/tests/test_hash_and_timing.py 1970-01-01 01:00:00.000000000 +0100 +++ new/asv_runner-0.3.0/tests/test_hash_and_timing.py 2026-08-04 13:53:18.024634600 +0200 @@ -0,0 +1,80 @@ +# asv_runner#43 hashing + asv_runner#33 timing scale regression. + +import os +import sys +import time +import unittest + +_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if _ROOT not in sys.path: + sys.path.insert(0, _ROOT) + +from asv_runner.benchmarks._base import code_fingerprint # noqa: E402 +from asv_runner.benchmarks.time import TimeBenchmark # noqa: E402 + + +class TestCodeFingerprint(unittest.TestCase): + def test_whitespace_and_comment_stable(self): + a = "def f():\n return 1 + 2\n" + b = "def f():\n return 1+2 # trailing comment\n" + self.assertEqual(code_fingerprint(a), code_fingerprint(b)) + + def test_semantic_change_differs(self): + a = "def f():\n return 1\n" + b = "def f():\n return 2\n" + self.assertNotEqual(code_fingerprint(a), code_fingerprint(b)) + + +class TestTimingScale(unittest.TestCase): + """asv_runner#33: samples are per-call seconds, not systematically ~half.""" + + def test_sleep_sample_near_wall_duration(self): + delay = 0.05 + + def time_sleep(): + time.sleep(delay) + + time_sleep.number = 1 + time_sleep.repeat = 3 + time_sleep.warmup_time = 0 + time_sleep.sample_time = 0.01 + time_sleep.min_run_count = 1 + time_sleep.rounds = 1 + time_sleep.processes = 1 + + b = TimeBenchmark("m.time_sleep", time_sleep, [time_sleep]) + b._load_vars() + result = b.run() + samples = result["samples"] + self.assertEqual(result["number"], 1) + # Each sample is total timeit time / number => ~delay seconds. + median = sorted(samples)[len(samples) // 2] + # Allow OS scheduling noise; fail if ~50% systematic (would be ~0.025). + self.assertGreater(median, delay * 0.7, msg=f"samples={samples!r}") + self.assertLess(median, delay * 2.5, msg=f"samples={samples!r}") + + +if __name__ == "__main__": + unittest.main() + + +class TestVersionBackfill(unittest.TestCase): + def test_primary_version_is_sha256_code_alts_token(self): + from hashlib import sha256 + + from asv_runner.benchmarks._base import code_fingerprint + from asv_runner.benchmarks.track import TrackBenchmark + + def track_x(): + """doc""" + return 1 + + b = TrackBenchmark("m.track_x", track_x, [track_x]) + # getsourcelines works for real functions in this file + expected = sha256(b.code.encode("utf-8")).hexdigest() + self.assertEqual(b.version, expected) + token = code_fingerprint(b.code) + if token != expected: + self.assertIn(token, b.version_alts) + else: + self.assertEqual(b.version_alts, ()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/tests/test_setup_repeat.py new/asv_runner-0.3.0/tests/test_setup_repeat.py --- old/asv_runner-0.2.1/tests/test_setup_repeat.py 1970-01-01 01:00:00.000000000 +0100 +++ new/asv_runner-0.3.0/tests/test_setup_repeat.py 2026-08-04 13:53:18.024634600 +0200 @@ -0,0 +1,128 @@ +# asv#966: with setup hooks present, every timed call must observe +# freshly set-up state. Auto-calibrated number resolves to 1; warmup +# re-runs setup between calls; an explicitly set number is honored and +# batches with setup interleaved between individually timed calls. + +import os +import sys +import unittest + +_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if _ROOT not in sys.path: + sys.path.insert(0, _ROOT) + +from asv_runner.benchmarks.time import TimeBenchmark # noqa: E402 + + +def _make_benchmark(suite, method_name): + method = getattr(suite, method_name) + b = TimeBenchmark(f"Suite.{method_name}", method, [method, suite]) + b.do_setup() + return b + + +class TestSetupBeforeEachTimedCall(unittest.TestCase): + def test_auto_number_resolves_to_one_with_mutating_setup(self): + """Reproducer from asv#966 under default (CPython) warmup.""" + + class Suite: + repeat = 5 + min_run_count = 1 + rounds = 1 + + def setup(self): + self.x = [] + + def time_mutate(self): + assert len(self.x) == 0 + self.x.append(0) + + result = _make_benchmark(Suite(), "time_mutate").run() + self.assertEqual(result["number"], 1) + self.assertGreaterEqual(len(result["samples"]), 1) + + def test_explicit_number_one_survives_warmup(self): + """Warmup calls redo setup too: number=1 keeps its documented + semantics (asv#966, the explicit-number report).""" + + class Suite: + number = 1 + repeat = 3 + min_run_count = 1 + rounds = 1 + + def setup(self): + self.x = [] + + def time_mutate(self): + assert len(self.x) == 0 + self.x.append(0) + + result = _make_benchmark(Suite(), "time_mutate").run() + self.assertEqual(result["number"], 1) + self.assertGreaterEqual(len(result["samples"]), 1) + + def test_explicit_number_wins_over_setup(self): + """A benign setup keeps timeit batching when number is explicit.""" + + class Suite: + number = 7 + repeat = 2 + warmup_time = 0 + min_run_count = 1 + rounds = 1 + + def setup(self): + self.data = list(range(8)) + + def time_read(self): + return sum(self.data) + + result = _make_benchmark(Suite(), "time_read").run() + self.assertEqual(result["number"], 7) + self.assertGreaterEqual(len(result["samples"]), 1) + + def test_explicit_number_batches_with_fresh_setup(self): + """Explicit number>1 with a mutating setup: setup re-runs before + every call and the batch still reports the requested number.""" + + class Suite: + number = 10 + repeat = 3 + warmup_time = 0.05 + min_run_count = 1 + rounds = 1 + + def setup(self): + self.x = [] + + def time_mutate(self): + assert len(self.x) == 0 + self.x.append(0) + + result = _make_benchmark(Suite(), "time_mutate").run() + self.assertEqual(result["number"], 10) + self.assertGreaterEqual(len(result["samples"]), 1) + self.assertTrue(all(s >= 0 for s in result["samples"])) + + def test_auto_number_calibrates_without_setup(self): + def time_fast(): + return 1 + 1 + + time_fast.number = 0 # auto + time_fast.repeat = 2 + time_fast.warmup_time = 0 + time_fast.sample_time = 0.001 + time_fast.min_run_count = 1 + time_fast.rounds = 1 + time_fast.processes = 1 + + b = TimeBenchmark("m.time_fast", time_fast, [time_fast]) + b.do_setup() + result = b.run() + self.assertGreater(result["number"], 1) + self.assertGreaterEqual(len(result["samples"]), 1) + + +if __name__ == "__main__": + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/tests/test_smoke_import.py new/asv_runner-0.3.0/tests/test_smoke_import.py --- old/asv_runner-0.2.1/tests/test_smoke_import.py 1970-01-01 01:00:00.000000000 +0100 +++ new/asv_runner-0.3.0/tests/test_smoke_import.py 2026-08-04 13:53:18.024634600 +0200 @@ -0,0 +1,38 @@ +"""Smoke tests shipped with CI-only PRs (no behavior change to the package).""" + +import ast +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +PKG = ROOT / "asv_runner" + + +class TestSmokeImport(unittest.TestCase): + def test_import_package(self): + import asv_runner # noqa: F401 + + def test_import_public_entrypoint_modules(self): + # Subprocess CLI surface used by asv.benchmark + import asv_runner.check # noqa: F401 + import asv_runner.discovery # noqa: F401 + import asv_runner.run # noqa: F401 + import asv_runner.server # noqa: F401 + import asv_runner.setup_cache # noqa: F401 + import asv_runner.timing # noqa: F401 + + def test_package_is_py37_syntax(self): + # Static gate: no 3.8+ syntax nodes in runtime package. + for path in sorted(PKG.rglob("*.py")): + src = path.read_text(encoding="utf-8") + tree = ast.parse(src, filename=str(path)) + for node in ast.walk(tree): + name = type(node).__name__ + self.assertNotEqual(name, "NamedExpr", msg=str(path)) + self.assertNotEqual(name, "Match", msg=str(path)) + # feature_version is 3.8+ on the AST API; still validates parse on host + ast.parse(src, feature_version=(3, 7)) + + +if __name__ == "__main__": + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asv_runner-0.2.1/tests/test_timeraw_env.py new/asv_runner-0.3.0/tests/test_timeraw_env.py --- old/asv_runner-0.2.1/tests/test_timeraw_env.py 1970-01-01 01:00:00.000000000 +0100 +++ new/asv_runner-0.3.0/tests/test_timeraw_env.py 2026-08-04 13:53:18.024634600 +0200 @@ -0,0 +1,156 @@ +# Timeraw env design (asv#1471) + identity fingerprint. + +import os +import sys +import unittest + +_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if _ROOT not in sys.path: + sys.path.insert(0, _ROOT) + +from asv_runner.benchmarks.mark import benchmark # noqa: E402 +from asv_runner.benchmarks.timeraw import ( # noqa: E402 + TimerawBenchmark, + _normalize_timeraw_env, + _SeparateProcessTimer, +) + + +class TestTimerawEnvDesign(unittest.TestCase): + def test_normalize_rejects_non_dict_and_none_values(self): + self.assertIsNone(_normalize_timeraw_env(None)) + self.assertIsNone(_normalize_timeraw_env({})) + with self.assertRaises(TypeError): + _normalize_timeraw_env([("A", "1")]) + with self.assertRaises(TypeError): + _normalize_timeraw_env({"A": None}) + + def test_attribute_and_decorator_set_env(self): + def timeraw_a(): + return "pass" + + timeraw_a.env = {"IS_PERF": "1"} + b = TimerawBenchmark("m.timeraw_a", timeraw_a, [timeraw_a]) + b._load_vars() + self.assertEqual(b._timeraw_env["IS_PERF"], "1") + + @benchmark(env={"IS_PERF": "2"}) + def timeraw_b(): + return "pass" + + b2 = TimerawBenchmark("m.timeraw_b", timeraw_b, [timeraw_b]) + b2._load_vars() + self.assertEqual(b2._timeraw_env["IS_PERF"], "2") + + def test_child_environ_merge_and_override(self): + timer = _SeparateProcessTimer(lambda: "pass", env={"PATH": "override-path"}) + child = timer._child_environ() + self.assertEqual(child["PATH"], "override-path") + if "HOME" in os.environ: + self.assertEqual(child["HOME"], os.environ["HOME"]) + + def test_timeit_sees_env_in_subprocess(self): + def timeraw_probe(): + return """ + import os + assert os.environ.get("ASV_TIMERAW_PROBE") == "yes" + """ + + timeraw_probe.env = {"ASV_TIMERAW_PROBE": "yes"} + b = TimerawBenchmark("m.timeraw_probe", timeraw_probe, [timeraw_probe]) + b._load_vars() + elapsed = b._get_timer().timeit(1) + self.assertIsInstance(elapsed, float) + + def test_timer_does_not_renormalize(self): + timer = _SeparateProcessTimer(lambda: "pass", env={"K": "v"}) + self.assertEqual(timer.env["K"], "v") + + def test_env_changes_default_version(self): + def timeraw_x(): + return "pass" + + timeraw_x.env = {"A": "1"} + b1 = TimerawBenchmark("m.timeraw_x", timeraw_x, [timeraw_x]) + + def timeraw_y(): + return "pass" + + timeraw_y.env = {"A": "2"} + b2 = TimerawBenchmark("m.timeraw_y", timeraw_y, [timeraw_y]) + self.assertNotEqual(b1.version, b2.version) + + def timeraw_z(): + return "pass" + + timeraw_z.env = {"A": "1"} + timeraw_z.version = "fixed" + b3 = TimerawBenchmark("m.timeraw_z", timeraw_z, [timeraw_z]) + self.assertEqual(b3.version, "fixed") + + def test_env_fingerprint_no_newline_or_equals_collision(self): + from asv_runner.benchmarks.timeraw import _env_fingerprint + + a = _env_fingerprint({"A": "1\nB=2"}) + b = _env_fingerprint({"A": "1", "B": "2"}) + self.assertNotEqual(a, b) + + c = _env_fingerprint({"A": "B=1"}) + d = _env_fingerprint({"A=B": "1"}) + self.assertNotEqual(c, d) + + def timeraw_nl(): + return "pass" + + timeraw_nl.env = {"A": "1\nB=2"} + b_nl = TimerawBenchmark("m.timeraw_nl", timeraw_nl, [timeraw_nl]) + + def timeraw_two(): + return "pass" + + timeraw_two.env = {"A": "1", "B": "2"} + b_two = TimerawBenchmark("m.timeraw_two", timeraw_two, [timeraw_two]) + self.assertNotEqual(b_nl.version, b_two.version) + + def timeraw_eq_val(): + return "pass" + + timeraw_eq_val.env = {"A": "B=1"} + b_eq_val = TimerawBenchmark( + "m.timeraw_eq_val", timeraw_eq_val, [timeraw_eq_val] + ) + + def timeraw_eq_key(): + return "pass" + + timeraw_eq_key.env = {"A=B": "1"} + b_eq_key = TimerawBenchmark( + "m.timeraw_eq_key", timeraw_eq_key, [timeraw_eq_key] + ) + self.assertNotEqual(b_eq_val.version, b_eq_key.version) + + +if __name__ == "__main__": + unittest.main() + + +class TestTimerawStderrForward(unittest.TestCase): + def test_child_stderr_reaches_parent(self): + """asv test_timeraw_benchmark looks for markers on the runner process stderr.""" + import io + + from asv_runner.benchmarks.timeraw import _SeparateProcessTimer + + def stmt(): + return "import sys; sys.stderr.write('MARK')" + + timer = _SeparateProcessTimer(stmt) + buf = io.StringIO() + old = sys.stderr + try: + sys.stderr = buf + timer.timeit(3) + finally: + sys.stderr = old + self.assertIn("MARK", buf.getvalue()) + self.assertEqual(buf.getvalue().count("MARK"), 3)
