@@ -74,6 +74,20 @@ def __init__(self):
7474 # A dict with object names as keys and a list of their object types as values, with weak references
7575 self .__type_dict = {}
7676
77+ def _snapshot_items (self ):
78+ """Return a stable snapshot of __type_dict items.
79+
80+ Some callers iterate over __type_dict while other threads or
81+ weakref finalizers may modify it. Creating a list snapshot (with
82+ a retry loop) prevents RuntimeError: dictionary changed size during iteration.
83+ """
84+ while True :
85+ try :
86+ return list (self .__type_dict .items ())
87+ except RuntimeError :
88+ # Dict changed during snapshot creation, retry
89+ continue
90+
7791 def vertices (self ) -> List [str ]:
7892 """Returns the vertices of a map.
7993
@@ -109,7 +123,15 @@ def returned_objs(self) -> List[str]:
109123
110124 def _nested_get (self , obj_type : str ) -> List [str ]:
111125 """Access a nested object in root by key sequence."""
112- return [key for key , item in self .__type_dict .items () if obj_type in item .type ]
126+ # Create a stable snapshot of the dict items to avoid RuntimeError
127+ # when the dict is modified during iteration (e.g., by finalizers).
128+ while True :
129+ try :
130+ items = self ._snapshot_items ()
131+ return [key for key , item in items if obj_type in item .type ]
132+ except RuntimeError :
133+ # In case the snapshot itself raises (very rare), retry
134+ continue
113135
114136 def get_item_by_key (self , item_id : str ) -> object :
115137 if item_id in self ._store :
@@ -143,10 +165,13 @@ def add_vertex(self, obj: object, obj_type: str = None):
143165 # but the finalizer hasn't run yet
144166 if name in self .__type_dict :
145167 del self .__type_dict [name ]
168+
146169 self ._store [name ] = obj
147- self .__type_dict [name ] = _EntryList () # Add objects type to the list of types
148- self .__type_dict [name ].finalizer = weakref .finalize (self ._store [name ], self .prune , name )
149- self .__type_dict [name ].type = obj_type
170+
171+ entry_list = _EntryList ()
172+ entry_list .finalizer = weakref .finalize (obj , self .prune , name )
173+ entry_list .type = obj_type
174+ self .__type_dict [name ] = entry_list # Add objects type to the list of types
150175
151176 def add_edge (self , start_obj : object , end_obj : object ):
152177 if start_obj .unique_name in self .__type_dict :
@@ -167,8 +192,11 @@ def __generate_edges(self) -> list:
167192 vertices
168193 """
169194 edges = []
170- for vertex in self .__type_dict :
171- for neighbour in self .__type_dict [vertex ]:
195+ # Iterate over a snapshot of items and snapshot neighbour lists to
196+ # avoid concurrent modification issues.
197+ for vertex , neighbours in self ._snapshot_items ():
198+ neighbours_snapshot = list (neighbours )
199+ for neighbour in neighbours_snapshot :
172200 if {neighbour , vertex } not in edges :
173201 edges .append ({vertex , neighbour })
174202 return edges
@@ -190,12 +218,10 @@ def prune(self, key: str):
190218
191219 def find_isolated_vertices (self ) -> list :
192220 """returns a list of isolated vertices."""
193- graph = self .__type_dict
194221 isolated = []
195- for vertex in graph :
196- print (isolated , vertex )
197- if not graph [vertex ]:
198- isolated += [vertex ]
222+ for vertex , neighbours in self ._snapshot_items ():
223+ if not list (neighbours ):
224+ isolated .append (vertex )
199225 return isolated
200226
201227 def find_path (self , start_vertex : str , end_vertex : str , path = []) -> list :
@@ -247,9 +273,10 @@ def reverse_route(self, end_vertex: str, start_vertex: Optional[str] = None) ->
247273 path_length = sys .maxsize
248274 optimum_path = []
249275 if start_vertex is None :
250- # We now have to find where to begin.....
251- for possible_start , vertices in self .__type_dict .items ():
252- if end_vertex in vertices :
276+ # We now have to find where to begin..... Iterate over a snapshot
277+ for possible_start , vertices in self ._snapshot_items ():
278+ vertices_snapshot = list (vertices )
279+ if end_vertex in vertices_snapshot :
253280 temp_path = self .find_path (possible_start , end_vertex )
254281 if len (temp_path ) < path_length :
255282 path_length = len (temp_path )
@@ -270,7 +297,7 @@ def is_connected(self, vertices_encountered=None, start_vertex=None) -> bool:
270297 start_vertex = vertices [0 ]
271298 vertices_encountered .add (start_vertex )
272299 if len (vertices_encountered ) != len (vertices ):
273- for vertex in graph [start_vertex ]:
300+ for vertex in list ( graph [start_vertex ]) :
274301 if vertex not in vertices_encountered and self .is_connected (vertices_encountered , vertex ):
275302 return True
276303 else :
0 commit comments