Repository: ManimCommunity/manim

Issue:
Attribute error on hot reload for interactive embed
## Description of bug / unexpected behavior
<!-- Add a clear and concise description of the problem you encountered. -->
I am running a scene using the opengl renderer with the options:

`--renderer opengl -pql`

and the scene:

```py
class Test(Scene):
    def construct(self):
        s = OpenGLSquare()
        s.set_color(GREEN)
        self.add(s)
        self.interactive_embed()
```

If I try to change the source code e.g. changing `GREEN` to `BLUE` it triggers the scene to attempt to reload but it crashes on this part of the code:

```
                    if not tup[0].endswith("keyboard"):
                        shell.pt_app.app.exit(exception=EOFError)
```

It doesn't end with keyboard so seems like it is expecting "keyboard" to be there, then it doesn't manage to exit gracefully neither as shell.pt_app isn't set. 

`AttributeError: 'NoneType' object has no attribute 'app'`

pt_app only seems to be set when the interactive shell's "simple_prompt" flag is set to false, so maybe some config change needed or something. 

## Expected behavior
<!-- Add a clear and concise description of what you expected to happen. -->
I was expecting that after saving the file the scene would reload with the new color

## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->
Run the below code, change the color of the circle and save the file. It should trigger a reload. It might be environment related so I am not sure if this is happening to anyone else.

<details><summary>Code for reproducing the problem</summary>

```py
class Test(Scene):
    def construct(self):
        s = OpenGLSquare()
        s.set_color(GREEN)
        self.add(s)
        self.interactive_embed()
```

</details>


## Additional media files
<!-- Paste in the files manim produced on rendering the code above. -->

<details><summary>Images/GIFs</summary>

<!-- PASTE MEDIA HERE -->

</details>


## Logs
<details><summary>Terminal output</summary>
<!-- Add "-v DEBUG" when calling manim to generate more detailed logs -->

```
Manim Community v0.6.0

/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/runpy.py:125: RuntimeWarning: 'manim.__main__' found in sys.modules after import of package 'manim', but prior to execution of 'manim.__main__'; this may result in unpredictable behaviour
  warn(RuntimeWarning(msg))
Python 3.7.4 (v3.7.4:e09359112e, Jul  8 2019, 14:54:52) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.24.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: Traceback (most recent call last):
╭──────────────────────────────────────────────────────────────────────────────╮
│ File                                                                         │
│"/Users/ryan/PycharmProjects/manim-community/manim/cli/render/commands.py",   │
│line 109, in render                                                           │
│    106                     scene_classes = scene_classes_from_file(file)     │
│    107                     SceneClass = scene_classes[0]                     │
│    108                     scene = SceneClass(renderer)                      │
│  ❱ 109                     status = scene.render()                           │
│    110                     if status:                                        │
│    111                         continue                                      │
│    112                     else:                                             │
│ File "/Users/ryan/PycharmProjects/manim-community/manim/scene/scene.py", line│
│210, in render                                                                │
│     207         """                                                          │
│     208         self.setup()                                                 │
│     209         try:                                                         │
│  ❱  210             self.construct()                                         │
│     211         except EndSceneEarlyException:                               │
│     212             pass                                                     │
│     213         except RerunSceneException as e:                             │
│ File "../scenes/playground.py", line 112, in construct                       │
│    109         s = OpenGLSquare()                                            │
│    110         s.set_color(GREEN)                                            │
│    111         self.add(s)                                                   │
│  ❱ 112         self.interactive_embed()                                      │
│    113                                                                       │
│    114 # class Example(Scene):                                               │
│    115 #                                                                     │
│ File "/Users/ryan/PycharmProjects/manim-community/manim/scene/scene.py", line│
│1042, in interactive_embed                                                    │
│    1039                                                                      │
│    1040         self.camera.model_matrix = self.camera.default_model_matrix  │
│    1041                                                                      │
│  ❱ 1042         self.interact(shell, keyboard_thread)                        │
│    1043                                                                      │
│    1044     def interact(self, shell, keyboard_thread):                      │
│    1045         event_handler = RerunSceneHandler(self.queue)                │
│ File "/Users/ryan/PycharmProjects/manim-community/manim/scene/scene.py", line│
│1061, in interact                                                             │
│    1058                 if tup[0].startswith("rerun"):                       │
│    1059                     # Intentionally skip calling join() on the file t│
│    1060                     if not tup[0].endswith("keyboard"):              │
│  ❱ 1061                         shell.pt_app.app.exit(exception=EOFError)    │
│    1062                     keyboard_thread.join()                           │
│    1063                                                                      │
│    1064                     kwargs = tup[2]                                  │
╰──────────────────────────────────────────────────────────────────────────────╯
AttributeError: 'NoneType' object has no attribute 'app'
```

