Aqt\sound.py:316: ResourceWarning: unclosed file <_io.BufferedWriter name=5>

Sometimes when I am review, this randomly pops up Enable tracemalloc to get the object allocation traceback. Then, I enabled traceback:

c:\python>.\python.exe --version
Python 3.8.0
...
import tracemalloc

tracemalloc.start()

And I got this:
\\?\F:\bazel\anki\rybdqld5\execroot\ankidesktop\bazel-out\x64_windows-fastbuild\bin\qt\runanki.exe.runfiles\ankidesktop\qt\aqt\sound.py:316: ResourceWarning: unclosed file <_io.BufferedWriter name=5> self._process = None Object allocated at (most recent call last): File "c:\python\lib\subprocess.py", lineno 838 self.stdin = io.open(p2cwrite, 'wb', bufsize)

Appointed code on sound.py:316

File: anki2/qt/aqt/sound.py
306:     def _wait_for_termination(self, tag: AVTag) -> None:
307:         self._taskman.run_on_main(
308:             lambda: gui_hooks.av_player_did_begin_playing(self, tag)
309:         )
310:
311:         while True:
312:             # should we abort playing?
313:             if self._terminate_flag:
314:                 self._process.terminate()
315:                 self._process.wait(1)
316:                 self._process = None
317:                 return
318:
319:             # wait for completion
320:             try:
321:                 self._process.wait(0.1)
322:                 if self._process.returncode != 0:
323:                     print(f"player got return code: {self._process.returncode}")
324:                 self._process = None
325:                 return
326:             except subprocess.TimeoutExpired:
327:                 # process still running, repeat loop
328:                 pass
329:
330:     def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:

Appointed code on subprocess.py:838

File: C:/python/Lib/subprocess.py
684: class Popen(object):
685:     """ Execute a child program in a new process.
686: 
687:     For a complete description of the arguments see the Python documentation.
688: 
689:     Arguments:
690:       args: A string, or a sequence of program arguments.
691: 
692:       bufsize: supplied as the buffering argument to the open() function when
693:           creating the stdin/stdout/stderr pipe file objects
694: 
695:       executable: A replacement program to execute.
696: 
697:       stdin, stdout and stderr: These specify the executed programs' standard
698:           input, standard output and standard error file handles, respectively.
699: 
700:       preexec_fn: (POSIX only) An object to be called in the child process
701:           just before the child is executed.
702: 
703:       close_fds: Controls closing or inheriting of file descriptors.
704: 
705:       shell: If true, the command will be executed through the shell.
706: 
707:       cwd: Sets the current directory before the child is executed.
708: 
709:       env: Defines the environment variables for the new process.
710: 
711:       text: If true, decode stdin, stdout and stderr using the given encoding
712:           (if set) or the system default otherwise.
713: 
714:       universal_newlines: Alias of text, provided for backwards compatibility.
715: 
716:       startupinfo and creationflags (Windows only)
717: 
718:       restore_signals (POSIX only)
719: 
720:       start_new_session (POSIX only)
721: 
722:       pass_fds (POSIX only)
723: 
724:       encoding and errors: Text mode encoding and error handling to use for
725:           file objects stdin, stdout and stderr.
726: 
727:     Attributes:
728:         stdin, stdout, stderr, pid, returncode
729:     """
730:     _child_created = False  # Set here since __del__ checks it
731: 
732:     def __init__(self, args, bufsize=-1, executable=None,
733:                  stdin=None, stdout=None, stderr=None,
734:                  preexec_fn=None, close_fds=True,
735:                  shell=False, cwd=None, env=None, universal_newlines=None,
736:                  startupinfo=None, creationflags=0,
737:                  restore_signals=True, start_new_session=False,
738:                  pass_fds=(), *, encoding=None, errors=None, text=None):
739:         """Create new Popen instance."""
740:         _cleanup()
741:         # Held while anything is calling waitpid before returncode has been
742:         # updated to prevent clobbering returncode if wait() or poll() are
743:         # called from multiple threads at once.  After acquiring the lock,
744:         # code must re-check self.returncode to see if another thread just
745:         # finished a waitpid() call.
746:         self._waitpid_lock = threading.Lock()
747: 
748:         self._input = None
749:         self._communication_started = False
750:         if bufsize is None:
751:             bufsize = -1  # Restore default
752:         if not isinstance(bufsize, int):
753:             raise TypeError("bufsize must be an integer")
754: 
755:         if _mswindows:
756:             if preexec_fn is not None:
757:                 raise ValueError("preexec_fn is not supported on Windows "
758:                                  "platforms")
759:         else:
760:             # POSIX
761:             if pass_fds and not close_fds:
762:                 warnings.warn("pass_fds overriding close_fds.", RuntimeWarning)
763:                 close_fds = True
764:             if startupinfo is not None:
765:                 raise ValueError("startupinfo is only supported on Windows "
766:                                  "platforms")
767:             if creationflags != 0:
768:                 raise ValueError("creationflags is only supported on Windows "
769:                                  "platforms")
770: 
771:         self.args = args
772:         self.stdin = None
773:         self.stdout = None
774:         self.stderr = None
775:         self.pid = None
776:         self.returncode = None
777:         self.encoding = encoding
778:         self.errors = errors
779: 
780:         # Validate the combinations of text and universal_newlines
781:         if (text is not None and universal_newlines is not None
782:             and bool(universal_newlines) != bool(text)):
783:             raise SubprocessError('Cannot disambiguate when both text '
784:                                   'and universal_newlines are supplied but '
785:                                   'different. Pass one or the other.')
786: 
787:         # Input and output objects. The general principle is like
788:         # this:
789:         #
790:         # Parent                   Child
791:         # ------                   -----
792:         # p2cwrite   ---stdin--->  p2cread
793:         # c2pread    <--stdout---  c2pwrite
794:         # errread    <--stderr---  errwrite
795:         #
796:         # On POSIX, the child objects are file descriptors.  On
797:         # Windows, these are Windows file handles.  The parent objects
798:         # are file descriptors on both platforms.  The parent objects
799:         # are -1 when not using PIPEs. The child objects are -1
800:         # when not redirecting.
801: 
802:         (p2cread, p2cwrite,
803:          c2pread, c2pwrite,
804:          errread, errwrite) = self._get_handles(stdin, stdout, stderr)
805: 
806:         # We wrap OS handles *before* launching the child, otherwise a
807:         # quickly terminating child could make our fds unwrappable
808:         # (see #8458).
809: 
810:         if _mswindows:
811:             if p2cwrite != -1:
812:                 p2cwrite = msvcrt.open_osfhandle(p2cwrite.Detach(), 0)
813:             if c2pread != -1:
814:                 c2pread = msvcrt.open_osfhandle(c2pread.Detach(), 0)
815:             if errread != -1:
816:                 errread = msvcrt.open_osfhandle(errread.Detach(), 0)
817: 
818:         self.text_mode = encoding or errors or text or universal_newlines
819: 
820:         # How long to resume waiting on a child after the first ^C.
821:         # There is no right value for this.  The purpose is to be polite
822:         # yet remain good for interactive users trying to exit a tool.
823:         self._sigint_wait_secs = 0.25  # 1/xkcd221.getRandomNumber()
824: 
825:         self._closed_child_pipe_fds = False
826: 
827:         if self.text_mode:
828:             if bufsize == 1:
829:                 line_buffering = True
830:                 # Use the default buffer size for the underlying binary streams
831:                 # since they don't support line buffering.
832:                 bufsize = -1
833:             else:
834:                 line_buffering = False
835: 
836:         try:
837:             if p2cwrite != -1:
838:                 self.stdin = io.open(p2cwrite, 'wb', bufsize)
839:                 if self.text_mode:
840:                     self.stdin = io.TextIOWrapper(self.stdin, write_through=True,
841:                             line_buffering=line_buffering,
842:                             encoding=encoding, errors=errors)
843:             if c2pread != -1:
844:                 self.stdout = io.open(c2pread, 'rb', bufsize)
845:                 if self.text_mode:
846:                     self.stdout = io.TextIOWrapper(self.stdout,
847:                             encoding=encoding, errors=errors)
848:             if errread != -1:
849:                 self.stderr = io.open(errread, 'rb', bufsize)
850:                 if self.text_mode:
851:                     self.stderr = io.TextIOWrapper(self.stderr,
852:                             encoding=encoding, errors=errors)
853: 
854:             self._execute_child(args, executable, preexec_fn, close_fds,
855:                                 pass_fds, cwd, env,
856:                                 startupinfo, creationflags, shell,
857:                                 p2cread, p2cwrite,
858:                                 c2pread, c2pwrite,
859:                                 errread, errwrite,
860:                                 restore_signals, start_new_session)
861:         except:
862:             # Cleanup if the child failed starting.
863:             for f in filter(None, (self.stdin, self.stdout, self.stderr)):
864:                 try:
865:                     f.close()
866:                 except OSError:
867:                     pass  # Ignore EBADF or other errors.
868: 
869:             if not self._closed_child_pipe_fds:
870:                 to_close = []
871:                 if stdin == PIPE:
872:                     to_close.append(p2cread)
873:                 if stdout == PIPE:
874:                     to_close.append(c2pwrite)
875:                 if stderr == PIPE:
876:                     to_close.append(errwrite)
877:                 if hasattr(self, '_devnull'):
878:                     to_close.append(self._devnull)
879:                 for fd in to_close:
880:                     try:
881:                         if _mswindows and isinstance(fd, Handle):
882:                             fd.Close()
883:                         else:
884:                             os.close(fd)
885:                     except OSError:
886:                         pass
887: 
888:             raise
889: 

