diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c4e4c9..251eb0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,11 +5,11 @@ jobs: strategy: fail-fast: false matrix: - os: ['ubuntu-latest'] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + os: ["ubuntu-latest"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] include: - os: macos-latest - python-version: '3.13' + python-version: "3.13" # - os: windows-latest # TODO: Fix the Windows test that runs in an infinite loop # python-version: '3.13' runs-on: ${{ matrix.os }} @@ -28,4 +28,4 @@ jobs: run: pip install python-magic-bin - run: LC_ALL=en_US.UTF-8 pytest shell: bash - timeout-minutes: 15 # Limit Windows infinite loop. + timeout-minutes: 15 # Limit Windows infinite loop. diff --git a/magic/loader.py b/magic/loader.py index e6edc7b..f8d59fa 100644 --- a/magic/loader.py +++ b/magic/loader.py @@ -7,6 +7,7 @@ logger = logging.getLogger(__name__) + def _lib_candidates_linux(): """Yield possible libmagic library names on Linux. @@ -51,7 +52,7 @@ def _lib_candidates(): "darwin": _lib_candidates_macos, "linux": _lib_candidates_linux, "win32": _lib_candidates_windows, - "sunos5": _lib_candidates_linux, + "sunos5": _lib_candidates_linux, }.get(sys.platform) if func is None: raise ImportError("python-magic: Unsupported platform: " + sys.platform) @@ -61,17 +62,20 @@ def _lib_candidates(): def load_lib(): + exc = [] for lib in _lib_candidates(): # find_library returns None when lib not found if lib is None: continue - if not os.path.exists(lib): - continue try: return ctypes.CDLL(lib) - except OSError: - logger.warning("Failed to load: " + lib, exc_info=True) + except OSError as e: + exc.append(e) + + msg = "\n".join([str(e) for e in exc]) # It is better to raise an ImportError since we are importing magic module - raise ImportError("python-magic: failed to find libmagic. Check your installation") + raise ImportError( + "python-magic: failed to find libmagic. Check your installation: \n" + msg + ) diff --git a/setup.py b/setup.py index d98b731..54aff08 100644 --- a/setup.py +++ b/setup.py @@ -8,41 +8,43 @@ def read(file_name): """Read a text file and return the content as a string.""" - with io.open(os.path.join(os.path.dirname(__file__), file_name), - encoding='utf-8') as f: + with io.open( + os.path.join(os.path.dirname(__file__), file_name), encoding="utf-8" + ) as f: return f.read() + setuptools.setup( - name='python-magic', - description='File type identification using libmagic', - author='Adam Hupp', - author_email='adam@hupp.org', + name="python-magic", + description="File type identification using libmagic", + author="Adam Hupp", + author_email="adam@hupp.org", url="http://github.com/ahupp/python-magic", - version='0.4.28', - long_description=read('README.md'), - long_description_content_type='text/markdown', - packages=['magic'], + version="0.4.28", + long_description=read("README.md"), + long_description_content_type="text/markdown", + packages=["magic"], package_data={ - 'magic': ['py.typed', '*.pyi', '**/*.pyi'], + "magic": ["py.typed", "*.pyi", "**/*.pyi"], }, keywords="mime magic file", license="MIT", - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", classifiers=[ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: Implementation :: CPython', + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", ], ) - diff --git a/test/python_magic_test.py b/test/python_magic_test.py index b557762..5076044 100755 --- a/test/python_magic_test.py +++ b/test/python_magic_test.py @@ -5,6 +5,7 @@ import shutil import sys import tempfile +from typing import List, Union import unittest import pytest @@ -19,140 +20,162 @@ import magic + @dataclass class TestFile: file_name: str - mime_results: list[str] - text_results: list[str] - no_check_elf_results: list[str] | None + mime_results: List[str] + text_results: List[str] + no_check_elf_results: Union[List[str], None] buf_equals_file: bool = True + # magic_descriptor is broken (?) in centos 7, so don't run those tests SKIP_FROM_DESCRIPTOR = bool(os.environ.get("SKIP_FROM_DESCRIPTOR")) -COMMON_PLAIN = [ - {}, - {"check_soft": True}, - {"check_soft": False}, - {"check_json": True}, - {"check_json": False}, -] - -NO_SOFT = {"check_soft": False} - -COMMON_MIME = [{"mime": True, **k} for k in COMMON_PLAIN] +COMMON_PLAIN = [{}] +NO_SOFT = [{"check_soft": False}] +COMMON_MIME = [{"mime": True}] CASES = { - "magic._pyc_": [ - (COMMON_MIME, [ - "application/octet-stream", - "text/x-bytecode.python", - "application/x-bytecode.python", - ]), + b"magic._pyc_": [ + ( + COMMON_MIME, + [ + "application/octet-stream", + "text/x-bytecode.python", + "application/x-bytecode.python", + ], + ), (COMMON_PLAIN, ["python 2.4 byte-compiled"]), (NO_SOFT, ["data"]), ], - "test.pdf": [ + b"test.pdf": [ (COMMON_MIME, ["application/pdf"]), - (COMMON_PLAIN, [ - "PDF document, version 1.2", - "PDF document, version 1.2, 2 pages", - "PDF document, version 1.2, 2 page(s)", - ]), + ( + COMMON_PLAIN, + [ + "PDF document, version 1.2", + "PDF document, version 1.2, 2 pages", + "PDF document, version 1.2, 2 page(s)", + ], + ), (NO_SOFT, ["ASCII text"]), ], - "test.gz": [ + b"test.gz": [ (COMMON_MIME, ["application/gzip", "application/x-gzip"]), - (COMMON_PLAIN, [ - 'gzip compressed data, was "test", from Unix, last modified: Sun Jun 29 01:32:52 2008', - 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix', - 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix, original size 15', - 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix, original size modulo 2^32 15', - 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix, truncated', - ]), - ({"extension": True}, [ - # some versions return '' for the extensions of a gz file, - # including w/ the command line. Who knows... - "gz/tgz/tpz/zabw/svgz/adz/kmy/xcfgz", - "gz/tgz/tpz/zabw/svgz", - "", - "???", - ]), + ( + COMMON_PLAIN, + [ + 'gzip compressed data, was "test", from Unix, last modified: Sun Jun 29 01:32:52 2008', + 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix', + 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix, original size 15', + 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix, original size modulo 2^32 15', + 'gzip compressed data, was "test", last modified: Sun Jun 29 01:32:52 2008, from Unix, truncated', + ], + ), + ( + [{"extension": True}], + [ + # some versions return '' for the extensions of a gz file, + # including w/ the command line. Who knows... + "gz/tgz/tpz/zabw/svgz/adz/kmy/xcfgz", + "gz/tgz/tpz/zabw/svgz", + "", + "???", + ], + ), (NO_SOFT, ["data"]), ], - "test.snappy.parquet": [ + b"test.snappy.parquet": [ (COMMON_MIME, ["application/octet-stream"]), (COMMON_PLAIN, ["Apache Parquet", "Par archive data"]), (NO_SOFT, ["data"]), ], - "test.json": [ - # TODO: soft, no_json + b"test.json": [ (COMMON_MIME, ["application/json"]), (COMMON_PLAIN, ["JSON text data"]), - ({"mime": True, "check_json": False}, [ - "data", - ]), - (NO_SOFT, ["JSON text data"]) + ( + [{"mime": True, "check_json": False}], + [ + "text/plain", + ], + ), + (NO_SOFT, ["JSON text data"]), ], - "elf-NetBSD-x86_64-echo": [ + b"elf-NetBSD-x86_64-echo": [ # TODO: soft, no elf - (COMMON_PLAIN, [ - "ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)", - "ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /libexec/ld.elf_so, for NetBSD 8.0, not stripped", - ]), - (COMMON_MIME, [ - "application/x-pie-executable", - "application/x-sharedlib", - ]), - ({"check_elf": False}, [ - "ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)", - ]), + ( + COMMON_PLAIN, + [ + "ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)", + "ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /libexec/ld.elf_so, for NetBSD 8.0, not stripped", + ], + ), + ( + COMMON_MIME, + [ + "application/x-pie-executable", + "application/x-sharedlib", + ], + ), + ( + [{"check_elf": False}], + [ + "ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)", + ], + ), # TODO: sometimes # "ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /libexec/ld.elf_so, for NetBSD 8.0, not stripped", - (NO_SOFT, ["data"]), ], - "test.txt": [ + b"text.txt": [ (COMMON_MIME, ["text/plain"]), (COMMON_PLAIN, ["ASCII text"]), - ({"mime_encoding": True}, [ - "us-ascii", - ]), + ( + [{"mime_encoding": True}], + [ + "us-ascii", + ], + ), (NO_SOFT, ["ASCII text"]), ], - "text-iso8859-1.txt": [ - ({"mime_encoding": True}, [ - "iso-8859-1", - ]), + b"text-iso8859-1.txt": [ + ( + [{"mime_encoding": True}], + [ + "iso-8859-1", + ], + ), ], b"\xce\xbb": [ (COMMON_MIME, ["text/plain"]), ], - "b\xce\xbb".decode("utf-8"): [ - (COMMON_MIME, ["text/plain"]), + b"name_use.jpg": [ + ([{"extension": True}], ["jpeg/jpg/jpe/jfif"]), ], - "name_use.jpg": [ - ({"extension": True}, [ - "jpeg/jpg/jpe/jfif" - ]), + b"keep-going.jpg": [ + (COMMON_MIME, ["image/jpeg"]), + ( + [{"mime": True, "keep_going": True}], + [ + "image/jpeg\\012- application/octet-stream", + ], + ), ], - "keep-going.jpg": [ - (COMMON_MIME, [ - "image/jpeg" - ]), - ({"mime": True, "keep_going": True}, [ - "image/jpeg\\012- application/octet-stream", - ]) + b"../../magic/loader.py": [ + ( + COMMON_MIME, + [ + "text/x-python", + "text/x-script.python", + ], + ) ], - "test.py": [ - (COMMON_MIME, [ - "text/x-python", - "text/x-script.python", - ]) - ] } + class MagicTest(unittest.TestCase): TESTDATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "testdata")) @@ -165,7 +188,6 @@ def test_version(self): def test_fs_encoding(self): self.assertEqual("utf-8", sys.getfilesystemencoding().lower()) - def test_from_file_str_and_bytes(self): filename = os.path.join(self.TESTDATA_DIR, "test.pdf") @@ -174,7 +196,6 @@ def test_from_file_str_and_bytes(self): "application/pdf", magic.from_file(filename.encode("utf-8"), mime=True) ) - def test_all_cases(self): # TODO: # * MAGIC_EXTENSION not supported @@ -184,21 +205,24 @@ def test_all_cases(self): shutil.copyfile(os.path.join(MagicTest.TESTDATA_DIR, "lambda"), dest) os.environ["TZ"] = "UTC" try: - for file_name, cases in CASES: - filename = os.path.join(self.TESTDATA_DIR, file_name) - for flags, outputs in cases: - m = magic.Magic(**flags) - with open(filename) as f: - self.assertIn(m.from_descriptor(f.fileno()), outputs) - - self.assertIn(m.from_file(filename), outputs) - - fname_bytes = filename.encode("utf-8") - self.assertIn(m.from_file(fname_bytes), outputs) - - with open(file_name, "rb") as f: - buf_result = m.from_buffer(f.read(1024)) - self.assertIn(buf_result, outputs) + for filename, cases in CASES.items(): + filename = os.path.join(self.TESTDATA_DIR.encode("utf-8"), filename) + print("test case ", filename, file=sys.stderr) + for flag_variants, outputs in cases: + for flags in flag_variants: + print("flags", flags, file=sys.stderr) + m = magic.Magic(**flags) + with open(filename) as f: + self.assertIn(m.from_descriptor(f.fileno()), outputs) + + self.assertIn(m.from_file(filename), outputs) + + fname_str = filename.decode("utf-8") + self.assertIn(m.from_file(fname_str), outputs) + + with open(filename, "rb") as f: + buf_result = m.from_buffer(f.read(1024)) + self.assertIn(buf_result, outputs) finally: del os.environ["TZ"] os.unlink(dest) @@ -222,7 +246,6 @@ def test_unicode_result_raw(self): else: raise unittest.SkipTest("Magic file doesn't return expected type.") - def test_errors(self): m = magic.Magic() self.assertRaises(IOError, m.from_file, "nonexistent") @@ -233,7 +256,6 @@ def test_errors(self): finally: del os.environ["MAGIC"] - def test_rethrow(self): old = magic.magic_buffer try: diff --git a/tox.ini b/tox.ini index b6ed98c..5c1648b 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = py310, py311, py312, + py313, mypy [testenv]