From b29ea176667db9fe49053e47dcac72534b081e69 Mon Sep 17 00:00:00 2001 From: Ebrahim Namdari <142662279+IbrahimNamdari@users.noreply.github.com> Date: Fri, 15 May 2026 16:11:02 +0330 Subject: [PATCH] Merge pull request #28967 from IbrahimNamdari:add-video-context-manager Python: add context manager compatibility for VideoCapture and VideoWriter #28967 ### Description This PR implements the context manager protocol (`with` statement support) for `cv2.VideoCapture` and `cv2.VideoWriter` objects in Python. ### Problem Currently, these classes do not support the context manager protocol, leading to a `TypeError: ... object does not support the context manager protocol` when used with a `with` block. As reported in issue #28956, if an exception occurs before an explicit `.release()` call, the video resource or file remains locked in the system[cite: 298, 309]. ### Identification & Motivation By analyzing the issue and the Python binding structure of OpenCV, I identified that while the core logic resides in C++, the Pythonic behavior can be enhanced by injecting `__enter__` and `__exit__` methods during the module initialization. This ensures that resources are automatically and safely released even if the program execution is interrupted by an error[cite: 298, 325]. ### Solution I have modified `modules/python/package/cv2/__init__.py` to dynamically inject the necessary methods: 1. Defined `_video_enter` to return the object itself (standard context manager behavior). 2. Defined `_video_exit` to trigger the `.release()` method automatically. 3. Injected these methods into `VideoCapture` and `VideoWriter` classes immediately after the `bootstrap()` function to ensure the native C++ classes are correctly loaded into the global namespace. Fixes #28956 --- .../videoio/misc/python/pyopencv_videoio.hpp | 30 +++++++++++++++++++ .../videoio/misc/python/test/test_videoio.py | 18 +++++++++++ 2 files changed, 48 insertions(+) diff --git a/modules/videoio/misc/python/pyopencv_videoio.hpp b/modules/videoio/misc/python/pyopencv_videoio.hpp index ab26e00c82..a4bc8e5494 100644 --- a/modules/videoio/misc/python/pyopencv_videoio.hpp +++ b/modules/videoio/misc/python/pyopencv_videoio.hpp @@ -141,4 +141,34 @@ bool pyopencv_to(PyObject* obj, Ptr& p, const ArgInfo&) return false; } +static PyObject* pycvVideoEnter(PyObject* self, PyObject* /*args*/, PyObject* /*kw*/) { + Py_INCREF(self); + return self; +} + +static PyObject* pycvVideoCaptureExit(PyObject* self, PyObject* /*args*/, PyObject* /*kw*/) { + Ptr* obj_getp = nullptr; + if (pyopencv_VideoCapture_getp(self, obj_getp) && obj_getp && *obj_getp) { + (*obj_getp)->release(); + } + Py_RETURN_NONE; +} + +static PyObject* pycvVideoWriterExit(PyObject* self, PyObject* /*args*/, PyObject* /*kw*/) { + Ptr* obj_getp = nullptr; + if (pyopencv_VideoWriter_getp(self, obj_getp) && obj_getp && *obj_getp) { + (*obj_getp)->release(); + } + Py_RETURN_NONE; +} + +#define PYOPENCV_EXTRA_METHODS_VideoCapture \ + {"__enter__", CV_PY_FN_WITH_KW(pycvVideoEnter), "Context manager enter"}, \ + {"__exit__", CV_PY_FN_WITH_KW(pycvVideoCaptureExit), "Context manager exit"}, + +#define PYOPENCV_EXTRA_METHODS_VideoWriter \ + {"__enter__", CV_PY_FN_WITH_KW(pycvVideoEnter), "Context manager enter"}, \ + {"__exit__", CV_PY_FN_WITH_KW(pycvVideoWriterExit), "Context manager exit"}, + + #endif // HAVE_OPENCV_VIDEOIO diff --git a/modules/videoio/misc/python/test/test_videoio.py b/modules/videoio/misc/python/test/test_videoio.py index 181ade769d..43f5bb92ff 100644 --- a/modules/videoio/misc/python/test/test_videoio.py +++ b/modules/videoio/misc/python/test/test_videoio.py @@ -5,6 +5,7 @@ import numpy as np import cv2 as cv import io import sys +import tempfile from tests_common import NewOpenCVTests @@ -87,5 +88,22 @@ class Bindings(NewOpenCVTests): self.assertTrue(hasFrame) self.assertEqual(frame.shape, (576, 768, 3)) + def test_context_manager(self): + video_file = self.find_file("cv/video/768x576.avi") + + with cv.VideoCapture(video_file) as cap: + self.assertTrue(cap.isOpened(), "VideoCapture should be opened within context manager") + + with tempfile.NamedTemporaryFile(suffix='.avi') as tmp: + with cv.VideoWriter(tmp.name, cv.VideoWriter_fourcc(*'MJPG'), 25, (640, 480)) as writer: + self.assertTrue(isinstance(writer, cv.VideoWriter)) + + try: + with cv.VideoCapture(video_file) as cap: + self.assertTrue(cap.isOpened()) + raise RuntimeError("Testing context manager exception safety") + except RuntimeError: + pass + if __name__ == '__main__': NewOpenCVTests.bootstrap()