1 # SPDX-License-Identifier: GPL-2.0-or-later
3 """Define the POV render engine from generic Blender RenderEngine class."""
6 import builtins
as __builtin__
9 from sys
import platform
13 from bpy
.utils
import register_class
, unregister_class
16 def console_get(context
):
17 #context = bpy.context
18 for win
in context
.window_manager
.windows
:
19 if win
.screen
is not None:
21 for area
in scr
.areas
:
22 if area
.type == 'CONSOLE':
23 for space
in area
.spaces
:
24 if space
.type == 'CONSOLE':
25 return area
, space
, win
, scr
26 return None, None, None, None
28 def console_write(txt
):
29 area
, space
, window
, screen
= console_get()
32 context
= bpy
.context
.copy()
36 region
=area
.regions
[-1],
40 with bpy
.context
.temp_override(**context
):
41 for line
in txt
.split("\n"):
42 bpy
.ops
.console
.scrollback_append(text
=line
, type='INFO')
44 class RENDER_OT_test(bpy.types.Operator):
45 bl_idname = 'pov.oha_test'
47 bl_options = {'REGISTER', 'UNDO'}
49 txt: bpy.props.StringProperty(
53 def execute(self, context):
55 console_write(self.txt)
58 self.report({'INFO'}, 'Printing report to Info window.')
61 def console_print(*args, **kwargs):
63 #screens = (win.screen for win in context.window_manager.windows if win.screen is not None)
64 for win in context.window_manager.windows:
65 if win.screen is not None:
68 if a.type == 'CONSOLE':
72 c['space_data'] = a.spaces.active
73 c['region'] = a.regions[-1]
76 s = " ".join([str(arg) for arg in args])
77 for line in s.split("\n"):
78 bpy.ops.console.scrollback_append(c, text=line, type='INFO')
80 except BaseException as e:
82 print('An exception occurred: {}'.format(e))
86 def print(*args, **kwargs):
87 console_print(*args, **kwargs) # to Python Console
88 __builtin__.print(*args, **kwargs) # to System Console
91 user_dir
= bpy
.utils
.resource_path('USER')
92 preview_dir
= os
.path
.join(user_dir
, "preview")
93 # Make sure Preview directory exists and is empty
94 smoke_path
= os
.path
.join(preview_dir
, "smoke.df3")
96 class PovRender(bpy
.types
.RenderEngine
):
97 """Define the external renderer"""
99 bl_idname
= 'POVRAY_RENDER'
100 bl_label
= "Persitence Of Vision"
101 bl_use_eevee_viewport
= True
102 bl_use_shading_nodes_custom
= False
106 def _locate_binary():
107 """Identify POV engine"""
108 addon_prefs
= bpy
.context
.preferences
.addons
[__package__
].preferences
110 # Use the system preference if its set.
111 if pov_binary
:= addon_prefs
.filepath_povray
:
112 if os
.path
.exists(pov_binary
):
114 # Implicit else, as here return was still not triggered:
115 print("User Preferences path to povray %r NOT FOUND, checking $PATH" % pov_binary
)
118 # assume if there is a 64bit binary that the user has a 64bit capable OS
119 if platform
.startswith('win'):
122 win_reg_key
= winreg
.OpenKey(
123 winreg
.HKEY_CURRENT_USER
, "Software\\POV-Ray\\v3.7\\Windows"
125 win_home
= winreg
.QueryValueEx(win_reg_key
, "Home")[0]
127 # First try 64bits UberPOV
128 pov_binary
= os
.path
.join(win_home
, "bin", "uberpov64.exe")
129 if os
.path
.exists(pov_binary
):
132 # Then try 64bits POV
133 pov_binary
= os
.path
.join(win_home
, "bin", "pvengine64.exe")
134 if os
.path
.exists(pov_binary
):
137 # search the path all os's
138 pov_binary_default
= "povray"
140 os_path_ls
= os
.getenv("PATH").split(':') + [""]
142 for dir_name
in os_path_ls
:
143 pov_binary
= os
.path
.join(dir_name
, pov_binary_default
)
144 if os
.path
.exists(pov_binary
):
148 def _export(self
, depsgraph
, pov_path
, image_render_path
):
149 """gather all necessary output files paths user defined and auto generated and export there"""
151 scene
= bpy
.context
.scene
152 if scene
.pov
.tempfiles_enable
:
153 self
._temp
_file
_in
= tempfile
.NamedTemporaryFile(suffix
=".pov", delete
=False).name
154 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
155 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
156 self
._temp
_file
_out
= tempfile
.NamedTemporaryFile(suffix
=".png", delete
=False).name
157 # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name
158 self
._temp
_file
_ini
= tempfile
.NamedTemporaryFile(suffix
=".ini", delete
=False).name
159 log_path
= os
.path
.join(tempfile
.gettempdir(), "alltext.out")
161 self
._temp
_file
_in
= pov_path
+ ".pov"
162 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
163 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
164 self
._temp
_file
_out
= image_render_path
+ ".png"
165 # self._temp_file_out = image_render_path + ".tga"
166 self
._temp
_file
_ini
= pov_path
+ ".ini"
167 scene_path
= scene
.pov
.scene_path
168 abs_log_path
= bpy
.path
.abspath(scene_path
)
169 log_path
= os
.path
.join(abs_log_path
, "alltext.out")
171 self._temp_file_in = "/test.pov"
172 # PNG with POV 3.7, can show the background color with alpha. In the long run using the
173 # POV-Ray interactive preview like bishop 3D could solve the preview for all formats.
174 self._temp_file_out = "/test.png"
175 #self._temp_file_out = "/test.tga"
176 self._temp_file_ini = "/test.ini"
179 self
._temp
_file
_log
= log_path
180 # self._temp_file_log = log_path.replace('\\', '/') # unnecessary relying on os.path
182 if scene
.pov
.text_block
== "":
184 def info_callback(txt
):
185 self
.update_stats("", "POV-Ray 3.7: " + txt
)
187 # os.makedirs(user_dir, exist_ok=True) # handled with previews
188 os
.makedirs(preview_dir
, exist_ok
=True)
190 render
.write_pov(self
._temp
_file
_in
, scene
, info_callback
)
194 def _render(self
, depsgraph
):
195 """Export necessary files and render image."""
196 scene
= bpy
.context
.scene
198 os
.remove(self
._temp
_file
_out
) # so as not to load the old file
202 pov_binary
= PovRender
._locate
_binary
()
204 print("POV-Ray 3.7: could not execute povray, possibly POV-Ray isn't installed")
207 render
.write_pov_ini(
208 self
._temp
_file
_ini
, self
._temp
_file
_log
, self
._temp
_file
_in
, self
._temp
_file
_out
211 print("***-STARTING-***")
214 # Always add user preferences include path field when specified
215 if (pov_documents
:= bpy
.context
.preferences
.addons
[__package__
].preferences
.docpath_povray
)!="":
216 extra_args
.append("+L"+ pov_documents
)
217 if scene
.pov
.command_line_switches
!= "":
218 extra_args
.extend(iter(scene
.pov
.command_line_switches
.split(" ")))
219 self
._is
_windows
= False
220 if platform
.startswith('win'):
221 self
._is
_windows
= True
222 if "/EXIT" not in extra_args
and not scene
.pov
.pov_editor
:
223 extra_args
.append("/EXIT")
225 # added -d option to prevent render window popup which leads to segfault on linux
226 extra_args
.append("-d")
230 self
._process
= subprocess
.Popen(
231 [pov_binary
, self
._temp
_file
_ini
] + extra_args
,
232 stdout
=subprocess
.PIPE
,
233 stderr
=subprocess
.STDOUT
,
237 print("POV-Ray 3.7: could not execute '%s'" % pov_binary
)
240 traceback
.print_exc()
241 print("***-DONE-***")
245 print("Engine ready!...")
246 print("Command line arguments passed: " + str(extra_args
))
250 """Delete temp files and unpacked ones"""
251 for f
in (self
._temp
_file
_in
, self
._temp
_file
_ini
, self
._temp
_file
_out
):
257 # Wait a bit before retrying file might be still in use by Blender,
258 # and Windows does not know how to delete a file in use!
259 time
.sleep(self
.DELAY
)
260 for i
in render
.unpacked_images
:
266 # Wait a bit before retrying file might be still in use by Blender,
267 # and Windows does not know how to delete a file in use!
268 time
.sleep(self
.DELAY
)
269 # avoid some crashes if memory leaks from one render to the next?
270 #self.free_blender_memory()
272 def render(self
, depsgraph
):
273 """Export necessary files from text editor and render image."""
275 scene
= bpy
.context
.scene
277 x
= int(r
.resolution_x
* r
.resolution_percentage
* 0.01)
278 y
= int(r
.resolution_y
* r
.resolution_percentage
* 0.01)
279 print("\n***INITIALIZING***")
281 # This makes some tests on the render, returning True if all goes good, and False if
282 # it was finished one way or the other.
283 # It also pauses the script (time.sleep())
285 time
.sleep(self
.DELAY
)
287 # User interrupts the rendering
288 if self
.test_break():
290 self
._process
.terminate()
291 print("***POV INTERRUPTED***")
296 poll_result
= self
._process
.poll()
297 except AttributeError:
298 print("***CHECK POV PATH IN PREFERENCES***")
300 # POV process is finisehd, one way or the other
301 if poll_result
is not None:
303 print("***POV PROCESS FAILED : %s ***" % poll_result
)
304 self
.update_stats("", "POV-Ray 3.7: Failed")
309 if bpy
.context
.scene
.pov
.text_block
!= "":
310 if scene
.pov
.tempfiles_enable
:
311 self
._temp
_file
_in
= tempfile
.NamedTemporaryFile(suffix
=".pov", delete
=False).name
312 self
._temp
_file
_out
= tempfile
.NamedTemporaryFile(suffix
=".png", delete
=False).name
313 # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name
314 self
._temp
_file
_ini
= tempfile
.NamedTemporaryFile(suffix
=".ini", delete
=False).name
315 self
._temp
_file
_log
= os
.path
.join(tempfile
.gettempdir(), "alltext.out")
317 pov_path
= scene
.pov
.text_block
318 image_render_path
= os
.path
.splitext(pov_path
)[0]
319 self
._temp
_file
_out
= os
.path
.join(preview_dir
, image_render_path
)
320 self
._temp
_file
_in
= os
.path
.join(preview_dir
, pov_path
)
321 self
._temp
_file
_ini
= os
.path
.join(
322 preview_dir
, (os
.path
.splitext(self
._temp
_file
_in
)[0] + ".INI")
324 self
._temp
_file
_log
= os
.path
.join(preview_dir
, "alltext.out")
328 os.remove(self._temp_file_in) # so as not to load the old file
332 print(scene
.pov
.text_block
)
333 text
= bpy
.data
.texts
[scene
.pov
.text_block
]
334 with
open(self
._temp
_file
_in
, "w") as file:
335 # Why are the newlines needed?
337 file.write(text
.as_string())
341 # has to be called to update the frame on exporting animations
342 scene
.frame_set(scene
.frame_current
)
344 pov_binary
= PovRender
._locate
_binary
()
347 print("Could not execute POV-Ray, which installation possibly isn't standard ?")
350 # start ini UI options export
351 self
.update_stats("", "POV-Ray 3.7: Exporting ini options from Blender")
353 render
.write_pov_ini(
360 print("***-STARTING-***")
364 if scene
.pov
.command_line_switches
!= "":
365 for new_arg
in scene
.pov
.command_line_switches
.split(" "):
366 extra_args
.append(new_arg
)
368 if platform
.startswith('win'):
369 if "/EXIT" not in extra_args
and not scene
.pov
.pov_editor
:
370 extra_args
.append("/EXIT")
372 # added -d option to prevent render window popup which leads to segfault on linux
373 extra_args
.append("-d")
377 if scene
.pov
.sdl_window_enable
and not platform
.startswith(
379 ): # segfault on linux == False !!!
380 env
= {'POV_DISPLAY_SCALED': 'off'}
381 env
.update(os
.environ
)
382 self
._process
= subprocess
.Popen(
383 [pov_binary
, self
._temp
_file
_ini
],
384 stdout
=subprocess
.PIPE
,
385 stderr
=subprocess
.STDOUT
,
389 self
._process
= subprocess
.Popen(
390 [pov_binary
, self
._temp
_file
_ini
] + extra_args
,
391 stdout
=subprocess
.PIPE
,
392 stderr
=subprocess
.STDOUT
,
396 print("POV-Ray 3.7: could not execute '%s'" % pov_binary
)
399 traceback
.print_exc()
400 print("***-DONE-***")
404 print("Engine ready!...")
405 print("Command line arguments passed: " + str(extra_args
))
407 self
.update_stats("", "POV-Ray 3.7: Parsing File")
409 # Indented in main function now so repeated here but still not working
410 # to bring back render result to its buffer
412 if os
.path
.exists(self
._temp
_file
_out
):
413 xmin
= int(r
.border_min_x
* x
)
414 ymin
= int(r
.border_min_y
* y
)
415 xmax
= int(r
.border_max_x
* x
)
416 ymax
= int(r
.border_max_y
* y
)
417 result
= self
.begin_result(0, 0, x
, y
)
418 lay
= result
.layers
[0]
420 time
.sleep(self
.DELAY
)
422 lay
.load_from_file(self
._temp
_file
_out
)
424 print("***POV ERROR WHILE READING OUTPUT FILE***")
425 self
.end_result(result
)
426 # print(self._temp_file_log) #bring the pov log to blender console with proper path?
429 ) as f
: # The with keyword automatically closes the file when you are done
430 print(f
.read()) # console_write(f.read())
432 self
.update_stats("", "")
434 if scene
.pov
.tempfiles_enable
or scene
.pov
.deletefiles_enable
:
439 # if r.image_settings.file_format == 'OPENEXR':
441 # render.image_settings.color_mode = 'RGBA'
444 # r.image_settings.file_format = 'TARGA'
445 # r.image_settings.color_mode = 'RGBA'
447 blend_scene_name
= bpy
.data
.filepath
.split(os
.path
.sep
)[-1].split(".")[0]
450 image_render_path
= ""
452 # has to be called to update the frame on exporting animations
453 scene
.frame_set(scene
.frame_current
)
455 if not scene
.pov
.tempfiles_enable
:
458 pov_path
= bpy
.path
.abspath(scene
.pov
.scene_path
).replace('\\', '/')
460 if bpy
.data
.is_saved
:
461 pov_path
= bpy
.path
.abspath("//")
463 pov_path
= tempfile
.gettempdir()
464 elif pov_path
.endswith("/"):
466 pov_path
= bpy
.path
.abspath("//")
468 pov_path
= bpy
.path
.abspath(scene
.pov
.scene_path
)
470 if not os
.path
.exists(pov_path
):
472 os
.makedirs(pov_path
)
473 except BaseException
as e
:
475 print('An exception occurred: {}'.format(e
))
478 traceback
.print_exc()
480 print("POV-Ray 3.7: Cannot create scenes directory: %r" % pov_path
)
482 "", "POV-Ray 3.7: Cannot create scenes directory %r" % pov_path
489 image_render_path = bpy.path.abspath(scene.pov.renderimage_path).replace('\\','/')
490 if image_render_path == "":
491 if bpy.data.is_saved:
492 image_render_path = bpy.path.abspath("//")
494 image_render_path = tempfile.gettempdir()
495 #print("Path: " + image_render_path)
496 elif path.endswith("/"):
497 if image_render_path == "/":
498 image_render_path = bpy.path.abspath("//")
500 image_render_path = bpy.path.abspath(scene.pov.)
501 if not os.path.exists(path):
502 print("POV-Ray 3.7: Cannot find render image directory")
503 self.update_stats("", "POV-Ray 3.7: Cannot find render image directory")
509 if scene
.pov
.scene_name
== "":
510 if blend_scene_name
!= "":
511 pov_scene_name
= blend_scene_name
513 pov_scene_name
= "untitled"
515 pov_scene_name
= scene
.pov
.scene_name
516 if os
.path
.isfile(pov_scene_name
):
517 pov_scene_name
= os
.path
.basename(pov_scene_name
)
518 pov_scene_name
= pov_scene_name
.split('/')[-1].split('\\')[-1]
519 if not pov_scene_name
:
520 print("POV-Ray 3.7: Invalid scene name")
521 self
.update_stats("", "POV-Ray 3.7: Invalid scene name")
524 pov_scene_name
= os
.path
.splitext(pov_scene_name
)[0]
526 print("Scene name: " + pov_scene_name
)
527 print("Export path: " + pov_path
)
528 pov_path
= os
.path
.join(pov_path
, pov_scene_name
)
529 pov_path
= os
.path
.realpath(pov_path
)
531 image_render_path
= pov_path
532 # print("Render Image path: " + image_render_path)
535 self
.update_stats("", "POV-Ray 3.7: Exporting data from Blender")
536 self
._export
(depsgraph
, pov_path
, image_render_path
)
537 self
.update_stats("", "POV-Ray 3.7: Parsing File")
539 if not self
._render
(depsgraph
):
540 self
.update_stats("", "POV-Ray 3.7: Not found")
545 # x = int(r.resolution_x * r.resolution_percentage * 0.01)
546 # y = int(r.resolution_y * r.resolution_percentage * 0.01)
548 # Wait for the file to be created
549 # XXX This is no more valid, as 3.7 always creates output file once render is finished!
550 parsing
= re
.compile(br
"= \[Parsing\.\.\.\] =")
551 rendering
= re
.compile(br
"= \[Rendering\.\.\.\] =")
552 percent
= re
.compile(r
"\(([0-9]{1,3})%\)")
553 # print("***POV WAITING FOR FILE***")
558 # POV in Windows did not output its stdout/stderr, it displayed them in its GUI
559 # But now writes file
561 self
.update_stats("", "POV-Ray 3.7: Rendering File")
563 t_data
= self
._process
.stdout
.read(10000)
568 # XXX This is working for UNIX, not sure whether it might need adjustments for
570 # First replace is for windows
571 t_data
= str(t_data
).replace('\\r\\n', '\\n').replace('\\r', '\r')
572 lines
= t_data
.split('\\n')
573 last_line
+= lines
[0]
575 print('\n'.join(lines
), end
="")
576 last_line
= lines
[-1]
578 if rendering
.search(data
):
579 _pov_rendering
= True
580 match
= percent
.findall(str(data
))
582 self
.update_stats("", "POV-Ray 3.7: Rendering File (%s%%)" % match
[-1])
584 self
.update_stats("", "POV-Ray 3.7: Rendering File")
586 elif parsing
.search(data
):
587 self
.update_stats("", "POV-Ray 3.7: Parsing File")
589 if os
.path
.exists(self
._temp
_file
_out
):
590 # print("***POV FILE OK***")
591 # self.update_stats("", "POV-Ray 3.7: Rendering")
595 xmin
= int(r
.border_min_x
* x
)
596 ymin
= int(r
.border_min_y
* y
)
597 xmax
= int(r
.border_max_x
* x
)
598 ymax
= int(r
.border_max_y
* y
)
600 # print("***POV UPDATING IMAGE***")
601 result
= self
.begin_result(0, 0, x
, y
)
602 # XXX, tests for border render.
603 # result = self.begin_result(xmin, ymin, xmax - xmin, ymax - ymin)
604 # result = self.begin_result(0, 0, xmax - xmin, ymax - ymin)
605 lay
= result
.layers
[0]
607 # This assumes the file has been fully written We wait a bit, just in case!
608 time
.sleep(self
.DELAY
)
610 lay
.load_from_file(self
._temp
_file
_out
)
611 # XXX, tests for border render.
612 # lay.load_from_file(self._temp_file_out, xmin, ymin)
614 print("***POV ERROR WHILE READING OUTPUT FILE***")
616 # Not needed right now, might only be useful if we find a way to use temp raw output of
617 # pov 3.7 (in which case it might go under _test_wait()).
620 # possible the image wont load early on.
622 lay.load_from_file(self._temp_file_out)
623 # XXX, tests for border render.
624 #lay.load_from_file(self._temp_file_out, xmin, ymin)
625 #lay.load_from_file(self._temp_file_out, xmin, ymin)
629 # Update while POV-Ray renders
631 # print("***POV RENDER LOOP***")
633 # test if POV-Ray exists
634 if self._process.poll() is not None:
635 print("***POV PROCESS FINISHED***")
640 if self.test_break():
642 self._process.terminate()
643 print("***POV PROCESS INTERRUPTED***")
649 # Would be nice to redirect the output
650 # stdout_value, stderr_value = self._process.communicate() # locks
652 # check if the file updated
653 new_size = os.path.getsize(self._temp_file_out)
655 if new_size != prev_size:
659 time.sleep(self.DELAY)
662 self
.end_result(result
)
664 print("***NO POV OUTPUT IMAGE***")
666 print("***POV INPUT FILE WRITTEN***")
668 # print(filename_log) #bring the pov log to blender console with proper path?
671 self
._temp
_file
_log
, encoding
='utf-8'
672 ) as f
: # The with keyword automatically closes the file when you are done
674 if isinstance(msg
, str):
677 elif type(msg
) == bytes
:
678 #stdmsg = msg.split('\n')
679 stdmsg
= msg
.encode('utf-8', "replace")
680 # stdmsg = msg.encode("utf-8", "replace")
682 # stdmsg = msg.decode(encoding)
684 # msg.encode('utf-8').decode('utf-8')
685 stdmsg
.replace("\t", " ")
686 print(stdmsg
) # console_write(stdmsg) # todo fix segfault and use
687 except FileNotFoundError
:
688 print("No render log to read")
689 self
.update_stats("", "")
691 if scene
.pov
.tempfiles_enable
or scene
.pov
.deletefiles_enable
:
694 sound_on
= bpy
.context
.preferences
.addons
[__package__
].preferences
.use_sounds
695 finished_render_message
= "\'Et Voilà!\'"
697 if platform
.startswith('win') and sound_on
:
698 # Could not find tts Windows command so playing beeps instead :-)
699 # "Korobeiniki"(Коробе́йники)
700 # aka "A-Type" Tetris theme
703 winsound
.Beep(494, 250) # B
704 winsound
.Beep(370, 125) # F
705 winsound
.Beep(392, 125) # G
706 winsound
.Beep(440, 250) # A
707 winsound
.Beep(392, 125) # G
708 winsound
.Beep(370, 125) # F#
709 winsound
.Beep(330, 275) # E
710 winsound
.Beep(330, 125) # E
711 winsound
.Beep(392, 125) # G
712 winsound
.Beep(494, 275) # B
713 winsound
.Beep(440, 125) # A
714 winsound
.Beep(392, 125) # G
715 winsound
.Beep(370, 275) # F
716 winsound
.Beep(370, 125) # F
717 winsound
.Beep(392, 125) # G
718 winsound
.Beep(440, 250) # A
719 winsound
.Beep(494, 250) # B
720 winsound
.Beep(392, 250) # G
721 winsound
.Beep(330, 350) # E
723 winsound
.Beep(440, 250) # A
724 winsound
.Beep(440, 150) # A
725 winsound
.Beep(523, 125) # D8
726 winsound
.Beep(659, 250) # E8
727 winsound
.Beep(587, 125) # D8
728 winsound
.Beep(523, 125) # C8
729 winsound
.Beep(494, 250) # B
730 winsound
.Beep(494, 125) # B
731 winsound
.Beep(392, 125) # G
732 winsound
.Beep(494, 250) # B
733 winsound
.Beep(440, 150) # A
734 winsound
.Beep(392, 125) # G
735 winsound
.Beep(370, 250) # F#
736 winsound
.Beep(370, 125) # F#
737 winsound
.Beep(392, 125) # G
738 winsound
.Beep(440, 250) # A
739 winsound
.Beep(494, 250) # B
740 winsound
.Beep(392, 250) # G
741 winsound
.Beep(330, 300) # E
743 # Mac supports natively say command
744 elif platform
== "darwin":
745 # We don't want the say command to block Python,
746 # so we add an ampersand after the message
747 # but if the os TTS package isn't up to date it
748 # still does thus, the try except clause
750 os
.system("say -v Amelie %s &" % finished_render_message
)
751 except BaseException
as e
:
753 print("your Mac may need an update, try to restart computer")
755 # While Linux frequently has espeak installed or at least can suggest
756 # Maybe windows could as well ?
757 elif platform
== "linux":
758 # We don't want the espeak command to block Python,
759 # so we add an ampersand after the message
760 # but if the espeak TTS package isn't installed it
761 # still does thus, the try except clause
763 os
.system("echo %s | espeak &" % finished_render_message
)
764 except BaseException
as e
:
780 for cls
in reversed(classes
):
781 unregister_class(cls
)