Skip to content

Add context manager protocol for .NET IDisposable types #2568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
## Unreleased

### Added

- Add context manager protocol for .NET IDisposable types, allowing use of `with` statements for IDisposable objects (#9c73c35)

### Changed
### Fixed

Expand Down
28 changes: 28 additions & 0 deletions doc/source/python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,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
---------------

Expand Down
6 changes: 6 additions & 0 deletions src/runtime/Mixins/CollectionMixinsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public IEnumerable<PyType> GetBaseTypes(Type type, IList<PyType> 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;
Expand Down
16 changes: 16 additions & 0 deletions src/runtime/Mixins/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down
118 changes: 118 additions & 0 deletions tests/test_disposable.py
Original file line number Diff line number Diff line change
@@ -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()
Loading