diff --git a/magic/__init__.py b/magic/__init__.py index d56caaf..10e540f 100644 --- a/magic/__init__.py +++ b/magic/__init__.py @@ -15,6 +15,7 @@ >>> """ +from __future__ import annotations import sys import os @@ -398,6 +399,39 @@ def magic_getparam(cookie, param): return val.value +# Convert magic extensions to imghdr extensions +imghdr_exts = {"ascii": None, "data": None, "iso-8859": None, "openexr": "exr", "riff": "webp", "sgi": "rgb", "sun": "rast"} # , "jpeg": "jpg", "jpg": "jpeg", "openexr": "exr", "sgi": "rgb", "sun": "rast", "riff": "webp", "tif": "tiff"} + + +def what(file: os.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 magic import what + # --- + # Or replace... + import imghdr + ext = imghdr.what(...) + # with... + import magic + ext = magic.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 + """ + if not h: + return from_file(file).split()[0].lower() + + if isinstance(h, str): + h = bytes.fromhex(h) + ext = from_buffer(h).split()[0].lower() + return imghdr_exts.get(ext, ext) + + _has_version = False if hasattr(libmagic, "magic_version"): _has_version = True diff --git a/magic/loader.py b/magic/loader.py index e6edc7b..8ce54ee 100644 --- a/magic/loader.py +++ b/magic/loader.py @@ -12,7 +12,8 @@ def _lib_candidates_linux(): This is necessary because alpine is bad """ - yield "libmagic.so.1" + for filename in ("libmagic.so", "libmagic.so.1"): + yield find_library(filename) def _lib_candidates_macos(): diff --git a/test/test_what.py b/test/test_what.py new file mode 100644 index 0000000..c9a6740 --- /dev/null +++ b/test/test_what.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +from pathlib import Path +from sys import version_info +from warnings import filterwarnings + +import pytest + +from magic 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() + + +@pytest.mark.skipif(imghdr_what is None, reason="imghdr was removed from the standard library in Python 3.13") +@pytest.mark.parametrize("file", [ + "keep-going.jpg", + "name_use.jpg" +]) +def test_what_from_file(file, h=None): + """Run each test with a path string and a pathlib.Path.""" + # expected = file.split(".")[-1] + # if expected == "jpeg": + # expected = "jpg" + file = f"test/testdata/{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="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 = [ + ("exr", "762f3101"), + ("exr", b"\x76\x2f\x31\x01"), + ("gif", "474946383761"), + ("gif", b"GIF87a"), + ("gif", b"GIF89a"), + ("rast", b"\x59\xA6\x6A\x95"), + ("rgb", b"\001\332"), + ("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. + 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", + [ + ("bmp", "424d"), + ("bmp", "424d787878785c3030305c303030"), + ("bmp", b"BM"), + ("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"), + ("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"), + ("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 magic. + TODO: (cclauss) Fix these magic 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