11from __future__ import annotations
2+
23import collections
4+ import dataclasses
35import json
6+ import os
47import pathlib
5- from typing import TypeVar , Literal
8+ from typing import TypeVar , Literal , TYPE_CHECKING
69
710import pytest
11+ from _pytest .stash import StashKey
812
9- T = TypeVar ("T" )
13+ if TYPE_CHECKING :
14+ from typing_extensions import TypeAlias
1015
16+ T = TypeVar ("T" )
17+ JustifyItemsStrategy : TypeAlias = Literal ["none" , "file" , "scope" ]
1118
12- @pytest .hookimpl
13- def pytest_addoption (parser : pytest .Parser ) -> None :
14- group = parser .getgroup ("cdist" )
15- group .addoption ("--cdist-group" , action = "store" , default = None )
16- group .addoption ("--cdist-report" , action = "store_true" , default = False )
17- group .addoption (
18- "--cdist-report-dir" , action = "store" , default = "." , type = pathlib .Path
19- )
20- group .addoption ("--cdist-justify-items" , action = "store" , default = "none" )
21- group .addoption (
22- "--cdist-group-steal" ,
23- action = "store" ,
24- default = None ,
25- help = "make a group steal a percentage of items from other groups. '1:30' would "
26- "make group 1 steal 30 % of items from all other groups)" ,
27- )
19+ _CDIST_CONFIG_KEY = StashKey ["CdistConfig | None" ]()
2820
2921
3022def _partition_list (items : list [T ], chunk_size : int ) -> list [list [T ]]:
@@ -83,6 +75,9 @@ def _justify_items(
8375
8476 last_file = get_boundary (items [- 1 ])
8577 next_group = groups [i + 1 if i < (len (groups ) - 1 ) else 0 ]
78+ if not next_group :
79+ continue
80+
8681 next_file = get_boundary (next_group [0 ])
8782
8883 if last_file == next_file :
@@ -118,53 +113,143 @@ def _justify_xdist_groups(groups: list[list[pytest.Item]]) -> list[list[pytest.I
118113 return groups
119114
120115
116+ @dataclasses .dataclass
117+ class CdistConfig :
118+ current_group : int
119+ total_groups : int
120+ justify_items_strategy : JustifyItemsStrategy = "none"
121+ group_steal : tuple [int , int ] | None = None
122+ write_report : bool = False
123+ report_dir : pathlib .Path = pathlib .Path ("." )
124+
125+ def cli_options (self , config : pytest .Config ) -> str :
126+ opts = [f"--cdist-group={ self .current_group + 1 } /{ self .total_groups } " ]
127+
128+ if (
129+ self .justify_items_strategy != "none"
130+ and "cdist-justify-items" not in config .inicfg
131+ ):
132+ opts .append (f"--cdist-justify-items={ self .justify_items_strategy } " )
133+
134+ if self .group_steal and "cdist-group-steal" not in config .inicfg :
135+ opts .append (
136+ f"--cdist-group-steal={ self .group_steal [0 ] + 1 } :{ self .group_steal [1 ]} "
137+ )
138+
139+ if self .write_report :
140+ opts .append ("--cdist-report" )
141+
142+ if (
143+ self .report_dir != pathlib .Path ("." )
144+ and "cdist-report-dir" not in config .inicfg
145+ ):
146+ opts .append (f"--cdist-report-dir={ str (self .report_dir )} " )
147+
148+ return " " .join (opts )
149+
150+ @classmethod
151+ def from_pytest_config (cls , config : pytest .Config ) -> CdistConfig | None :
152+ cdist_option = config .getoption ("cdist_group" )
153+
154+ if cdist_option is None :
155+ return None
156+
157+ report_dir = pathlib .Path (
158+ config .getoption ("cdist_report_dir" , None )
159+ or config .getini ("cdist-report-dir" )
160+ )
161+
162+ write_report : bool = config .getoption ("cdist_report" )
163+
164+ justify_items_strategy : JustifyItemsStrategy = config .getoption (
165+ "cdist_justify_items" ,
166+ default = None ,
167+ ) or config .getini ("cdist-justify-items" )
168+
169+ group_steal = _get_group_steal_opt (
170+ config .getoption ("cdist_group_steal" ) or config .getini ("cdist-group-steal" )
171+ )
172+
173+ current_group , total_groups = map (int , cdist_option .split ("/" ))
174+ if not 0 < current_group <= total_groups :
175+ raise pytest .UsageError (f"Unknown group { current_group } " )
176+
177+ # using whole numbers (2/2) is more intuitive for the CLI,
178+ # but here we want to use the group numbers for zero-based indexing
179+ current_group -= 1
180+
181+ return cls (
182+ total_groups = total_groups ,
183+ current_group = current_group ,
184+ report_dir = report_dir ,
185+ write_report = write_report ,
186+ justify_items_strategy = justify_items_strategy ,
187+ group_steal = group_steal ,
188+ )
189+
190+
191+ @pytest .hookimpl
192+ def pytest_addoption (parser : pytest .Parser ) -> None :
193+ group = parser .getgroup ("cdist" )
194+ group .addoption ("--cdist-group" , action = "store" , default = None )
195+ group .addoption ("--cdist-report" , action = "store_true" , default = False )
196+ group .addoption ("--cdist-report-dir" , action = "store" )
197+ group .addoption ("--cdist-justify-items" , action = "store" )
198+ group .addoption (
199+ "--cdist-group-steal" ,
200+ action = "store" ,
201+ default = None ,
202+ help = "make a group steal a percentage of items from other groups. '1:30' would "
203+ "make group 1 steal 30%% of items from all other groups)" ,
204+ )
205+
206+ parser .addini ("cdist-justify-items" , help = "justify items strategy" , default = "none" )
207+ parser .addini (
208+ "cdist-report-dir" , help = "cdist report dir" , default = "." , type = "paths"
209+ )
210+ parser .addini ("cdist-group-steal" , help = "cdist group steal" , default = None )
211+
212+
213+ def pytest_configure (config : pytest .Config ) -> None :
214+ cdist_config = CdistConfig .from_pytest_config (config )
215+ config .stash [_CDIST_CONFIG_KEY ] = cdist_config
216+
217+
121218def pytest_collection_modifyitems (
122219 session : pytest .Session , config : pytest .Config , items : list [pytest .Item ]
123220) -> None :
124- cdist_option = config .getoption ( "cdist_group" )
221+ cdist_config = config .stash . get ( _CDIST_CONFIG_KEY , None )
125222
126- if cdist_option is None :
223+ if cdist_config is None :
127224 return
128225
129- report_dir : pathlib .Path = config .getoption ("cdist_report_dir" )
130- write_report : bool = config .getoption ("cdist_report" )
131- justify_items_strategy : Literal ["none" , "file" , "scope" ] = config .getoption (
132- "cdist_justify_items"
133- )
134- group_steal = _get_group_steal_opt (config .getoption ("cdist_group_steal" ))
135-
136- current_group , total_groups = map (int , cdist_option .split ("/" ))
137- if not 0 < current_group <= total_groups :
138- raise pytest .UsageError (f"Unknown group { current_group } " )
226+ groups = _partition_list (items , cdist_config .total_groups )
139227
140- # using whole numbers (2/2) is more intuitive for the CLI,
141- # but here we want to use the group numbers for zero-based indexing
142- current_group -= 1
228+ if cdist_config .justify_items_strategy != "none" :
229+ groups = _justify_items (groups , strategy = cdist_config .justify_items_strategy )
143230
144- groups = _partition_list (items , total_groups )
145- if justify_items_strategy != "none" :
146- groups = _justify_items (groups , strategy = justify_items_strategy )
231+ if os .getenv ("PYTEST_XDIST_WORKER" ):
232+ groups = _justify_xdist_groups (groups )
147233
148- # if os.getenv("PYTEST_XDIST_WORKER"):
149- groups = _justify_xdist_groups (groups )
150-
151- if group_steal is not None :
152- target_group , amount_to_steal = group_steal
234+ if cdist_config .group_steal is not None :
235+ target_group , amount_to_steal = cdist_config .group_steal
153236 groups = _distribute_with_bias (
154237 groups ,
155238 target = target_group ,
156239 bias = amount_to_steal ,
157240 )
158241
159- new_items = groups .pop (current_group )
242+ new_items = groups .pop (cdist_config . current_group )
160243 deselect = [item for group in groups for item in group ]
161244
162- if write_report :
163- report_dir .joinpath (f"pytest_cdist_report_{ current_group + 1 } .json" ).write_text (
245+ if cdist_config .write_report :
246+ cdist_config .report_dir .joinpath (
247+ f"pytest_cdist_report_{ cdist_config .current_group + 1 } .json"
248+ ).write_text (
164249 json .dumps (
165250 {
166- "group" : current_group + 1 ,
167- "total_groups" : total_groups ,
251+ "group" : cdist_config . current_group + 1 ,
252+ "total_groups" : cdist_config . total_groups ,
168253 "collected" : [i .nodeid for i in items ],
169254 "selected" : [i .nodeid for i in new_items ],
170255 }
@@ -177,3 +262,10 @@ def pytest_collection_modifyitems(
177262
178263 if deselect :
179264 config .hook .pytest_deselected (items = deselect )
265+
266+
267+ def pytest_report_header (config : pytest .Config ) -> list [str ]:
268+ cdist_config = config .stash .get (_CDIST_CONFIG_KEY , None )
269+ if cdist_config is None :
270+ return []
271+ return ["cdist options: " + cdist_config .cli_options (config )]
0 commit comments