Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].

- Support `del obj[...]` for types derived from `IList<T>` and `IDictionary<K, V>`
- 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
Expand Down
28 changes: 28 additions & 0 deletions doc/source/python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------

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
111 changes: 111 additions & 0 deletions tests/test_disposable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import pytest

from System.IO import MemoryStream, FileStream, FileMode, File, Path, StreamWriter


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:
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
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
Loading