В прошлый раз мы узнали как создавать модули и пакеты. Сегодня мы возьмём этот пакет и посмотрим, как при помощи distutils мы можем сделать этот пакет доступным для других людей. Мы научимся:
создавать файл setup.py
создавать source distribution
создавать установщик для Windows
Начнём!
Пишем наш первый скрипт “setup.py”
Когда вы пишете ваш скрипт setup.py, Вы должны держать в голове его расположение. Если он будет находиться в папке с вашим пакетом, то это приведёт к ошибке при попытке им воспользоваться. Если он будет расположен где-то далеко, то скорее всего Вы при установке добавите ещё и те файлы, которые бы распространять не хотели. Поэтому для простоты мы создадим новую папку, куда и скопируем созданную вчера папку “mymath”. И тут же сохраним наш setup.py. Структура новой папки будет выглядеть так:
myNewFolder/ mymath/ setup.py
Внимание: Если у Вас нет кода, созданного в прошлой статье, его можно скачать тут
Теперь неплохо бы этот файл наполнить кодом:
fromdistutils.coreimport setup
setup(name = "mymath", version = "0.1", description = "A Simple Math Package", author = "Mike Driscoll", author_email = "mike@somedomain.com", url = "http://www.blog.pythonlibrary.org/2012/07/08/python-201-creating-modules-and-packages/", packages=["mymath"] )
Что же, выглядит не плохо. Если мы хотим добавить в наш setup.py ещё какие-то пакеты, то мы просто перечисляем их в списке packages. Кроме того есть список py_modules, куда можно добавить отдельные модули. Если Вы будете рыться в документации, то Вы обнаружите, что Вы можете добавлять даже файлы, не являющиеся скриптами Python используя класс Extension библиотеки distutils. Например, это может понадобиться для добавления файлов С или чего-то в этом роде, что можно встретить в сторонних пакетах, вроде lxml.
Distutils: Как распространять исходники и установщики для Windows
После того, как мы сохранили наш файл со скриптом, мы можем создать архив с исходниками, который будет использован для распространения нашего замечательного пакета. Откройте терминал (или командную строку в Windows) и перейдите в созданную вами папку. Затем выполните следующую команду
Что бы всё это могло значить? Только то, что distutils создал новую папку, названную dist, в которой теперь находится zip файл (mymath-0.1.zip) со всеми файлами вашего пакета. Если же Вы работаете на *nix — то получите tarball.
Далее, если Вы хотите создать установщик под Windows — это тоже будет легко сделать. Вам для этого понадобится такая команда:
python setup.py bdist_wininst
Эта команда создаст папку build (на которую Вы можете не обращать внимание) и файл с названием mymath-0.1.win32.exe в вашей папке dist, который Вы можете запустить под Windows для установки вашего пакета. Так давайте попробуем!
Как установить ваш пакет?
В случае исходников, Вам надо их распаковать / разархивировать и затем выполнить следующую команду:
python setup.py install
Если всё было сделано верно и всё работает верно, то пакет будет установлен в вашей системе (для той версии python, которую Вы используете для запуска этой команды — прим. пер.). Если Вы запустите установщик Windows, то возникнет такой мастер установки:
Обратите внимание, что тут показаны все метаданные, которые мы указывали в нашем скрипте setup.py. Круто, правда? В любом случае, если Вы выполните все шаги мастера — Вы опять же установите пакет на свою систему.
Внимание: при такой установке пакет будет установлен в вашу основную версию Python. Если Вы не хотите этого — используйте virtualenv.
Итоги
Теперь Вы должны уметь создавать установщики и устанавливать свои пакеты при помощи distutils. Если Вы хотите загрузить ваш пакет в Python Packages Index (PyPI) — смотрите руководство. Удачи и кодируйте удовольствие!
Сегодня мы посмотрим на простую библиотеку для создания PDF, которая называется pyfpdf и является портом библиотеки FPDF для PHP. Она не может заменить Reportlab, но даёт Вам более чем простую возможность создавать PDF файлы. Давайте на неё посмотрим.
Установка pyfpdf
К сожалению, у этого пакета не setup.py или eggs, что облегчило бы нам его установку. Вместо этого Вам придётся скачать его, разархивировать и скопировать получившуюся папку в папку site-packages. Последняя версия создаёт папку pyfpdf-1.54b, так что Вам придётся переименовать её в pyfpdf, если Вы хотите использовать примеры из их руководства или из этой статьи. Кроме того, Вы можете просто скопировать его в virtualenv.
Тест-драйв pyfpdf
Как и при знакомстве с любой новой библиотекой, для того, чтобы понять, как она работает, Вам надо написать несколько примеров. Вот простейший код, с помощью которого Вы можете создать PDF:
import pyfpdf
pdf = pyfpdf.FPDF(format='letter') pdf.add_page() pdf.set_font("Arial", size=12) pdf.cell(200, 10, txt="Welcome to Python!", align="C") pdf.output("tutorial.pdf")
Обратите внимание, что когда Вы инициализируете ваш FPDF объект, Вы должны сообщить ему, что хотите, чтобы результат был с размером «letter». По умолчанию его значение «A4». Далее мы должны добавить страницу, задать шрифт и поместить туда какой-нибудь текст. Вызов pdf.cell немного не очевиден. Первые два аргумента — ширина и высота и определяет место, где будет располагаться ваш текст. Параметр align принимает в качестве значения один символ. В нашем случае мы центрируем текст, передавая символ «С». Последняя строка принимает два параметра — имя файла и путь. Если путь не задан, то файл будет располагаться в рабочей папке скрипта.
А что, если мы хотим добавить ещё одну строку? Это можно сделать, добавляя текст в конец файла, добавив ещё одну «ячейку». Если Вам нужен разрыв строки — можно использовать такой код:
import pyfpdf
pdf = pyfpdf.FPDF(format='letter') pdf.add_page() pdf.set_font("Arial", size=12) pdf.cell(200, 10, txt="Welcome to Python!", ln=1, align="C") pdf.cell(200,10,'Powered by FPDF',0,1,'C') pdf.output("tutorial.pdf")
Добавляем Цацки: заголовки, нижние колонтитулы и разрывы страниц
В руководстве показано, как можно добавить заголовок, нижний колонтитул и разрыв страниц. Но этот способ не работает, так как надо изменить имя метода и код надо переписать, используя this вместо self (не знаю о чём он — прим. пер.). Вот что должно быть в идеале:
import pyfpdf
######################################################################## class MyPDF(pyfpdf.FPDF): """"""
#---------------------------------------------------------------------- def header(self): """ Заголовок на каждой странице """ # добавляем логотип self.image("logo.png", x=10, y=8, w=23) # располагаем логотип справа self.cell(w=80)
#---------------------------------------------------------------------- def footer(self): """ Нижний колонтитул на каждой странице """ # располагаем footer в 15mm от конца страницы self.set_y(-15)
# отображаем номер страницы по центру pageNum = "Page %s/{nb}"%self.page_no() self.cell(0, 10, pageNum, align="C")
#---------------------------------------------------------------------- if __name__ == "__main__": pdf = MyPDF() pdf.alias_nb_pages() pdf.add_page() pdf.set_font("Times", size=12)
# добавляем несколько строк на страницу for i inrange(1, 50): pdf.cell(0, 10, "Line number %s"% i, border=0, ln=1) pdf.output("tutorial2.pdf")
Итак, мы создали подкласс FPDF, переопределили его методы header и footer, так как в оригинальном классе они всего лишь заглушки. В нашем заголовке мы создаём объект изображения и задаём его координаты х/у и его ширину w. Кроме того, вы можете передать и его высоту, если Вас беспокоит соотношение сторон. После этого мы его позиционируем и добавляем строку текста для заголовка. Наконец, мы добавляем разрыв строки.
Footer мы помещаем на 15 мм выше конца страницы. Его шрифт — 8pt Arial Italic. Ошибка в официальном руководстве была в том, что он вызывал self.PageNo(), который не существует. Зато есть метод page_no, который, видимо, его заменяет, так что я его и испо
льзовал. Наконец, в конце скрипта мы создаём наш PDF объект и записываем в него несколько строк. Если Вы запустите этот скрипт, Вы получите 3-х страничный документ.
Подведение итогов
В руководстве говорится так же об использовании цветов, но это я оставлю Вам в качестве домашнего задания. Ничего по поводу рисования, вставки таблиц или графиков, встраивания собственных шрифтов и многих других вещей, доступных в Reportlab, я не нашёл, но, опять же, это и предполагалось в качестве простой библиотеки для создания PDF. Если Вам нужны более продвинутые инструменты — смотрите в сторону Reportlab или проектов, созданных на его основе (rst2pds или xhtml2pdf).
Я всегда наблюдаю за библиотеками для Pytho для работы с PDF и в один прекрасный день я столкнулся сpdfrw. Это похоже на аналогpyPDF, так как позволяет Вам читать и записывать PDFы, объединять их и использовать Reportlab для пометки их водяными знаками, кроме всего прочего. Проект немного мёртв, так как последнее обновление было в 2011 году, но с учётом того, что pyPDF обновлялся в 2010 — это не так уж и плохо. В этой статье мы проведём небольшой тест-драйв pdfrw и посмотрим, на что он годен. Поехали!
Заметка об установке: К сожалению у этой библиотеки нет скрипта setup.py, так что Вам придётся получить исходники с Google Code а затем скопировать папку pdfrw в site-packages или в ваш virtualenv.
Я думаю, у этого проекта большой потенциал. К счастью, мы можем проявить к нему достаточно интереса, чтобы снова запустит его, ну или найти что-то новенькое.
Для новичков сейчас не совсем понятно, что это такое. Сейчас разъясню.
Мы можем писать программу, но её нужно как то запускать. Не совсем понимаю, почему это не предусмотрено при установке питона… Разработчики наверное думают, что все в интернете профессиональные программисты.
Но если честно, то запустить программу на питоне без прописки патча как то не удобно и не каждый догадается как. Читать →
Я недавно выпустил версию 0.10 pylibftdi — «минимального питонного интерфейса для libftdi», предназначенного для наиболее лёгкого (из всех возможных?) способов работы с чипами и модулями FTDI имитирующими последовательный порт при подключении к usb. Версия 0.10 добавляет несколько новых возможностей и исправляет несколько ошибок.
Долгое время я был в плену заблуждения, что номера версий должны соответствовать правилам десятичной системы, и что после версии 0.9 должна идти версия 1.0. Но, поскольку, я хочу, чтобы версия 1.0 была стабильной (а нынешний статус я всё ещё рассматриваю как «бета»), я попал в тупик. В итоге я решил, что новая версия будет иметь номер 0.10. В любом случае, я всё ещё хочу улучшить документацию (и не только) перед выпуском 1.0.
Изменения в 0.10
Запуск юнит-тестов теперь проще благодаря некоторым реорганизациям — просто выполните python -m unittest discover в папке верхнего уровня
Поддержка устройств FT232H — это другой PID (USB product ID) для тех устройств, которые я раньше использовал — в основном FT245BM/RL, FT232R/RL. Все эти устройства имели PID 0x6001, тогда как новый FT232H имел PID 0x6014. Я экспериментировал с дополнительными параметрами для определения VID и PID целевого устройства, но это оказалось слишком сложно для конечного пользователя, а я бы хотел, чтобы pylibftdi можно было бы использовать с параметрами по умолчанию для большинства основных операций. В итоге я взял два списка (USB_VID_LIST,USB_PID_LIST) и код поиска устройства должен просто провести итерации по их декартову произведению (то есть вложенный цикл, реализованный через itertools.product). Так что добавление новых PID в будущем будет простым — надо лишь добавить его в USB_VID_LIST, и тогда устройство может быть открыто без параметров конструктором Device() если на USB шине есть только FTDI устройство.
Устройство при открытии устанавливается в параллельный режим. В рассылке libftdi был спор о том, как эту логику реализовать, но в pylibftdi это сделать достаточно легко. Вообще это позволяет избежать неопределённого состояния, так как раньше, если предыдущее приложение, которое работало с устройством в режиме BitBang, то новое открытие устройства при помощи Device() отрывало его тоже в режиме BitBang, а не в ожидаемом параллельном режиме (для устройств, которые поддерживают оба режима).
В конструктор Device() добавлен параметр 'buffer_size' (по умолчанию равен 0, что соответствует поведению раньше), который разбивает на куски операции чтения и записи. Это позволяет избежать проблемы, когда вызов (например) dev.write ('hello' * 100000) при передаче 9600 занимал невероятно много времени, а так как он протекал в контексте библиотеки (через вызов ctypes), его нельзя было прервать при помощи Ctrl-C
Убрана устаревшая возможность использовать Driver() как синоним Device()
Обновлено: Я уже сделал два дополнительных релиза в последние часы — текущая версия — 0.10.2. Одним из значимых изменений является то, что теперь субпакет exmaples включён в sdist, так что python -m pylibftdi.examples.led_flash будет работать, если у Вас диод подключён к D0 на устройстве.
Планы на будущее: причесать, добавить примеры, улучшить документацию…
создавать модальные диалоговые окна и окна с сообщениями
использовать стандартные диалоги
создавать помощников
показывать советы при запуске
создавать и использовать валидаторы
В то время как фреймы используются для продолжительных взаимодействий с пользователем, диалоги обычно нужны для того, чтобы получить некоторое количество информации от пользователя и передать её на обработку дальше. Зачастую диалоговые окна открываются в модальном режиме, что не позволяет другим фреймам перехватывать события, пока диалог не закрыт. В это главе мы обсудим виды диалогов, доступных в wxPython, хотя кроме них Вы можете создать и свои собственные варианты. Среди предустановленных диалогов есть как простые, только для вывода информации, так и более сложные, например, для открытия файлов.
9.1 Работа с модальными диалогами
Модальные диалоги используются или для того, чтобы быстро вывести / получить какую-либо информацию, либо когда без новых данных от пользователя невозможно двигаться дальше. wxPython содержит несколько функций для отображения таких диалогов: диалог с сообщением, со строкой для ввода и с выбором из списка. В следующем разделе мы посмотрим, как эти диалоги выглядят и как Вы можете облегчить себе жизнь с помощью имеющихся функций.
9.1.1 Как создать модальный диалог
Модальный диалог не позволяет другим виджетам получать события от пользователя до тех пор, пока диалог не будет закрыт. Как видно из рисунка 9.1, по внешнему виду не всегда можно отличить, является ли открытое окно диалогом или фреймом.
В этом листинге надо отметить несколько деталей: при инициализации кнопка добавляется непосредственно к диалогу, а не на панель. Вообще, панели гораздо реже используются в диалогах, чем во фреймах. Отчасти потому, что диалоги должны быть как можно проще, но главная причина в том, что функционал панели (стандартный фон и переключение между элементами с помощью табуляции) уже реализован в диалогах.
Для отображения диалога как модального окна используется метод ShowModal(), имеющийся от метода Show(), который используется для отображения фреймов. Ваше приложение останавливается в точке вызова ShowModal() до тех пор, пока диалоговое окно не будет закрыто. В этот момент единственным, кто может взаимодействовать с пользователем является диалог (понятно, что другие приложения продолжают работать в обычном режиме).
Программно диалог можно закрыть с помощью метода EndModal(retCode), где retCode — числовое значение, которое возвращается методом ShowModal(). Обычно оно используется для того, чтобы сообщить приложению каким образом было закрыто диалоговое окно и на основании этого принять соответствующее решение. При этом важно помнить, что прекращение отображения диалога ещё не значит его уничтожения — даже после этого он продолжает существовать, благодаря чему после его закрытия Вы всё ещё можете получить данные, введённые пользователем. Пример этого мы увидим в следующем разделе.
Возможно, Вы заметили, что в листинге не было определено ни одного обработчика, что должно было заставить Вас задуматься: «Почему же тогда диалог реагирует на нажатие кнопок?» Никакого секрета тут нет — это поведение уже определено в wx.Dialog. Есть два предопределённых ID, которые имеют особое значение для диалогов:
когда нажимается wx.Button с ID равным wx.ID_OK, диалог закрывается и в точку вызова ShowModal() возвращается значение wx.ID_OK
аналогично действует нажатие на кнопку со значением wx.ID_CANCEL, возвращающая соответствующее значение
после чего приложению остаётся лишь корректно обработать значения кнопок.
Листинг 9.1 показывает стандартный способ работы с модальными диалогами. После активации диалога значение, которое он возвращает, используется в качестве условия в выражении if. В нашем случае мы всего лишь выводим на экран выбор пользователя. Однако обычно в случае результата wx.ID_OK предпринимаются более сложные действия, основанные на выборе пользователя, такие как открытие файла или выбор цвета.
Обычно Вы должны явным образом уничтожить диалог когда Вы заканчиваете его использовать, так как после этого он может быть утилизован сборщиком мусора Python. Если же Вы хотите повторно использовать диалог без того, чтобы тратить время на его создание (например, если это какой-то сложный экземпляр), то Вы можете сохранить на него ссылку и отобразить его позже, вызвав метод ShowModal(). Но при этом важно помнить, что перед завершением программы диалог всё равно придется уничтожить, так как иначе MainLoop() будет рассматривать его как окно верхнего уровня и не сможет корректно завершить работу.
9.1.2 Как вывести пользователю сообщение?
Три наиболее простых диалога для взаимодействия с пользователем — это wx.MessageDialog, выводящий сообщение, wx.TextEntryDialog, предлагающий пользователю ввести короткий текст и wx.SingleChoiceDialog, позволяющий пользователю выбрать варианты из предоставленного списка. В следующих трёх секциях мы обсудим эти три диалога.
Диалог сообщения, как следует из названия, отображает короткое сообщение и закрывается по нажатию кнопки. Обычно он используется для вывода предупреждений или получения ответа на вопрос да / нет. На рисунке 9.2 отображён такой диалог.
рис 9.2
Использование такого диалога не вызовет у Вас затруднений: в листинге 9.2 представлено два способа создания такого диалога.
Листинг 9.2
importwx if__name__
=="__main__": app=wx.PySimpleApp() # с использованием классов dlg=wx.MessageDialog(None,"Is this explanation OK?", 'A Message Box',wx.YES_NO|wx.ICON_QUESTION) retCode=dlg.ShowModal() if(retCode==wx.ID_YES): print"yes" else: print"no" dlg.Destroy() # с использованием функции retCode=wx.MessageBox("Is this way easier?","Via Function", wx.YES_NO|wx.ICON_QUESTION)
Мы создаём два диалоговых окна, одно за другим. В первый раз мы создаём экземпляр класса wx.MessageDialog, после чего вызываем его метод ShowModal(). Используем класс wx.MessageDialog
Используя конструктор класса wx.MessageDialog Вы можете задать сообщение в диалоге и выбрать для него необходимые кнопки:
message — текст, который будет отображаться в диалоге; если Вы хотите разделить его на несколько строк — используйте 'n'. caption отображается в заголовке диалога. pos позволяет Вам определить место на экране, где будет отображён диалог, однако Windows этот аргумент игнорирует.
Флаги стиля можно разделить на две группы. Первая содержит кнопки, которые будут отображаться в диалоге:
Стиль
Описание
wx.CANCEL
Добавляет кнопку «отменить» с идентификатором wx.ID_CANCEL
wx.NO_DEFAULT
Если установлен флаг wx.YES_NO, то кнопкой по умолчанию будет «нет»
wx.OK
Добавляет кнопку «да» с идентификатором wx.ID_OK
wx.YES_DEFAULT
Если установлен флаг wx.YES_NO, то кнопкой по умолчанию будет «да». Такая настройка применяется по умолчанию
wx.YES_NO
Добавляет кнопки «да» и «нет» со значениями wx.ID_OK и wx.ID_NO соответственно
Вторая группа флагов отвечает за знак, который будет отображён рядом с текстом:
Стиль
Описание
wx.ICON_ERROR
Знак ошибки
wx.ICON_EXCLAMATION
Восклицательный знак
wx.ICON_HAND
То же, что и wx.ICON_ERROR
wx.ICON_INFORMATION
Знак информационного сообщения (буква i)
wx.ICON_QUESTION
Вопросительный знак
И наконец, Вы можете использовать флаг стиля wx.STAY_ON_TOP, чтобы отобразить сообщение поверх всех окон в системе.
Как можно видеть из листинга, для того, чтобы отобразить диалог, вызывается метод ShowModal(). В зависимости от отображаемых кнопок Вы получите в ответ wx.ID_OK, wx.ID_CANCEL, wx.ID_YES или wx.ID_NO. Как обычно, результат используется для управления дальнейшей работой программы.
Использование функции wx.MessageBox()
В листинге 9.2 показан и более короткий способ отображения информационного окна: специальная функция wx.MessageBox() сама создаёт диалог, вызывает его метод ShowModall() и возвращает одно из четырёх значений: wx.ID_OK, wx.ID_CANCEL, wx.ID_YES или wx.ID_NO. Сам вызов функции проще конструктора wx.MessageDialog: wx.MessageBox(message,caption=»Message»,style=wx.OK) В этом примере message,caption и style имеют те же значения, что и в конструкторе класса и Вы можете использовать те же самые флаги стиля. Как Вы увидите дальше в этой главе, многие из предустановленных диалогов имеют схожие функции для их быстрого вызова. Пока Вы планируете использовать диалог всего один раз, можете спокойно выбирать любой из этих двух способов. Но если Вы хотите сохранить диалог в памяти для дальнейшего использования — лучше создать собственный экземпляр. Однако для таких простых диалогов, как наш, экономии времени Вы, конечно, не достигните.
Для того, чтобы отобразить более объемны
й текст (например, лицензию) Вы можете использовать доступный только для Python класс wx.lib.dialogs.ScrolledMessageDialog, использующий такой конструктор: wx.lib.dialogs.ScrolledMessageDialog(parent, msg, caption, pos=wx.wxDefaultPosition, size=(500,300)) Этот класс не использует встроенные диалоги, а конструирует свой из доступных виджетов. Он отображает только кнопку «ОК» и не принимает флагов стиля.
9.1.3 Как получить короткий текст от пользователя?
Второй простой тип диалога — wx.TextEntryDialog, используемый для получения ввода от пользователя небольшого объема информации. Обычно Вы встречаетесь с ним, когда от Вас требуют имя пользователя или пароль при запуске программы. На рисунке 9.3 отображён типичный представитель этого класса.
рис 9.3
Код, вызвавший его к жизни, приведён в листинге 9.3
Листинг9.3
importwx if__name__=="__main__": app=wx.PySimpleApp() dialog=wx.TextEntryDialog(None, "What kind of text would you like to enter?", "Text Entry","Default Value",style=wx.OK|wx.CANCEL) ifdialog.ShowModal()==wx.ID_OK: print"You entered: %s"%dialog.GetValue() dialog.Destroy()
Как и в предыдущем разделе, мы создаём экземпляр класса диалога, в данном случае wx.TextEntryDialog, однако конструктор этого класса чуть более сложный, чем предыдущий:wx.TextEntryDialog(parent, message, caption=»Please enter text», defaultValue=»», style=wx.OK | wx.CANCEL | wx.CENTRE, pos=wx.DefaultPosition) message — текст приглашения к вводу, caption — текст, отображаемый в заголовке окна. defaultValue, если задано, отображает значение, появляющееся в поле для ввода при отображении диалога. Флаги стиля могут содержать wx.OK и wx.CANCEL для отображения соответствующих кнопок.
В данном диалоге можно использовать и некоторые флаги стиля виджета wx.TextCtrl. Наиболее полезные из них: wx.TE_PASSWORD для замены символов звёздочками, wx.TE_MULTILINE, позволяющий вводить несколько строк текста, и wx.TE_LEFT, wx.TE_CENTRE с wx.TE_RIGHT, определяющие выравнивание вводимого текста.
Последняя строка из этого листинга иллюстрирует ещё одно отличие от информационного диалога: введённая пользователем информация сохраняется в экземпляре диалога и может быть использована после его закрытия. В нашем примере мы получаем её с помощью метода GetValue(). Не стоит забывать, что если пользователь нажал на «отмену», это значит, что он не хочет использовать введённое значение, поэтому имеет смысл проводить проверку, какая кнопка была нажата. Кроме того, Вы можете ввести значение программно с помощью метода SetValue().
Кроме того, у нас есть ещё три удобных функции для этих целей:
wx.GetTextFromUser()
wx.GetPasswordFromUser()
wx.GetNumberFromUser()
Наиболее похожим образом на наш пример действует функция wx.GetTextFromUser(): wx.GetTextFromUser(message, caption=»Input text», default_value=»», parent=None) Все значения в этом примере идентичны конструктору класса. Если пользователь нажимает «ок», то функция возвращает введёный текст, иначе Вы получите пустую строку. Для получения пароля надо использовать функцию wx.GetPasswordFromUser(): wx.GetPasswordFromUser(message, caption=»Input text», default_value=»», parent=None) И тут с аргументами у Вас не должно возникнуть проблем. Вводимый текст, как понятно, заменяется звёздочками. Возвращаемые значения аналогичны предыдущей фу