classification
Title: time.tzname on Python 3.3.0 for Windows is decoded by wrong encoding
Type: behavior Stage: patch review
Components: Extension Modules, Windows Versions: Python 3.6, Python 3.5, Python 3.4
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: amaury.forgeotdarc, belopolsky, eryksun, jcea, msmhrt, ocean-city, p-ganssle, prikryl, vstinner
Priority: normal Keywords: 3.3regression, patch

Created on 2012-10-25 11:56 by msmhrt, last changed 2018-07-05 15:47 by p-ganssle.

Files
File name Uploaded Description Edit
tzname_bug.py prikryl, 2015-09-18 17:25 The example uploaded as is.
Pull Requests
URL Status Linked Edit
PR 3740 closed denis-osipov, 2017-09-25 07:07
Messages (22)
msg173755 - (view) Author: Masami HIRATA (msmhrt) Date: 2012-10-25 11:56
OS: Windows 7 Starter Edition SP1 (32-bit) Japanese version
Python: 3.3.0 for Windows x86 (python-3.3.0.msi)

time.tzname on Python 3.3.0 for Windows is decoded by wrong encoding.

C:\Python33>python.exe
Python 3.3.0 (v3.3.0:bd8afb90ebf2, Sep 29 2012, 10:55:48) [MSC v.1600 32 bit (In
tel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import time
>>> time.tzname[0]
'\x93\x8c\x8b\x9e (\x95W\x8f\x80\x8e\x9e)'
>>> time.tzname[0].encode('iso-8859-1').decode('mbcs')
'東京 (標準時)'
>>>

'東京 (標準時)' means 'Tokyo (Standard Time)' in Japanese.
time.tzname on Python 3.2.3 for Windows works correctly.

C:\Python32>python.exe
Python 3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)] on win
32
Type "help", "copyright", "credits" or "license" for more information.
>>> import time
>>> time.tzname[0]
'東京 (標準時)'
>>>
msg173758 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2012-10-25 13:47
I see in 3.3 PyUnicode_DecodeFSDefaultAndSize() was replaced by PyUnicode_DecodeLocale().

What show sys.getdefaultencoding(), sys.getfilesystemencoding(), and locale.getpreferredencoding()?
msg173772 - (view) Author: Amaury Forgeot d'Arc (amaury.forgeotdarc) * (Python committer) Date: 2012-10-25 17:31
Looking at the CRT source code, tznames should be decoded with mbcs.
See also http://mail.python.org/pipermail/python-3000/2007-August/009290.html
msg173784 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2012-10-25 18:06
As I understand, OP has UTF-8 locale.
msg173798 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2012-10-25 20:30
>I see in 3.3 PyUnicode_DecodeFSDefaultAndSize() was replaced
> by PyUnicode_DecodeLocale().

Related changes:

 - 8620e6901e58 for the issue #5905
 - 279b0aee0cfb for the issue #13560

I wrote 8620e6901e58 for Linux, when the wcsftime() function is missing.

The problem is the changeset 279b0aee0cfb: it introduces a regression on Windows. It looks like PyUnicode_DecodeFSDefault() and PyUnicode_DecodeFSDefault() use a different encoding on Windows.

I suppose that we need to add an #ifdef MS_WINDOWS to use PyUnicode_DecodeFSDefault() on Windows, and PyUnicode_DecodeFSDefault() on Linux.

See also the issue #10653: time.strftime() uses strftime() (bytes) instead of wcsftime() (unicode) on Windows, because wcsftime() and tzname format the timezone differently.
msg173806 - (view) Author: Masami HIRATA (msmhrt) Date: 2012-10-25 22:55
> What show sys.getdefaultencoding(), sys.getfilesystemencoding(), and locale.getpreferredencoding()?

