Repository: python-telegram-bot/python-telegram-bot

Issue:
Deprecation: `imghdr` module is being deprecated in Python 3.11
The `imghdr` module is being deprected in `3.11` and will be removed in `3.13`. See [PEP 594](https://peps.python.org/pep-0594/#imghdr) for details. 

We currently use it only in one area: https://github.com/python-telegram-bot/python-telegram-bot/blob/be8f4f7aad9c3ded333950d00ecde57dbdda59c2/telegram/_files/inputfile.py#L108

Alternatives would be:
1. Just use the `mimetype` module? We were using only the `mimetype` module until [@b83a659](https://github.com/python-telegram-bot/python-telegram-bot/commit/b83a659) where we used a 'hack' to detect the type from the byte stream. This was then [replaced](https://github.com/python-telegram-bot/python-telegram-bot/commit/5dc1e4cac19cdd34d7cad4688afa0277a0c8a436) by the `imghdr` module which did the same thing better.
2. Use a library called [filetype](https://github.com/h2non/filetype.py) to detect images. This was [recommended](https://peps.python.org/pep-0594/#deprecated-modules) by the PEP.
3. Just copy the python [source](https://github.com/python/cpython/blob/3.11/Lib/imghdr.py) of the `imghdr` module into ours?

 


Discussion:
Nice catch 👍 I'd be okay with just dropping this completely. It's only revelant if you do something like `await bot.send_photo(chat_id, photo=b'data')` and we even provide the `filename` argument that will be used to derive a proper mimetype in that case. So the cases where we fall back to the default mime type should be rare/avoidable enough.
@harshil21 Interesting topic.

I remember the image detection being a wanted feature. How many users are really using it I can&#x27;t tell. Otoh, deleting this feature will reduce code complexity and it&#x27;s always nice.

If we do decide to keep the feature, the pep suggests (I wouldn&#x27;t say recommend at all, just suggests) several libraries, not only &quot;filetype&quot;. There&#x27;s also the library &quot;puremagic&quot; which should be considered. Personally I prefer libraries without compiled code dependencies.

In any case, I wouldn&#x27;t want to copy the original imghdr to our codebase, as it will force us to support it, and it&#x27;s really not what ptb is about.
> I remember the image detection being a wanted feature.

Interesting to know 👍 I noticed that at the time this was added, the `filename` parameter didn't exist yet - that was introduced only in #1168.

Resolving patch:
diff --git a/telegram/_files/inputfile.py b/telegram/_files/inputfile.py
--- a/telegram/_files/inputfile.py
+++ b/telegram/_files/inputfile.py
@@ -18,7 +18,6 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram InputFile."""
 
-import imghdr
 import logging
 import mimetypes
 from pathlib import Path
@@ -35,7 +34,11 @@ class InputFile:
     """This object represents a Telegram InputFile.
 
     .. versionchanged:: 20.0
-        The former attribute ``attach`` was renamed to :attr:`attach_name`.
+
+        * The former attribute ``attach`` was renamed to :attr:`attach_name`.
+        * Method ``is_image`` was removed. If you pass :obj:`bytes` to :paramref:`obj` and would
+          like to have the mime type automatically guessed, please pass :paramref:`filename`
+          in addition.
 
     Args:
         obj (:term:`file object` | :obj:`bytes` | :obj:`str`): An open file descriptor or the files
@@ -82,39 +85,13 @@ def __init__(
         ):
             filename = Path(obj.name).name  # type: ignore[union-attr]
 
-        image_mime_type = self.is_image(self.input_file_content)
-        if image_mime_type:
-            self.mimetype = image_mime_type
-        elif filename:
-            self.mimetype = mimetypes.guess_type(filename)[0] or _DEFAULT_MIME_TYPE
+        if filename:
+            self.mimetype = mimetypes.guess_type(filename, strict=False)[0] or _DEFAULT_MIME_TYPE
         else:
             self.mimetype = _DEFAULT_MIME_TYPE
 
         self.filename = filename or self.mimetype.replace("/", ".")
 
-    @staticmethod
-    def is_image(stream: bytes) -> Optional[str]:
-        """Check if the content file is an image by analyzing its headers.
-
-        Args:
-            stream (:obj:`bytes`): A byte stream representing the content of a file.
-
-        Returns:
-            :obj:`str` | :obj:`None`: The mime-type of an image, if the input is an image, or
-            :obj:`None` else.
-
-        """
-        try:
-            image = imghdr.what(None, stream)
-            if image:
-                return f"image/{image}"
-            return None
-        except Exception:
-            logger.debug(
-                "Could not parse file content. Assuming that file is not an image.", exc_info=True
-            )
-            return None
-
     @property
     def field_tuple(self) -> FieldTuple:
         """Field tuple representing the contents of the file for upload to the Telegram servers.


Test patch:
diff --git a/tests/data/telegram.midi b/tests/data/telegram.midi
new file mode 100644
Binary files /dev/null and b/tests/data/telegram.midi differ
diff --git a/tests/test_inputfile.py b/tests/test_inputfile.py
--- a/tests/test_inputfile.py
+++ b/tests/test_inputfile.py
@@ -16,7 +16,6 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
-import logging
 import subprocess
 import sys
 from io import BytesIO
@@ -46,8 +45,8 @@ def test_subprocess_pipe(self, png_file):
         in_file = InputFile(proc.stdout)
 
         assert in_file.input_file_content == png_file.read_bytes()
-        assert in_file.mimetype == "image/png"
-        assert in_file.filename == "image.png"
+        assert in_file.mimetype == "application/octet-stream"
+        assert in_file.filename == "application.octet-stream"
 
         try:
             proc.kill()
@@ -69,8 +68,17 @@ def test_attach(self, attach):
     def test_mimetypes(self, caplog):
         # Only test a few to make sure logic works okay
         assert InputFile(data_file("telegram.jpg").open("rb")).mimetype == "image/jpeg"
-        assert InputFile(data_file("telegram.webp").open("rb")).mimetype == "image/webp"
+        # For some reason python can guess the type on macOS
+        assert InputFile(data_file("telegram.webp").open("rb")).mimetype in [
+            "application/octet-stream",
+            "image/webp",
+        ]
         assert InputFile(data_file("telegram.mp3").open("rb")).mimetype == "audio/mpeg"
+        # For some reason windows drops the trailing i
+        assert InputFile(data_file("telegram.midi").open("rb")).mimetype in [
+            "audio/mid",
+            "audio/midi",
+        ]
 
         # Test guess from file
         assert InputFile(BytesIO(b"blah"), filename="tg.jpg").mimetype == "image/jpeg"
@@ -84,11 +92,7 @@ def test_mimetypes(self, caplog):
         assert InputFile(BytesIO(b"blah")).mimetype == "application/octet-stream"
 
         # Test string file
-        with caplog.at_level(logging.DEBUG):
-            assert InputFile(data_file("text_file.txt").open()).mimetype == "text/plain"
-
-            assert len(caplog.records) == 1
-            assert caplog.records[0].getMessage().startswith("Could not parse file content")
+        assert InputFile(data_file("text_file.txt").open()).mimetype == "text/plain"
 
     def test_filenames(self):
         assert InputFile(data_file("telegram.jpg").open("rb")).filename == "telegram.jpg"
@@ -111,7 +115,10 @@ def __init__(self, f):
             def read(self):
                 return self.f.read()
 
-        assert InputFile(MockedFileobject(data_file("telegram.jpg"))).filename == "image.jpeg"
+        assert (
+            InputFile(MockedFileobject(data_file("telegram.jpg"))).filename
+            == "application.octet-stream"
+        )
         assert (
             InputFile(MockedFileobject(data_file("telegram.jpg")), filename="blah").filename
             == "blah"