Skip to content

gh-92455: Respect case-sensitive mimetype suffixes#148782

Merged
serhiy-storchaka merged 20 commits into
python:mainfrom
yuanx749:fix-mimetypes-case-sensitive-add-type
Jun 15, 2026
Merged

gh-92455: Respect case-sensitive mimetype suffixes#148782
serhiy-storchaka merged 20 commits into
python:mainfrom
yuanx749:fix-mimetypes-case-sensitive-add-type

Conversation

@yuanx749

Copy link
Copy Markdown
Contributor

Fixes gh-92455.

As described in the issue, the current implementation cannot find uppercase
extensions added via mimetypes.add_type(). This appears to be a regression from gh-30229.

This restores the lookup order documented for mimetypes.guess_type():
type suffixes are tried case-sensitively first, then case-insensitively.

@ifrh ifrh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot approve the changes formaly, but they look good for me.

Comment thread Lib/test/test_mimetypes.py Outdated
eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None))

def test_added_types_case_sensitive_preferred(self):
self.db.add_type("text/x-r-script", ".R")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"text/x-r-script" could be added to the database in future. It is better to use something less likely to be added.

Please add also tests for strict=False.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. I changed the test type and added tests for strict=False.

@serhiy-storchaka

Copy link
Copy Markdown
Member

#30229 changed also the code related to suffix_map. I wonder if it needs correction too:

        while True:
            if ext in self.suffix_map:
                base, ext = posixpath.splitext(base + self.suffix_map[ext])
                continue
            ext_lower = ext.lower():
            if ext_lower in self.suffix_map:
                base, ext = posixpath.splitext(base + self.suffix_map[ext_lower])
                continue
            break

@kumaraditya303, what are your thoughts?

@kumaraditya303

Copy link
Copy Markdown
Contributor

what are your thoughts?

I think we should do the same for suffix_map i.e. first try case sensitively then case-insensitively.

@yuanx749

yuanx749 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Would you prefer handling suffix_map in this PR, or keeping this focused on types_map?

@serhiy-storchaka

Copy link
Copy Markdown
Member

It is better to handle all in one PR. Also please update the documentation.

@yuanx749

yuanx749 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Added a failing test, then updated suffix_map to try the original suffix before the lower case suffix. I also updated the documentation. The existing strict=False lookup order is unchanged.

@read-the-docs-community

read-the-docs-community Bot commented Jun 8, 2026

Copy link
Copy Markdown

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are open questions: Should we try case-sensitive in non-strict mapping before case-insensitive in strict mapping? What to do with asymmetry in registering upper-case and lower-case extensions? I do not see clear answers on them, so we can leave them to other issues (when we get clear answers).

I think that the documentation for add_type() should be updated. Registered lower-case extensions will match any case.

@yuanx749

yuanx749 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

There are open questions: Should we try case-sensitive in non-strict mapping before case-insensitive in strict mapping? What to do with asymmetry in registering upper-case and lower-case extensions? I do not see clear answers on them, so we can leave them to other issues (when we get clear answers).

I think that the documentation for add_type() should be updated. Registered lower-case extensions will match any case.

The original lookup behavior is now preserved. I have also updated the add_type() and MimeTypes.add_type() docstrings and the corresponding documentation.

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. 👍

@serhiy-storchaka serhiy-storchaka enabled auto-merge (squash) June 15, 2026 14:42
@serhiy-storchaka serhiy-storchaka merged commit 46107ad into python:main Jun 15, 2026
93 of 95 checks passed
@yuanx749 yuanx749 deleted the fix-mimetypes-case-sensitive-add-type branch June 15, 2026 15:15
@bedevere-bot

Copy link
Copy Markdown

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot aarch64 Android 3.x (tier-3) has failed when building commit 46107ad.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/1594/builds/5095) and take a look at the build logs.
  4. Check if the failure is related to this commit (46107ad) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/1594/builds/5095

Failed tests:

  • test_urllib2
  • test_urllibnet

Failed subtests:

  • test_ftp_error - test.test_urllib2.HandlerTests.test_ftp_error
  • test_getcode - test.test_urllibnet.urlopenNetworkTests.test_getcode

Summary of the results of the build (if available):

==

Click to see traceback logs
Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 1531, in ftp_open
    host = socket.gethostbyname(host)
socket.gaierror: [Errno 7] No address associated with hostname
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/test/test_urllib2.py", line 815, in test_ftp_error
    urlopen("ftp://www.pythontest.net/")
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 489, in open
    response = self._open(req, data)
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 506, in _open
    result = self._call_chain(self.handle_open, protocol, protocol +
                              '_open', req)
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 466, in _call_chain
    result = func(*args)
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 1533, in ftp_open
    raise URLError(msg)
urllib.error.URLError: <urlopen error [Errno 7] No address associated with hostname>
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/test/test_urllib2.py", line 817, in test_ftp_error
    self.assertEqual(raised.reason,
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
                     f"ftp error: {exception.args[0]}")
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: gaierror(7, 'No address associated with hostname') != 'ftp error: 500 OOPS: cannot change directory:/nonexistent'
 
----------------------------------------------------------------------
Ran 81 tests in 0.074s
 
FAILED (failures=1, skipped=3)


Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 1531, in ftp_open
    host = socket.gethostbyname(host)
socket.gaierror: [Errno 7] No address associated with hostname
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/test/test_urllib2.py", line 815, in test_ftp_error
    urlopen("ftp://www.pythontest.net/")
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 489, in open
    response = self._open(req, data)
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 506, in _open
    result = self._call_chain(self.handle_open, protocol, protocol +
                              '_open', req)
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 466, in _call_chain
    result = func(*args)
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/urllib/request.py", line 1533, in ftp_open
    raise URLError(msg)
urllib.error.URLError: <urlopen error [Errno 7] No address associated with hostname>
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/test/test_urllib2.py", line 817, in test_ftp_error
    self.assertEqual(raised.reason,
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
                     f"ftp error: {exception.args[0]}")
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: gaierror(7, 'No address associated with hostname') != 'ftp error: 500 OOPS: cannot change directory:/nonexistent'
 
----------------------------------------------------------------------
Ran 81 tests in 0.050s
 
FAILED (failures=1, skipped=3)


Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/test/test_urllibnet.py", line 107, in test_getcode
    self.assertEqual(e.exception.code, 404)
                     ^^^^^^^^^^^^^^^^
AttributeError: 'URLError' object has no attribute 'code'
 
----------------------------------------------------------------------
Ran 12 tests in 0.034s
 
FAILED (errors=1, skipped=10)


Traceback (most recent call last):
  File "/data/user/0/org.python.testbed/files/python/lib/python3.16/test/test_urllibnet.py", line 107, in test_getcode
    self.assertEqual(e.exception.code, 404)
                     ^^^^^^^^^^^^^^^^
AttributeError: 'URLError' object has no attribute 'code'
 
----------------------------------------------------------------------
Ran 12 tests in 0.031s
 
FAILED (errors=1, skipped=10)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants