Skip to content

Commit

Permalink
Merge branch 'main' into python-313
Browse files Browse the repository at this point in the history
  • Loading branch information
trexfeathers authored Feb 24, 2025
2 parents addd8d7 + 5769d3b commit 26a21c2
Show file tree
Hide file tree
Showing 18 changed files with 868 additions and 807 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ concurrency:
jobs:
manifest:
name: "check-manifest"
uses: scitools/workflows/.github/workflows/[email protected].1
uses: scitools/workflows/.github/workflows/[email protected].2
2 changes: 1 addition & 1 deletion .github/workflows/refresh-lockfiles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ on:

jobs:
refresh_lockfiles:
uses: scitools/workflows/.github/workflows/[email protected].1
uses: scitools/workflows/.github/workflows/[email protected].2
secrets: inherit
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ repos:
additional_dependencies: [tomli]

- repo: https://github.com/PyCQA/flake8
rev: 7.1.1
rev: 7.1.2
hooks:
- id: flake8
types: [file, python]
Expand Down
51 changes: 5 additions & 46 deletions benchmarks/asv.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,20 @@
"project": "scitools-iris",
"project_url": "https://github.com/SciTools/iris",
"repo": "..",
"environment_type": "delegated",
"environment_type": "delegated-iris",
"show_commit_url": "https://github.com/scitools/iris/commit/",
"branches": ["upstream/main"],

"benchmark_dir": "./benchmarks",
"env_dir": ".asv/env",
"results_dir": ".asv/results",
"html_dir": ".asv/html",
"plugins": [".asv_delegated"],

"delegated_env_commands_comment": [
"The command(s) that create/update an environment correctly for the",
"checked-out commit. Command(s) format follows `build_command`:",
" https://asv.readthedocs.io/en/stable/asv.conf.json.html#build-command-install-command-uninstall-command",

"The commit key indicates the earliest commit where the command(s)",
"will work.",

"Differences from `build_command`:",
" * See: https://asv.readthedocs.io/en/stable/asv.conf.json.html#build-command-install-command-uninstall-command",
" * Env vars limited to those set outside build time.",
" (e.g. `{conf_dir}` available but `{build_dir}` not)",
" * Run in the same environment as the ASV install itself.",

"Mandatory format for the first 'command' within each commit:",
" * `ENV_PARENT=path/to/parent/directory/of/env-directory`",
" * Can contain env vars (e.g. `{conf_dir}`)",
" * `ENV_PARENT` available as `{env_parent}` in subsequent commands",
" * The environment will be detected as the most recently updated",
" environment in `{env_parent}`."

],
"delegated_env_commands": {
"59738a4": [
"ENV_PARENT={conf_dir}/.asv/env/nox313",
"PY_VER=3.13 nox --envdir={env_parent} --session=tests --install-only --no-error-on-external-run --verbose"
],
"c8a663a0": [
"ENV_PARENT={conf_dir}/.asv/env/nox312",
"PY_VER=3.12 nox --envdir={env_parent} --session=tests --install-only --no-error-on-external-run --verbose"
],
"d58fca7e": [
"ENV_PARENT={conf_dir}/.asv/env/nox311",
"PY_VER=3.11 nox --envdir={env_parent} --session=tests --install-only --no-error-on-external-run --verbose"
],
"44fae030": [
"ENV_PARENT={conf_dir}/.asv/env/nox310",
"PY_VER=3.10 nox --envdir={env_parent} --session=tests --install-only --no-error-on-external-run --verbose"
]
},
"plugins": [".asv_delegated_iris"],

