Commit aa2d1b37 authored by R David Murray's avatar R David Murray

Merge: #14977: Make mailcap respect the order of the lines in the mailcap file.

parents 91c258cf 4e830165
"""Mailcap file handling. See RFC 1524.""" """Mailcap file handling. See RFC 1524."""
import os import os
import warnings
__all__ = ["getcaps","findmatch"] __all__ = ["getcaps","findmatch"]
def lineno_sort_key(entry):
# Sort in ascending order, with unspecified entries at the end
if 'lineno' in entry:
return 0, entry['lineno']
else:
return 1, 0
# Part 1: top-level interface. # Part 1: top-level interface.
def getcaps(): def getcaps():
...@@ -17,13 +27,14 @@ def getcaps(): ...@@ -17,13 +27,14 @@ def getcaps():
""" """
caps = {} caps = {}
lineno = 0
for mailcap in listmailcapfiles(): for mailcap in listmailcapfiles():
try: try:
fp = open(mailcap, 'r') fp = open(mailcap, 'r')
except OSError: except OSError:
continue continue
with fp: with fp:
morecaps = readmailcapfile(fp) morecaps, lineno = _readmailcapfile(fp, lineno)
for key, value in morecaps.items(): for key, value in morecaps.items():
if not key in caps: if not key in caps:
caps[key] = value caps[key] = value
...@@ -49,8 +60,15 @@ def listmailcapfiles(): ...@@ -49,8 +60,15 @@ def listmailcapfiles():
# Part 2: the parser. # Part 2: the parser.
def readmailcapfile(fp): def readmailcapfile(fp):
"""Read a mailcap file and return a dictionary keyed by MIME type."""
warnings.warn('readmailcapfile is deprecated, use getcaps instead',
DeprecationWarning, 2)
caps, _ = _readmailcapfile(fp, None)
return caps
def _readmailcapfile(fp, lineno):
"""Read a mailcap file and return a dictionary keyed by MIME type. """Read a mailcap file and return a dictionary keyed by MIME type.
Each MIME type is mapped to an entry consisting of a list of Each MIME type is mapped to an entry consisting of a list of
...@@ -76,6 +94,9 @@ def readmailcapfile(fp): ...@@ -76,6 +94,9 @@ def readmailcapfile(fp):
key, fields = parseline(line) key, fields = parseline(line)
if not (key and fields): if not (key and fields):
continue continue
if lineno is not None:
fields['lineno'] = lineno
lineno += 1
# Normalize the key # Normalize the key
types = key.split('/') types = key.split('/')
for j in range(len(types)): for j in range(len(types)):
...@@ -86,7 +107,7 @@ def readmailcapfile(fp): ...@@ -86,7 +107,7 @@ def readmailcapfile(fp):
caps[key].append(fields) caps[key].append(fields)
else: else:
caps[key] = [fields] caps[key] = [fields]
return caps return caps, lineno
def parseline(line): def parseline(line):
"""Parse one entry in a mailcap file and return a dictionary. """Parse one entry in a mailcap file and return a dictionary.
...@@ -165,6 +186,7 @@ def lookup(caps, MIMEtype, key=None): ...@@ -165,6 +186,7 @@ def lookup(caps, MIMEtype, key=None):
entries = entries + caps[MIMEtype] entries = entries + caps[MIMEtype]
if key is not None: if key is not None:
entries = [e for e in entries if key in e] entries = [e for e in entries if key in e]
entries = sorted(entries, key=lineno_sort_key)
return entries return entries
def subst(field, MIMEtype, filename, plist=[]): def subst(field, MIMEtype, filename, plist=[]):
......
...@@ -35,5 +35,5 @@ message/external-body; showexternal %s %{access-type} %{name} %{site} \ ...@@ -35,5 +35,5 @@ message/external-body; showexternal %s %{access-type} %{name} %{site} \
text/richtext; shownonascii iso-8859-8 -e richtext -p %s; test=test "`echo \ text/richtext; shownonascii iso-8859-8 -e richtext -p %s; test=test "`echo \
%{charset} | tr '[A-Z]' '[a-z]'`" = iso-8859-8; copiousoutput %{charset} | tr '[A-Z]' '[a-z]'`" = iso-8859-8; copiousoutput
video/mpeg; mpeg_play %s
video/*; animate %s video/*; animate %s
video/mpeg; mpeg_play %s
\ No newline at end of file
import mailcap import mailcap
import os import os
import copy
import test.support import test.support
import unittest import unittest
...@@ -13,43 +14,55 @@ MAILCAPDICT = { ...@@ -13,43 +14,55 @@ MAILCAPDICT = {
[{'compose': 'moviemaker %s', [{'compose': 'moviemaker %s',
'x11-bitmap': '"/usr/lib/Zmail/bitmaps/movie.xbm"', 'x11-bitmap': '"/usr/lib/Zmail/bitmaps/movie.xbm"',
'description': '"Movie"', 'description': '"Movie"',
'view': 'movieplayer %s'}], 'view': 'movieplayer %s',
'lineno': 4}],
'application/*': 'application/*':
[{'copiousoutput': '', [{'copiousoutput': '',
'view': 'echo "This is \\"%t\\" but is 50 \\% Greek to me" \\; cat %s'}], 'view': 'echo "This is \\"%t\\" but is 50 \\% Greek to me" \\; cat %s',
'lineno': 5}],
'audio/basic': 'audio/basic':
[{'edit': 'audiocompose %s', [{'edit': 'audiocompose %s',
'compose': 'audiocompose %s', 'compose': 'audiocompose %s',
'description': '"An audio fragment"', 'description': '"An audio fragment"',
'view': 'showaudio %s'}], 'view': 'showaudio %s',
'lineno': 6}],
'video/mpeg': 'video/mpeg':
[{'view': 'mpeg_play %s'}], [{'view': 'mpeg_play %s', 'lineno': 13}],
'application/postscript': 'application/postscript':
[{'needsterminal': '', 'view': 'ps-to-terminal %s'}, [{'needsterminal': '', 'view': 'ps-to-terminal %s', 'lineno': 1},
{'compose': 'idraw %s', 'view': 'ps-to-terminal %s'}], {'compose': 'idraw %s', 'view': 'ps-to-terminal %s', 'lineno': 2}],
'application/x-dvi': 'application/x-dvi':
[{'view': 'xdvi %s'}], [{'view': 'xdvi %s', 'lineno': 3}],
'message/external-body': 'message/external-body':
[{'composetyped': 'extcompose %s', [{'composetyped': 'extcompose %s',
'description': '"A reference to data stored in an external location"', 'description': '"A reference to data stored in an external location"',
'needsterminal': '', 'needsterminal': '',
'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}'}], 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
'lineno': 10}],
'text/richtext': 'text/richtext':
[{'test': 'test "`echo %{charset} | tr \'[A-Z]\' \'[a-z]\'`" = iso-8859-8', [{'test': 'test "`echo %{charset} | tr \'[A-Z]\' \'[a-z]\'`" = iso-8859-8',
'copiousoutput': '', 'copiousoutput': '',
'view': 'shownonascii iso-8859-8 -e richtext -p %s'}], 'view': 'shownonascii iso-8859-8 -e richtext -p %s',
'lineno': 11}],
'image/x-xwindowdump': 'image/x-xwindowdump':
[{'view': 'display %s'}], [{'view': 'display %s', 'lineno': 9}],
'audio/*': 'audio/*':
[{'view': '/usr/local/bin/showaudio %t'}], [{'view': '/usr/local/bin/showaudio %t', 'lineno': 7}],
'video/*': 'video/*':
[{'view': 'animate %s'}], [{'view': 'animate %s', 'lineno': 12}],
'application/frame': 'application/frame':
[{'print': '"cat %s | lp"', 'view': 'showframe %s'}], [{'print': '"cat %s | lp"', 'view': 'showframe %s', 'lineno': 0}],
'image/rgb': 'image/rgb':
[{'view': 'display %s'}] [{'view': 'display %s', 'lineno': 8}]
} }
# For backwards compatibility, readmailcapfile() and lookup() still support
# the old version of mailcapdict without line numbers.
MAILCAPDICT_DEPRECATED = copy.deepcopy(MAILCAPDICT)
for entry_list in MAILCAPDICT_DEPRECATED.values():
for entry in entry_list:
entry.pop('lineno')
class HelperFunctionTest(unittest.TestCase): class HelperFunctionTest(unittest.TestCase):
...@@ -75,12 +88,14 @@ class HelperFunctionTest(unittest.TestCase): ...@@ -75,12 +88,14 @@ class HelperFunctionTest(unittest.TestCase):
def test_readmailcapfile(self): def test_readmailcapfile(self):
# Test readmailcapfile() using test file. It should match MAILCAPDICT. # Test readmailcapfile() using test file. It should match MAILCAPDICT.
with open(MAILCAPFILE, 'r') as mcf: with open(MAILCAPFILE, 'r') as mcf:
with self.assertWarns(DeprecationWarning):
d = mailcap.readmailcapfile(mcf) d = mailcap.readmailcapfile(mcf)
self.assertDictEqual(d, MAILCAPDICT) self.assertDictEqual(d, MAILCAPDICT_DEPRECATED)
def test_lookup(self): def test_lookup(self):
# Test without key # Test without key
expected = [{'view': 'mpeg_play %s'}, {'view': 'animate %s'}] expected = [{'view': 'animate %s', 'lineno': 12},
{'view': 'mpeg_play %s', 'lineno': 13}]
actual = mailcap.lookup(MAILCAPDICT, 'video/mpeg') actual = mailcap.lookup(MAILCAPDICT, 'video/mpeg')
self.assertListEqual(expected, actual) self.assertListEqual(expected, actual)
...@@ -89,10 +104,16 @@ class HelperFunctionTest(unittest.TestCase): ...@@ -89,10 +104,16 @@ class HelperFunctionTest(unittest.TestCase):
expected = [{'edit': 'audiocompose %s', expected = [{'edit': 'audiocompose %s',
'compose': 'audiocompose %s', 'compose': 'audiocompose %s',
'description': '"An audio fragment"', 'description': '"An audio fragment"',
'view': 'showaudio %s'}] 'view': 'showaudio %s',
'lineno': 6}]
actual = mailcap.lookup(MAILCAPDICT, 'audio/basic', key) actual = mailcap.lookup(MAILCAPDICT, 'audio/basic', key)
self.assertListEqual(expected, actual) self.assertListEqual(expected, actual)
# Test on user-defined dicts without line numbers
expected = [{'view': 'mpeg_play %s'}, {'view': 'animate %s'}]
actual = mailcap.lookup(MAILCAPDICT_DEPRECATED, 'video/mpeg')
self.assertListEqual(expected, actual)
def test_subst(self): def test_subst(self):
plist = ['id=1', 'number=2', 'total=3'] plist = ['id=1', 'number=2', 'total=3']
# test case: ([field, MIMEtype, filename, plist=[]], <expected string>) # test case: ([field, MIMEtype, filename, plist=[]], <expected string>)
...@@ -151,14 +172,16 @@ class FindmatchTest(unittest.TestCase): ...@@ -151,14 +172,16 @@ class FindmatchTest(unittest.TestCase):
'edit': 'audiocompose %s', 'edit': 'audiocompose %s',
'compose': 'audiocompose %s', 'compose': 'audiocompose %s',
'description': '"An audio fragment"', 'description': '"An audio fragment"',
'view': 'showaudio %s' 'view': 'showaudio %s',
'lineno': 6
} }
audio_entry = {"view": "/usr/local/bin/showaudio %t"} audio_entry = {"view": "/usr/local/bin/showaudio %t", 'lineno': 7}
video_entry = {'view': 'animate %s'} video_entry = {'view': 'animate %s', 'lineno': 12}
message_entry = { message_entry = {
'composetyped': 'extcompose %s', 'composetyped': 'extcompose %s',
'description': '"A reference to data stored in an external location"', 'needsterminal': '', 'description': '"A reference to data stored in an external location"', 'needsterminal': '',
'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}' 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
'lineno': 10,
} }
# test case: (findmatch args, findmatch keyword args, expected output) # test case: (findmatch args, findmatch keyword args, expected output)
...@@ -168,7 +191,7 @@ class FindmatchTest(unittest.TestCase): ...@@ -168,7 +191,7 @@ class FindmatchTest(unittest.TestCase):
cases = [ cases = [
([{}, "video/mpeg"], {}, (None, None)), ([{}, "video/mpeg"], {}, (None, None)),
([c, "foo/bar"], {}, (None, None)), ([c, "foo/bar"], {}, (None, None)),
([c, "video/mpeg"], {}, ('mpeg_play /dev/null', {'view': 'mpeg_play %s'})), ([c, "video/mpeg"], {}, ('animate /dev/null', video_entry)),
([c, "audio/basic", "edit"], {}, ("audiocompose /dev/null", audio_basic_entry)), ([c, "audio/basic", "edit"], {}, ("audiocompose /dev/null", audio_basic_entry)),
([c, "audio/basic", "compose"], {}, ("audiocompose /dev/null", audio_basic_entry)), ([c, "audio/basic", "compose"], {}, ("audiocompose /dev/null", audio_basic_entry)),
([c, "audio/basic", "description"], {}, ('"An audio fragment"', audio_basic_entry)), ([c, "audio/basic", "description"], {}, ('"An audio fragment"', audio_basic_entry)),
......
...@@ -843,6 +843,7 @@ Julia Lawall ...@@ -843,6 +843,7 @@ Julia Lawall
Chris Lawrence Chris Lawrence
Mark Lawrence Mark Lawrence
Chris Laws Chris Laws
Michael Lazar
Brian Leair Brian Leair
Mathieu Leduc-Hamel Mathieu Leduc-Hamel
Amandine Lee Amandine Lee
......
...@@ -122,6 +122,9 @@ Core and Builtins ...@@ -122,6 +122,9 @@ Core and Builtins
Library Library
------- -------
- Issue #14977: mailcap now respects the order of the lines in the mailcap
files ("first match"), as required by RFC 1542. Patch by Michael Lazar.
- Issue #28025: Convert all ssl module constants to IntEnum and IntFlags. - Issue #28025: Convert all ssl module constants to IntEnum and IntFlags.
SSLContext properties now return flags and enums. SSLContext properties now return flags and enums.
...@@ -145,6 +148,10 @@ Library ...@@ -145,6 +148,10 @@ Library
- Issue #24277: The new email API is no longer provisional, and the docs - Issue #24277: The new email API is no longer provisional, and the docs
have been reorganized and rewritten to emphasize the new API. have been reorganized and rewritten to emphasize the new API.
- Issue #22450: urllib now includes an "Accept: */*" header among the
default headers. This makes the results of REST API requests more
consistent and predictable especially when proxy servers are involved.
- lib2to3.pgen3.driver.load_grammar() now creates a stable cache file - lib2to3.pgen3.driver.load_grammar() now creates a stable cache file
between runs given the same Grammar.txt input regardless of the hash between runs given the same Grammar.txt input regardless of the hash
randomization setting. randomization setting.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment