From 946690f4be5af166c94eba133b9462848131360b Mon Sep 17 00:00:00 2001 From: Drying <144075925+dailyun@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:39:46 +0800 Subject: [PATCH 1/2] Add a python -m magic command-line entry point --- README.md | 14 ++++++++++ magic/__main__.py | 34 +++++++++++++++++++++++ test/cli_test.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 magic/__main__.py create mode 100644 test/cli_test.py diff --git a/README.md b/README.md index 4bd907a3..a5df8c09 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,20 @@ You can also combine the flag options: 'text/plain' ``` +## Command line + +Identify one or more files using the same libmagic installation as the Python API: + +```sh +python -m magic example.pdf +python -m magic --mime example.pdf image.jpg +``` + +Each result is printed as `filename: description` (or `filename: MIME type`). +Errors raised by file access or libmagic go to stderr; remaining files are +still inspected, and the command exits with status 1. Use `--` before a filename +that starts with a dash. This requires libmagic just like the Python API. + ## Installation The current stable version of python-magic is available on PyPI and diff --git a/magic/__main__.py b/magic/__main__.py new file mode 100644 index 00000000..c6b81941 --- /dev/null +++ b/magic/__main__.py @@ -0,0 +1,34 @@ +"""Identify files with ``python -m magic``.""" + +from __future__ import print_function # noqa: UP010 - Keep the advertised Python 2.7 syntax. + +import argparse +import sys + +from magic import MagicException, from_file + + +def main(argv=None): + parser = argparse.ArgumentParser( + prog="python -m magic", description="Identify file types using libmagic" + ) + parser.add_argument( + "--mime", action="store_true", help="print MIME types instead of descriptions" + ) + parser.add_argument("files", nargs="+", help="files to identify") + args = parser.parse_args(argv) + + status = 0 + for filename in args.files: + try: + result = from_file(filename, mime=args.mime) + except (OSError, MagicException) as error: + print("{}: {}".format(filename, error), file=sys.stderr) # noqa: UP032 + status = 1 + else: + print("{}: {}".format(filename, result)) # noqa: UP032 + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/cli_test.py b/test/cli_test.py new file mode 100644 index 00000000..a14f6342 --- /dev/null +++ b/test/cli_test.py @@ -0,0 +1,71 @@ +import importlib +import subprocess +import sys +from pathlib import Path + +import magic + +ROOT = Path(__file__).resolve().parent.parent +PDF = ROOT / "test" / "testdata" / "test.pdf" + + +def run_cli(*args): + return subprocess.run( + [sys.executable, "-m", "magic"] + list(args), + cwd=str(ROOT), + check=False, + capture_output=True, + text=True, + ) + + +def test_module_description(): + result = run_cli(str(PDF)) + assert result.returncode == 0 + assert result.stdout == f"{PDF}: {magic.from_file(str(PDF))}\n" + assert result.stderr == "" + + +def test_module_mime(): + result = run_cli("--mime", str(PDF)) + assert result.returncode == 0 + assert result.stdout == f"{PDF}: application/pdf\n" + assert result.stderr == "" + + +def test_module_continues_after_missing_file(tmp_path): + missing = str(tmp_path / "not found.pdf") + result = run_cli(missing, str(PDF)) + assert result.returncode == 1 + assert result.stdout == f"{PDF}: {magic.from_file(str(PDF))}\n" + assert missing in result.stderr + assert "Traceback" not in result.stderr + + +def test_module_handles_unicode_file_name(tmp_path): + path = tmp_path / "a unicode \u00e9 file.txt" + path.write_text("plain text\n", encoding="utf-8") + result = run_cli(str(path)) + assert result.returncode == 0 + assert result.stdout == f"{path}: {magic.from_file(str(path))}\n" + + +def test_module_requires_file(): + result = run_cli() + assert result.returncode == 2 + assert "usage:" in result.stderr + assert "Traceback" not in result.stderr + + +def test_module_reports_libmagic_failure(monkeypatch, capsys): + cli = importlib.import_module("magic.__main__") + + def fail(filename, mime=False): + raise magic.MagicException("libmagic failed") + + monkeypatch.setattr(cli, "from_file", fail) + assert cli.main(["example.pdf"]) == 1 + captured = capsys.readouterr() + assert captured.out == "" + assert "example.pdf" in captured.err + assert "libmagic failed" in captured.err From e927a9af17528886f4a6fb5e4a4d3403c883b46a Mon Sep 17 00:00:00 2001 From: Drying <144075925+dailyun@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:25:08 +0800 Subject: [PATCH 2/2] Keep CLI tests compatible with older supported Python versions --- test/cli_test.py | 54 +++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/test/cli_test.py b/test/cli_test.py index a14f6342..940356c2 100644 --- a/test/cli_test.py +++ b/test/cli_test.py @@ -1,53 +1,59 @@ +# Keep these tests compatible with the Python 2.7+ matrix in tox.ini. +# ruff: noqa: UP032 import importlib +import os import subprocess import sys -from pathlib import Path +from collections import namedtuple import magic -ROOT = Path(__file__).resolve().parent.parent -PDF = ROOT / "test" / "testdata" / "test.pdf" +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PDF = os.path.join(ROOT, "test", "testdata", "test.pdf") +CLIResult = namedtuple("CLIResult", "returncode stdout stderr") def run_cli(*args): - return subprocess.run( + process = subprocess.Popen( [sys.executable, "-m", "magic"] + list(args), - cwd=str(ROOT), - check=False, - capture_output=True, - text=True, + cwd=ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, ) + stdout, stderr = process.communicate() + return CLIResult(process.returncode, stdout, stderr) def test_module_description(): - result = run_cli(str(PDF)) + result = run_cli(PDF) assert result.returncode == 0 - assert result.stdout == f"{PDF}: {magic.from_file(str(PDF))}\n" + assert result.stdout == "{}: {}\n".format(PDF, magic.from_file(PDF)) assert result.stderr == "" def test_module_mime(): - result = run_cli("--mime", str(PDF)) + result = run_cli("--mime", PDF) assert result.returncode == 0 - assert result.stdout == f"{PDF}: application/pdf\n" + assert result.stdout == "{}: application/pdf\n".format(PDF) assert result.stderr == "" -def test_module_continues_after_missing_file(tmp_path): - missing = str(tmp_path / "not found.pdf") - result = run_cli(missing, str(PDF)) +def test_module_continues_after_missing_file(tmpdir): + missing = str(tmpdir.join("not found.pdf")) + result = run_cli(missing, PDF) assert result.returncode == 1 - assert result.stdout == f"{PDF}: {magic.from_file(str(PDF))}\n" + assert result.stdout == "{}: {}\n".format(PDF, magic.from_file(PDF)) assert missing in result.stderr assert "Traceback" not in result.stderr -def test_module_handles_unicode_file_name(tmp_path): - path = tmp_path / "a unicode \u00e9 file.txt" - path.write_text("plain text\n", encoding="utf-8") +def test_module_handles_unicode_file_name(tmpdir): + path = tmpdir.join(b"a unicode \xc3\xa9 file.txt".decode("utf-8")) + path.write_binary(b"plain text\n") result = run_cli(str(path)) assert result.returncode == 0 - assert result.stdout == f"{path}: {magic.from_file(str(path))}\n" + assert result.stdout == "{}: {}\n".format(path, magic.from_file(str(path))) def test_module_requires_file(): @@ -65,7 +71,7 @@ def fail(filename, mime=False): monkeypatch.setattr(cli, "from_file", fail) assert cli.main(["example.pdf"]) == 1 - captured = capsys.readouterr() - assert captured.out == "" - assert "example.pdf" in captured.err - assert "libmagic failed" in captured.err + stdout, stderr = capsys.readouterr() + assert stdout == "" + assert "example.pdf" in stderr + assert "libmagic failed" in stderr