Search code examples
pythonplistplistlib

How to force plistlib in Python to save escaped special characters?


I am having a kind of a problem with module plistlib. It works fine, except when saving plists. It doesn't save apostrophe as special character. It does save & as & which is fine, but it saves apostrophe as ' (instead of ') which is not fine. I have a lot of plists with a lot of text and when I change something (batch change with script) it gives me a headache with git diff, because every single ' will become '.

How to force plistlib to save plist with all special characters escaped (after all, there is only 5 of them)?


Solution

  • I will answer my own question since I dug around and found answer.

    Problem is with function _escape(text) in module plistlib. It escapes only &, < and > although Xcode with its plist reader escapes all five characters (&, <, >, ' and "), that is why I think this module should also. It would also be a good addition to module plistlib something like ElementTree's parameter entities in function escape(). That parameter is a dict with additional characters to replace. Similar addition to plistlib's function save_plist() would be a good idea so that we can escape additional characters.

    My solution is based on so called monkey patching. Basically, I copied whole function _escape(text) and just added additional escapes (' and "):

    from plistlib import _controlCharPat
    
    def patched_escape(text):
        m = _controlCharPat.search(text)
        if m is not None:
            raise ValueError("strings can't contains control characters; "
                             "use bytes instead")
        text = text.replace("\r\n", "\n")       # convert DOS line endings
        text = text.replace("\r", "\n")         # convert Mac line endings
        text = text.replace("&", "&amp;")       # escape '&'
        text = text.replace("<", "&lt;")        # escape '<'
        text = text.replace(">", "&gt;")        # escape '>'
        text = text.replace("'", "&apos;")      # escape '''
        text = text.replace("\"", "&quot;")     # escape '"'
    
        return text
    

    And now in my script I replaced plistlib's function _escape(text) with mine:

    plistlib._escape = patched_escape
    

    Now plistlib correctly escapes and saves plists. Usual warnings regarding monkey patching apply also here. I don't have other callers, just this script so it is fine to do this.