mirror of
https://github.com/python/cpython.git
synced 2025-10-10 00:43:41 +00:00
#14977: Make mailcap respect the order of the lines in the mailcap file.
This is required by RFC 1542, so despite the subtle behavior change we are treating it as a bug. Patch by Michael Lazar.
This commit is contained in:
parent
ae9e5f032d
commit
347dc95cd3
5 changed files with 76 additions and 27 deletions
|
@ -1,9 +1,19 @@
|
||||||
"""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():
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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():
|
||||||
|
|
||||||
|
|
||||||
# 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):
|
||||||
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):
|
||||||
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):
|
||||||
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} \
|
||||||
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
|
|
@ -1,6 +1,7 @@
|
||||||
import mailcap
|
import mailcap
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import copy
|
||||||
import test.support
|
import test.support
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
@ -14,43 +15,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):
|
||||||
|
|
||||||
|
@ -76,12 +89,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:
|
||||||
d = mailcap.readmailcapfile(mcf)
|
with self.assertWarns(DeprecationWarning):
|
||||||
self.assertDictEqual(d, MAILCAPDICT)
|
d = mailcap.readmailcapfile(mcf)
|
||||||
|
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)
|
||||||
|
|
||||||
|
@ -90,10 +105,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>)
|
||||||
|
@ -152,14 +173,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)
|
||||||
|
@ -169,7 +192,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)),
|
||||||
|
|
|
@ -833,6 +833,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
|
||||||
|
|
|
@ -62,6 +62,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 #24594: Validates persist parameter when opening MSI database
|
- Issue #24594: Validates persist parameter when opening MSI database
|
||||||
|
|
||||||
- Issue #28047: Fixed calculation of line length used for the base64 CTE
|
- Issue #28047: Fixed calculation of line length used for the base64 CTE
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue