Skip to content

Commit 328f8b8

Browse files
arhadthedevAA-Turnerhugovk
authored
gh-93096: Make mimetypes CLI tool public (#93097)
Co-authored-by: Adam Turner <[email protected]> Co-authored-by: Hugo van Kemenade <[email protected]>
1 parent 119bcfa commit 328f8b8

File tree

7 files changed

+179
-79
lines changed

7 files changed

+179
-79
lines changed

Doc/library/cmdline.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ The following modules have a command-line interface.
2424
* :mod:`!idlelib`
2525
* :ref:`inspect <inspect-module-cli>`
2626
* :ref:`json <json-commandline>`
27-
* :mod:`mimetypes`
27+
* :ref:`mimetypes <mimetypes-cli>`
2828
* :mod:`pdb`
2929
* :mod:`pickle`
3030
* :ref:`pickletools <pickletools-cli>`

Doc/library/mimetypes.rst

+95-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ An example usage of the module::
191191

192192
.. _mimetypes-objects:
193193

194-
MimeTypes Objects
194+
MimeTypes objects
195195
-----------------
196196

197197
The :class:`MimeTypes` class may be useful for applications which may want more
@@ -307,3 +307,97 @@ than one MIME-type database; it provides an interface similar to the one of the
307307

308308
When *strict* is ``True`` (the default), the mapping will be added to the
309309
official MIME types, otherwise to the non-standard ones.
310+
311+
312+
.. _mimetypes-cli:
313+
314+
Command-line usage
315+
------------------
316+
317+
The :mod:`!mimetypes` module can be executed as a script from the command line.
318+
319+
.. code-block:: sh
320+
321+
python -m mimetypes [-h] [-e] [-l] type [type ...]
322+
323+
The following options are accepted:
324+
325+
.. program:: mimetypes
326+
327+
.. cmdoption:: -h
328+
--help
329+
330+
Show the help message and exit.
331+
332+
.. cmdoption:: -e
333+
--extension
334+
335+
Guess extension instead of type.
336+
337+
.. cmdoption:: -l
338+
--lenient
339+
340+
Additionally search for some common, but non-standard types.
341+
342+
By default the script converts MIME types to file extensions.
343+
However, if ``--extension`` is specified,
344+
it converts file extensions to MIME types.
345+
346+
For each ``type`` entry, the script writes a line into the standard output
347+
stream. If an unknown type occurs, it writes an error message into the
348+
standard error stream and exits with the return code ``1``.
349+
350+
351+
.. mimetypes-cli-example:
352+
353+
Command-line example
354+
--------------------
355+
356+
Here are some examples of typical usage of the :mod:`!mimetypes` command-line
357+
interface:
358+
359+
.. code-block:: console
360+
361+
$ # get a MIME type by a file name
362+
$ python -m mimetypes filename.png
363+
type: image/png encoding: None
364+
365+
$ # get a MIME type by a URL
366+
$ python -m mimetypes https://example.com/filename.txt
367+
type: text/plain encoding: None
368+
369+
$ # get a complex MIME type
370+
$ python -m mimetypes filename.tar.gz
371+
type: application/x-tar encoding: gzip
372+
373+
$ # get a MIME type for a rare file extension
374+
$ python -m mimetypes filename.pict
375+
error: unknown extension of filename.pict
376+
377+
$ # now look in the extended database built into Python
378+
$ python -m mimetypes --lenient filename.pict
379+
type: image/pict encoding: None
380+
381+
$ # get a file extension by a MIME type
382+
$ python -m mimetypes --extension text/javascript
383+
.js
384+
385+
$ # get a file extension by a rare MIME type
386+
$ python -m mimetypes --extension text/xul
387+
error: unknown type text/xul
388+
389+
$ # now look in the extended database again
390+
$ python -m mimetypes --extension --lenient text/xul
391+
.xul
392+
393+
$ # try to feed an unknown file extension
394+
$ python -m mimetypes filename.sh filename.nc filename.xxx filename.txt
395+
type: application/x-sh encoding: None
396+
type: application/x-netcdf encoding: None
397+
error: unknown extension of filename.xxx
398+
399+
$ # try to feed an unknown MIME type
400+
$ python -m mimetypes --extension audio/aac audio/opus audio/future audio/x-wav
401+
.aac
402+
.opus
403+
error: unknown type audio/future

Doc/whatsnew/3.14.rst

+7
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,13 @@ json
652652
mimetypes
653653
---------
654654

655+
* Document the command-line for :mod:`mimetypes`.
656+
It now exits with ``1`` on failure instead of ``0``
657+
and ``2`` on incorrect command-line parameters instead of ``1``.
658+
Also, errors are printed to stderr instead of stdout and their text is made
659+
tighter.
660+
(Contributed by Oleg Iarygin and Hugo van Kemenade in :gh:`93096`.)
661+
655662
* Add MS and :rfc:`8081` MIME types for fonts:
656663

657664
* Embedded OpenType: ``application/vnd.ms-fontobject``

Lib/mimetypes.py

+31-43
Original file line numberDiff line numberDiff line change
@@ -670,50 +670,38 @@ def _default_mime_types():
670670

671671

672672
def _main():
673-
import getopt
673+
"""Run the mimetypes command-line interface."""
674674
import sys
675-
676-
USAGE = """\
677-
Usage: mimetypes.py [options] type
678-
679-
Options:
680-
--help / -h -- print this message and exit
681-
--lenient / -l -- additionally search of some common, but non-standard
682-
types.
683-
--extension / -e -- guess extension instead of type
684-
685-
More than one type argument may be given.
686-
"""
687-
688-
def usage(code, msg=''):
689-
print(USAGE)
690-
if msg: print(msg)
691-
sys.exit(code)
692-
693-
try:
694-
opts, args = getopt.getopt(sys.argv[1:], 'hle',
695-
['help', 'lenient', 'extension'])
696-
except getopt.error as msg:
697-
usage(1, msg)
698-
699-
strict = 1
700-
extension = 0
701-
for opt, arg in opts:
702-
if opt in ('-h', '--help'):
703-
usage(0)
704-
elif opt in ('-l', '--lenient'):
705-
strict = 0
706-
elif opt in ('-e', '--extension'):
707-
extension = 1
708-
for gtype in args:
709-
if extension:
710-
guess = guess_extension(gtype, strict)
711-
if not guess: print("I don't know anything about type", gtype)
712-
else: print(guess)
713-
else:
714-
guess, encoding = guess_type(gtype, strict)
715-
if not guess: print("I don't know anything about type", gtype)
716-
else: print('type:', guess, 'encoding:', encoding)
675+
from argparse import ArgumentParser
676+
677+
parser = ArgumentParser(description='map filename extensions to MIME types')
678+
parser.add_argument(
679+
'-e', '--extension',
680+
action='store_true',
681+
help='guess extension instead of type'
682+
)
683+
parser.add_argument(
684+
'-l', '--lenient',
685+
action='store_true',
686+
help='additionally search for common but non-standard types'
687+
)
688+
parser.add_argument('type', nargs='+', help='a type to search')
689+
args = parser.parse_args()
690+
691+
if args.extension:
692+
for gtype in args.type:
693+
guess = guess_extension(gtype, not args.lenient)
694+
if guess:
695+
print(guess)
696+
else:
697+
sys.exit(f"error: unknown type {gtype}")
698+
else:
699+
for gtype in args.type:
700+
guess, encoding = guess_type(gtype, not args.lenient)
701+
if guess:
702+
print('type:', guess, 'encoding:', encoding)
703+
else:
704+
sys.exit(f"error: media type unknown for {gtype}")
717705

718706

719707
if __name__ == '__main__':

Lib/test/test_mimetypes.py

+39-34
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import os
44
import sys
55
import unittest.mock
6+
from os import linesep
67

78
from test import support
89
from test.support import os_helper
10+
from test.support.script_helper import run_python_until_end
911
from platform import win32_edition
1012

1113
try:
@@ -390,50 +392,53 @@ def test__all__(self):
390392

391393
class MimetypesCliTestCase(unittest.TestCase):
392394

393-
def mimetypes_cmd(self, *args, **kwargs):
394-
support.patch(self, sys, "argv", [sys.executable, *args])
395-
with support.captured_stdout() as output:
396-
mimetypes._main()
397-
return output.getvalue().strip()
395+
def mimetypes_cmd(cls, *args, **kwargs):
396+
result, _ = run_python_until_end('-m', 'mimetypes', *args)
397+
return result.rc, result.out.decode(), result.err.decode()
398398

399399
def test_help_option(self):
400-
support.patch(self, sys, "argv", [sys.executable, "-h"])
401-
with support.captured_stdout() as output:
402-
with self.assertRaises(SystemExit) as cm:
403-
mimetypes._main()
404-
405-
self.assertIn("Usage: mimetypes.py", output.getvalue())
406-
self.assertEqual(cm.exception.code, 0)
400+
retcode, out, err = self.mimetypes_cmd('-h')
401+
self.assertEqual(retcode, 0)
402+
self.assertStartsWith(out, 'usage: ')
403+
self.assertEqual(err, '')
407404

408405
def test_invalid_option(self):
409-
support.patch(self, sys, "argv", [sys.executable, "--invalid"])
410-
with support.captured_stdout() as output:
411-
with self.assertRaises(SystemExit) as cm:
412-
mimetypes._main()
413-
414-
self.assertIn("Usage: mimetypes.py", output.getvalue())
415-
self.assertEqual(cm.exception.code, 1)
406+
retcode, out, err = self.mimetypes_cmd('--invalid')
407+
self.assertEqual(retcode, 2)
408+
self.assertEqual(out, '')
409+
self.assertStartsWith(err, 'usage: ')
416410

417411
def test_guess_extension(self):
418-
eq = self.assertEqual
419-
420-
extension = self.mimetypes_cmd("-l", "-e", "image/jpg")
421-
eq(extension, ".jpg")
412+
retcode, out, err = self.mimetypes_cmd('-l', '-e', 'image/jpg')
413+
self.assertEqual(retcode, 0)
414+
self.assertEqual(out, f'.jpg{linesep}')
415+
self.assertEqual(err, '')
422416

423-
extension = self.mimetypes_cmd("-e", "image/jpg")
424-
eq(extension, "I don't know anything about type image/jpg")
417+
retcode, out, err = self.mimetypes_cmd('-e', 'image/jpg')
418+
self.assertEqual(retcode, 1)
419+
self.assertEqual(out, '')
420+
self.assertEqual(err, f'error: unknown type image/jpg{linesep}')
425421

426-
extension = self.mimetypes_cmd("-e", "image/jpeg")
427-
eq(extension, ".jpg")
422+
retcode, out, err = self.mimetypes_cmd('-e', 'image/jpeg')
423+
self.assertEqual(retcode, 0)
424+
self.assertEqual(out, f'.jpg{linesep}')
425+
self.assertEqual(err, '')
428426

429427
def test_guess_type(self):
430-
eq = self.assertEqual
431-
432-
type_info = self.mimetypes_cmd("-l", "foo.pic")
433-
eq(type_info, "type: image/pict encoding: None")
434-
435-
type_info = self.mimetypes_cmd("foo.pic")
436-
eq(type_info, "I don't know anything about type foo.pic")
428+
retcode, out, err = self.mimetypes_cmd('-l', 'foo.webp')
429+
self.assertEqual(retcode, 0)
430+
self.assertEqual(out, f'type: image/webp encoding: None{linesep}')
431+
self.assertEqual(err, '')
432+
433+
@unittest.skipIf(
434+
sys.platform == 'darwin',
435+
'macOS lists common_types in mime.types thus making them always known'
436+
)
437+
def test_guess_type_conflicting_with_mimetypes(self):
438+
retcode, out, err = self.mimetypes_cmd('foo.pic')
439+
self.assertEqual(retcode, 1)
440+
self.assertEqual(out, '')
441+
self.assertEqual(err, f'error: media type unknown for foo.pic{linesep}')
437442

438443
if __name__ == "__main__":
439444
unittest.main()

Misc/ACKS

+1
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,7 @@ Oleg Höfling
849849
Robert Hölzl
850850
Stefan Hölzl
851851
Catalin Iacob
852+
Oleg Iarygin
852853
Mihai Ibanescu
853854
Ali Ikinci
854855
Aaron Iles
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Document the command-line for :mod:`mimetypes`.
2+
It now exits with ``1`` on failure instead of ``0``
3+
and ``2`` on incorrect command-line parameters instead of ``1``.
4+
Also, errors are printed to stderr instead of stdout and their text is made
5+
tighter. Patch by Oleg Iarygin and Hugo van Kemenade.

0 commit comments

Comments
 (0)