Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

For Python 3.13: A drop-in replacement for imghdr.what() #178

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions filetype/filetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import absolute_import

from os import PathLike

from .match import match
from .types import TYPES, Type

Expand Down Expand Up @@ -96,3 +98,34 @@ def add_type(instance):
raise TypeError('instance must inherit from filetype.types.Type')

types.insert(0, instance)


# Convert filetype extensions to imghdr extensions
imghdr_exts = {"jpg": "jpeg", "tif": "tiff"}


def what(file: PathLike | str | None, h: bytes | None) -> str:
"""A drop-in replacement for `imghdr.what()` which was removed from the standard
library in Python 3.13.

Usage:
```python
# Replace...
from imghdr import what
# with...
from filetype import what
# ---
# Or replace...
import imghdr
ext = imghdr.what(...)
# with...
import filetype
ext = filetype.what(...)
```

imghdr documentation: https://docs.python.org/3.12/library/imghdr.html
imghdr source code: https://github.com/python/cpython/blob/3.12/Lib/imghdr.py
"""
image_type = guess(h) if h else guess(file)
ext = str(image_type) if image_type else None
return imghdr_exts.get(ext, ext)
6 changes: 6 additions & 0 deletions filetype/types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ def __init__(self, mime, extension):
self.__mime = mime
self.__extension = extension

def __str__(self):
return f"{self.extension}"

def __repr__(self):
return f"{self.__class__.__name__}({self.__mime}, {self.__extension})"

@property
def mime(self):
return self.__mime
Expand Down
130 changes: 130 additions & 0 deletions tests/test_what.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from __future__ import annotations

from pathlib import Path
from sys import version_info
from warnings import filterwarnings

import pytest

from filetype.filetype import what

filterwarnings("ignore", message="'imghdr' is deprecated")
try: # imghdr was removed from the standard library in Python 3.13
from imghdr import what as imghdr_what
except ModuleNotFoundError:
imghdr_what = None

# file_tests = sorted(test_func.__name__[5:] for test_func in imghdr.tests)
# file_tests = "bmp exr gif jpg pbm pgm png ppm ras rgb tif webp xbm".split()
file_tests = "gif jpg png tif".split() # TODO: (cclauss) Add the remaining files


@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13")
@pytest.mark.parametrize("file", file_tests)
def test_what_from_file(file, h=None):
"""Run each test with a path string and a pathlib.Path."""
file = f"tests/fixtures/sample.{file}"
assert what(file, h) == imghdr_what(file, h)
file = Path(file).resolve()
assert what(file, h) == imghdr_what(file, h)


@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13")
def ztest_what_from_file_none(file="tests/fixtures/sample_skippable.zst", h=None):
assert what(file, h) == imghdr_what(file, h) is None
file = Path(file).resolve()
assert what(file, h) == imghdr_what(file, h) is None


string_tests = [
("bmp", "424d"),
("bmp", "424d787878785c3030305c303030"),
("bmp", b"BM"),
("gif", "474946383761"),
("gif", b"GIF87a"),
("gif", b"GIF89a"),
("png", "89504e470d0a1a0a"),
("png", b"\211PNG\r\n\032\n"),
(None, "decafbad"),
(None, b"decafbad"),
]


@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13")
@pytest.mark.parametrize("expected, h", string_tests)
def test_what_from_string(expected, h):
if isinstance(h, str): # In imgdir.what() h must be bytes, not str.
h = bytes.fromhex(h)
assert imghdr_what(None, h) == what(None, h) == expected


@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13")
@pytest.mark.parametrize(
"expected, h",
[
("jpeg", "ffd8ffdb"),
("jpeg", b"\xff\xd8\xff\xdb"),
],
)
def test_what_from_string_py311(expected, h):
"""
These tests fail with imghdr on Python < 3.11.
TODO: (cclauss) Document these imghdr fails on Python < 3.11
"""
if isinstance(h, str): # In imgdir.what() h must be bytes, not str.
h = bytes.fromhex(h)
assert what(None, h) == expected
if version_info < (3, 11): # TODO: Document these imghdr fails
expected = None
assert imghdr_what(None, h) == expected


@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13")
@pytest.mark.parametrize(
"expected, h",
[
# ("exr", "762f3101"),
("exr", b"\x76\x2f\x31\x01"),
("jpeg", b"______JFIF"),
("jpeg", b"______Exif"),
("pbm", b"P1 "),
("pbm", b"P1\n"),
("pbm", b"P1\r"),
("pbm", b"P1\t"),
("pbm", b"P4 "),
("pbm", b"P4\n"),
("pbm", b"P4\r"),
("pbm", b"P4\t"),
("pgm", b"P2 "),
("pgm", b"P2\n"),
("pgm", b"P2\r"),
("pgm", b"P2\t"),
("pgm", b"P5 "),
("pgm", b"P5\n"),
("pgm", b"P5\r"),
("pgm", b"P5\t"),
("ppm", b"P3 "),
("ppm", b"P3\n"),
("ppm", b"P3\r"),
("ppm", b"P3\t"),
("ppm", b"P6 "),
("ppm", b"P6\n"),
("ppm", b"P6\r"),
("ppm", b"P6\t"),
("rast", b"\x59\xA6\x6A\x95"),
("rgb", b"\001\332"),
("tiff", b"II"),
("tiff", b"MM"),
("webp", b"RIFF____WEBP"),
("xbm", b"#define "),
],
)
def test_what_from_string_todo(expected, h):
"""
These tests pass with imghdr but fail with filetype.
TODO: (cclauss) Fix these filetype fails
"""
if isinstance(h, str): # In imgdir.what() h must be bytes, not str.
h = bytes.fromhex(h)
assert imghdr_what(None, h) == expected
assert what(None, h) is None
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ deps =
commands = flake8 {toxinidir} --extend-exclude tests,docs,build,dist,venv,.venv --extend-ignore=E501

[testenv:doc]
basepython = python3
basepython = python3.10 # pdoc3/pdoc#438
deps =
pdoc3
commands = pdoc --html --force --output-dir docs filetype
Expand Down
Loading