diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3e1c5f5..d2add50 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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"] diff --git a/puremagic/main.py b/puremagic/main.py index 76281e4..394f16a 100644 --- a/puremagic/main.py +++ b/puremagic/main.py @@ -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( @@ -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() diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000..2c3b177 --- /dev/null +++ b/test/test_main.py @@ -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 = [ + ("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