Repository: python/cpython
Pull request #131408: gh-93096: fix `test_mimetypes.test_guess_type_conflicting_with_mimetypes`
Author: login: picnixz
Labels: name: skip news
Created: 2025-03-18T12:04:08Z | Merged: 2025-04-08T09:08:59Z

Using `run_python_until_end()` ignores `setUpModule()`. In particular, mocking `mimetypes.knownfiles` has no effect for the CLI tests and leads to issues on platforms defining non-standard MIME types such as macOS or openSUSE.

I think this was overlooked in #93097 and this may be the reason why finding the correct extension was tedious. Now, all platforms should support the test correctly (previously, the macOS files were still looked up as `mimetypes.knownfiles` was not unset for CLI tests).

<!--
Thanks for your contribution!
Please read this comment in its entirety. It's quite important.

# Pull Request title

It should be in the following format:

```
gh-NNNNN: Summary of the changes made
```

Where: gh-NNNNN refers to the GitHub issue number.

Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue.

# Backport Pull Request title

If this is a backport PR (PR made against branches other than `main`),
please ensure that the PR title is in the following format:

```
[X.Y] <title from the original PR> (GH-NNNN)
```

Where: [X.Y] is the branch name, e.g. [3.6].

GH-NNNN refers to the PR number from `main`.

-->


<!-- gh-issue-number: gh-93096 -->
* Issue: gh-93096
<!-- /gh-issue-number -->



Diff:
diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py
index 6b94fe3c4df756..67660e4f0368ce 100644
--- a/Lib/mimetypes.py
+++ b/Lib/mimetypes.py
@@ -669,9 +669,7 @@ def _default_mime_types():
 _default_mime_types()
 
 
-def _main():
-    """Run the mimetypes command-line interface."""
-    import sys
+def _parse_args(args):
     from argparse import ArgumentParser
 
     parser = ArgumentParser(description='map filename extensions to MIME types')
@@ -686,23 +684,30 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
+    return args, parser.format_help()
+
+
+def _main(args=None):
+    """Run the mimetypes command-line interface and return a text to print."""
+    import sys
+
+    args, help_text = _parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
-                print(guess)
-            else:
-                sys.exit(f"error: unknown type {gtype}")
+                return str(guess)
+            sys.exit(f"error: unknown type {gtype}")
     else:
         for gtype in args.type:
             guess, encoding = guess_type(gtype, not args.lenient)
             if guess:
-                print('type:', guess, 'encoding:', encoding)
-            else:
-                sys.exit(f"error: media type unknown for {gtype}")
+                return f"type: {guess} encoding: {encoding}"
+            sys.exit(f"error: media type unknown for {gtype}")
+    return parser.format_help()
 
 
 if __name__ == '__main__':
-    _main()
+    print(_main())
diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py
index b5d1f50099e16a..2da1c13800e875 100644
--- a/Lib/test/test_mimetypes.py
+++ b/Lib/test/test_mimetypes.py
@@ -1,14 +1,13 @@
 import io
 import mimetypes
 import os
+import shlex
 import sys
 import unittest.mock
-from os import linesep
-
+from platform import win32_edition
 from test import support
 from test.support import os_helper
 from test.support.script_helper import run_python_until_end
-from platform import win32_edition
 
 try:
     import _winapi
@@ -390,55 +389,52 @@ def test__all__(self):
         support.check__all__(self, mimetypes)
 
 