<!-- Insert screenshots here (only when absolutely necessary, we prefer copy/pasted output!) -->

</details>


## System specifications

<details><summary>System Details</summary>

- OS (11.2.3 (Big Sur)):
- RAM: 8 GB 1867 MHz DDR3
- Python version (`python/py/python3 --version`): 3.7.4
- Installed modules (provide output from `pip list`):
```
appnope             0.1.2
arrow               0.17.0
attrs               20.3.0
backcall            0.2.0
cairocffi           1.2.0
certifi             2020.12.5
cffi                1.14.4
chardet             4.0.0
click               7.1.2
click-default-group 1.2.2
cloup               0.7.0
colorama            0.4.4
colour              0.1.5
commonmark          0.9.1
cycler              0.10.0
decorator           4.4.2
ffprobe             0.5
glcontext           2.3.3
idna                2.10
importlib-metadata  3.1.0
iniconfig           1.1.1
ipython             7.24.1
ipython-genutils    0.2.0
jedi                0.18.0
Jinja2              2.11.2
jinja2-time         0.2.0
kiwisolver          1.3.1
make                0.1.6.post2
manim               0.6.0
manimce             0.1.0
ManimPango          0.2.4
mapbox-earcut       0.12.10
MarkupSafe          1.1.1
matplotlib          3.4.2
matplotlib-inline   0.1.2
moderngl            5.6.4
moderngl-window     2.3.0
multipledispatch    0.6.0
networkx            2.5
numpy               1.19.4
packaging           20.7
pangocairocffi      0.4.0
pangocffi           0.8.0
parso               0.8.2
pexpect             4.8.0
pickleshare         0.7.5
Pillow              8.2.0
pip                 20.3.3
pluggy              0.13.1
progressbar         2.5
prompt-toolkit      3.0.18
ptyprocess          0.7.0
py                  1.9.0
pycairo             1.20.0
pycparser           2.20
pydub               0.24.1
pyglet              1.5.15
Pygments            2.7.2
pyparsing           2.4.7
pyrr                0.10.3
pytest              6.1.2
python-dateutil     2.8.1
requests            2.25.1
rich                6.2.0
scipy               1.5.4
setuptools          50.3.2
six                 1.15.0
toml                0.10.2
tqdm                4.54.0
traitlets           5.0.5
typing-extensions   3.7.4.3
urllib3             1.26.4
watchdog            2.1.2
wcwidth             0.2.5
zipp                3.4.0

```
</details>

<details><summary>LaTeX details</summary>

+ LaTeX distribution (e.g. TeX Live 2020):
+ Installed LaTeX packages:
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
</details>

<details><summary>FFMPEG</summary>

Output of `ffmpeg -version`:

