77Preps an environment via custom user scripts, then uses that as the
88benchmarking environment.
99
10+ This module is intended as the generic code that can be shared between
11+ repositories. Providing a functional benchmarking environment relies on correct
12+ subclassing of the :class:`Delegated` class to specialise it for the repo in
13+ question.
14+
1015"""
1116
17+ from abc import ABC , abstractmethod
1218from contextlib import contextmanager , suppress
1319from os import environ
14- from os .path import getmtime
1520from pathlib import Path
1621import sys
1722
18- from asv import util as asv_util
1923from asv .console import log
2024from asv .environment import Environment , EnvironmentUnavailable
2125from asv .repo import Repo
22- from asv .util import ProcessError
23-
24-
25- class EnvPrepCommands :
26- """A container for the environment preparation commands for a given commit.
27-
28- Designed to read a value from the `delegated_env_commands` in the ASV
29- config, and validate that the command(s) are structured correctly.
30- """
31-
32- ENV_PARENT_VAR = "ENV_PARENT"
33- env_parent : Path
34- commands : list [str ]
35-
36- def __init__ (self , environment : Environment , raw_commands : tuple [str ]):
37- env_var = self .ENV_PARENT_VAR
38- raw_commands_list = list (raw_commands )
3926
40- (first_command ,) = environment ._interpolate_commands (raw_commands_list [0 ])
41- env : dict
42- command , env , return_codes , cwd = first_command
4327
44- valid = command == []
45- valid = valid and return_codes == {0 }
46- valid = valid and cwd is None
47- valid = valid and list (env .keys ()) == [env_var ]
48- if not valid :
49- message = (
50- "First command MUST ONLY "
51- f"define the { env_var } env var, with no command e.g: "
52- f"`{ env_var } =foo/`. Got: \n { raw_commands_list [0 ]} "
53- )
54- raise ValueError (message )
55-
56- self .env_parent = Path (env [env_var ]).resolve ()
57- self .commands = raw_commands_list [1 :]
58-
59-
60- class CommitFinder (dict [str , EnvPrepCommands ]):
61- """A specialised dict for finding the appropriate env prep script for a commit."""
62-
63- def __call__ (self , repo : Repo , commit_hash : str ):
64- """Return the latest env prep script that is earlier than the given commit."""
65-
66- def validate_commit (commit : str , is_lookup : bool ) -> None :
67- try :
68- _ = repo .get_date (commit )
69- except ProcessError :
70- if is_lookup :
71- message_start = "Lookup commit"
72- else :
73- message_start = "Requested commit"
74- repo_path = getattr (repo , "_path" , "unknown" )
75- message = f"{ message_start } : { commit } not found in repo: { repo_path } "
76- raise KeyError (message )
77-
78- for lookup in self .keys ():
79- validate_commit (lookup , is_lookup = True )
80- validate_commit (commit_hash , is_lookup = False )
81-
82- def parent_distance (parent_hash : str ) -> int :
83- range_spec = repo .get_range_spec (parent_hash , commit_hash )
84- parents = repo .get_hashes_from_range (range_spec )
85-
86- if parent_hash [:8 ] == commit_hash [:8 ]:
87- distance = 0
88- elif len (parents ) == 0 :
89- distance = - 1
90- else :
91- distance = len (parents )
92- return distance
93-
94- parentage = {commit : parent_distance (commit ) for commit in self .keys ()}
95- parentage = {k : v for k , v in parentage .items () if v >= 0 }
96- if len (parentage ) == 0 :
97- message = f"No env prep script available for commit: { commit_hash } ."
98- raise KeyError (message )
99- else :
100- parentage = dict (sorted (parentage .items (), key = lambda item : item [1 ]))
101- commit = next (iter (parentage ))
102- content = self [commit ]
103- return content
104-
105-
106- class Delegated (Environment ):
28+ class Delegated (Environment , ABC ):
10729 """Manage a benchmark environment using custom user scripts, run at each commit.
10830
10931 Ignores user input variations - ``matrix`` / ``pythons`` /
11032 ``exclude``, since environment is being managed outside ASV.
11133
11234 A vanilla :class:`asv.environment.Environment` is created for containing
11335 the expected ASV configuration files and checked-out project. The actual
114- 'functional' environment is created/updated using the command(s) specified
115- in the config ``delegated_env_commands` `, then the location is recorded via
36+ 'functional' environment is created/updated using
37+ :meth:`_prep_env_override `, then the location is recorded via
11638 a symlink within the ASV environment. The symlink is used as the
11739 environment path used for any executable calls (e.g.
11840 ``python my_script.py``).
11941
42+ Intended as the generic parent class that can be shared between
43+ repositories. Providing a functional benchmarking environment relies on
44+ correct subclassing of this class to specialise it for the repo in question.
45+
46+ Warnings
47+ --------
48+ :class:`Delegated` is an abstract base class. It MUST ONLY be used via
49+ subclasses implementing their own :meth:`_prep_env_override`, and also
50+ :attr:`tool_name`, which must be unique.
51+
12052 """
12153
12254 tool_name = "delegated"
@@ -180,20 +112,6 @@ def __init__(self, conf, python, requirements, tagged_env_vars):
180112 """Preserves the 'true' path of the environment so that self._path can
181113 be safely modified and restored."""
182114
183- env_commands = getattr (conf , "delegated_env_commands" )
184- try :
185- env_prep_commands = {
186- commit : EnvPrepCommands (self , commands )
187- for commit , commands in env_commands .items ()
188- }
189- except ValueError as err :
190- message = f"Problem handling `delegated_env_commands`:\n { err } "
191- log .error (message )
192- raise EnvironmentUnavailable (message )
193- self ._env_prep_lookup = CommitFinder (** env_prep_commands )
194- """An object that can be called downstream to get the appropriate
195- env prep script for a given repo and commit."""
196-
197115 @property
198116 def _path_delegated (self ) -> Path :
199117 """The path of the symlink to the delegated environment."""
@@ -241,63 +159,42 @@ def _setup(self):
241159 message += "Correct environment will be set up at the first commit checkout."
242160 log .warning (message )
243161
244- def _prep_env (self , repo : Repo , commit_hash : str ) -> None :
162+ @abstractmethod
163+ def _prep_env_override (self , env_parent_dir : Path ) -> Path :
164+ """Run aspects of :meth:`_prep_env` that vary between repos.
165+
166+ This is the method that is expected to do the preparing
167+ (:meth:`_prep_env` only performs pre- and post- steps). MUST be
168+ overridden in any subclass environments before they will work.
169+
170+ Parameters
171+ ----------
172+ env_parent_dir : Path
173+ The directory that the prepared environment should be placed in.
174+
175+ Returns
176+ -------
177+ Path
178+ The path to the prepared environment.
179+ """
180+ pass
181+
182+ def _prep_env (self , commit_hash : str ) -> None :
245183 """Prepare the delegated environment for the given commit hash."""
246184 message = (
247185 f"Running delegated environment management for: { self .name } "
248186 f"at commit: { commit_hash [:8 ]} "
249187 )
250188 log .info (message )
251189
252- env_prep : EnvPrepCommands
253- try :
254- env_prep = self ._env_prep_lookup (repo , commit_hash )
255- except KeyError as err :
256- message = f"Problem finding env prep commands: { err } "
257- log .error (message )
258- raise EnvironmentUnavailable (message )
259-
190+ env_parent = Path (self ._env_dir ).resolve ()
260191 new_env_per_commit = self .COMMIT_ENVS_VAR in environ
261192 if new_env_per_commit :
262- env_parent = env_prep .env_parent / commit_hash [:8 ]
263- else :
264- env_parent = env_prep .env_parent
265-
266- # See :meth:`Environment._interpolate_commands`.
267- # All ASV-namespaced env vars are available in the below format when
268- # interpolating commands:
269- # ASV_FOO_BAR = {foo_bar}
270- # We want the env parent path to be one of those available.
271- global_key = f"ASV_{ EnvPrepCommands .ENV_PARENT_VAR } "
272- self ._global_env_vars [global_key ] = str (env_parent )
273-
274- # The project checkout.
275- build_dir = Path (self ._build_root ) / self ._repo_subdir
276-
277- # Run the script(s) for delegated environment creation/updating.
278- # (An adaptation of :meth:`Environment._interpolate_and_run_commands`).
279- for command , env , return_codes , cwd in self ._interpolate_commands (
280- env_prep .commands
281- ):
282- local_envs = dict (environ )
283- local_envs .update (env )
284- if cwd is None :
285- cwd = str (build_dir )
286- _ = asv_util .check_output (
287- command ,
288- timeout = self ._install_timeout ,
289- cwd = cwd ,
290- env = local_envs ,
291- valid_return_codes = return_codes ,
292- )
193+ env_parent = env_parent / commit_hash [:8 ]
194+
195+ delegated_env_path = self ._prep_env_override (env_parent )
196+ assert delegated_env_path .is_relative_to (env_parent )
293197
294- # Find the environment created/updated by running env_prep.commands.
295- # The most recently updated directory in env_parent.
296- delegated_env_path = sorted (
297- env_parent .glob ("*" ),
298- key = getmtime ,
299- reverse = True ,
300- )[0 ]
301198 # Record the environment's path via a symlink within this environment.
302199 self ._symlink_to_delegated (delegated_env_path )
303200
@@ -307,7 +204,7 @@ def _prep_env(self, repo: Repo, commit_hash: str) -> None:
307204 def checkout_project (self , repo : Repo , commit_hash : str ) -> None :
308205 """Check out the working tree of the project at given commit hash."""
309206 super ().checkout_project (repo , commit_hash )
310- self ._prep_env (repo , commit_hash )
207+ self ._prep_env (commit_hash )
311208
312209 @contextmanager
313210 def _delegate_path (self ):
0 commit comments