Repository: Ch00k/ffmpy

Issue:
[Feature] Replace subprocess.Popen with psutil.Popen
This is more so to gauge interest in this feature. The `psutil` library is used for finding process information (such as CPU / memory usage) and overall system information. It also provides a wrapper to `subprocess.Popen` in their own `psutil.Popen` that makes tracking this information much easier.

I created my own Python class that inherits from `ffmpy.FFmpeg` where I replaced the process opening method so I can track resource usage, but I was curious if this is something others would want added to this library.

Hints from issue comments:
@Ch00k: I think this is a good idea. I would, however, instead of replacing `subprocess.Popen` with `psutil.Popen`, make `psutil` an optional dependency, and, if it is installed, use `psutil.Popen` instead of `subprocess.Popen`. This way we'd keep backwards compatibility, and keep the package dependency-free by default.

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 534b3bb..c3f51c0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,8 +47,10 @@ jobs:
           architecture: x64
       - run: |
           pip install "cryptography<44.0.0" poetry  # cryptography 44.0.0 is not compatible with pypy3.8
-          poetry install --with dev
-      - run: poetry run pytest --cov=ffmpy --cov-report xml
+          poetry install
+          poetry run pytest --cov=ffmpy --cov-report xml
+          poetry install --extras psutil
+          poetry run pytest --cov=ffmpy --cov-report xml --cov-append
       - uses: codecov/codecov-action@v4
         if: matrix.python-version == 3.12
         with:
diff --git a/ffmpy/ffmpy.py b/ffmpy/ffmpy.py
index 3d1f513..45a225e 100644
--- a/ffmpy/ffmpy.py
+++ b/ffmpy/ffmpy.py
@@ -6,6 +6,15 @@
 import subprocess
 from typing import IO, Any, Mapping, Sequence
 
+try:
+    from psutil import Popen  # noqa: F401
+
+    popen: type[subprocess.Popen | Popen]
+except ImportError:
+    popen = subprocess.Popen
+else:
+    popen = Popen
+
 
 class FFmpeg:
     """Wrapper for various `FFmpeg <https://www.ffmpeg.org/>`_ related applications (ffmpeg,
@@ -56,7 +65,7 @@ def __init__(
             self._cmd += _merge_args_opts(outputs)
 
         self.cmd = subprocess.list2cmdline(self._cmd)
-        self.process: subprocess.Popen | None = None
+        self.process: subprocess.Popen | Popen | None = None
 
     def __repr__(self) -> str:
         return f"<{self.__class__.__name__!r} {self.cmd!r}>"
@@ -100,7 +109,7 @@ def run(
             `FFExecutableNotFoundError` in case the executable path passed was not valid
         """
         try:
-            self.process = subprocess.Popen(
+            self.process = popen(
                 self._cmd, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, env=env, **kwargs
             )
         except OSError as e:
diff --git a/poetry.lock b/poetry.lock
index 131d85d..99edcd7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -213,6 +213,36 @@ files = [
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
+[[package]]
+name = "psutil"
+version = "6.1.1"
+description = "Cross-platform lib for process and system monitoring in Python."
+optional = true
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
+files = [
+    {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"},
+    {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"},
+    {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"},
+    {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"},
+    {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"},
+    {file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"},
+    {file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"},
+    {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"},
+    {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"},
+    {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"},
+    {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"},
+    {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"},
+    {file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"},
+    {file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"},
+    {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"},
+    {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"},
+    {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"},
+]
+
+[package.extras]
+dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"]
+test = ["pytest", "pytest-xdist", "setuptools"]
+
 [[package]]
 name = "pytest"
 version = "8.3.4"
@@ -305,7 +335,10 @@ files = [
     {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 ]
 
+[extras]
+psutil = ["psutil"]
+
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "871996c704071e8c416519c845ac27560b453b8d3ba5b1f9c2536ecd1e20b32d"
+content-hash = "ffdb378a823b702a828bd6c7de9a9c86e2dbb2fe4d09318ed19c9b42486bde9c"
diff --git a/pyproject.toml b/pyproject.toml
index ae27c85..d5dfcff 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,12 +44,16 @@ packages = [{ include = "ffmpy" }]
 
 [tool.poetry.dependencies]
 python = "^3.8"
+psutil = { version = ">=5.5.0", optional = true } # 5.5.0 was realease around the same time as Python 3.8
 
 [tool.poetry.group.dev.dependencies]
 mypy = "^1.13.0"
 pytest = "^8.3.4"
 pytest-cov = "^5.0.0"
 
+[tool.poetry.extras]
+psutil = ["psutil"]
+
 [build-system]
 requires = ["poetry-core"]
 build-backend = "poetry.core.masonry.api"


Test patch:
diff --git a/tests/test_cmd_execution.py b/tests/test_cmd_execution.py
index 576e242..49b68bb 100644
--- a/tests/test_cmd_execution.py
+++ b/tests/test_cmd_execution.py
@@ -202,7 +202,7 @@ def test_terminate_process() -> None:
     assert ff.process.returncode == -15
 
 
-@mock.patch("ffmpy.ffmpy.subprocess.Popen")
+@mock.patch("ffmpy.ffmpy.popen")
 def test_custom_env(popen_mock: mock.MagicMock) -> None:
     ff = FFmpeg()
     popen_mock.return_value.communicate.return_value = ("output", "error")
@@ -213,7 +213,7 @@ def test_custom_env(popen_mock: mock.MagicMock) -> None:
     )
 
 
-@mock.patch("ffmpy.ffmpy.subprocess.Popen")
+@mock.patch("ffmpy.ffmpy.popen")
 def test_arbitraty_popen_kwargs(popen_mock: mock.MagicMock) -> None:
     ff = FFmpeg()
     popen_mock.return_value.communicate.return_value = ("output", "error")