I am not sure why I am using Python 3.8.0

I tried to download Python 3.8 but I was only finding source tarballs, then, I found this 3.8.0.

I am going to update to 3.8.10 and see if the problem persists.

Maybe self._process.stdin.close() after the .wait()s will help? I don’t see Python exposing a clean API for this outside of a context manager, but maybe I’ve missed something.

I think it would help to call self._process.stdin.close(). I disabled some add-ons and the problem did not happen today. After testing, I narrowed it down to a single addon: GitHub - glutanimate/speed-focus-mode: Speed Focus Mode add-on for Anki

Anki 2.1.48 (dev) Python 3.8.10 Qt 5.15.2 PyQt 5.15.2
Platform: Windows 10
Flags: frz=False ao=True sv=2
Add-ons, last update check: 2021-09-08 19:30:59

\\?\F:\bazel\anki\rybdqld5\execroot\ankidesktop\bazel-out\x64_windows-fastbuild\bin\qt\runanki.exe.runfiles\ankidesktop\qt\aqt\sound.py:324: ResourceWarning: unclosed file <_io.BufferedWriter name=5>
  self._process = None
Object allocated at (most recent call last):
  File "c:\python\lib\subprocess.py", lineno 842
    self.stdin = io.open(p2cwrite, 'wb', bufsize)

It happens right after the addons shows this pop up (with a sound):
image

Now I know how to reproduce, I tried your suggestion to close the stdin and it fixed the problem. I double-checked it to be sure. I opened a pull request with a fix for it: Fix ResourceWarning: unclosed file <_io.BufferedWriter name=5> by evandrocoan · Pull Request #1364 · ankitects/anki · GitHub

Perhaps the addon is not doing something he should?

At least, it seems appropriate to close self._process.stdin.close(): speed-focus-mode/reviewer.py at 849f7960a8801ed87300cf91411ce7e43b6fc647 · glutanimate/speed-focus-mode · GitHub

File: Anki2/addons21/speed-focus-mode/reviewer.py
223:     elif action == "alert":
224:         av_player.clear_queue_and_maybe_interrupt()
225:         av_player.play_file(ALERT_PATH)
226:         timeout = deck_config.get("autoAlert", default_configs["autoAlert"])
227:         tooltip(
228:             "Wake up! You have been looking at <br>"
229:             "the question for <b>{}</b> seconds!".format(timeout),
230:             period=6000,
231:         )

@addons_zz thanks for digging into this!

If you guys have any suggestions as to how SFM could improve its use of Anki’s sound API here, I’d definitely appreciate it. But to me this seems like a very canonical and innocuous use and naively I’d expect Anki’s sound API to abstract any file closing / context management away.

Looks like the issue’s on Anki’s end. Thanks for the PR Evandro!