C:\Python33>python.exe
Python 3.3.0 (v3.3.0:bd8afb90ebf2, Sep 29 2012, 10:55:48) [MSC v.1600 32 bit (In
tel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getdefaultencoding()
'utf-8'
>>> sys.getfilesystemencoding()
'mbcs'
>>> import locale
>>> locale.getpreferredencoding()
'cp932'
>>>

'cp932' is the same as 'mbcs' in the Japanese environment.
msg173824 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2012-10-26 07:07
> >>> sys.getfilesystemencoding()
> 'mbcs'
> >>> import locale
> >>> locale.getpreferredencoding()
> 'cp932'
> >>>
>
> 'cp932' is the same as 'mbcs' in the Japanese environment.

And what is the value.of locale.getpreferredencoding(False)?
msg173827 - (view) Author: Masami HIRATA (msmhrt) Date: 2012-10-26 09:08
> And what is the value.of locale.getpreferredencoding(False)?

>>> import locale
>>> locale.getpreferredencoding(False)
'cp932'
>>>
msg174161 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2012-10-29 23:19
See also the issue #836035.
msg174164 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2012-10-29 23:59
According to CRT source code:
 - tzset() uses WideCharToMultiByte(lc_cp, 0, tzinfo.StandardName, -1, tzname[0], _TZ_STRINGS_SIZE - 1, NULL, &defused) with lc_cp = ___lc_codepage_func().
 - wcsftime("%z") and wcsftime("%Z") use _mbstowcs_s_l() to decode the time zone name

I tried to call ___lc_codepage_func(): it returns 0. I suppose that it means that mbstowcs() and wcstombs() use the ANSI code page.

Instead of trying to bet what is the correct encoding, it would be simpler (and safer) to read the Unicode version of the tzname array: StandardName and DaylightName of GetTimeZoneInformation().

If anything is changed, time.strftime(), time.strptime(), datetime.datetime.strftime() and time.tzname must be checked (with "%Z" format).
msg174165 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2012-10-30 00:28
"Instead of trying to bet what is the correct encoding, it would be simpler (and safer) to read the Unicode version of the tzname array: StandardName and DaylightName of GetTimeZoneInformation()."

GetTimeZoneInformation() formats correctly timezone names, but it reintroduces #10653 issue: time.strftime("%Z") formats the timezone name differently.

See also issue #13029 which is a duplicate of #10653, but contains useful information.

--

Example on Windows 7 with a french setup configured to Tokyo's timezone.

Using GetTimeZoneInformation(), time.tzname is ("Tokyo", "Tokyo (heure d\u2019\xe9t\xe9)"). U+2019 is the "RIGHT SINGLE QUOTATION MARK". This character is usually replaced with U+0027 (APOSTROPHE) in ASCII.

time.strftime("%Z") gives "Tokyo (heure d'\x81\x66ete)" (if it is implemented using strftime() or wcsftime()).

--

If I understood correctly, Python 3.3 has two issues on Windows:

 * time.tzname is decoded from the wrong encoding
 * time.strftime("%Z") gives an invalid output

The real blocker issue is a bug in strftime() and wcsftime() in Windows CRT. A solution is to replace "%Z" with the timezone name before calling strftime() or wcsftime(), aka working around the Windows CRT bug.
msg176408 - (view) Author: Masami HIRATA (msmhrt) Date: 2012-11-26 12:14
Is there any progress on this issue?
msg224325 - (view) Author: Mark Lawrence (BreamoreBoy) * Date: 2014-07-30 16:50
Could somebody respond to the originator please.
msg251013 - (view) Author: Petr Prikryl (prikryl) Date: 2015-09-18 17:25
I have just observed behaviour for the Czech locale. I tried to avoid collisions with stdout encoding, writing the strings into a file using UTF-8 encoding:

tzname_bug.py
--------------------------------------------------
#!python3
import time
import sys
with open('tzname_bug.txt', 'w', encoding='utf-8') as f:
    f.write(sys.version + '\n')
    f.write('Should be: Střední Evropa (běžný čas) | Střední Evropa (letní čas)\n')        
    f.write('but it is: ' + time.tzname[0] + ' | ' + time.tzname[1] + '\n')        
    f.write('    types: ' + repr(type(time.tzname[0])) + ' | ' + repr(type(time.tzname[1])) + '\n')
    f.write('Should be as ascii: ' + ascii('Střední Evropa (běžný čas) | Střední Evropa (letní čas)') + '\n')        
    f.write('but it is as ascii: ' + ascii(time.tzname[0]) + ' | ' + ascii(time.tzname[1]) + '\n')        
-----------------------------------

It creates the tzname_bug.txt with the content (copy/pasted from UNICODE-capable editor (Notepad++, the indicator at the right bottom corner shows UTF-8.
-----------------------------------
3.5.0 (v3.5.0:374f501f4567, Sep 13 2015, 02:27:37) [MSC v.1900 64 bit (AMD64)]
Should be: Střední Evropa (běžný čas) | Střední Evropa (letní čas)
but it is: Støední Evropa (bìžný èas) | Støední Evropa (letní èas)
    types: <class 'str'> | <class 'str'>
Should be as ascii: 'St\u0159edn\xed Evropa (b\u011b\u017en\xfd \u010das) | St\u0159edn\xed Evropa (letn\xed \u010das)'
but it is as ascii: 'St\xf8edn\xed Evropa (b\xec\x9en\xfd \xe8as)' | 'St\xf8edn\xed Evropa (letn\xed \xe8as)'
-----------------------------------
msg251068 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2015-09-19 09:34
To decode the tzname strings, Python calls mbstowcs, which on Windows uses Latin-1 in the "C" locale. However, in this locale the tzname strings are actually encoded using the system ANSI codepage (e.g. 1250 for Central/Eastern Europe). So it ends up decoding ANSI strings as Latin-1 mojibake. For example:

    >>> s
    'Střední Evropa (běžný čas) | Střední Evropa (letní čas)'
    >>> s.encode('1250').decode('latin-1')
    'Støední Evropa (bì\x9ený èas) | Støední Evropa (letní èas)'

You can work around the inconsistency by calling setlocale(LC_ALL, "") before anything imports the time module. This should set a locale that's not "C", in which case the codepage should be consistent. Of course, this won't help if you can't control when the time module is first imported. 

The latter wouldn't be a issue if time.tzset were implemented on Windows. You can at least use ctypes to call the CRT's _tzset function. This solves the problem with time.strftime('%Z'). You can also get the CRT's tzname by calling the exported __tzname function. Here's a Python 3.5 example that sets the current thread to use Russian and creates a new tzname tuple:

    import ctypes
    import locale

    kernel32 = ctypes.WinDLL('kernel32')
    ucrtbase = ctypes.CDLL('ucrtbase')

    MUI_LANGUAGE_NAME = 8
    kernel32.SetThreadPreferredUILanguages(MUI_LANGUAGE_NAME, 
                                           'ru-RU\0', None)
    locale.setlocale(locale.LC_ALL, 'ru-RU')

    # reset tzname in current locale
    ucrtbase._tzset()
    ucrtbase.__tzname.restype = ctypes.POINTER(ctypes.c_char_p * 2)
    c_tzname = ucrtbase.__tzname()[0]
    tzname = tuple(tz.decode('1251') for tz in c_tzname)

    # print Cyrillic characters to the console
    kernel32.SetConsoleOutputCP(1251)
    stdout = open(1, 'w', buffering=1, encoding='1251', closefd=0)

    >>> print(tzname, file=stdout)
    ('Время в формате UTC', 'Время в формате UTC')
msg251098 - (view) Author: Petr Prikryl (prikryl) Date: 2015-09-19 18:34
I have worked around a bit differently -- the snippet from the code:

    result = time.tzname[0]    # simplified version of the original code.

    # Because of the bug in Windows libraries, Python 3.3 tried to work around
    # some issues. However, the shit hit the fan, and the bug bubbled here.
    # The `time.tzname` elements are (unicode) strings; however, they were
    # filled with bad content. See https://bugs.python.org/issue16322 for details.
    # Actually, wrong characters were passed instead of the good ones.
    # This code should be skipped later by versions of Python that will fix
    # the issue.
    import platform
    if platform.system() == 'Windows':
        # The concrete example for Czech locale:
        # - cp1250 (windows-1250) is used as native encoding
        # - the time.tzname[0] should start with 'Střední Evropa'
        # - the ascii('Střední Evropa') should return "'St\u0159edn\xed Evropa'"
        # - because of the bug it returns "'St\xf8edn\xed Evropa'"
        #
        # The 'ř' character has unicode code point `\u0159` (that is hex)
        # and the `\xF8` code in cp1250. The `\xF8` was wrongly used
        # as a Unicode code point `\u00F8` -- this is for the Unicode
        # character 'ø' that is observed in the string.
        #
        # To fix it, the `result` string must be reinterpreted with a different
        # encoding. When working with Python 3 strings, it can probably
        # done only through the string representation and `eval()`. Here
        # the `eval()` is not very dangerous because the string was obtained
        # from the OS library, and the values are limited to certain subset.
        #
        # The `ascii()` literal is prefixed by `binary` type prefix character,
        # `eval`uated, and the binary result is decoded to the correct string.
        local_encoding = locale.getdefaultlocale()[1]
        b = eval('b' + ascii(result))
        result = b.decode(local_encoding)
msg251259 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2015-09-21 20:49
> local_encoding = locale.getdefaultlocale()[1]

Use locale.getpreferredencoding().

> b = eval('b' + ascii(result))
> result = b.decode(local_encoding)

It's simpler and more reliable to use 'latin-1' and 'mbcs' (ANSI). For example:

    result = result.encode('latin-1').decode('mbcs')

If setlocale(LC_CTYPE, "") is called before importing the time module, then tzname is already correct. In this case, the above is either harmless or raises a UnicodeEncodeError that can be handled. OTOH, your approach silently corrupts the value:

    >>> result = 'Střední Evropa (běžný čas)'
    >>> b = eval('b' + ascii(result))
    >>> b.decode('1251')
    'St\\u0159ednн Evropa (b\\u011b\\u017enэ \\u010das)'

Back to the issue. In review, on initial import of the time module, if the CRT is using the default "C" locale, we have this inconsistency in which the time functions encode/decode tzname as ANSI and mbstowcs decodes tzname as Latin-1. (Plus strftime in the new CRT calls wcsftime, which adds another transcoding layer to compound the mojibake goodness.)

If time.tzset is implemented on Windows, then at startup an application can set the locale (specifically LC_CTYPE for tzname, and LC_TIME for strftime) and then call time.tzset(). 

Example with Russian system locale:

Initially we're in the "C" locale and the CRT's tzname is in ANSI. time.tzname incorrectly decodes this as Latin-1 since that's what mbstowcs uses in the "C" locale:

    >>> time.tzname[0]
    '\xc2\xf0\xe5\xec\xff \xe2 \xf4\xee\xf0\xec\xe0\xf2\xe5 UTC'

The way the CRT's strftime is implemented compounds the problem:

    >>> time.strftime('%Z')
    'A?aiy a oi?iaoa UTC'

It's implemented by calling the wide-character function, wcsftime. Just like Python, this gets a wide-character string by calling mbstowcs on the ANSI tzname. Then the CRT's strftime encodes the wide-character string back as a best-fit ANSI string, and finally time.strftime decodes the result as Latin-1 via mbstowcs. The result is mutated mojibake:

    >>> time.tzname[0].encode('mbcs', 'replace').decode('latin-1')
    'A?aiy a oi?iaoa UTC'

Ironically, Python stopped calling wcsftime on Windows because of these problems, but changes to the code since then, plus the new CRT, have brought the problem back, and worse. See my comment in issue 10653, msg243660.

Fix this by setting the locale and calling _tzset:

    >>> import ctypes, locale
    >>> locale.setlocale(locale.LC_ALL, '')
    'Russian_Russia.1251'
    >>> ctypes.cdll.ucrtbase._tzset()
    0
    >>> time.strftime('%Z')
    'Время в формате UTC'

If time.tzset were implemented on Windows, calling it would reload the time.tzname tuple.
msg251264 - (view) Author: Petr Prikryl (prikryl) Date: 2015-09-21 21:43
@eryksun: I see. In my case, I can set the locale before importing the time module. However, the code (asciidoc3.py) will be used as a module, and I cannot know if the user imported the time module or not.

Instead of your suggestion 
    result = result.encode('latin-1').decode('mbcs')

I was thinking to create a module say wordaround16322.py like this:

---------------
import locale
locale.setlocale(locale.LC_ALL, '')

import importlib
import time
importlib.reload(time)
---------------

I thought that reloading the time module would be the same as importing is later, after setting locale. If that worked, the module could be simply imported wherever it was needed. However, it does not work when imported after importing time. What is the reason? Does reload() work
only for modules coded as Python sources? Is there any other approach that would implement the workaroundXXX.py module?
msg251289 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2015-09-22 06:07
> import locale
> locale.setlocale(locale.LC_ALL, '')
>
> import importlib
> import time
> importlib.reload(time)
>
> it does not work when imported after importing time. 
> What is the reason? Does reload() work only for 
> modules coded as Python sources? 

The import system won't reinitialize a builtin or dynamic extension module. Reloading just returns a reference to the existing module. It won't even reload a PEP 489 multi-phase extension module. (But you can create and exec a new instance of a multi-phase extension module.) 

> Is there any other approach that would implement the 
> workaroundXXX.py module?

If the user's default locale and the current thread's preferred language are compatible with the system ANSI encoding [1], then you don't actually need to call _tzset nor worry about time.tzname. Call setlocale(LC_CTYPE, ''), and then call time.strftime('%Z') to get the timezone name. 

If you use Win32 directly instead of the CRT, then none of this ANSI business is an issue. Just call GetTimeZoneInformation to get the standard and daylight names as wide-character strings. You have that option via ctypes.

[1]: A user can select a default locale (language) that's unrelated to the system ANSI locale (the ANSI setting is per machine, located under Region->Administrative). Also, the preferred language can be selected dynamically by calling SetThreadPreferredUILanguages or SetProcessPreferredUILanguages. All three could be incompatible with each other, in which case you have to explicitly set the locale (e.g. "ru-RU" instead of an empty string) and call _tzset.
msg251308 - (view) Author: Petr Prikryl (prikryl) Date: 2015-09-22 11:30
@eryksun: Thanks for your help. I have finaly ended with your...

"Call setlocale(LC_CTYPE, ''), and then call time.strftime('%Z') to get the timezone name."
msg302936 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2017-09-25 09:36
bpo-31549 has been marked as a duplicate of this issue.
msg302937 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2017-09-25 09:37
Formatting timezone on Windows in the right encoding is an old Python (especially Python 3) issue:

https://bugs.python.org/issue1040
https://bugs.python.org/issue8304
https://bugs.python.org/issue10653
https://bugs.python.org/issue16322#msg174164
History
Date User Action Args
2018-07-05 15:47:37p-gansslesetnosy: + p-ganssle
2017-09-25 15:53:06BreamoreBoysetnosy: - BreamoreBoy
2017-09-25 09:37:31vstinnersetmessages: + msg302937
2017-09-25 09:36:47vstinnersetmessages: + msg302936
2017-09-25 08:01:52serhiy.storchakalinkissue31549 superseder
2017-09-25 07:07:34denis-osipovsetkeywords: + patch
stage: patch review
pull_requests: + pull_request3726
2015-09-22 11:30:54prikrylsetmessages: + msg251308
2015-09-22 06:07:23eryksunsetmessages: + msg251289
2015-09-21 21:43:01prikrylsetmessages: + msg251264
2015-09-21 20:49:45eryksunsetmessages: + msg251259
versions: + Python 3.6
2015-09-19 18:34:55prikrylsetmessages: + msg251098
2015-09-19 09:34:48eryksunsetnosy: + eryksun
messages: + msg251068
2015-09-18 17:25:39prikrylsetfiles: + tzname_bug.py
nosy: + prikryl
messages: + msg251013

2014-07-30 16:50:45BreamoreBoysetnosy: + BreamoreBoy

messages: + msg224325
versions: + Python 3.5, - Python 3.3
2012-11-26 12:14:57msmhrtsetmessages: + msg176408
2012-10-30 10:03:40serhiy.storchakasetnosy: - serhiy.storchaka
2012-10-30 00:28:26vstinnersetnosy: + ocean-city
messages: + msg174165
2012-10-29 23:59:23vstinnersetmessages: + msg174164
2012-10-29 23:19:11vstinnersetmessages: + msg174161
2012-10-28 03:55:38jceasetnosy: + jcea
2012-10-26 09:08:31msmhrtsetmessages: + msg173827
2012-10-26 07:07:48vstinnersetmessages: + msg173824
2012-10-25 22:55:46msmhrtsetmessages: + msg173806
2012-10-25 20:30:20vstinnersetmessages: + msg173798
2012-10-25 19:12:46r.david.murraysetnosy: + belopolsky, vstinner
2012-10-25 18:06:07serhiy.storchakasetmessages: + msg173784
2012-10-25 17:31:57amaury.forgeotdarcsetnosy: + amaury.forgeotdarc
messages: + msg173772
2012-10-25 13:47:12serhiy.storchakasetversions: + Python 3.4
nosy: + serhiy.storchaka

messages: + msg173758

components: + Extension Modules
keywords: + 3.3regression
2012-10-25 11:56:50msmhrtcreate