-class MimetypesCliTestCase(unittest.TestCase):
-
-    def mimetypes_cmd(cls, *args, **kwargs):
-        result, _ = run_python_until_end('-m', 'mimetypes', *args)
-        return result.rc, result.out.decode(), result.err.decode()
-
-    def test_help_option(self):
-        retcode, out, err = self.mimetypes_cmd('-h')
-        self.assertEqual(retcode, 0)
-        self.assertStartsWith(out, 'usage: ')
-        self.assertEqual(err, '')
-
-    def test_invalid_option(self):
-        retcode, out, err = self.mimetypes_cmd('--invalid')
-        self.assertEqual(retcode, 2)
-        self.assertEqual(out, '')
-        self.assertStartsWith(err, 'usage: ')
-
-    def test_guess_extension(self):
-        retcode, out, err = self.mimetypes_cmd('-l', '-e', 'image/jpg')
-        self.assertEqual(retcode, 0)
-        self.assertEqual(out, f'.jpg{linesep}')
-        self.assertEqual(err, '')
-
-        retcode, out, err = self.mimetypes_cmd('-e', 'image/jpg')
-        self.assertEqual(retcode, 1)
-        self.assertEqual(out, '')
-        self.assertEqual(err, f'error: unknown type image/jpg{linesep}')
-
-        retcode, out, err = self.mimetypes_cmd('-e', 'image/jpeg')
-        self.assertEqual(retcode, 0)
-        self.assertEqual(out, f'.jpg{linesep}')
-        self.assertEqual(err, '')
-
-    def test_guess_type(self):
-        retcode, out, err = self.mimetypes_cmd('-l', 'foo.webp')
-        self.assertEqual(retcode, 0)
-        self.assertEqual(out, f'type: image/webp encoding: None{linesep}')
-        self.assertEqual(err, '')
-
-    @unittest.skipIf(
-        sys.platform == 'darwin',
-        'macOS lists common_types in mime.types thus making them always known'
-    )
-    def test_guess_type_conflicting_with_mimetypes(self):
-        retcode, out, err = self.mimetypes_cmd('foo.pic')
-        self.assertEqual(retcode, 1)
-        self.assertEqual(out, '')
-        self.assertEqual(err, f'error: media type unknown for foo.pic{linesep}')
+class CommandLineTest(unittest.TestCase):
+    def test_parse_args(self):
+        args, help_text = mimetypes._parse_args("-h")
+        self.assertTrue(help_text.startswith("usage: "))
+
+        args, help_text = mimetypes._parse_args("--invalid")
+        self.assertTrue(help_text.startswith("usage: "))
+
+        args, _ = mimetypes._parse_args(shlex.split("-l -e image/jpg"))
+        self.assertTrue(args.extension)
+        self.assertTrue(args.lenient)
+        self.assertEqual(args.type, ["image/jpg"])
+
+        args, _ = mimetypes._parse_args(shlex.split("-e image/jpg"))
+        self.assertTrue(args.extension)
+        self.assertFalse(args.lenient)
+        self.assertEqual(args.type, ["image/jpg"])
+
+        args, _ = mimetypes._parse_args(shlex.split("-l foo.webp"))
+        self.assertFalse(args.extension)
+        self.assertTrue(args.lenient)
+        self.assertEqual(args.type, ["foo.webp"])
+
+        args, _ = mimetypes._parse_args(shlex.split("foo.pic"))
+        self.assertFalse(args.extension)
+        self.assertFalse(args.lenient)
+        self.assertEqual(args.type, ["foo.pic"])
+
+
+    def test_invocation(self):
+        for command, expected in [
+            ("-l -e image/jpg", ".jpg"),
+            ("-e image/jpeg", ".jpg"),
+            ("-l foo.webp", "type: image/webp encoding: None"),
+        ]:
+            self.assertEqual(mimetypes._main(shlex.split(command)), expected)
+
+
+    def test_invocation_error(self):
+        for command, expected in [
+            ("-e image/jpg", "error: unknown type image/jpg"),
+            ("foo.pic", "error: media type unknown for foo.pic"),
+        ]:
+            with self.assertRaisesRegex(SystemExit, expected):
+                mimetypes._main(shlex.split(command))
+
 
 if __name__ == "__main__":
     unittest.main()


Reviews:
user: login: hugovk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T14:38:42Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T17:23:06Z
user: login: hugovk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T17:43:01Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T17:47:54Z
user: login: hugovk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T12:48:53Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-30T09:23:22Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T18:10:38Z
user: login: hugovk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T14:57:19Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T15:05:24Z
user: login: AA-Turner
body: An alternative solution to `sys.exit()` with a string or the esoteric `parser.exit()` would just be to directly raise `SystemExit`, would this work?:
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T07:50:16Z
user: login: hugovk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T07:54:46Z
user: login: AA-Turner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T07:58:53Z
user: login: hugovk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T08:17:35Z
user: login: hugovk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T08:19:38Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T08:31:45Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T08:36:15Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T08:37:37Z
user: login: hugovk
body: Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T09:00:27Z

