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() #76

Merged
merged 2 commits into from
May 22, 2024
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
build:

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
Expand Down
31 changes: 31 additions & 0 deletions puremagic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"multi_part_dict",
]

# Convert puremagic extensions to imghdr extensions
imghdr_exts = {"dib": "bmp", "jfif": "jpeg", "jpg": "jpeg", "rst": "rast", "sun": "rast", "tif": "tiff"}

here = os.path.abspath(os.path.dirname(__file__))

PureMagic = namedtuple(
Expand Down Expand Up @@ -387,5 +390,33 @@ def command_line_entry(*args):
print("'{0}' : could not be Identified".format(fn))


def what(file: Union[os.PathLike, str, None], h: Union[str, bytes, None]) -> Optional[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 puremagic import what
# ---
# Or replace...
import imghdr
ext = imghdr.what(...)
# with...
import puremagic
ext = puremagic.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
"""
try:
ext = (from_string(h) if h else from_file(file or "")).lstrip(".")
except PureError:
return None # imghdr.what() returns None if it cannot find a match.
return imghdr_exts.get(ext, ext)


if __name__ == "__main__":
command_line_entry()
Empty file added test/__init__.py
Empty file.
131 changes: 131 additions & 0 deletions test/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
from binascii import unhexlify
from pathlib import Path
from sys import version_info
from warnings import filterwarnings

import pytest

from puremagic.main 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 # type: ignore[assignment]

file_tests = "bmp gif jpg png tif webp".split()


@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"test/resources/images/test.{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 test_what_from_file_none(file="test/resources/fake_file", 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 = [
cclauss marked this conversation as resolved.
Show resolved Hide resolved
("bmp", "424d"),
("bmp", "424d787878785c3030305c303030"),
("bmp", b"BM"),
("exr", "762f3101"),
("exr", b"\x76\x2f\x31\x01"),
("gif", "474946383761"),
("gif", b"GIF87a"),
("gif", b"GIF89a"),
("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"),
("png", "89504e470d0a1a0a"),
("png", b"\211PNG\r\n\032\n"),
("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"),
# ("tiff", b"II"), # unhexlify(b'4949')
# ("tiff", b"I I"), # unhexlify(b'492049')
("tiff", b"II*\x00"), # unhexlify(b'49492a00')
("tiff", b"II\\x2a\\x00"), # unhexlify(b'49495c7832615c783030')
("tiff", b"MM\x00*"), # unhexlify(b'4d4d002a')
("tiff", b"MM\x00+"), # unhexlify(b'4d4d002b')
("tiff", b"MM\\x00\\x2a"), # unhexlify(b'4d4d5c7830305c783261')
("webp", b"RIFF____WEBP"),
(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.
"""
if isinstance(h, str): # In imgdir.what() h must be bytes, not str.
h = unhexlify(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",
[
("jpeg", b"______JFIF"),
("jpeg", b"______Exif"),
("rgb", b"\001\332"),
("tiff", b"II"),
("tiff", b"MM"),
("xbm", b"#define "),
],
)
def test_what_from_string_todo(expected, h):
"""
These tests pass with imghdr but fail with puremagic.
"""
assert imghdr_what(None, h) == expected
assert what(None, h) is None