История одного глюка или дело о букве «Р»
На днях столкнулся со странным глюком, найденным в OutWiker‘е, на исправление которого вместе с поимкой виновника ушло два вечера.
Не буду долго описывать то, как я наткнулся на этот глюк, лучше перейдем сразу к его сути. А суть состоит в том, что для хранения и парсинга настроек страниц в программе используется стандартный класс ConfigParser (с некоторой тонкой оболочкой над ним, но в данном случае это не важно) — очень уж в нем удобно разбирать строки вида xxx = yyy. Так вот, неожиданно оказалось, что если yyy кончается на русскую букву «Р», то при преобразовании yyy в unicode бросается исключение о том, что кодек utf8 не может преобразовать строку, так как данные неожиданно закончились (‘utf8’ codec can’t decode byte 0xd0 in position __: unexpected end of data). Причем, в середине строки буква «Р» нормально преобразовывалась. Добавление пробела к концу строки, которая кончалась на «Р», ничего не дало, зато навело на мысль, что дело может быть в обрезании строки при выкидывания начальных и концевых пробелов, переводов строк и т.п. При этом все остальные русские буквы на конце обрабатывались без проблем.
В ходе отладки выяснилось, что буква «Р» в UTF-8 записывается двумя байтами: D0A0, а после прочтения строки, которая кончается на эту букву, от этих двух байтов остается только D0, а A0 пропадает. Естественно, кодек не может преобразовать этот «недосимвол» во что-то осмысленное, поэтому и ругается. Осталось понять кто виноват в том, что этот символ так криво обрабатывается.
Первым подозреваемым стал, разумеется, ConfigParser. К счастью, он полностью написан на питоне, что позволило без особых проблем в его коде найти место, где D0A0 обрезается до D0. Это оказалась следующая строка optval = optval.strip() внутри метода _read(), который, собственно, и разбирает текст файла.
Удивительно, но строку портила внешне безобидная функция strip(), которая верой и правдой служила не один год. Первым решением было сделать свой класс, производный от ConfigParser, перегрузить в нем метод _read(), заменив его точно таким же методом, но с закомментаренной строкой обрезания строк (в конце концов, строку можно «оголить» и после преобразования к unicode). Этот метод сработал, правда, при этом пришлось подправить еще одну строку внутри strip(), заменив переменную DEFAULTSECT на ConfigParser.DEFAULTSECT, но это мелочь.
После этого все заработало, но хотелось получить этот же глюк в лабораторных условиях с наименьшим количеством строк. Пишу маленький скрипт, который ничего не делает кроме того, что читает единственный параметр, значение которого оканчивается на «Р»… И скрипт выполняется без проблем, «Р» остается на месте.
Еще десяток раз просмотрев использование ConfigParser‘а в OutWiker’е и в тесте, понимаю, что разницы никакой — ну невозможно было ошибиться в этих четырех строчках, однако, тест работает, а тот же код в программе — нет. Хуже того, тест отлично справляется с файлами из вики, на которых получается исключение в OutWiker’е. В этот момент я понимаю, что все устои рушатся и я уже не доверяю даже таблице умножения, не говоря уже таблице символов Unicode.
Через какое-то время до меня начинает доходить, что только одна вещь могла затронуть казалось бы элементарную функцию обрезания строки — локализация. В последнее время я как раз добавлял в OutWiker поддержку многоязычности интерфейса. А, учитывая, что поведение под Виндой и Линуксом отличается, новым главным подозреваемым становится wxPython. И действительно, во время добавления поддержки gettext при локализации я взял отсюда код из раздела Example 2 по ссылке (надо же спихнуть вину на того, у кого я списал 🙂 ), который по сути мне не нужен, но добавляет какую-то внутреннюю локализацию в wxPython. Следующим подозреваемым стал класс wx.Locale.
Оказалось, что стоит только закомментарить строку mylocale = wx.Locale(…, wx.LOCALE_LOAD_DEFAULT), как ConfigParser начинал правильно обрабатывать «Р» на конце. На этом я решил остановиться, потому что копаться внутри wxPython / wxWidgets мне уже стало лень. Просто удалил все, что связано с этим классом, тем более, что все-равно все прекрасно работает и без него.
В общем, надо сказать, неприятный глюк, а при использовании wxPython уже не первый раз сталкиваюсь с ситуацией, когда поведение это библиотеки под Windows и Linux отличается.
PS. Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
Leave a comment