@@ -292,7 +292,7 @@ def parse_config_file(
292292 stdout : TextIO | None = None ,
293293 stderr : TextIO | None = None ,
294294) -> None :
295- """Parse a config file into an Options object.
295+ """Parse a config file into an Options object, following config extend arguments .
296296
297297 Errors are written to stderr but are not fatal.
298298
@@ -301,30 +301,104 @@ def parse_config_file(
301301 stdout = stdout or sys .stdout
302302 stderr = stderr or sys .stderr
303303
304+ ret = _parse_and_extend_config_file (
305+ options = options ,
306+ set_strict_flags = set_strict_flags ,
307+ filename = filename ,
308+ stdout = stdout ,
309+ stderr = stderr ,
310+ visited = set (),
311+ )
312+
313+ if ret is None :
314+ return
315+
316+ file_read , mypy_updates , mypy_report_dirs , module_updates = ret
317+
318+ options .config_file = file_read
319+ os .environ ["MYPY_CONFIG_FILE_DIR" ] = os .path .dirname (os .path .abspath (file_read ))
320+
321+ for k , v in mypy_updates .items ():
322+ setattr (options , k , v )
323+
324+ options .report_dirs .update (mypy_report_dirs )
325+
326+ for glob , updates in module_updates .items ():
327+ options .per_module_options [glob ] = updates
328+
329+
330+ def _merge_updates (existing : dict [str , object ], new : dict [str , object ]) -> None :
331+ existing ["disable_error_code" ] = list (
332+ set (existing .get ("disable_error_code" , []) + new .pop ("disable_error_code" ))
333+ )
334+ existing ["enable_error_code" ] = list (
335+ set (existing .get ("enable_error_code" , []) + new .pop ("enable_error_code" ))
336+ )
337+ existing .update (new )
338+
339+
340+ def _parse_and_extend_config_file (
341+ options : Options ,
342+ set_strict_flags : Callable [[], None ],
343+ filename : str | None ,
344+ stdout : TextIO ,
345+ stderr : TextIO ,
346+ visited : set [str ],
347+ ) -> tuple [str , dict [str , object ], dict [str , str ], dict [str , dict [str , object ]]] | None :
304348 ret = (
305349 _parse_individual_file (filename , stderr )
306350 if filename is not None
307351 else _find_config_file (stderr )
308352 )
309353 if ret is None :
310- return
354+ return None
311355 parser , config_types , file_read = ret
312356
313- options .config_file = file_read
314- os .environ ["MYPY_CONFIG_FILE_DIR" ] = os .path .dirname (os .path .abspath (file_read ))
357+ abs_file_read = os .path .abspath (file_read )
358+ if abs_file_read in visited :
359+ print (f"Circular extend detected: { abs_file_read } " , file = stderr )
360+ return None
361+ visited .add (abs_file_read )
362+
363+ mypy_updates : dict [str , object ] = {}
364+ mypy_report_dirs : dict [str , str ] = {}
365+ module_updates : dict [str , dict [str , object ]] = {}
315366
316367 if "mypy" not in parser :
317368 if filename or os .path .basename (file_read ) not in defaults .SHARED_CONFIG_NAMES :
318369 print (f"{ file_read } : No [mypy] section in config file" , file = stderr )
319370 else :
320371 section = parser ["mypy" ]
372+
373+ extend = parser ["mypy" ].pop ("extend" , None )
374+ if extend :
375+ cwd = os .getcwd ()
376+ try :
377+ # process extend relative to the directory where we found current config
378+ os .chdir (os .path .dirname (abs_file_read ))
379+ parse_ret = _parse_and_extend_config_file (
380+ options = options ,
381+ set_strict_flags = set_strict_flags ,
382+ filename = os .path .abspath (expand_path (extend )),
383+ stdout = stdout ,
384+ stderr = stderr ,
385+ visited = visited ,
386+ )
387+ finally :
388+ os .chdir (cwd )
389+
390+ if parse_ret is None :
391+ print (f"{ extend } is not a valid path to extend from { abs_file_read } " , file = stderr )
392+ else :
393+ _ , mypy_updates , mypy_report_dirs , module_updates = parse_ret
394+
321395 prefix = f"{ file_read } : [mypy]: "
322396 updates , report_dirs = parse_section (
323397 prefix , options , set_strict_flags , section , config_types , stderr
324398 )
325- for k , v in updates . items ():
326- setattr ( options , k , v )
327- options . report_dirs .update (report_dirs )
399+ # extend and overwrite existing values with new ones
400+ _merge_updates ( mypy_updates , updates )
401+ mypy_report_dirs .update (report_dirs )
328402
329403 for name , section in parser .items ():
330404 if name .startswith ("mypy-" ):
@@ -367,7 +441,10 @@ def parse_config_file(
367441 file = stderr ,
368442 )
369443 else :
370- options .per_module_options [glob ] = updates
444+ # extend and overwrite existing values with new ones
445+ _merge_updates (module_updates .setdefault (glob , {}), updates )
446+
447+ return file_read , mypy_updates , mypy_report_dirs , module_updates
371448
372449
373450def get_prefix (file_read : str , name : str ) -> str :
0 commit comments