@@ -1293,9 +1293,14 @@ def __init__(self,
12931293 contain invalid UTF-8 characters
12941294 """
12951295
1296+ self ._closed = False
12961297 self ._reader = None
12971298 self ._own_stream = None
12981299
1300+ # This is used to keep track of a file
1301+ # we may have opened ourselves, and that we need to close later
1302+ self ._backing_file = None
1303+
12991304 if stream is None :
13001305 # If we don't get a stream as param:
13011306 # Create a stream from the file path in format_or_path
@@ -1342,13 +1347,13 @@ def __init__(self,
13421347 )
13431348
13441349 # Store the file to close it later
1345- self ._file_like_stream = file
1350+ self ._backing_file = file
13461351
13471352 except Exception as e :
13481353 if self ._own_stream :
13491354 self ._own_stream .close ()
1350- if hasattr ( self , '_file_like_stream' ) :
1351- self ._file_like_stream .close ()
1355+ if self . _backing_file :
1356+ self ._backing_file .close ()
13521357 raise C2paError .Io (
13531358 Reader ._ERROR_MESSAGES ['io_error' ].format (
13541359 str (e )))
@@ -1402,12 +1407,13 @@ def __init__(self,
14021407 )
14031408 )
14041409
1405- self ._file_like_stream = file
1410+ # File stream we opened and own
1411+ self ._backing_file = file
14061412 except Exception as e :
14071413 if self ._own_stream :
14081414 self ._own_stream .close ()
1409- if hasattr ( self , '_file_like_stream' ) :
1410- self ._file_like_stream .close ()
1415+ if self . _backing_file :
1416+ self ._backing_file .close ()
14111417 raise C2paError .Io (
14121418 Reader ._ERROR_MESSAGES ['io_error' ].format (
14131419 str (e )))
@@ -1460,60 +1466,83 @@ def __enter__(self):
14601466 def __exit__ (self , exc_type , exc_val , exc_tb ):
14611467 self .close ()
14621468
1469+ def __del__ (self ):
1470+ """Ensure resources are cleaned up if close() wasn't called.
1471+
1472+ This destructor handles cleanup without causing double frees.
1473+ It only cleans up if the object hasn't been explicitly closed.
1474+ """
1475+ self ._cleanup_resources ()
1476+
1477+ def _ensure_valid_state (self ):
1478+ """Ensure the reader is in a valid state for operations.
1479+
1480+ Raises:
1481+ C2paError: If the reader is closed or invalid
1482+ """
1483+ if self ._closed or not self ._reader :
1484+ raise C2paError ("Reader is closed" )
1485+
1486+ def _cleanup_resources (self ):
1487+ """Internal cleanup method that releases native resources.
1488+
1489+ This method handles the actual cleanup logic and can be called
1490+ from both close() and __del__ without causing double frees.
1491+ """
1492+ try :
1493+ # Only cleanup if not already closed and we have a valid reader
1494+ if hasattr (self , '_closed' ) and not self ._closed :
1495+ # Clean up reader
1496+ if hasattr (self , '_reader' ) and self ._reader :
1497+ try :
1498+ _lib .c2pa_reader_free (self ._reader )
1499+ except Exception :
1500+ # Cleanup failure doesn't raise exceptions
1501+ pass
1502+ finally :
1503+ self ._reader = None
1504+
1505+ # Clean up stream
1506+ if hasattr (self , '_own_stream' ) and self ._own_stream :
1507+ try :
1508+ self ._own_stream .close ()
1509+ except Exception :
1510+ # Cleanup failure doesn't raise exceptions
1511+ pass
1512+ finally :
1513+ self ._own_stream = None
1514+
1515+ # Clean up backing file
1516+ if self ._backing_file :
1517+ try :
1518+ self ._backing_file .close ()
1519+ except Exception :
1520+ # Cleanup failure doesn't raise exceptions
1521+ pass
1522+ finally :
1523+ self ._backing_file = None
1524+
1525+ self ._closed = True
1526+ except Exception :
1527+ # Ensure we don't raise exceptions during cleanup
1528+ pass
1529+
14631530 def close (self ):
1464- """Release the reader resources.
1531+ """Release the reader resources safely .
14651532
14661533 This method ensures all resources are properly cleaned up,
14671534 even if errors occur during cleanup.
14681535 Errors during cleanup are logged but not raised to ensure cleanup.
14691536 Multiple calls to close() are handled gracefully.
14701537 """
1471-
1472- # Track if we've already cleaned up
1473- if not hasattr (self , '_closed' ):
1474- self ._closed = False
1475-
14761538 if self ._closed :
1477- return
1539+ return # Already closed, safe to return
14781540
14791541 try :
1480- # Clean up reader
1481- if hasattr (self , '_reader' ) and self ._reader :
1482- try :
1483- _lib .c2pa_reader_free (self ._reader )
1484- except Exception as e :
1485- print (
1486- Reader ._ERROR_MESSAGES ['reader_cleanup_error' ].format (
1487- str (e )), file = sys .stderr )
1488- finally :
1489- self ._reader = None
1490-
1491- # Clean up stream
1492- if hasattr (self , '_own_stream' ) and self ._own_stream :
1493- try :
1494- self ._own_stream .close ()
1495- except Exception as e :
1496- print (
1497- Reader ._ERROR_MESSAGES ['stream_error' ].format (
1498- str (e )), file = sys .stderr )
1499- finally :
1500- self ._own_stream = None
1501-
1502- # Clean up file
1503- if hasattr (self , '_file_like_stream' ):
1504- try :
1505- self ._file_like_stream .close ()
1506- except Exception as e :
1507- print (
1508- Reader ._ERROR_MESSAGES ['file_error' ].format (
1509- str (e )), file = sys .stderr )
1510- finally :
1511- self ._file_like_stream = None
1512-
1513- # Clear any stored strings
1514- if hasattr (self , '_strings' ):
1515- self ._strings .clear ()
1542+ # Use the internal cleanup method
1543+ self ._cleanup_resources ()
15161544 except Exception as e :
1545+ # Log any unexpected errors during close
15171546 print (
15181547 Reader ._ERROR_MESSAGES ['cleanup_error' ].format (
15191548 str (e )), file = sys .stderr )
@@ -1529,9 +1558,7 @@ def json(self) -> str:
15291558 Raises:
15301559 C2paError: If there was an error getting the JSON
15311560 """
1532-
1533- if not self ._reader :
1534- raise C2paError ("Reader is closed" )
1561+ self ._ensure_valid_state ()
15351562 result = _lib .c2pa_reader_json (self ._reader )
15361563
15371564 if result is None :
@@ -1555,13 +1582,12 @@ def resource_to_stream(self, uri: str, stream: Any) -> int:
15551582 Raises:
15561583 C2paError: If there was an error writing the resource to stream
15571584 """
1558- if not self ._reader :
1559- raise C2paError ("Reader is closed" )
1585+ self ._ensure_valid_state ()
15601586
1561- self . _uri_str = uri .encode ('utf-8' )
1587+ uri_str = uri .encode ('utf-8' )
15621588 with Stream (stream ) as stream_obj :
15631589 result = _lib .c2pa_reader_resource_to_stream (
1564- self ._reader , self . _uri_str , stream_obj ._stream )
1590+ self ._reader , uri_str , stream_obj ._stream )
15651591
15661592 if result < 0 :
15671593 error = _parse_operation_result_for_error (_lib .c2pa_error ())
0 commit comments