From a96d396b4f89dbced5d1ded3314d8eace69f0c14 Mon Sep 17 00:00:00 2001 From: Denis Akhiyarov Date: Mon, 11 May 2026 04:25:15 -0700 Subject: [PATCH] Add context manager protocol for .NET IDisposable types (#2568) * Add context manager protocol for .NET IDisposable types --------- Co-authored-by: den-run-ai Co-authored-by: Benedikt Reinartz (cherry picked from commit 25e0ccf39f52716bb856a91ecb7c38de726b1d07) --- AUTHORS.md | 2 +- CHANGELOG.md | 87 ++- doc/source/python.rst | 595 ++++++++++++++++++ .../Mixins/CollectionMixinsProvider.cs | 6 + src/runtime/Mixins/collections.py | 16 + tests/test_disposable.py | 111 ++++ 6 files changed, 815 insertions(+), 2 deletions(-) create mode 100644 doc/source/python.rst create mode 100644 tests/test_disposable.py diff --git a/AUTHORS.md b/AUTHORS.md index 9edd75517..8f7fce3f7 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)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d72a3a0..9c7a93c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,92 @@ project adheres to [Semantic Versioning][]. This document follows the conventions laid out in [Keep a CHANGELOG][]. -## [Unreleased][] +## Unreleased + +### Added + +- 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 + +- Fixed crash when trying to `del clrObj[...]` for non-arrays +- ci: properly exclude job (#2542) + +## [3.0.5](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.5) - 2024-12-13 + +### Added + +- Support for Python 3.13 (#2454) + + +## [3.0.4](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.4) - 2024-09-19 + +### Added + +- Added `ToPythonAs()` extension method to allow for explicit conversion + using a specific type. ([#2311][i2311]) +- Added `IComparable` and `IEquatable` implementations to `PyInt`, `PyFloat`, + and `PyString` to compare with primitive .NET types like `long`. + +### Changed + +- Added a `FormatterFactory` member in RuntimeData to create formatters with + parameters. For compatibility, the `FormatterType` member is still present + and has precedence when defining both `FormatterFactory` and `FormatterType` +- Added a post-serialization and a pre-deserialization step callbacks to + extend (de)serialization process +- Added an API to stash serialized data on Python capsules + +### Fixed + +- Fixed RecursionError for reverse operators on C# operable types from python. See #2240 +- Fixed crash when .NET event has no `AddMethod` +- Fixed probing for assemblies in `sys.path` failing when a path in `sys.path` + has invalid characters. See #2376 +- Fixed possible access violation exception on shutdown. See ([#1977][i1977]) + +## [3.0.3](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.3) - 2023-10-11 + +### Added + +- Support for Python 3.12 + +### Changed + +- Use enum name in `repr` + +## [3.0.2](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.2) - 2023-08-29 + +### Fixed + +- Fixed error occuring when inheriting a class containing a virtual generic method +- Make a second call to `pythonnet.load` a no-op, as it was intended +- Added support for multiple inheritance when inheriting from a class and/or multiple interfaces +- Fixed error occuring when calling `GetBuffer` for anything other than `PyBUF.SIMPLE` +- Bumped `clr_loader` dependency to incorporate patches + +## [3.0.1](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.1) - 2022-11-03 + +### Added + +- Support for Python 3.11 + +### Changed + +- Allow decoders to override conversion of types derived from primitive types + +### Fixed + +- Fixed objects leaking when Python attached event handlers to them even if they were later removed +- Fixed `PyInt` conversion to `BigInteger` and `System.String` produced incorrect result for values between 128 and 255. +- Fixed implementing a generic interface with a Python class + + +## [3.0.0](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.0) - 2022-09-29 ### Added diff --git a/doc/source/python.rst b/doc/source/python.rst new file mode 100644 index 000000000..d39081eba --- /dev/null +++ b/doc/source/python.rst @@ -0,0 +1,595 @@ +Embedding .NET into Python +========================== + +Getting Started +--------------- + +A key goal for this project has been that Python.NET should “work just +the way you’d expect in Python”, except for cases that are .NET-specific +(in which case the goal is to work “just the way you’d expect in C#”). + +A good way to start is to interactively explore .NET usage in python +interpreter by following along with the examples in this document. If +you get stuck, there are also a number of demos and unit tests located +in the source directory of the distribution that can be helpful as +examples. Additionally, refer to the `wiki on +GitHub `__, especially the +**Tutorials** there. + +Installation +~~~~~~~~~~~~ + +Python.NET is available as a source release on +`GitHub `__ and as a +platform-independent binary wheel or source distribution from the `Python +Package Index `__. + +Installing from PyPI can be done using ``pip install pythonnet``. + +To build from source (either the ``sdist`` or clone or snapshot of the +repository), only the .NET6 SDK (or newer) and Python itself are required. If +``dotnet`` is on the ``PATH``, building can be done using + +.. code:: bash + + python setup.py build + + +Loading a Runtime +~~~~~~~~~~~~~~~~~ + +All runtimes supported by clr-loader can be used, which are + +Mono (``mono``) + Default on Linux and macOS, supported on all platforms. + +.NET Framework (``netfx``) + Default on Windows and also only supported there. Must be at least version + 4.6.1, with 4.7.2 or later recommended. For .NET 4.6 support, the wheel has + to be built with the environment variable `PYTHONNET_BUILD_NET46_SUPPORT=1`. + +.NET Core (``coreclr``) + Self-contained is not supported, must be at least version 3.1. + +The runtime must be configured **before** ``clr`` is imported, otherwise the +default runtime will be initialized and used. Information on the runtime in use +can be retrieved using :py:func:`pythonnet.get_runtime_info`). + +A runtime can be selected in three different ways: + +Calling ``pythonnet.load`` +.......................... + +The function :py:func:`pythonnet.load` can be called explicitly. A single +string parameter (like ``load("coreclr")`` will select the respective runtime. +All keyword arguments are passed to the underlying +``clr_loader.get_`` function. + +.. code:: python + + from pythonnet import load + + load("coreclr", runtime_config="/path/to/runtimeconfig.json") + +.. note:: + All runtime implementations can be initialized without additional parameters. + While previous versions of ``clr_loader`` required a ``runtimeconfig.json`` + to load .NET Core, this requirement was lifted for the version used in + ``pythonnet``. + +Via Environment Variables +......................... + +The same configurability is exposed as environment variables. + +``PYTHONNET_RUNTIME`` + selects the runtime (e.g. ``PYTHONNET_RUNTIME=coreclr``) + +``PYTHONNET__`` + is passed on as a keyword argument (e.g. ``PYTHONNET_MONO_LIBMONO=/path/to/libmono.so``) + +The equivalent configuration to the ``load`` example would be + +.. code:: bash + + PYTHONNET_RUNTIME=coreclr + PYTHONNET_CORECLR_RUNTIME_CONFIG=/path/to/runtimeconfig.json + +.. note:: + Only string parameters are supported this way. It has the advantage, though, + that the same configuration will be used for subprocesses as well. + +Constructing a ``Runtime`` instance +................................... + +The runtime can also be explicitly constructed using using the +``clr_loader.get_*`` factory functions, and then set up using +:py:func:`pythonnet.set_runtime`: + +.. code:: python + + from pythonnet import set_runtime + from clr_loader import get_coreclr + + rt = get_coreclr(runtime_config="/path/to/runtimeconfig.json") + set_runtime(rt) + +This method is only recommended, if very fine-grained control over the runtime +construction is required. + + +Importing Modules +~~~~~~~~~~~~~~~~~ + +Python.NET allows CLR namespaces to be treated essentially as Python +packages. + +.. code:: python + + from System import String + from System.Collections import * + +Types from any loaded assembly may be imported and used in this manner. +To load an assembly, use the ``AddReference`` function in the ``clr`` +module: + +.. code:: python + + import clr + clr.AddReference("System.Windows.Forms") + from System.Windows.Forms import Form + +.. note:: + Earlier releases of Python.NET relied on “implicit loading” to + support automatic loading of assemblies whose names corresponded to an + imported namespace. This is not supported anymore, all assemblies have to be + loaded explicitly with ``AddReference``. + +Python.NET uses the PYTHONPATH (``sys.path``) to look for assemblies to load, in +addition to the usual application base and the GAC (if applicable). To ensure +that you can import an assembly, put the directory containing the assembly in +``sys.path``. + +Interacting with .NET +--------------------- + +Using Classes +~~~~~~~~~~~~~ + +Python.NET allows you to use any non-private classes, structs, +interfaces, enums or delegates from Python. To create an instance of a +managed class, you use the standard instantiation syntax, passing a set +of arguments that match one of its public constructors: + +.. code:: python + + from System.Drawing import Point + + p = Point(5, 5) + +In many cases, Python.NET can determine the correct constructor to call +automatically based on the arguments. In some cases, it may be necessary +to call a particular overloaded constructor, which is supported by a +special ``__overloads__`` attribute. + +.. note:: + For compatibility with IronPython, the same functionality is available with + the ``Overloads`` attribute. + +.. code:: python + + from System import String, Char, Int32 + + s = String.Overloads[Char, Int32]('A', 10) + s = String.__overloads__[Char, Int32]('A', 10) + +Using Generics +~~~~~~~~~~~~~~ + +Pythonnet also supports generic types. A generic type must be bound to +create a concrete type before it can be instantiated. Generic types +support the subscript syntax to create bound types: + +.. code:: python + + from System.Collections.Generic import Dictionary + from System import * + + dict1 = Dictionary[String, String]() + dict2 = Dictionary[String, Int32]() + dict3 = Dictionary[String, Type]() + +.. note:: + For backwards-compatibility reasons, this will also work with some native + Python types which are mapped to corresponding .NET types (in particular + ``str -> System.String`` and ``int -> System.Int32``). Since these mappings + are not really one-to-one and can lead to surprising results, use of this + functionality is discouraged and will generate a warning in the future. + +Managed classes can also be subclassed in Python, though members of the +Python subclass are not visible to .NET code. See the ``helloform.py`` +file in the ``/demo`` directory of the distribution for a simple Windows +Forms example that demonstrates subclassing a managed class. + +Fields and Properties +~~~~~~~~~~~~~~~~~~~~~ + +You can get and set fields and properties of CLR objects just as if they +were regular attributes: + +.. code:: python + + from System import Environment + + name = Environment.MachineName + Environment.ExitCode = 1 + +Using Indexers +~~~~~~~~~~~~~~ + +If a managed object implements one or more indexers, one can call the +indexer using standard Python indexing syntax: + +.. code:: python + + from System.Collections import Hashtable + + table = Hashtable() + table["key 1"] = "value 1" + +Overloaded indexers are supported, using the same notation one would use +in C#: + +.. code:: python + + items[0, 2] + items[0, 2, 3] + +Using Methods +~~~~~~~~~~~~~ + +Methods of CLR objects behave generally like normal Python methods. +Static methods may be called either through the class or through an +instance of the class. All public and protected methods of CLR objects +are accessible to Python: + +.. code:: python + + from System import Environment + + drives = Environment.GetLogicalDrives() + +It is also possible to call managed methods "unbound" (passing the +instance as the first argument) just as with Python methods. This is +most often used to explicitly call methods of a base class. + +.. note:: + There is one caveat related to calling unbound methods: it is + possible for a managed class to declare a static method and an instance + method with the same name. Since it is not possible for the runtime to + know the intent when such a method is called unbound, the static method + will always be called. + +The docstring of CLR a method (``__doc__``) can be used to view the +signature of the method, including overloads if the CLR method is +overloaded. You can also use the Python ``help`` method to inspect a +managed class: + +.. code:: python + + from System import Environment + + print(Environment.GetFolderPath.__doc__) + + help(Environment) + + +Advanced Usage +-------------- + +Overloaded and Generic Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While Python.NET will generally be able to figure out the right version +of an overloaded method to call automatically, there are cases where it +is desirable to select a particular method overload explicitly. + +Like constructors, all CLR methods have a ``__overloads__`` property to allow +selecting particular overloads explicitly. + +.. note:: + For compatibility with IronPython, the same functionality is available with + the ``Overloads`` attribute. + +.. code:: python + + from System import Console, Boolean, String, UInt32 + + Console.WriteLine.__overloads__[Boolean](True) + Console.WriteLine.Overloads[String]("string") + Console.WriteLine.__overloads__[UInt32](42) + +Similarly, generic methods may be bound at runtime using the subscript +syntax directly on the method: + +.. code:: python + + someobject.SomeGenericMethod[UInt32](10) + someobject.SomeGenericMethod[String]("10") + +Out and Ref parameters +~~~~~~~~~~~~~~~~~~~~~~ + +When a managed method has ``out`` or ``ref`` parameters, the arguments +appear as normal arguments in Python, but the return value of the method +is modified. There are 3 cases: + +1. If the method is ``void`` and has one ``out`` or ``ref`` parameter, + the method returns the value of that parameter to Python. For + example, if ``someobject`` has a managed method with signature + ``void SomeMethod1(out arg)``, it is called like so: + +.. code:: python + + new_arg = someobject.SomeMethod1(arg) + +where the value of ``arg`` is ignored, but its type is used for overload +resolution. + +2. If the method is ``void`` and has multiple ``out``/``ref`` + parameters, the method returns a tuple containing the ``out``/``ref`` + parameter values. For example, if ``someobject`` has a managed method + with signature ``void SomeMethod2(out arg, ref arg2)``, it is called + like so: + +.. code:: python + + new_arg, new_arg2 = someobject.SomeMethod2(arg, arg2) + +3. Otherwise, the method returns a tuple containing the return value + followed by the ``out``/``ref`` parameter values. For example: + +.. code:: python + + found, new_value = dictionary.TryGetValue(key, value) + +Delegates and Events +~~~~~~~~~~~~~~~~~~~~ + +Delegates defined in managed code can be implemented in Python. A +delegate type can be instantiated and passed a callable Python object to +get a delegate instance. The resulting delegate instance is a true +managed delegate that will invoke the given Python callable when it is +called: + +.. code:: python + + def my_handler(source, args): + print('my_handler called!') + + # instantiate a delegate + d = AssemblyLoadEventHandler(my_handler) + + # use it as an event handler + AppDomain.CurrentDomain.AssemblyLoad += d + +Delegates with ``out`` or ``ref`` parameters can be implemented in +Python by following the convention described in `Out and Ref +parameters <#out-and-ref-parameters>`__. + +Multicast delegates can be implemented by adding more callable objects +to a delegate instance: + +.. code:: python + + d += self.method1 + d += self.method2 + d() + +Events are treated as first-class objects in Python, and behave in many +ways like methods. Python callbacks can be registered with event +attributes, and an event can be called to fire the event. + +Note that events support a convenience spelling similar to that used in +C#. You do not need to pass an explicitly instantiated delegate instance +to an event (though you can if you want). Events support the ``+=`` and +``-=`` operators in a way very similar to the C# idiom: + +.. code:: python + + def handler(source, args): + print('my_handler called!') + + # register event handler + object.SomeEvent += handler + + # unregister event handler + object.SomeEvent -= handler + + # fire the event + result = object.SomeEvent(...) + +Exception Handling +~~~~~~~~~~~~~~~~~~ + +Managed exceptions can be raised and caught in the same way as ordinary Python +exceptions: + +.. code:: python + + from System import NullReferenceException + + try: + raise NullReferenceException("aiieee!") + except NullReferenceException as e: + print(e.Message) + print(e.Source) + +Using Arrays +~~~~~~~~~~~~ + +The type ``System.Array`` supports the subscript syntax in order to make +it easy to create managed arrays from Python: + +.. code:: python + + from System import Array, Int32 + + myarray = Array[Int32](10) + +Managed arrays support the standard Python sequence protocols: + +.. code:: python + + items = SomeObject.GetArray() + + # Get first item + v = items[0] + items[0] = v + + # Get last item + v = items[-1] + items[-1] = v + + # Get length + l = len(items) + + # Containment test + test = v in items + +Multidimensional arrays support indexing using the same notation one +would use in C#: + +.. code:: python + + items[0, 2] + + items[0, 2, 3] + +Using Collections +~~~~~~~~~~~~~~~~~ + +Managed arrays and managed objects that implement the ``IEnumerable`` or +``IEnumerable`` interface can be iterated over using the standard iteration +Python idioms: + +.. code:: python + + domain = System.AppDomain.CurrentDomain + + 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 +--------------- + +Type conversion under Python.NET is fairly straightforward - most +elemental Python types (string, int, long, etc.) convert automatically +to compatible managed equivalents (String, Int32, etc.) and vice-versa. + +Custom type conversions can be implemented as :ref:`Codecs `. + +Types that do not have a logical equivalent in Python are exposed as +instances of managed classes or structs (System.Decimal is an example). + +The .NET architecture makes a distinction between ``value types`` and +``reference types``. Reference types are allocated on the heap, and +value types are allocated either on the stack or in-line within an +object. + +A process called ``boxing`` is used in .NET to allow code to treat a +value type as if it were a reference type. Boxing causes a separate copy +of the value type object to be created on the heap, which then has +reference type semantics. + +Understanding boxing and the distinction between value types and +reference types can be important when using Python.NET because the +Python language has no value type semantics or syntax - in Python +“everything is a reference”. + +Here is a simple example that demonstrates an issue. If you are an +experienced C# programmer, you might write the following code: + +.. code:: python + + items = System.Array.CreateInstance(Point, 3) + for i in range(3): + items[i] = Point(0, 0) + + items[0].X = 1 # won't work!! + +While the spelling of ``items[0].X = 1`` is the same in C# and Python, +there is an important and subtle semantic difference. In C# (and other +compiled-to-IL languages), the compiler knows that Point is a value type +and can do the Right Thing here, changing the value in place. + +In Python however, “everything’s a reference”, and there is really no +spelling or semantic to allow it to do the right thing dynamically. The +specific reason that ``items[0]`` itself doesn’t change is that when you +say ``items[0]``, that getitem operation creates a Python object that +holds a reference to the object at ``items[0]`` via a GCHandle. That +causes a ValueType (like Point) to be boxed, so the following setattr +(``.X = 1``) *changes the state of the boxed value, not the original +unboxed value*. + +The rule in Python is essentially: + + the result of any attribute or item access is a boxed value + +and that can be important in how you approach your code. + +Because there are no value type semantics or syntax in Python, you may +need to modify your approach. To revisit the previous example, we can +ensure that the changes we want to make to an array item aren’t “lost” +by resetting an array member after making changes to it: + +.. code:: python + + items = System.Array.CreateInstance(Point, 3) + for i in range(3): + items[i] = Point(0, 0) + + # This _will_ work. We get 'item' as a boxed copy of the Point + # object actually stored in the array. After making our changes + # we re-set the array item to update the bits in the array. + + item = items[0] + item.X = 1 + items[0] = item + +This is not unlike some of the cases you can find in C# where you have +to know about boxing behavior to avoid similar kinds of ``lost update`` +problems (generally because an implicit boxing happened that was not +taken into account in the code). + +This is the same thing, just the manifestation is a little different in +Python. See the .NET documentation for more details on boxing and the +differences between value types and reference types. 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 a82032472..ad169fb40 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): self.Dispose() diff --git a/tests/test_disposable.py b/tests/test_disposable.py new file mode 100644 index 000000000..3c8fb1159 --- /dev/null +++ b/tests/test_disposable.py @@ -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