Review comments:
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: hugovk
body: Why split these?
created_at: 2025-03-18T14:38:42Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: picnixz
body: The problem is that `sys.exit(msg)` raises SystemExit but with its `code` attribute being the message and not some integer. OTOH, if the parser fails to parse something, it prints on stderr and then uses sys.exit(2). So I would need to handle an integer `code` coming from a parser error, or a string `code` coming from there (in which case I would also need to define the error code to be 1 explicitly):

```py
with redirect(...):
    try:
        main()
    except SystemExit as exc:
        if isinstance(exc.code, int):
            retcodce = exc.code
            errmsg = ...  # not even sure it's intercepted by the redirection btw
        else:
            retcode = 1
            errmsg = exc.code
```

And then the stream redirections become half-useless.
created_at: 2025-03-18T17:23:06Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: hugovk
body: These both print to stderr with exit code 1 when running something like `./python.exe -m mimetypes --extension thing`:
```python
sys.exit(f"error: unknown type {gtype}")
```

```python
print(f"error: unknown type {gtype}", file=sys.stderr)
sys.exit(1)
```
Let's keep the original.

created_at: 2025-03-23T17:43:01Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: picnixz
body: > sys.exit(f"error: unknown type {gtype}")

Yes, from a CLI perspective, but here we're invoking the `main()` function directly (we're not using subprocess). The reason we can't use subprocess is because we need to mock the module but with subprocess, the module gets re-imported normally and mocks are lost (or am I missing something obvious?)
created_at: 2025-03-23T17:47:54Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: hugovk
body: Ah, it was me missing something, you were referring to the tests :)

In that case, I suggest we step back, and refactor to make it easier to test, rather than worrying about specific return code values and stdout/stderr.

Here's a refactor to make it similar to `random`/`test_random`, based on https://pythontest.com/testing-argparse-apps/:

https://github.com/picnixz/cpython/compare/fix/test/conflicting-mime-types-93096...hugovk:cpython:3.14-fix/test/conflicting-mime-types-93096-refactor?expand=1

This splits out testing of parsing, and CLI output into successful and error cases.
created_at: 2025-03-24T12:48:53Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,14 @@
+import contextlib
 import io
 import mimetypes
 import os
 import sys
+import tempfile
path: Lib/test/test_mimetypes.py
user: login: picnixz
body: ```suggestion
```
created_at: 2025-03-30T09:23:22Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: picnixz
body: I've changed the approach a bit by using `parser.exit()`. It's slightly less work but the tests now correctly check that we're raising a SystemExit with an error code and not an error message. I also don't need to bother about linesep themselves.
created_at: 2025-04-04T18:10:38Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: hugovk
body: Hmm, I didn't know about `parser.exit()` before. I'd still prefer `sys.exit()` as it feels cleaner and much more widely known -- I only see maybe one use in the 8 GB of source code on my disk (not counting CPython, and even little use there).

I'd kind of like us to avoid capturing stdout/stderr and testing actual exit codes, that feels like we're testing the argparse implementation.

What do you think of https://github.com/picnixz/cpython/compare/fix/test/conflicting-mime-types-93096...hugovk:cpython:3.14-fix/test/conflicting-mime-types-93096-refactor?expand=1 ?

created_at: 2025-04-07T14:57:19Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: picnixz
body: I understand the rationale behind but the design seems a bit far-fetched if one reads the module, without knowing that it was to accomodate for tests.

> I'd kind of like us to avoid capturing stdout/stderr and testing actual exit codes, that feels like we're testing the argparse implementation.

That I can also understand, but OTOH, we should be testing the exit codes correctly as any script can decide its own exit code (namely, if I do `python -m module`, it's up to `module` to decide the exit code).

My main concern is that we're calling `parser.help_text()` even if we don't need it (this is the part bothering me actually). I feel that it's fine to test the captured output (we're doing this for other CLIs I think?)