"command_comment": [
"We know that the Nox command takes care of installation in each",
"environment, and in the case of Iris no specialised uninstall or",
"build commands are needed to get it working.",
"The inherited setup of the Iris test environment takes care of ",
"Iris-installation too, and in the case of Iris no specialised ",
"uninstall or build commands are needed to get it working either.",

"We do however need to install the custom benchmarks for them to be",
"usable."
Expand Down
195 changes: 46 additions & 149 deletions benchmarks/asv_delegated.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,116 +7,48 @@
Preps an environment via custom user scripts, then uses that as the
benchmarking environment.
This module is intended as the generic code that can be shared between
repositories. Providing a functional benchmarking environment relies on correct
subclassing of the :class:`Delegated` class to specialise it for the repo in
question.
"""

from abc import ABC, abstractmethod
from contextlib import contextmanager, suppress
from os import environ
from os.path import getmtime
from pathlib import Path
import sys

from asv import util as asv_util
from asv.console import log
from asv.environment import Environment, EnvironmentUnavailable
from asv.repo import Repo
from asv.util import ProcessError


class EnvPrepCommands:
"""A container for the environment preparation commands for a given commit.
Designed to read a value from the `delegated_env_commands` in the ASV
config, and validate that the command(s) are structured correctly.
"""

ENV_PARENT_VAR = "ENV_PARENT"
env_parent: Path
commands: list[str]

def __init__(self, environment: Environment, raw_commands: tuple[str]):
env_var = self.ENV_PARENT_VAR
raw_commands_list = list(raw_commands)

(first_command,) = environment._interpolate_commands(raw_commands_list[0])
env: dict
command, env, return_codes, cwd = first_command

valid = command == []
valid = valid and return_codes == {0}
valid = valid and cwd is None
valid = valid and list(env.keys()) == [env_var]
if not valid:
message = (
"First command MUST ONLY "
f"define the {env_var} env var, with no command e.g: "
f"`{env_var}=foo/`. Got: \n {raw_commands_list[0]}"
)
raise ValueError(message)

self.env_parent = Path(env[env_var]).resolve()
self.commands = raw_commands_list[1:]


class CommitFinder(dict[str, EnvPrepCommands]):
"""A specialised dict for finding the appropriate env prep script for a commit."""

def __call__(self, repo: Repo, commit_hash: str):
"""Return the latest env prep script that is earlier than the given commit."""

def validate_commit(commit: str, is_lookup: bool) -> None:
try:
_ = repo.get_date(commit)
except ProcessError:
if is_lookup:
message_start = "Lookup commit"
else:
message_start = "Requested commit"
repo_path = getattr(repo, "_path", "unknown")
message = f"{message_start}: {commit} not found in repo: {repo_path}"
raise KeyError(message)

for lookup in self.keys():
validate_commit(lookup, is_lookup=True)
validate_commit(commit_hash, is_lookup=False)

def parent_distance(parent_hash: str) -> int:
range_spec = repo.get_range_spec(parent_hash, commit_hash)
parents = repo.get_hashes_from_range(range_spec)

if parent_hash[:8] == commit_hash[:8]:
distance = 0
elif len(parents) == 0:
distance = -1
else:
distance = len(parents)
return distance

parentage = {commit: parent_distance(commit) for commit in self.keys()}
parentage = {k: v for k, v in parentage.items() if v >= 0}
if len(parentage) == 0:
message = f"No env prep script available for commit: {commit_hash} ."
raise KeyError(message)
else:
parentage = dict(sorted(parentage.items(), key=lambda item: item[1]))
commit = next(iter(parentage))
content = self[commit]
return content


class Delegated(Environment):
class Delegated(Environment, ABC):
"""Manage a benchmark environment using custom user scripts, run at each commit.
Ignores user input variations - ``matrix`` / ``pythons`` /
``exclude``, since environment is being managed outside ASV.
A vanilla :class:`asv.environment.Environment` is created for containing
the expected ASV configuration files and checked-out project. The actual
'functional' environment is created/updated using the command(s) specified
in the config ``delegated_env_commands``, then the location is recorded via
'functional' environment is created/updated using
:meth:`_prep_env_override`, then the location is recorded via
a symlink within the ASV environment. The symlink is used as the
environment path used for any executable calls (e.g.
``python my_script.py``).
Intended as the generic parent class that can be shared between
repositories. Providing a functional benchmarking environment relies on
correct subclassing of this class to specialise it for the repo in question.
Warnings
--------
:class:`Delegated` is an abstract base class. It MUST ONLY be used via
subclasses implementing their own :meth:`_prep_env_override`, and also
:attr:`tool_name`, which must be unique.
"""

tool_name = "delegated"
Expand Down Expand Up @@ -180,20 +112,6 @@ def __init__(self, conf, python, requirements, tagged_env_vars):
"""Preserves the 'true' path of the environment so that self._path can
be safely modified and restored."""

env_commands = getattr(conf, "delegated_env_commands")
try:
env_prep_commands = {
commit: EnvPrepCommands(self, commands)
for commit, commands in env_commands.items()
}
except ValueError as err:
message = f"Problem handling `delegated_env_commands`:\n{err}"
log.error(message)
raise EnvironmentUnavailable(message)
self._env_prep_lookup = CommitFinder(**env_prep_commands)
"""An object that can be called downstream to get the appropriate
env prep script for a given repo and commit."""

@property
def _path_delegated(self) -> Path:
"""The path of the symlink to the delegated environment."""
Expand Down Expand Up @@ -241,63 +159,42 @@ def _setup(self):
message += "Correct environment will be set up at the first commit checkout."
log.warning(message)

def _prep_env(self, repo: Repo, commit_hash: str) -> None:
@abstractmethod
def _prep_env_override(self, env_parent_dir: Path) -> Path:
"""Run aspects of :meth:`_prep_env` that vary between repos.
This is the method that is expected to do the preparing
(:meth:`_prep_env` only performs pre- and post- steps). MUST be
overridden in any subclass environments before they will work.
Parameters
----------
env_parent_dir : Path
The directory that the prepared environment should be placed in.
Returns
-------
Path
The path to the prepared environment.
"""
pass

def _prep_env(self, commit_hash: str) -> None:
"""Prepare the delegated environment for the given commit hash."""
message = (
f"Running delegated environment management for: {self.name} "
f"at commit: {commit_hash[:8]}"
)
log.info(message)

env_prep: EnvPrepCommands
try:
env_prep = self._env_prep_lookup(repo, commit_hash)
except KeyError as err:
message = f"Problem finding env prep commands: {err}"
log.error(message)
raise EnvironmentUnavailable(message)

env_parent = Path(self._env_dir).resolve()
new_env_per_commit = self.COMMIT_ENVS_VAR in environ
if new_env_per_commit:
env_parent = env_prep.env_parent / commit_hash[:8]
else:
env_parent = env_prep.env_parent

# See :meth:`Environment._interpolate_commands`.
# All ASV-namespaced env vars are available in the below format when
# interpolating commands:
# ASV_FOO_BAR = {foo_bar}
# We want the env parent path to be one of those available.
global_key = f"ASV_{EnvPrepCommands.ENV_PARENT_VAR}"
self._global_env_vars[global_key] = str(env_parent)

# The project checkout.
build_dir = Path(self._build_root) / self._repo_subdir

# Run the script(s) for delegated environment creation/updating.
# (An adaptation of :meth:`Environment._interpolate_and_run_commands`).
for command, env, return_codes, cwd in self._interpolate_commands(
env_prep.commands
):
local_envs = dict(environ)
local_envs.update(env)
if cwd is None:
cwd = str(build_dir)
_ = asv_util.check_output(
command,
timeout=self._install_timeout,
cwd=cwd,
env=local_envs,
valid_return_codes=return_codes,
)
env_parent = env_parent / commit_hash[:8]

delegated_env_path = self._prep_env_override(env_parent)
assert delegated_env_path.is_relative_to(env_parent)

# Find the environment created/updated by running env_prep.commands.
# The most recently updated directory in env_parent.
delegated_env_path = sorted(
env_parent.glob("*"),
key=getmtime,
reverse=True,
)[0]
# Record the environment's path via a symlink within this environment.
self._symlink_to_delegated(delegated_env_path)

Expand All @@ -307,7 +204,7 @@ def _prep_env(self, repo: Repo, commit_hash: str) -> None:
def checkout_project(self, repo: Repo, commit_hash: str) -> None:
"""Check out the working tree of the project at given commit hash."""
super().checkout_project(repo, commit_hash)
self._prep_env(repo, commit_hash)
self._prep_env(commit_hash)

@contextmanager
def _delegate_path(self):
Expand Down
Loading

0 comments on commit 26a21c2

Please sign in to comment.