Skip to content

Commit 25e0ccf

Browse files
den-run-aiden-run-aifilmor
authored
Add context manager protocol for .NET IDisposable types (pythonnet#2568)
* Add context manager protocol for .NET IDisposable types --------- Co-authored-by: den-run-ai <macmone@Deniss-Air.hsd1.ca.comcast.net> Co-authored-by: Benedikt Reinartz <filmor@gmail.com>
1 parent dc69411 commit 25e0ccf

6 files changed

Lines changed: 164 additions & 1 deletion

File tree

AUTHORS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- Barton Cline ([@BartonCline](https://github.com/BartonCline))
1010
- Brian Lloyd ([@brianlloyd](https://github.com/brianlloyd))
1111
- David Anthoff ([@davidanthoff](https://github.com/davidanthoff))
12-
- Denis Akhiyarov ([@denfromufa](https://github.com/denfromufa))
12+
- Denis Akhiyarov ([@den-run-ai](https://github.com/den-run-ai))
1313
- Tony Roberts ([@tonyroberts](https://github.com/tonyroberts))
1414
- Victor Uriarte ([@vmuriart](https://github.com/vmuriart))
1515

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1111

1212
- Support `del obj[...]` for types derived from `IList<T>` and `IDictionary<K, V>`
1313
- Support for .NET Framework 4.6.1 (#2701)
14+
- Add context manager protocol for .NET IDisposable types, allowing use of `with` statements
15+
for IDisposable objects (#2568)
1416

1517
### Changed
1618
### Fixed

doc/source/python.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,34 @@ Python idioms:
480480
for item in domain.GetAssemblies():
481481
name = item.GetName()
482482
483+
Using Context Managers (IDisposable)
484+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
485+
486+
.NET types that implement ``IDisposable`` can be used with Python's context manager
487+
protocol using the standard ``with`` statement. This automatically calls the object's
488+
``Dispose()`` method when exiting the ``with`` block:
489+
490+
.. code:: python
491+
492+
from System.IO import MemoryStream, StreamWriter
493+
494+
# Use a MemoryStream as a context manager
495+
with MemoryStream() as stream:
496+
# The stream is automatically disposed when exiting the with block
497+
writer = StreamWriter(stream)
498+
writer.Write("Hello, context manager!")
499+
writer.Flush()
500+
501+
# Do something with the stream
502+
stream.Position = 0
503+
# ...
504+
505+
# After exiting the with block, the stream is disposed
506+
# Attempting to use it here would raise an exception
507+
508+
This works for any .NET type that implements ``IDisposable``, making resource
509+
management much cleaner and safer in Python code.
510+
483511
Type Conversion
484512
---------------
485513

src/runtime/Mixins/CollectionMixinsProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ public IEnumerable<PyType> GetBaseTypes(Type type, IList<PyType> existingBases)
6363
newBases.Add(new PyType(this.Mixins.GetAttr("IteratorMixin")));
6464
}
6565

66+
// context managers (for IDisposable)
67+
if (interfaces.Contains(typeof(IDisposable)))
68+
{
69+
newBases.Add(new PyType(this.Mixins.GetAttr("ContextManagerMixin")));
70+
}
71+
6672
if (newBases.Count == existingBases.Count)
6773
{
6874
return existingBases;

src/runtime/Mixins/collections.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@
55

66
import collections.abc as col
77

8+
class ContextManagerMixin:
9+
"""Implements Python's context manager protocol for .NET IDisposable types"""
10+
def __enter__(self):
11+
"""Return self for use in the with block"""
12+
return self
13+
14+
def __exit__(self, exc_type, exc_val, exc_tb):
15+
"""Call Dispose() when exiting the with block"""
16+
if hasattr(self, 'Dispose'):
17+
self.Dispose()
18+
else:
19+
from System import IDisposable
20+
IDisposable(self).Dispose()
21+
# Return False to indicate that exceptions should propagate
22+
return False
23+
824
class IteratorMixin(col.Iterator):
925
def close(self):
1026
if hasattr(self, 'Dispose'):

tests/test_disposable.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import pytest
2+
3+
from System.IO import MemoryStream, FileStream, FileMode, File, Path, StreamWriter
4+
5+
6+
def test_memory_stream_context_manager():
7+
"""Test that MemoryStream can be used as a context manager"""
8+
data = bytes([1, 2, 3, 4, 5])
9+
10+
with MemoryStream() as stream:
11+
# Convert Python bytes to .NET byte array for proper writing
12+
from System import Array, Byte
13+
14+
dotnet_bytes = Array[Byte](data)
15+
stream.Write(dotnet_bytes, 0, len(dotnet_bytes))
16+
17+
assert stream.Length == 5
18+
stream.Position = 0
19+
20+
# Create a .NET byte array to read into
21+
buffer = Array[Byte](5)
22+
stream.Read(buffer, 0, 5)
23+
24+
# Convert back to Python bytes for comparison
25+
result = bytes(buffer)
26+
assert result == data
27+
28+
# The stream should be disposed (closed) after the with block
29+
with pytest.raises(Exception):
30+
stream.Position = 0 # This should fail because the stream is closed
31+
32+
33+
def test_file_stream_context_manager(tmpdir: str):
34+
"""Test that FileStream can be used as a context manager"""
35+
# Create a temporary file path
36+
temp_path = Path.Combine(str(tmpdir), Path.GetRandomFileName())
37+
38+
try:
39+
# Write data to the file using with statement
40+
data = "Hello, context manager!"
41+
with FileStream(temp_path, FileMode.Create) as fs:
42+
writer = StreamWriter(fs)
43+
writer.Write(data)
44+
writer.Flush()
45+
46+
# Verify the file was written and stream was closed
47+
assert File.Exists(temp_path)
48+
content = File.ReadAllText(temp_path)
49+
assert content == data
50+
51+
# The stream should be disposed after the with block
52+
with pytest.raises(Exception):
53+
fs.Position = 0 # This should fail because the stream is closed
54+
finally:
55+
# Clean up
56+
if File.Exists(temp_path):
57+
File.Delete(temp_path)
58+
59+
60+
def test_disposable_in_multiple_contexts():
61+
"""Test that using .NET IDisposable objects in multiple contexts works correctly"""
62+
# Create multiple streams and check that they're all properly disposed
63+
64+
# Create a list to track if streams were properly disposed
65+
# (we'll check this by trying to access the stream after disposal)
66+
streams_disposed = [False, False]
67+
68+
# Use nested context managers with .NET IDisposable objects
69+
with MemoryStream() as outer_stream:
70+
# Write some data to the outer stream
71+
from System import Array, Byte
72+
73+
outer_data = Array[Byte]([10, 20, 30])
74+
outer_stream.Write(outer_data, 0, len(outer_data))
75+
76+
# Check that the outer stream is usable
77+
assert outer_stream.Length == 3
78+
79+
with MemoryStream() as inner_stream:
80+
# Write different data to the inner stream
81+
inner_data = Array[Byte]([40, 50, 60, 70])
82+
inner_stream.Write(inner_data, 0, len(inner_data))
83+
84+
# Check that the inner stream is usable
85+
assert inner_stream.Length == 4
86+
87+
# Try to use the inner stream - should fail because it's disposed
88+
try:
89+
inner_stream.Position = 0
90+
except Exception:
91+
streams_disposed[1] = True
92+
93+
# Try to use the outer stream - should fail because it's disposed
94+
try:
95+
outer_stream.Position = 0
96+
except Exception:
97+
streams_disposed[0] = True
98+
99+
# Verify both streams were properly disposed
100+
assert all(streams_disposed)
101+
102+
103+
def test_exception_handling():
104+
"""Test that exceptions propagate correctly through the context manager"""
105+
with pytest.raises(ValueError):
106+
with MemoryStream() as stream:
107+
raise ValueError("Test exception")
108+
109+
# Stream should be disposed despite the exception
110+
with pytest.raises(Exception):
111+
stream.Position = 0

0 commit comments

Comments
 (0)