created_at: 2025-04-07T15:05:24Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                # do not use parser.error() as it prints a help message
+                parser.exit(1, f"error: unknown type {gtype}\n")
path: Lib/mimetypes.py
user: login: AA-Turner
body: ```suggestion
                # do not use parser.error() as it prints a help message
                print(f"error: unknown type {gtype}", file=sys.stderr)
                raise SystemExit(1)
```
created_at: 2025-04-08T07:45:58Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                # do not use parser.error() as it prints a help message
+                parser.exit(1, f"error: unknown type {gtype}\n")
     else:
         for gtype in args.type:
             guess, encoding = guess_type(gtype, not args.lenient)
             if guess:
                 print('type:', guess, 'encoding:', encoding)
             else:
-                sys.exit(f"error: media type unknown for {gtype}")
+                # do not use parser.error() as it prints a help message
+                parser.exit(1, f"error: media type unknown for {gtype}\n")
path: Lib/mimetypes.py
user: login: AA-Turner
body: ```suggestion
                # do not use parser.error() as it prints a help message
                print(f"error: media type unknown for {gtype}", file=sys.stderr)
                raise SystemExit(1)
```
created_at: 2025-04-08T07:46:32Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                # do not use parser.error() as it prints a help message
+                parser.exit(1, f"error: unknown type {gtype}\n")
path: Lib/mimetypes.py
user: login: hugovk
body: ```python
sys.exit(f"error: unknown type {gtype}")
```
is much shorter and readable than
```python
print(f"error: unknown type {gtype}", file=sys.stderr)
raise SystemExit(1)
```
created_at: 2025-04-08T07:54:46Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                # do not use parser.error() as it prints a help message
+                parser.exit(1, f"error: unknown type {gtype}\n")
path: Lib/mimetypes.py
user: login: AA-Turner
body: I agree, but seemingly from @picnixz's [comment above](https://github.com/python/cpython/pull/131408#discussion_r2001575526) we can't use `sys.exit` here? 
created_at: 2025-04-08T07:58:53Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                # do not use parser.error() as it prints a help message
+                parser.exit(1, f"error: unknown type {gtype}\n")
path: Lib/mimetypes.py
user: login: hugovk
body: I don't understand why we can't use `sys.exit("message")` and get "message" on stderr with an exit code of 1.

No-one needs to call `_main()` programatically and redirect stdout and stderr, except this PR's tests, and I propose we follow the https://pythontest.com/testing-argparse-apps/ like https://github.com/picnixz/cpython/compare/fix/test/conflicting-mime-types-93096...hugovk:cpython:3.14-fix/test/conflicting-mime-types-93096-refactor?expand=1 so we don't need to do that and can test the help text directly and more simply.
created_at: 2025-04-08T08:17:35Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: hugovk
body: > I understand the rationale behind but the design seems a bit far-fetched if one reads the module, without knowing that it was to accomodate for tests.

Writing code that is easier to test means it can be better tested. We have ~17 files with a `[_]parse_args()` function.

> > I'd kind of like us to avoid capturing stdout/stderr and testing actual exit codes, that feels like we're testing the argparse implementation.
> 
> That I can also understand, but OTOH, we should be testing the exit codes correctly as any script can decide its own exit code (namely, if I do `python -m module`, it's up to `module` to decide the exit code).

We're delegating this stuff to `argparse`, and `argparse` is tested in `test_argparse`.

> My main concern is that we're calling `parser.help_text()` even if we don't need it (this is the part bothering me actually). I feel that it's fine to test the captured output (we're doing this for other CLIs I think?)

(`parser.format_help()` not `parser.help_text()`)

I think we can avoid programatically calling `_main` and redirecting stdout and stderr, and if we want to test the textual output of the CLI, we can do that more directly and simply.
created_at: 2025-04-08T08:19:38Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                print(f"error: unknown type {gtype}", file=sys.stderr)
+                sys.exit(1)
path: Lib/mimetypes.py
user: login: picnixz
body: > I think we can avoid programatically calling _main and redirecting stdout and stderr, and if we want to test the textual output of the CLI, we can do that more directly and simply.

I think I can buy the "more directly and simply" argument so I changed the PR.
created_at: 2025-04-08T08:31:45Z
author_association: MEMBER
diff_hunk: @@ -686,22 +686,24 @@ def _main():
         help='additionally search for common but non-standard types'
     )
     parser.add_argument('type', nargs='+', help='a type to search')
