From 15de5fa961935e2d892c7ab83eda818a36a57fa1 Mon Sep 17 00:00:00 2001 From: den-run-ai Date: Sun, 23 Mar 2025 14:06:49 -0700 Subject: [PATCH 1/4] Add context manager protocol for .NET IDisposable types --- .../Mixins/CollectionMixinsProvider.cs | 6 + src/runtime/Mixins/collections.py | 16 +++ tests/test_disposable.py | 118 ++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 tests/test_disposable.py diff --git a/src/runtime/Mixins/CollectionMixinsProvider.cs b/src/runtime/Mixins/CollectionMixinsProvider.cs index d1b19e4d8..2bd352d16 100644 --- a/src/runtime/Mixins/CollectionMixinsProvider.cs +++ b/src/runtime/Mixins/CollectionMixinsProvider.cs @@ -63,6 +63,12 @@ public IEnumerable GetBaseTypes(Type type, IList existingBases) newBases.Add(new PyType(this.Mixins.GetAttr("IteratorMixin"))); } + // context managers (for IDisposable) + if (interfaces.Contains(typeof(IDisposable))) + { + newBases.Add(new PyType(this.Mixins.GetAttr("ContextManagerMixin"))); + } + if (newBases.Count == existingBases.Count) { return existingBases; diff --git a/src/runtime/Mixins/collections.py b/src/runtime/Mixins/collections.py index 95a6d8162..e6eaef2e5 100644 --- a/src/runtime/Mixins/collections.py +++ b/src/runtime/Mixins/collections.py @@ -5,6 +5,22 @@ import collections.abc as col +class ContextManagerMixin: + """Implements Python's context manager protocol for .NET IDisposable types""" + def __enter__(self): + """Return self for use in the with block""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Call Dispose() when exiting the with block""" + if hasattr(self, 'Dispose'): + self.Dispose() + else: + from System import IDisposable + IDisposable(self).Dispose() + # Return False to indicate that exceptions should propagate + return False + class IteratorMixin(col.Iterator): def close(self): if hasattr(self, 'Dispose'): diff --git a/tests/test_disposable.py b/tests/test_disposable.py new file mode 100644 index 000000000..33edc07e3 --- /dev/null +++ b/tests/test_disposable.py @@ -0,0 +1,118 @@ +import os +import unittest +import clr + +# Import required .NET namespaces +clr.AddReference("System") +clr.AddReference("System.IO") +from System import IDisposable +from System.IO import MemoryStream, FileStream, FileMode, File, Path, StreamWriter + +class DisposableContextManagerTests(unittest.TestCase): + """Tests for Python's context manager protocol with .NET IDisposable objects""" + + def test_memory_stream_context_manager(self): + """Test that MemoryStream can be used as a context manager""" + data = bytes([1, 2, 3, 4, 5]) + + # Using with statement with MemoryStream + with MemoryStream() as stream: + # Convert Python bytes to .NET byte array for proper writing + from System import Array, Byte + dotnet_bytes = Array[Byte](data) + stream.Write(dotnet_bytes, 0, len(dotnet_bytes)) + + self.assertEqual(5, stream.Length) + stream.Position = 0 + + # Create a .NET byte array to read into + buffer = Array[Byte](5) + stream.Read(buffer, 0, 5) + + # Convert back to Python bytes for comparison + result = bytes(buffer) + self.assertEqual(data, result) + + # The stream should be disposed (closed) after the with block + with self.assertRaises(Exception): + stream.Position = 0 # This should fail because the stream is closed + + def test_file_stream_context_manager(self): + """Test that FileStream can be used as a context manager""" + # Create a temporary file path + temp_path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + + try: + # Write data to the file using with statement + data = "Hello, context manager!" + with FileStream(temp_path, FileMode.Create) as fs: + writer = StreamWriter(fs) + writer.Write(data) + writer.Flush() + + # Verify the file was written and stream was closed + self.assertTrue(File.Exists(temp_path)) + content = File.ReadAllText(temp_path) + self.assertEqual(data, content) + + # The stream should be disposed after the with block + with self.assertRaises(Exception): + fs.Position = 0 # This should fail because the stream is closed + finally: + # Clean up + if File.Exists(temp_path): + File.Delete(temp_path) + + def test_disposable_in_multiple_contexts(self): + """Test that using .NET IDisposable objects in multiple contexts works correctly""" + # Create multiple streams and check that they're all properly disposed + + # Create a list to track if streams were properly disposed + # (we'll check this by trying to access the stream after disposal) + streams_disposed = [False, False] + + # Use nested context managers with .NET IDisposable objects + with MemoryStream() as outer_stream: + # Write some data to the outer stream + from System import Array, Byte + outer_data = Array[Byte]([10, 20, 30]) + outer_stream.Write(outer_data, 0, len(outer_data)) + + # Check that the outer stream is usable + self.assertEqual(3, outer_stream.Length) + + with MemoryStream() as inner_stream: + # Write different data to the inner stream + inner_data = Array[Byte]([40, 50, 60, 70]) + inner_stream.Write(inner_data, 0, len(inner_data)) + + # Check that the inner stream is usable + self.assertEqual(4, inner_stream.Length) + + # Try to use the inner stream - should fail because it's disposed + try: + inner_stream.Position = 0 + except Exception: + streams_disposed[1] = True + + # Try to use the outer stream - should fail because it's disposed + try: + outer_stream.Position = 0 + except Exception: + streams_disposed[0] = True + + # Verify both streams were properly disposed + self.assertTrue(all(streams_disposed)) + + def test_exception_handling(self): + """Test that exceptions propagate correctly through the context manager""" + with self.assertRaises(ValueError): + with MemoryStream() as stream: + raise ValueError("Test exception") + + # Stream should be disposed despite the exception + with self.assertRaises(Exception): + stream.Position = 0 + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From d6fda69083fe9bb73874d58fdc77e18a04ba72e0 Mon Sep 17 00:00:00 2001 From: den-run-ai Date: Sun, 23 Mar 2025 14:18:48 -0700 Subject: [PATCH 2/4] update docs --- CHANGELOG.md | 2 ++ doc/source/python.rst | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a81368f..54a08fe52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. - Support `del obj[...]` for types derived from `IList` and `IDictionary` - Support for .NET Framework 4.6.1 (#2701) +- Add context manager protocol for .NET IDisposable types, allowing use of `with` statements + for IDisposable objects (#2568) ### Changed ### Fixed diff --git a/doc/source/python.rst b/doc/source/python.rst index 5bc39accb..d39081eba 100644 --- a/doc/source/python.rst +++ b/doc/source/python.rst @@ -480,6 +480,34 @@ Python idioms: for item in domain.GetAssemblies(): name = item.GetName() +Using Context Managers (IDisposable) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.NET types that implement ``IDisposable`` can be used with Python's context manager +protocol using the standard ``with`` statement. This automatically calls the object's +``Dispose()`` method when exiting the ``with`` block: + +.. code:: python + + from System.IO import MemoryStream, StreamWriter + + # Use a MemoryStream as a context manager + with MemoryStream() as stream: + # The stream is automatically disposed when exiting the with block + writer = StreamWriter(stream) + writer.Write("Hello, context manager!") + writer.Flush() + + # Do something with the stream + stream.Position = 0 + # ... + + # After exiting the with block, the stream is disposed + # Attempting to use it here would raise an exception + +This works for any .NET type that implements ``IDisposable``, making resource +management much cleaner and safer in Python code. + Type Conversion --------------- From 2256ff113d1a25308dba21a6c53149ef63be05b9 Mon Sep 17 00:00:00 2001 From: den-run-ai Date: Sun, 23 Mar 2025 14:23:35 -0700 Subject: [PATCH 3/4] author fix --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index 96e58ff46..723520f3c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -9,7 +9,7 @@ - Barton Cline ([@BartonCline](https://github.com/BartonCline)) - Brian Lloyd ([@brianlloyd](https://github.com/brianlloyd)) - David Anthoff ([@davidanthoff](https://github.com/davidanthoff)) -- Denis Akhiyarov ([@denfromufa](https://github.com/denfromufa)) +- Denis Akhiyarov ([@den-run-ai](https://github.com/den-run-ai)) - Tony Roberts ([@tonyroberts](https://github.com/tonyroberts)) - Victor Uriarte ([@vmuriart](https://github.com/vmuriart)) From 7ed43bd4f3cf6baf8cc47bd3b7fa8817a8c1ec5e Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 7 May 2026 19:28:05 +0200 Subject: [PATCH 4/4] Use pytest for the disposable tests --- tests/test_disposable.py | 221 +++++++++++++++++++-------------------- 1 file changed, 107 insertions(+), 114 deletions(-) diff --git a/tests/test_disposable.py b/tests/test_disposable.py index 33edc07e3..3c8fb1159 100644 --- a/tests/test_disposable.py +++ b/tests/test_disposable.py @@ -1,118 +1,111 @@ -import os -import unittest -import clr - -# Import required .NET namespaces -clr.AddReference("System") -clr.AddReference("System.IO") -from System import IDisposable +import pytest + from System.IO import MemoryStream, FileStream, FileMode, File, Path, StreamWriter -class DisposableContextManagerTests(unittest.TestCase): - """Tests for Python's context manager protocol with .NET IDisposable objects""" - - def test_memory_stream_context_manager(self): - """Test that MemoryStream can be used as a context manager""" - data = bytes([1, 2, 3, 4, 5]) - - # Using with statement with MemoryStream - with MemoryStream() as stream: - # Convert Python bytes to .NET byte array for proper writing - from System import Array, Byte - dotnet_bytes = Array[Byte](data) - stream.Write(dotnet_bytes, 0, len(dotnet_bytes)) - - self.assertEqual(5, stream.Length) - stream.Position = 0 - - # Create a .NET byte array to read into - buffer = Array[Byte](5) - stream.Read(buffer, 0, 5) - - # Convert back to Python bytes for comparison - result = bytes(buffer) - self.assertEqual(data, result) - - # The stream should be disposed (closed) after the with block - with self.assertRaises(Exception): - stream.Position = 0 # This should fail because the stream is closed - - def test_file_stream_context_manager(self): - """Test that FileStream can be used as a context manager""" - # Create a temporary file path - temp_path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) - - try: - # Write data to the file using with statement - data = "Hello, context manager!" - with FileStream(temp_path, FileMode.Create) as fs: - writer = StreamWriter(fs) - writer.Write(data) - writer.Flush() - - # Verify the file was written and stream was closed - self.assertTrue(File.Exists(temp_path)) - content = File.ReadAllText(temp_path) - self.assertEqual(data, content) - - # The stream should be disposed after the with block - with self.assertRaises(Exception): - fs.Position = 0 # This should fail because the stream is closed - finally: - # Clean up - if File.Exists(temp_path): - File.Delete(temp_path) - - def test_disposable_in_multiple_contexts(self): - """Test that using .NET IDisposable objects in multiple contexts works correctly""" - # Create multiple streams and check that they're all properly disposed - - # Create a list to track if streams were properly disposed - # (we'll check this by trying to access the stream after disposal) - streams_disposed = [False, False] - - # Use nested context managers with .NET IDisposable objects - with MemoryStream() as outer_stream: - # Write some data to the outer stream - from System import Array, Byte - outer_data = Array[Byte]([10, 20, 30]) - outer_stream.Write(outer_data, 0, len(outer_data)) - - # Check that the outer stream is usable - self.assertEqual(3, outer_stream.Length) - - with MemoryStream() as inner_stream: - # Write different data to the inner stream - inner_data = Array[Byte]([40, 50, 60, 70]) - inner_stream.Write(inner_data, 0, len(inner_data)) - - # Check that the inner stream is usable - self.assertEqual(4, inner_stream.Length) - - # Try to use the inner stream - should fail because it's disposed - try: - inner_stream.Position = 0 - except Exception: - streams_disposed[1] = True - - # Try to use the outer stream - should fail because it's disposed + +def test_memory_stream_context_manager(): + """Test that MemoryStream can be used as a context manager""" + data = bytes([1, 2, 3, 4, 5]) + + with MemoryStream() as stream: + # Convert Python bytes to .NET byte array for proper writing + from System import Array, Byte + + dotnet_bytes = Array[Byte](data) + stream.Write(dotnet_bytes, 0, len(dotnet_bytes)) + + assert stream.Length == 5 + stream.Position = 0 + + # Create a .NET byte array to read into + buffer = Array[Byte](5) + stream.Read(buffer, 0, 5) + + # Convert back to Python bytes for comparison + result = bytes(buffer) + assert result == data + + # The stream should be disposed (closed) after the with block + with pytest.raises(Exception): + stream.Position = 0 # This should fail because the stream is closed + + +def test_file_stream_context_manager(tmpdir: str): + """Test that FileStream can be used as a context manager""" + # Create a temporary file path + temp_path = Path.Combine(str(tmpdir), Path.GetRandomFileName()) + + try: + # Write data to the file using with statement + data = "Hello, context manager!" + with FileStream(temp_path, FileMode.Create) as fs: + writer = StreamWriter(fs) + writer.Write(data) + writer.Flush() + + # Verify the file was written and stream was closed + assert File.Exists(temp_path) + content = File.ReadAllText(temp_path) + assert content == data + + # The stream should be disposed after the with block + with pytest.raises(Exception): + fs.Position = 0 # This should fail because the stream is closed + finally: + # Clean up + if File.Exists(temp_path): + File.Delete(temp_path) + + +def test_disposable_in_multiple_contexts(): + """Test that using .NET IDisposable objects in multiple contexts works correctly""" + # Create multiple streams and check that they're all properly disposed + + # Create a list to track if streams were properly disposed + # (we'll check this by trying to access the stream after disposal) + streams_disposed = [False, False] + + # Use nested context managers with .NET IDisposable objects + with MemoryStream() as outer_stream: + # Write some data to the outer stream + from System import Array, Byte + + outer_data = Array[Byte]([10, 20, 30]) + outer_stream.Write(outer_data, 0, len(outer_data)) + + # Check that the outer stream is usable + assert outer_stream.Length == 3 + + with MemoryStream() as inner_stream: + # Write different data to the inner stream + inner_data = Array[Byte]([40, 50, 60, 70]) + inner_stream.Write(inner_data, 0, len(inner_data)) + + # Check that the inner stream is usable + assert inner_stream.Length == 4 + + # Try to use the inner stream - should fail because it's disposed try: - outer_stream.Position = 0 + inner_stream.Position = 0 except Exception: - streams_disposed[0] = True - - # Verify both streams were properly disposed - self.assertTrue(all(streams_disposed)) - - def test_exception_handling(self): - """Test that exceptions propagate correctly through the context manager""" - with self.assertRaises(ValueError): - with MemoryStream() as stream: - raise ValueError("Test exception") - - # Stream should be disposed despite the exception - with self.assertRaises(Exception): - stream.Position = 0 - -if __name__ == "__main__": - unittest.main() \ No newline at end of file + streams_disposed[1] = True + + # Try to use the outer stream - should fail because it's disposed + try: + outer_stream.Position = 0 + except Exception: + streams_disposed[0] = True + + # Verify both streams were properly disposed + assert all(streams_disposed) + + +def test_exception_handling(): + """Test that exceptions propagate correctly through the context manager""" + with pytest.raises(ValueError): + with MemoryStream() as stream: + raise ValueError("Test exception") + + # Stream should be disposed despite the exception + with pytest.raises(Exception): + stream.Position = 0