fix return-value/deprecated API bugs, accumulation lock leaks, code cleanup
Build Debian Packages / build (bookworm, debian12) (push) Successful in 8m56s
Build Debian Packages / build (trixie, debian13) (push) Successful in 9m3s

- raise instead of silent None for unsupported operands and empty negation
- validate before acquiring lock in Accumulation operators (no RLock leak)
- Accumulation._copy_provenance helper, ADC_Result._copy_metadata for all result paths
- shared _sandbox_stdout_redirect contextmanager for sandboxed scripts
- TemperatureMonitor dead code removal (gtk helper, traceback, unused properties)
- new unit tests incl. lock-freedom regression test
- gitignore sphinx generated doc/_autosummary/ and doc/damaris/
This commit is contained in:
markusro committed 2026-10-17 19:17:43 +02:00
1 parent 91ade4376c
commit f9d8de7697
26 files changed
+590 -236

No files matched your search

+76 -92
View File
@@ -52,6 +52,31 @@ from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCan
max_points_to_display = 0
def apply_font_css(widget, font_string):
"""
Non-deprecated replacement for the deprecated widget.modify_font():
converts a pango font description string (family, optional size in pt,
optional weight) into a CSS rule applied to the widget's style context.
Examples:
>>> apply_font_css(textview, "Monospace 10")
>>> apply_font_css(label, "sans bold 16")
"""
fd = pango.FontDescription(font_string)
decl = "font-family: %s;" % fd.get_family()
if fd.get_size() > 0 and not fd.get_size_is_absolute():
decl += " font-size: %.6gpt;" % (fd.get_size() / float(pango.SCALE))
if fd.get_weight() != pango.Weight.NORMAL:
decl += " font-weight: %d;" % fd.get_weight()
provider = gtk.CssProvider()
provider.load_from_data(("%s { %s }" % (widget.get_css_name(), decl)).encode("utf-8"))
context = widget.get_style_context()
context.add_provider(provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
# keep a reference so the provider is not garbage collected
setattr(widget, "_damaris_font_css_provider", provider)
import matplotlib.axes
import matplotlib.figure
@@ -267,12 +292,10 @@ class DamarisGUI:
def run( self ):
# prolong lifetime of clipboard till the very end (avoid error message)
self.main_clipboard = self.sw.main_clipboard
gdk.threads_enter( )
if self.start_immediately:
self.toolbar_run_button.emit("clicked")
gtk.main( )
gdk.threads_leave( )
self.si = None
self.sw = None
@@ -822,14 +845,12 @@ class DamarisGUI:
dump_size = os.stat( self.dump_filename ).st_size / 1e6
logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size)
gdk.threads_enter( )
if e_text:
self.experiment_script_statusbar_label.set_text( e_text )
if r_text:
self.data_handling_statusbar_label.set_text( r_text )
if b_text:
self.backend_statusbar_label.set_text( b_text )
gdk.threads_leave( )
still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f]
if len( still_running ) == 0:
@@ -838,9 +859,7 @@ class DamarisGUI:
if self.save_thread is None and self.dump_filename != "":
logger.info("All subprocesses ended, saving data pool")
# Disable run button during data saving to prevent UI confusion
gdk.threads_enter()
self.toolbar_run_button.set_sensitive(False)
gdk.threads_leave()
# thread to save data...
self.save_thread = threading.Thread( target=self.dump_states, name="dump states" )
@@ -850,10 +869,8 @@ class DamarisGUI:
self.state = DamarisGUI.Stop_State
if self.state == DamarisGUI.Stop_State:
gdk.threads_enter( )
self.toolbar_pause_button.set_sensitive( False )
self.toolbar_stop_button.set_sensitive( False )
gdk.threads_leave( )
if len( still_running ) != 0:
logger.warning("Subprocess(es) still running: %s", ', '.join( [s.getName( ) for s in still_running] ))
return True
@@ -878,12 +895,10 @@ class DamarisGUI:
# now everything is stopped
self.state = DamarisGUI.Edit_State
gdk.threads_enter( )
self.sw.enable_editing( )
self.toolbar_run_button.set_sensitive( True )
self.toolbar_stop_button.set_sensitive( False )
self.toolbar_pause_button.set_sensitive( False )
gdk.threads_leave( )
return False
# dump states?
@@ -1334,8 +1349,7 @@ class LogWindow:
self.damaris_gui = damaris_gui
self.textview = self.xml_gui.get_object( "messages_textview" )
self.textview.connect( "key-press-event", self.textview_keypress)
font_desc = pango.FontDescription("Monospace 10")
self.textview.modify_font(font_desc)
apply_font_css(self.textview, "Monospace 10")
self.textbuffer = self.textview.get_buffer( )
self.last_timetag = None
self( "Started in directory %s\n" % os.getcwd( ) )
@@ -1351,11 +1365,9 @@ class LogWindow:
timetag_int = math.floor( timetag )
timetag_ms = int( (timetag - timetag_int) * 1000 )
date_tag = time.strftime( "%Y%m%d %H:%M:%S.%%03d:\n", time.localtime( timetag_int ) ) % timetag_ms
gdk.threads_enter( )
self.textbuffer.place_cursor( self.textbuffer.get_end_iter( ) )
self.textbuffer.insert_at_cursor( date_tag + str( message ) )
self.textview.scroll_to_mark( self.textbuffer.get_insert( ), 0.1 , False, 0.5, 0.5)
gdk.threads_leave( )
def textview_keypress( self, widget, event, data=None ):
if event.state & gdk.ModifierType.CONTROL_MASK != 0:
@@ -1449,22 +1461,20 @@ class ScriptWidgets:
self.data_handling_textbuffer.set_language(langpython)
self.data_handling_textbuffer.set_highlight_syntax(True)
fontdesc = pango.FontDescription("monospace")
self.experiment_script_textview.set_buffer( self.experiment_script_textbuffer )
self.experiment_script_textview.set_show_line_numbers(True)
self.experiment_script_textview.set_auto_indent(True)
self.experiment_script_textview.set_insert_spaces_instead_of_tabs(True)
self.experiment_script_textview.set_tab_width(4)
self.experiment_script_textview.set_smart_home_end(True)
self.experiment_script_textview.modify_font(fontdesc)
apply_font_css(self.experiment_script_textview, "monospace")
self.data_handling_textview.set_buffer( self.data_handling_textbuffer )
self.data_handling_textview.set_show_line_numbers(True)
self.data_handling_textview.set_auto_indent(True)
self.data_handling_textview.set_insert_spaces_instead_of_tabs(True)
self.data_handling_textview.set_tab_width(4)
self.data_handling_textview.set_smart_home_end(True)
self.data_handling_textview.modify_font(fontdesc)
apply_font_css(self.data_handling_textview, "monospace")
keywords = """for if else elif in print try finally except global lambda not or pass def
class import from as return yield while continue break assert None True False AccumulatedValue
@@ -1918,7 +1928,6 @@ get_ylabel set_ylabel"""
parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title=open_dialog_title,
parent=parent_window,
action=gtk.FileChooserAction.OPEN,
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
)
dialog.set_default_response( gtk.ResponseType.OK )
@@ -1944,7 +1953,6 @@ get_ylabel set_ylabel"""
parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title="Open HDF5 File...",
parent=parent_window,
action=gtk.FileChooserAction.OPEN,
buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL)
)
dialog.set_default_response( gtk.ResponseType.OK )
@@ -2106,8 +2114,8 @@ get_ylabel set_ylabel"""
parent_window = self.xml_gui.get_object( "main_window" )
dialog = gtk.FileChooserDialog( title=dialog_title,
parent=parent_window,
action=gtk.FileChooserAction.SAVE,
buttons=("_Save", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) )
dialog.set_property("action", gtk.FileChooserAction.SAVE)
dialog.set_default_response( gtk.ResponseType.OK )
dialog.set_select_multiple( False )
@@ -2671,11 +2679,9 @@ pygobject version %(pygobject)s
title="Select Temperature Controller Device",
parent=None,
action=gtk.FileChooserAction.OPEN,
buttons=(
gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
gtk.STOCK_OPEN, gtk.ResponseType.OK
)
)
dialog.add_button("Cancel", gtk.ResponseType.CANCEL)
dialog.add_button("Open", gtk.ResponseType.OK)
# Set default folder to /dev/serial/by-id/ if it exists
default_folder = "/dev/serial/by-id/"
@@ -2767,10 +2773,10 @@ pygobject version %(pygobject)s
font = self.config_script_font_button.get_font_name( )
experiment_script_textview = self.xml_gui.get_object( "experiment_script_textview" )
if experiment_script_textview:
experiment_script_textview.modify_font( pango.FontDescription( font ) )
apply_font_css(experiment_script_textview, font)
data_handling_textview = self.xml_gui.get_object( "data_handling_textview" )
if data_handling_textview:
data_handling_textview.modify_font( pango.FontDescription( font ) )
apply_font_css(data_handling_textview, font)
def browse_backend_executable_dialog( self, widget ):
"""
@@ -3031,7 +3037,7 @@ class MonitorWidgets:
try:
# The coordinate label is stored in the toolbar's message attribute
if hasattr(self.matplot_toolbar, 'message'):
self.matplot_toolbar.message.modify_font(pango.FontDescription("sans bold 16"))
apply_font_css(self.matplot_toolbar.message, "sans bold 16")
except:
pass
@@ -3214,50 +3220,46 @@ class MonitorWidgets:
register a listener and save reference to data
assume to be in gtk/gdk lock
"""
gdk.threads_enter( )
try:
if self.data_pool is not None:
# maybe some extra cleanup needed
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
self.displayed_data[ 1 ] = None
self.displayed_data = [ None, None ]
self.data_pool.unregister_listener( self.datapool_listener )
self.data_pool = None
self.update_counter_lock.acquire( )
self.update_counter = 0
self.update_counter_lock.release( )
# display states
self.source_list_reset( )
self.__rescale = True
if self.data_pool is not None:
# maybe some extra cleanup needed
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ):
self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener )
self.displayed_data[ 1 ] = None
self.displayed_data = [ None, None ]
self.display_source_path_label.set_label( "" )
self.clear_display( )
self.data_pool.unregister_listener( self.datapool_listener )
self.data_pool = None
if data_pool is not None:
# keep track of data
self.data_pool = data_pool
self.data_pool.register_listener( self.datapool_listener )
# Populate source list with existing data since the listener
# was registered after the data was loaded
keys = sorted([k for k in data_pool if k != "__builtins__"])
for key in keys:
self.source_list_add(key)
# Select first data item if available
keys = [k for k in data_pool if not k.startswith("__")]
if keys:
keys.sort()
first_key = keys[0]
first_iter = self.source_list_find(first_key.split("/"))
if first_iter:
self.display_source_combobox.set_active_iter(first_iter)
finally:
gdk.threads_leave( )
self.update_counter_lock.acquire( )
self.update_counter = 0
self.update_counter_lock.release( )
# display states
self.source_list_reset( )
self.__rescale = True
self.displayed_data = [ None, None ]
self.display_source_path_label.set_label( "" )
self.clear_display( )
if data_pool is not None:
# keep track of data
self.data_pool = data_pool
self.data_pool.register_listener( self.datapool_listener )
# Populate source list with existing data since the listener
# was registered after the data was loaded
keys = sorted([k for k in data_pool if k != "__builtins__"])
for key in keys:
self.source_list_add(key)
# Select first data item if available
keys = [k for k in data_pool if not k.startswith("__")]
if keys:
keys.sort()
first_key = keys[0]
first_iter = self.source_list_find(first_key.split("/"))
if first_iter:
self.display_source_combobox.set_active_iter(first_iter)
#################### observing data structures and produce idle events
@@ -3348,11 +3350,7 @@ class MonitorWidgets:
if self.update_counter > 10:
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
else:
gdk.threads_enter( )
try:
self.update_display( )
finally:
gdk.threads_leave( )
self.update_display( )
else:
# unregister old one
if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ],
@@ -3366,20 +3364,13 @@ class MonitorWidgets:
if self.update_counter > 10:
logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter)
else:
gdk.threads_enter( )
try:
self.renew_display( )
finally:
gdk.threads_leave( )
self.renew_display( )
new_data_struct = None
elif event.what == DataPool.Event.new_key:
# update combo-box by inserting and rely on consistent information
gdk.threads_enter( )
self.source_list_add( event.subject )
gdk.threads_leave( )
elif event.what == DataPool.Event.deleted_key:
# update combo-box by removing and rely on consistent information
gdk.threads_enter( )
if (self.displayed_data[ 0 ] is not None and
self.displayed_data[ 0 ] == event.subject):
self.displayed_data = [ None, None ]
@@ -3389,13 +3380,10 @@ class MonitorWidgets:
# not necessary, because event will be submitted
#self.clear_display()
self.source_list_remove( event.subject )
gdk.threads_leave( )
elif event.what == DataPool.Event.destroy:
gdk.threads_enter( )
self.source_list_reset( )
self.displayed_data = [ None, None ]
self.clear_display( )
gdk.threads_leave( )
return
def update_display_idle_event( self, subject=None ):
@@ -3409,11 +3397,7 @@ class MonitorWidgets:
return
if self.displayed_data[ 0 ] is None or subject != self.displayed_data[ 0 ]:
return
gdk.threads_enter( )
try:
self.update_display( )
finally:
gdk.threads_leave( )
self.update_display( )
######################## events from buttons
@@ -4159,7 +4143,7 @@ class ScriptInterface:
# start them
if self.back_driver is not None:
self.back_driver.start( )
while (not self.back_driver.quit_flag.isSet( ) and \
while (not self.back_driver.quit_flag.is_set( ) and \
(self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)):
while gtk.events_pending():
gtk.main_iteration_do(False)