-    args = parser.parse_args()
+    args = parser.parse_args(args)
 
     if args.extension:
         for gtype in args.type:
             guess = guess_extension(gtype, not args.lenient)
             if guess:
                 print(guess)
             else:
-                sys.exit(f"error: unknown type {gtype}")
+                # do not use parser.error() as it prints a help message
+                parser.exit(1, f"error: unknown type {gtype}\n")
path: Lib/mimetypes.py
user: login: picnixz
body: > I don't understand why we can't use sys.exit("message") and get "message" on stderr with an exit code of 1.

While this is no more an issue, it would have been with the previous design where we don't have any exit code actually. `raise SystemExit(string)` has an explicit `code` being "string", not 1. The 1 is actually something from the CLI and it's not retrievable with a `try-except`. That's also why `argparse` is printing to stderr and explicitly using `sys.exit(1)` so that any SystemExit being caught would have a `code` attribute of 1 in this case:

```py
>>> try:
...     raise SystemExit("abc")
... except SystemExit as exc:
...     print(exc.code)
...
abc
```
created_at: 2025-04-08T08:36:15Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,14 @@
+import contextlib
path: Lib/test/test_mimetypes.py
user: login: picnixz
body: ```suggestion
```
created_at: 2025-04-08T08:37:29Z
author_association: MEMBER

PR comments:
user: login: picnixz
created_at: 2025-03-18T12:06:56Z
body: !buildbot macOS
author_association: MEMBER
user: login: bedevere-bot
created_at: 2025-03-18T12:06:59Z
body: :robot: New build scheduled with the buildbot fleet by @picnixz for commit 62863a0b655451d8ae1657c118d6c3f62e641ce4 :robot:

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F131408%2Fmerge

The command will test the builders whose names match following regular expression: `macOS`

The builders matched are:
- `x86-64 MacOS Intel NoGIL PR`
- `x86-64 macOS PR`
- `ARM64 MacOS M1 NoGIL PR`
- `x86-64 MacOS Intel ASAN NoGIL PR`
- `ARM64 MacOS M1 Refleaks NoGIL PR`
- `ARM64 macOS PR`

author_association: NONE
user: login: picnixz
created_at: 2025-03-18T14:13:38Z
body: !buildbot macOS
author_association: MEMBER
user: login: bedevere-bot
created_at: 2025-03-18T14:13:46Z
body: :robot: New build scheduled with the buildbot fleet by @picnixz for commit 7be23f021cd06c8e8cdefdcfa11dd1d7f7fac39f :robot:

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F131408%2Fmerge

The command will test the builders whose names match following regular expression: `macOS`

The builders matched are:
- `x86-64 MacOS Intel NoGIL PR`
- `x86-64 macOS PR`
- `ARM64 MacOS M1 NoGIL PR`
- `x86-64 MacOS Intel ASAN NoGIL PR`
- `ARM64 MacOS M1 Refleaks NoGIL PR`
- `ARM64 macOS PR`

author_association: NONE
user: login: picnixz
created_at: 2025-04-07T09:47:43Z
body: @hugovk friendly ping (I hope we can ship it before the release as it could affect openSUSE users)
author_association: MEMBER
user: login: picnixz
created_at: 2025-04-08T08:39:26Z
body: Mmh the CI seems to hang?
author_association: MEMBER
user: login: picnixz
created_at: 2025-04-08T09:08:27Z
body: Ah I wanted to write the commit message :') but you beat me to it
author_association: MEMBER
user: login: picnixz
created_at: 2025-04-08T11:47:47Z
body: Hum:

```text
FAIL: test_invocation_error (test.test_mimetypes.CommandLineTest.test_invocation_error)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/buildbot/Library/Developer/XCTestDevices/D0521D50-377E-44CF-8CFA-3BC0FF6BF873/data/Containers/Bundle/Application/F1096A52-2582-4077-A71C-A6AFE9D885CD/iOSTestbed.app/python/lib/python3.14/test/test_mimetypes.py", line 435, in test_invocation_error
    with self.assertRaisesRegex(SystemExit, expected):
         ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
AssertionError: SystemExit not raised
```

This is on iOS ARM64. Let's just skip this one on this maybe.
author_association: MEMBER