1515from __future__ import annotations
1616
1717import contextlib
18+ import datetime
1819import enum
1920import hashlib
2021import os
2324import shutil
2425import subprocess
2526import sys
27+ import time
2628import unicodedata
2729from typing import (
2830 TYPE_CHECKING ,
2931 Any ,
3032 NoReturn ,
3133)
3234
35+ import humanize
36+
3337import nox .command
3438import nox .virtualenv
3539from nox .logger import logger
@@ -1100,9 +1104,11 @@ def execute(self) -> Result:
11001104 f"Prerequisite session { dependency .friendly_name } was not"
11011105 " successful"
11021106 ),
1107+ duration = 0 ,
11031108 )
11041109 return self .result
11051110
1111+ start = time .perf_counter ()
11061112 try :
11071113 cwd = os .path .realpath (os .path .dirname (self .global_config .noxfile ))
11081114
@@ -1113,42 +1119,83 @@ def execute(self) -> Result:
11131119 self .func (session )
11141120
11151121 # Nothing went wrong; return a success.
1116- self .result = Result (self , Status .SUCCESS )
1122+ self .result = Result (
1123+ self , Status .SUCCESS , duration = time .perf_counter () - start
1124+ )
11171125
11181126 except nox .virtualenv .InterpreterNotFound as exc :
11191127 if self .global_config .error_on_missing_interpreters :
1120- self .result = Result (self , Status .FAILED , reason = str (exc ))
1128+ self .result = Result (
1129+ self ,
1130+ Status .FAILED ,
1131+ reason = str (exc ),
1132+ duration = time .perf_counter () - start ,
1133+ )
11211134 else :
11221135 logger .warning (
11231136 "Missing interpreters will error by default on CI systems."
11241137 )
1125- self .result = Result (self , Status .SKIPPED , reason = str (exc ))
1138+ self .result = Result (
1139+ self ,
1140+ Status .SKIPPED ,
1141+ reason = str (exc ),
1142+ duration = time .perf_counter () - start ,
1143+ )
11261144
11271145 except _SessionQuit as exc :
1128- self .result = Result (self , Status .ABORTED , reason = str (exc ))
1146+ self .result = Result (
1147+ self ,
1148+ Status .ABORTED ,
1149+ reason = str (exc ),
1150+ duration = time .perf_counter () - start ,
1151+ )
11291152
11301153 except _SessionSkip as exc :
1131- self .result = Result (self , Status .SKIPPED , reason = str (exc ))
1154+ self .result = Result (
1155+ self ,
1156+ Status .SKIPPED ,
1157+ reason = str (exc ),
1158+ duration = time .perf_counter () - start ,
1159+ )
11321160
11331161 except nox .command .CommandFailed :
1134- self .result = Result (self , Status .FAILED )
1162+ self .result = Result (
1163+ self , Status .FAILED , duration = time .perf_counter () - start
1164+ )
11351165
11361166 except KeyboardInterrupt :
11371167 logger .error (f"Session { self .friendly_name } interrupted." )
11381168 raise
11391169
11401170 except Exception as exc :
11411171 logger .exception (f"Session { self .friendly_name } raised exception { exc !r} " )
1142- self .result = Result (self , Status .FAILED )
1172+ self .result = Result (
1173+ self , Status .FAILED , duration = time .perf_counter () - start
1174+ )
11431175
11441176 return self .result
11451177
11461178
1179+ def _duration_str (seconds : float , text : str ) -> str :
1180+ time_str = humanize .naturaldelta (datetime .timedelta (seconds = seconds ))
1181+
1182+ # Might be "a moment" if short, return empty string in that case
1183+ if time_str == "a moment" :
1184+ return ""
1185+
1186+ return text .format (time = time_str )
1187+
1188+
11471189class Result :
11481190 """An object representing the result of a session."""
11491191
11501192 def __init__ (
1151- self , session : SessionRunner , status : Status , reason : str | None = None
1193+ self ,
1194+ session : SessionRunner ,
1195+ status : Status ,
1196+ reason : str | None = None ,
1197+ * ,
1198+ duration : float = 0.0 ,
11521199 ) -> None :
11531200 """Initialize the Result object.
11541201
@@ -1157,10 +1204,12 @@ def __init__(
11571204 The session runner which ran.
11581205 status (~nox.sessions.Status): The final result status.
11591206 reason (str): Additional info.
1207+ duration (float): Time taken in seconds.
11601208 """
11611209 self .session = session
11621210 self .status = status
11631211 self .reason = reason
1212+ self .duration = duration
11641213
11651214 def __bool__ (self ) -> bool :
11661215 return self .status .value > 0
@@ -1173,12 +1222,12 @@ def imperfect(self) -> str:
11731222 str: A word or phrase representing the status.
11741223 """
11751224 if self .status == Status .SUCCESS :
1176- return "was successful"
1225+ return "was successful" + _duration_str ( self . duration , " in {time}" )
11771226
11781227 status = self .status .name .lower ()
11791228 if self .reason :
1180- return f" { status } : { self .reason } "
1181-
1229+ duration_err = _duration_str ( self .duration , " (took {time})" )
1230+ return f" { status } : { self . reason } { duration_err } "
11821231 return status
11831232
11841233 def log (self , message : str ) -> None :
@@ -1208,4 +1257,5 @@ def serialize(self) -> dict[str, Any]:
12081257 "result" : self .status .name .lower (),
12091258 "result_code" : self .status .value ,
12101259 "signatures" : self .session .signatures ,
1260+ "duration" : self .duration ,
12111261 }
0 commit comments