```
ffmpeg version 4.3.1-tessus  https://evermeet.cx/ffmpeg/  Copyright (c) 2000-2020 the FFmpeg developers
built with Apple clang version 11.0.0 (clang-1100.0.33.17)
configuration: --cc=/usr/bin/clang --prefix=/opt/ffmpeg --extra-version=tessus --enable-avisynth --enable-fontconfig --enable-gpl --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libfreetype --enable-libgsm --enable-libmodplug --enable-libmp3lame --enable-libmysofa --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-version3 --pkg-config-flags=--static --disable-ffplay
libavutil      56. 51.100 / 56. 51.100
libavcodec     58. 91.100 / 58. 91.100
libavformat    58. 45.100 / 58. 45.100
libavdevice    58. 10.100 / 58. 10.100
libavfilter     7. 85.100 /  7. 85.100
libswscale      5.  7.100 /  5.  7.100
libswresample   3.  7.100 /  3.  7.100
libpostproc    55.  7.100 / 55.  7.100
```
</details>

## Additional comments
<!-- Add further context that you think might be relevant for this issue here. -->



Discussion:
I found out why shell.pt_app was none in the line `shell.pt_app.app.exit(exception=EOFError)`, I am launching this from pycharm, and since it is an IDE it uses the embedded terminal which isn't a real tty, so uses `simple_prompt` that has reduced features and doesn't set `pt_app`. The solution to this is to edit the run configurations in pycharm and set "Emulate terminal in output console".

I am able to change the scene in the ipython console, but when I try to edit the source could, it triggers a rerun and I get the error

```
Unhandled exception in FSEventsEmitter
Traceback (most recent call last):
  File "/Users/ryan/PycharmProjects/manim-community/venv/lib/python3.7/site-packages/watchdog/observers/fsevents.py", line 310, in run
    _fsevents.add_watch(self, self.watch, self.events_callback, self.pathnames)
RuntimeError: Cannot add watch <ObservedWatch: path=/Users/ryan/PycharmProjects/manim-community/manim/../scenes/playground.py, is_recursive=True> - it is already scheduled

```

Resolving patch:
diff --git a/manim/scene/scene.py b/manim/scene/scene.py
--- a/manim/scene/scene.py
+++ b/manim/scene/scene.py
@@ -1039,6 +1039,9 @@ def get_embedded_method(method_name):
             target=ipython,
             args=(shell, local_namespace),
         )
+        # run as daemon to kill thread when main thread exits
+        if not shell.pt_app:
+            keyboard_thread.daemon = True
         keyboard_thread.start()
 
         if self.dearpygui_imported and config["enable_gui"]:
@@ -1063,7 +1066,7 @@ def interact(self, shell, keyboard_thread):
         file_observer.start()
 
         self.quit_interaction = False
-        keyboard_thread_needs_join = True
+        keyboard_thread_needs_join = shell.pt_app is not None
         assert self.queue.qsize() == 0
 
         last_time = time.time()
@@ -1073,7 +1076,10 @@ def interact(self, shell, keyboard_thread):
                 if tup[0].startswith("rerun"):
                     # Intentionally skip calling join() on the file thread to save time.
                     if not tup[0].endswith("keyboard"):
-                        shell.pt_app.app.exit(exception=EOFError)
+                        if shell.pt_app:
+                            shell.pt_app.app.exit(exception=EOFError)
+                        file_observer.unschedule_all()
+                        raise RerunSceneException
                     keyboard_thread.join()
 
                     kwargs = tup[2]
@@ -1089,10 +1095,11 @@ def interact(self, shell, keyboard_thread):
                     #     ]
 
                     keyboard_thread.join()
+                    file_observer.unschedule_all()
                     raise RerunSceneException
                 elif tup[0].startswith("exit"):
                     # Intentionally skip calling join() on the file thread to save time.
-                    if not tup[0].endswith("keyboard"):
+                    if not tup[0].endswith("keyboard") and shell.pt_app:
                         shell.pt_app.app.exit(exception=EOFError)
                     keyboard_thread.join()
                     # Remove exit_keyboard from the queue if necessary.
@@ -1120,6 +1127,9 @@ def interact(self, shell, keyboard_thread):
             while self.queue.qsize() > 0:
                 self.queue.get()
 
+        file_observer.stop()
+        file_observer.join()
+
         if self.dearpygui_imported and config["enable_gui"]:
             dearpygui.core.stop_dearpygui()
 


Test patch: