Skip to content

mimetypes.guess_extension result changes after mimetypes.init() #49213

Closed

Description

@siona
mannequin
BPO 4963
Nosy @warsaw, @terryjreedy, @pitrou, @tiran, @abadger, @bitdancer, @bertjwregeer, @vadmium, @zooba, @maxking, @pombredanne, @davidkhess
PRs
  • bpo-4963: Fix for initialization and non-deterministic behavior issues in mimetypes #3062
  • [3.8] bpo-4963: Fix for initialization and non-deterministic behavior issues in mimetypes (GH-3062) #14375
  • [3.7] bpo-4963: Fix for initialization and non-deterministic behavior issues in mimetypes (GH-3062) #14376
  • Files
  • mimetypes-init-test.patch
  • issue4963.patch: stable guess_extension patch and test
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/zooba'
    closed_at = <Date 2019-06-25.16:14:49.543>
    created_at = <Date 2009-01-16.12:04:54.057>
    labels = ['easy', 'type-bug', '3.8', 'expert-email', '3.7', 'library', '3.9']
    title = 'mimetypes.guess_extension result changes after mimetypes.init()'
    updated_at = <Date 2021-01-10.02:17:07.465>
    user = 'https://bugs.python.org/siona'

    bugs.python.org fields:

    activity = <Date 2021-01-10.02:17:07.465>
    actor = 'terry.reedy'
    assignee = 'steve.dower'
    closed = True
    closed_date = <Date 2019-06-25.16:14:49.543>
    closer = 'steve.dower'
    components = ['Library (Lib)', 'email']
    creation = <Date 2009-01-16.12:04:54.057>
    creator = 'siona'
    dependencies = []
    files = ['17803', '34821']
    hgrepos = []
    issue_num = 4963
    keywords = ['patch', 'easy']
    message_count = 35.0
    messages = ['79955', '79961', '79962', '108650', '108674', '108704', '108707', '108746', '108755', '108853', '108934', '108967', '182465', '214948', '216104', '216764', '246649', '293248', '293249', '293258', '293261', '293262', '293626', '293628', '300092', '315145', '322534', '346456', '346457', '346534', '346537', '346539', '384730', '384731', '384750']
    nosy_count = 18.0
    nosy_names = ['barry', 'terry.reedy', 'pitrou', 'christian.heimes', 'wichert', 'a.badger', 'r.david.murray', 'siona', 'sascha_silbe', 'l0nwlf', 'X-Istence', 'martin.panter', 'steve.dower', 'wodny', 'maxking', 'pombredanne', 'sivert', 'dhess']
    pr_nums = ['3062', '14375', '14376']
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue4963'
    versions = ['Python 3.7', 'Python 3.8', 'Python 3.9']

    Activity

    1. siona commented on Jan 16, 2009

      sionamannequin
      MannequinAuthor

      Asking mimetypes to reload mime.types can cause guess_extension() to
      return a different result if multiple extensions are mapped to that mime
      type:

      >>> import mimetypes
      >>> mimetypes.guess_extension('image/jpeg')
      '.jpe'
      >>> mimetypes.init()
      >>> mimetypes.guess_extension('image/jpeg')
      '.jpeg'
      >>>

      This is because both the forward (extension to type) and inverse (type
      to extension) type mapping dicts are populated by iterating through the
      existing forward (extension to type) dict (types_map), then supplemented
      by reading from mime.types (or any other files given to init()). The
      fully populated forward dict becomes the new types_map. Initially,
      types_map is hard-coded, but when the type mapping dicts are
      repopulated, by explicitly or implicitly calling init() again, it is
      done by iterating over the types_map created by the first init() call,
      not the hard-coded one. If the iteration order for a set of extensions
      with the same type is different in these two versions of the forward
      dict, the order of extensions appearing for that type in the inverse
      dict will change. And so the behavior of guess_all_extensions() and
      hence guess_extension() will change.

    2. added
      stdlibStandard Library Python modules in the Lib/ directory
      type-bugAn unexpected behavior, bug, or error
      on Jan 16, 2009
    3. terryjreedy commented on Jan 16, 2009

      @terryjreedy
      Member

      3.0, WinXP

      import mimetypes
      print(mimetypes.guess_extension('image/jpeg'))
      mimetypes.init()
      print(mimetypes.guess_extension('image/jpeg'))

      gives

      .jpe
      .jpe
      

      I wonder at this answer since .jpg and occasionally .jpeg is standard in
      Windows usage, but the doc is unclear to me as to the actual intent of
      the function.

    4. siona commented on Jan 16, 2009

      sionamannequin
      MannequinAuthor

      Ah, yes, forgot to mention this is on Debian 4.0. I doubt you're going
      to run into it on a Windows system unless you explicitly give init() a
      mime.types file, looking at the knownfiles list used by default.

    5. pitrou commented on Jun 25, 2010

      @pitrou
      Member

      Can't reproduce under Mandriva Linux:

      >>> import mimetypes
      >>> print(mimetypes.guess_extension('image/jpeg'))
      .jpe
      >>> mimetypes.init()
      >>> print(mimetypes.guess_extension('image/jpeg'))
      .jpe

      The fact that it returns ".jpe" rather than ".jpg", however, could be a bug in itself (since the latter will really be expected by everyone, not the former).

    6. bitdancer commented on Jun 26, 2010

      @bitdancer
      Member

      I can't reproduce this either, and without a reproducer we might as well close it.

      Antoine it is possible that your fix for bpo-5853 inadvertently fixed this, but I don't feel like untangling the logic of the module enough to figure it out :) So I'm going to close it 'works for me'. If S Arrowsmith can reproduce it with 2.7r2, we can reopen.

      By the way, it produced 'jpe' for me, too...but, then, my system (Gentoo) /etc/mime.types file has 'jpe' as the first filetype for jpeg, so I don't think that association is Python's bug, per se. Though I may eventually have to address it in email6. (Also by the way, I tried switching the order and passing in the modified file explicitly on the explicit init, but that didn't change the behavior).

    7. siona commented on Jun 26, 2010

      sionamannequin
      MannequinAuthor

      Sorry, still there:

      Python 2.7rc2 (r27rc2:82137, Jun 26 2010, 11:27:59) 
      [GCC 4.3.2] on linux2
      Type "help", "copyright", "credits" or "license" for more information.
      >>> import mimetypes
      >>> mimetypes.guess_extension('image/jpeg')
      '.jpe'
      >>> mimetypes.init()
      >>> mimetypes.guess_extension('image/jpeg')
      '.jpeg'

      The fact that it's not reproducible on other Linux systems (I can't reproduce on the RedHat box I have to hand) might suggest there's something odd about Debian's mime.types . But I've just tried it passing init() the mime.types from the (working) RedHat box, and it's still producing the odd behaviour. (And I'm now on Debian 5.0, so it's not a Debian 4.0-specific issue either.) Wish I had a convenient Ubuntu install to try it on.

      Bizarre.

    8. l0nwlf commented on Jun 26, 2010

      l0nwlfmannequin
      Mannequin

      Can't reproduce.

      16:36:36 l0nwlf-MBP:~$ python2.7
      Python 2.7rc2+ (trunk:82148M, Jun 22 2010, 10:32:46) 
      [GCC 4.2.1 (Apple Inc. build 5646) (dot 1)] on darwin
      Type "help", "copyright", "credits" or "license" for more information.
      >>> import mimetypes
      >>> mimetypes.guess_extension('image/jpeg')
      '.jpe'
      >>> mimetypes.init()
      >>> mimetypes.guess_extension('image/jpeg')
      '.jpe'
      >>> 

      Results were same in python2.5, 2.6 too. I wonder whether this is machine specific or distro specific.

    9. bitdancer commented on Jun 26, 2010

      @bitdancer
      Member

      S Arrowsmith: can put a print statement into mimetypes.init, find out what files are loading, and paste the image/jpeg lines from each of those files here? That might provide a clue.

    10. siona commented on Jun 26, 2010

      sionamannequin
      MannequinAuthor
      >>> import mimetypes 
      >>> mimetypes.guess_extension('image/jpeg')
      /etc/mime.types
      '.jpe'
      >>> mimetypes.init()
      /etc/mime.types
      >>> mimetypes.guess_extension('image/jpeg')
      '.jpeg'
      >>> 
      $ grep jpeg /etc/mime.types
      image/jpeg					jpeg jpg jpe
      $

      That big chunk of whitespace is 5 tabs. Not very helpful, I fear.

    11. siona commented on Jun 28, 2010

      sionamannequin
      MannequinAuthor

      I've dug into it -- again -- and my original analysis still holds. Getting consistent guess_extension() results across an explicit init() call depends on dict.items() returning keys in the same order on two different dictionaries (the original, hard-coded types_map and the one created by the first, implicit init() call).

      Why should this be different on Debian to other Linuxes, even given same data as a "working" distribution? Is there something in the implementation details of dict.items() which is that distribution dependent?

      (A "fix", BTW, is to insert a call to _default_mime_types() either in init() or in MimeTypes.__init__ before it calls init().)

    12. bitdancer commented on Jun 29, 2010

      @bitdancer
      Member

      It must be that the different key order only happens on the one platform because of the quirky nature of dictionary construction. That is, there is *something* on that platform that is changing where things get hashed when the dictionary is recreated.

      The problem with fixing this is that any fix is going to change the behavior, unless we go to the lengths of recording the order of the initializations in add_type and replay it when init is called a second time. That solution is pretty much a non-starter :)

      The mimetypes docs say that init can be called more than once, They say that a MimeTypes object starts out "with the same database as provided by the rest of the module". The docs explain how the initial database state is created.

      What the docs don't do is say what *happens* when you call init more than once. There are two possibilities: either we (1) restart from the initial state, or we (2) start from the current (possibly modified) state of the database and then add whatever is specified in the init call. (Actually, there's a third possibility: we could also add back in anything from the default init that was deleted; but this halfway version is unlikely to be anyone's intent or expectation.)

      The actual implementation of the mimetypes module does (2) if and only if you pass init a list of files. If you don't then it does something that isn't even the third way above: it reloads *just* the data from the system files it managed to find, without reloading the data from the internal tables.

      Clearly this behavior is....odd. When no files are passed, init should do one of two things: either nothing, or reset the global db state to its initial value.

      It's not so clear what the behavior should be when you pass init one or more files. It is possible, even highly probable, that there is code out there that depends on the fact that doing so is additive.

      Given this analysis, I think that the best fix would be implement (and document) the following behavior for init:

      If called with no arguments, it rebuilds the module database from scratch

      If called with a list of files, it adds the contents of those files to the module database

      The second is a backward compatibility hack. Ideally it would be deprecated in favor of some sort of load_mime_files method.

      It is possible that the first will also break code, but I think it is less likely, and probably an acceptable risk in a new major release. But I'd be prepared to change it to 'init does nothing' if breakage showed up during RC testing.

      The problem with this "fix" is that it does not, in fact, address the root cause of the OP's bug report. The specific behavior he observes when calling init() would be fixed, but the underlying problem remains. If he were to instead instantiate a new MimeTypes db, then when it "copies" the module database, it will build its own database by running the old database in key order, and once again the results returned by guess_extension might mutate. This means that the new db is *not* a copy of the old db when it starts.

      That problem could be fixed by having MimeTypes.__init__ do a copy of the types_map and types_map_inv data structures instead of rebuilding them from scratch. This would mean shifting the initialization of these structures out of MimeTypes and in to init (in the 'reinitialize' code path) or perhaps into _default_mime_types, but I don't see that as a big problem, once init is doing a full reinitialization by default. (There is also the question of whether it should be a 'deep copy', but I don't think that is needed since a user would need to be doing something pretty hackish to run afoul of a shallow-copy-induced problem.)

      Can anyone see flaws in this analysis and proposed solution? I've marked the fix as easy since a python hacker should be able to knock out a solution in a day, but it isn't trivial. And I have no clue how to write a unit test for the MimeTypes.__init__ order-shifting bug.

      I'm also resetting the priority to normal since I consider the ambiguity of what calling init twice actually does to be a bigger issue than it sometimes changing the results of a function with 'guess' in its name :)

      I've attached a patch with a unit test for the 'init doesn't re-init' behavior.

      (By the way, it also appears to me from reading the code that read_mime_types is buggy in that it actually returns a merge of the loaded file with the current module DB state, but I haven't checked that observation.)

    13. 23 remaining items

    14. zooba commented on Jun 25, 2019

      @zooba
      Member

      New changeset 25fbe33 by Steve Dower in branch '3.8':
      bpo-4963: Fix for initialization and non-deterministic behavior issues in mimetypes (GH-14375)
      25fbe33

    15. davidkhess commented on Jun 25, 2019

      davidkhessmannequin
      Mannequin

      Thank you Steve!

      Nice to see this one make it across the finish line.

    16. zooba commented on Jun 25, 2019

      @zooba
      Member

      New changeset 2a99fd9 by Steve Dower in branch '3.7':
      bpo-4963: Fix for initialization and non-deterministic behavior issues in mimetypes (GH-14376)
      2a99fd9

    17. self-assigned this
      on Jun 25, 2019
    18. pombredanne commented on Jan 9, 2021

      pombredannemannequin
      Mannequin

      The changes introduced by this ticket in 9fc720e#r45794801 are problematic.

      I discovered this from having tests failing when testing on Python 3.7 and up

      The bug is that calling mimetypes.init(files) will NOT use my files, but instead use both my files and knownfiles.
      This was not the case before as knownfiles would be ignored as expected when I provide my of files list.

      This is a breaking API change IMHO and introduces a buggy unstability : even if I want to ignore knownfiles by providing my list of of files, knownfiles will always be added and this results in erratic and buggy behaviour as the content of "knownfiles" is completely random based on the OS version and else.

      The code I am using is here https://github.com/nexB/typecode/blob/ba07c04d23441d3469dc5de911376d408514ebd8/src/typecode/contenttype.py#L308

      I think we should reopen to fix (or create a new ticket)

    19. pombredanne commented on Jan 9, 2021

      pombredannemannequin
      Mannequin

      Actually this is problematic on multiples counts:

      1. the behaviour changes and this is a regression
      2. even if that new buggy behaviour was the one to use, it should not give preference to knownfiles ovr init-provided files, but at least take the provided files first and knownfiles second.
    20. terryjreedy commented on Jan 10, 2021

      @terryjreedy
      Member

      Phillipe: I was the first to comment, but had no futher involvement with this issue until now. Multiple people, including coredevs, considered the old behavior to be buggy and multiple people, including coredevs, contributed to the fix. You are unlikely to prevail arguing that the change is totally a mistake and should be totally reverted.

      the behaviour changes and this is a regression

      Code bug fixes are supposed to change behavior. We know that the change will break code that depends on the buggy behavior. That is why we include an updated change log with each release.

      The intended change is *not* a regression in itself. A regression in a bug fix is an unintended change to some other behavior. I don't believe (but could be mistaken) that you have argued or shown this.

      Assuming that you are not asking for complete reversion, I suggest that you open a new issue, referencing this one, but taking the current behavior as the given. Propose a revised behavior and argue that it is even better. If you want to argue that the current behavior is buggy (compared to the current docs) so that your proposed revision should be backported, make that a separate argument.

    21. transferred this issue fromon Apr 10, 2022
    22. added a commit that references this issue on Jun 1, 2022
    23. added a commit that references this issue on May 31, 2026
    24. added 3 commits that reference this issue on Aug 30, 2026
    25. added a commit that references this issue on Sep 12, 2026
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    3.7 (EOL)end of life3.8 (EOL)end of life3.9 (EOL)end of lifeeasystdlibStandard Library Python modules in the Lib/ directorytopic-emailtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions