@@ -39,7 +39,12 @@ class DebugControl:
3939
4040 show_repr_attr = False # For AutoReprMixin
4141
42- def __init__ (self , options : Iterable [str ], output : Optional [IO [str ]]) -> None :
42+ def __init__ (
43+ self ,
44+ options : Iterable [str ],
45+ output : Optional [IO [str ]],
46+ file_name : Optional [str ] = None ,
47+ ) -> None :
4348 """Configure the options and output file for debugging."""
4449 self .options = list (options ) + FORCED_DEBUG
4550 self .suppress_callers = False
@@ -49,6 +54,7 @@ def __init__(self, options: Iterable[str], output: Optional[IO[str]]) -> None:
4954 filters .append (add_pid_and_tid )
5055 self .output = DebugOutputFile .get_one (
5156 output ,
57+ file_name = file_name ,
5258 show_process = self .should ('process' ),
5359 filters = filters ,
5460 )
@@ -306,13 +312,11 @@ def __init__(
306312 if hasattr (os , 'getppid' ):
307313 self .write (f"New process: pid: { os .getpid ()!r} , parent pid: { os .getppid ()!r} \n " )
308314
309- SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
310- SINGLETON_ATTR = 'the_one_and_is_interim'
311-
312315 @classmethod
313316 def get_one (
314317 cls ,
315318 fileobj : Optional [IO [str ]] = None ,
319+ file_name : Optional [str ] = None ,
316320 show_process : bool = True ,
317321 filters : Iterable [Callable [[str ], str ]] = (),
318322 interim : bool = False ,
@@ -321,9 +325,9 @@ def get_one(
321325
322326 If `fileobj` is provided, then a new DebugOutputFile is made with it.
323327
324- If `fileobj` isn't provided, then a file is chosen
325- ( COVERAGE_DEBUG_FILE, or stderr), and a process-wide singleton
326- DebugOutputFile is made.
328+ If `fileobj` isn't provided, then a file is chosen (`file_name` if
329+ provided, or COVERAGE_DEBUG_FILE, or stderr), and a process-wide
330+ singleton DebugOutputFile is made.
327331
328332 `show_process` controls whether the debug file adds process-level
329333 information, and filters is a list of other message filters to apply.
@@ -338,27 +342,49 @@ def get_one(
338342 # Make DebugOutputFile around the fileobj passed.
339343 return cls (fileobj , show_process , filters )
340344
341- # Because of the way igor.py deletes and re-imports modules,
342- # this class can be defined more than once. But we really want
343- # a process-wide singleton. So stash it in sys.modules instead of
344- # on a class attribute. Yes, this is aggressively gross.
345- singleton_module = sys .modules .get (cls .SYS_MOD_NAME )
346- the_one , is_interim = getattr (singleton_module , cls .SINGLETON_ATTR , (None , True ))
345+ the_one , is_interim = cls ._get_singleton_data ()
347346 if the_one is None or is_interim :
348- if fileobj is None :
349- debug_file_name = os .environ .get ("COVERAGE_DEBUG_FILE" , FORCED_DEBUG_FILE )
350- if debug_file_name in ("stdout" , "stderr" ):
351- fileobj = getattr (sys , debug_file_name )
352- elif debug_file_name :
353- fileobj = open (debug_file_name , "a" )
347+ if file_name is not None :
348+ fileobj = open (file_name , "a" , encoding = "utf-8" )
349+ else :
350+ file_name = os .environ .get ("COVERAGE_DEBUG_FILE" , FORCED_DEBUG_FILE )
351+ if file_name in ("stdout" , "stderr" ):
352+ fileobj = getattr (sys , file_name )
353+ elif file_name :
354+ fileobj = open (file_name , "a" , encoding = "utf-8" )
354355 else :
355356 fileobj = sys .stderr
356357 the_one = cls (fileobj , show_process , filters )
357- singleton_module = types .ModuleType (cls .SYS_MOD_NAME )
358- setattr (singleton_module , cls .SINGLETON_ATTR , (the_one , interim ))
359- sys .modules [cls .SYS_MOD_NAME ] = singleton_module
358+ cls ._set_singleton_data (the_one , interim )
360359 return the_one
361360
361+ # Because of the way igor.py deletes and re-imports modules,
362+ # this class can be defined more than once. But we really want
363+ # a process-wide singleton. So stash it in sys.modules instead of
364+ # on a class attribute. Yes, this is aggressively gross.
365+
366+ SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
367+ SINGLETON_ATTR = 'the_one_and_is_interim'
368+
369+ @classmethod
370+ def _set_singleton_data (cls , the_one : DebugOutputFile , interim : bool ) -> None :
371+ """Set the one DebugOutputFile to rule them all."""
372+ singleton_module = types .ModuleType (cls .SYS_MOD_NAME )
373+ setattr (singleton_module , cls .SINGLETON_ATTR , (the_one , interim ))
374+ sys .modules [cls .SYS_MOD_NAME ] = singleton_module
375+
376+ @classmethod
377+ def _get_singleton_data (cls ) -> Tuple [Optional [DebugOutputFile ], bool ]:
378+ """Get the one DebugOutputFile."""
379+ singleton_module = sys .modules .get (cls .SYS_MOD_NAME )
380+ return getattr (singleton_module , cls .SINGLETON_ATTR , (None , True ))
381+
382+ @classmethod
383+ def _del_singleton_data (cls ) -> None :
384+ """Delete the one DebugOutputFile, just for tests to use."""
385+ if cls .SYS_MOD_NAME in sys .modules :
386+ del sys .modules [cls .SYS_MOD_NAME ]
387+
362388 def write (self , text : str ) -> None :
363389 """Just like file.write, but filter through all our filters."""
364390 assert self .outfile is not None
0 commit comments