Кросс-вмный (CLR/JVM) код на Python

    Это узкоспециализированная короткая заметка про то, как я запинывал write once, run everywhere тесты для библиотеки, портированной с C# на Java, при помощи Python.

    Смысл в следующем: есть большая, толстая и красивая библиотека, которая была по коммерческим соображениям портирована с C# на Java. API осталось почти одинаковым, naming conventions естественно сменились при переходе на другой язык. Нам нужно было написать толстую пачку тестов, проверяющих, что клон библиотеки работает идентично оригиналу (тесты на регрессии, иными словами). Для этого сравнивались результаты работы кода библиотек (некие бинарники и xml-метаданные). Тесты были нетривиальные, их было много, и что самое неприятное — они постоянно дописывались с одного конца командой из четырех человек. Некоторое время я старательно портировал их на Java, затем плюнул и предложил команде писать тесты на языке, который сразу можно было бы выполнять на CLR (со старой библиотекой) и на JVM (с клоном). Оказалось, они и сами уже некоторое время думали про Python, и вот как это получилось.

    1. Basics

    Ну, о примитивном. На CLR я код запускаю IronPython-ом, на JVM — Jython-ом соответственно. Из приятного — Jython сам предоставляет загружаемым Java-классам вместо геттеров-сеттеров механизм пропертей. Почему это приятно? Потому что при портировании на Java шарповые проперти естественно были заменены геттерами-сеттерами.

    2. Где я?

    Полезно знать, поверх какого рантайма мы сейчас выполняемся. Тут все просто.
    isDotNet = sys.version.find('IronPython') > -1
    


    3. Подключение нужных библиотек

    В IronPython и Jython это делается по разному (с помощью библиотечки clr в IronPython, и просто добавлением в path в Jython), да и конвенция именования модулей разная, так что унифицируем:

    def cpUseLibrary(lib_path, library):
        if isDotNet:
            sys.path.append(lib_path)
            import clr
            clr.AddReference('OurProduct20.' + library)
        else:
            sys.path.append(lib_path + '\\java_libs\\ourproduct20.' + library.lower() + '.jar')
    


    Используется это в рабочем коде следующим тупым образом:
    cpUseLibrary(path_to_bins, 'Core')
    cpUseLibrary(path_to_bins, 'Formats.Common')
    


    4. Загрузка нужных классов.

    Вот тут небольшая проблема. Вообще и тот, и другой интерпретаторы позволяют тупо делать import из нужных namespace-ов / пакаджей, но нам-то нужна динамическая конструкция, учитывая, опять же, разные конвенции именования пакетов.

    def cpImport(module, clazz, globs = globals()):
        # load class
        mname = '';
        if isDotNet:
            mname = 'OurProduct.' + module
        else:
            mname = 'com.ourcompany.ourproduct.' + module.lower()
        pckg = __import__(mname, globals(), locals(), [clazz], -1)
        aliased_class = getattr(pckg, clazz)
        globs[clazz] = aliased_class
    


    Смысл в использовании __import__(), как питоновского внутреннего механизма импорта из модулей. Собственно, запись «import from откуда-то что-то» в __import__() в конечном итоге и разворачивается. Текущая проблема, которую не дошли пока руки решить — это необходимость передавать globals() каждый раз в такой импорт (из-за того, что все кроссплатформенные методы вынесены в отдельный модуль). Выглядит это следующим образом:
    cpImport('Core', 'OurProductLicense', globals())
    cpImport('Formats', 'OurProductCommonFormats', globals())
    


    Не очень красиво, но опять же, работает унифицированно на обоих рантаймах.

    5. Решение проблемы конвенции именования

    Разница в именовании методов очень простая: в C# они с заглавной начинаются, в Java — со строчной. Вот чем удобны современные скриптовые языки, так это возможностью метапрограммирования. Не лисп, наверное, но все равно. Руби тут был бы еще сподручнее, но и так неплохо. Дописываем cpImport():

        aliased_class = getattr(pckg, clazz)
        # add uppercased aliases for its lowercased methods (unless there's already an uppercased method with the same name)
        original_methods = aliased_class.__dict__.copy()
        for name, method in original_methods.iteritems():
            if name[0:1] in string.uppercase:
                continue
            newname = name[0:1].upper() + name[1:]
            if hasattr(aliased_class, newname):
                continue
            setattr(aliased_class, newname, method)
        globs[clazz] = aliased_class
    


    Смысл тут в использовании setattr() для прописывания алиаса на метод. Фактически, всем методам, начинающимся со строчной буквы, мы прямо в классе создаем алиас, начинающийся с заглавной.

    Как это выглядит в целом. Файлик test.py:

    from cptest import isDotNet, cpUseLibrary, cpImport
    
    cpUseLibrary('Core')
    cpUseLibrary('Formats.Common')
    cpImport('Core', 'OurProductLicense', globals())
    cpImport('Formats', 'OurProductCommonFormats', globals())
    
    OurProductLicense.SetProductID(".NET Product ID" if isDotNet else "JavaProductID")
    OurProductCommonFormats.Initialize();
    


    Выполняется простым ipy test.py / jython test.py из одного скрипта без танцев с бубном. Что приятно.

    P.S. Возможные проблемы

    Ну, они довольно очевидны. Нам повезло, наши разработчики в процессе портирования достаточно скурпулезно придерживались одной конвенции именования и портирования имен. Поэтому библиотеки, методы и прочее, действительно просто вызывать так, как описано выше. Иначе пришлось бы составлять некую табличку исключений, и учитывать ее в методе cpImport(). Но в целом описанный подход мне очень понравился и выглядит живым решением такой вот редко, наверное, встречающейся проблемы.
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 10

      +8
      Несколько замечаний:
      • Параметр по-умолчанию, равный globals() — адское попадалово на сложно отслеживаемые ошибки, поскольку оператор def в python исполняется один раз, создавая переменную-функцию, у которой сразу будут прописаны эти значения. Именно с globals(), скорее всего, ничего криминального не произойдёт, оно и так возвращает ~указатель на словарь, но на будущее — это лучше учесть, что иметь mutable-значения по-умолчанию для функций — напрашиваться на очень сложные в отладке проблемы. В Вашем случае лучше вообще опустить это значение, всё равно Вы передаёте его явно.
      • К той же функции… не хотите передавать globals() явно — не передавайте, есть же sys._getframe() и у фрейма есть .f_back и .f_globals… Ну то есть код может выглядеть как-то так:
        import sys
        def myImport(module, cls, globs = None):
            if not globs:
                try:
                    fr = sys._getframe()
                    globs = fr.f_back.f_globals
                finally:
                    del fr # to remove possible ref-loops
            """do main import stuff"""
      • Вместо вот этого странного куска
                if name[0:1] in string.uppercase:
                    continue
                newname = name[0:1].upper() + name[1:]

        можно просто написать
                newname = name.capitalize()

      Правда, всё это я пишу для CPython, но не думаю, что IronPython/Jython различаются в этих вопросах.
        +3
        использование capitalize() — не даёт идентичный результат, если, например, в name есть другие заглавные буквы
          0
          Да, я думал это то, что мне нужно, но оно же сломает мне остальные заглавные буквы в названиях.
            0
            Хм, действительно, не проверил до конца… В любом случае, можно было написать
            newname = name[:1].upper() + name[1:]

            и не заморачиваться с проверкой — всё равно потом проверяется, нет ли такого атрибута, перед его созданием…
        0
        Случай, когда стоило использовать IronRuby и JRuby ;)
          0
          Есть принципиальные отличия? Думаю, если бы разработчики лучше знали Ruby, то взяли бы её.
            –1
            Проще писать тесты. Например, уже есть очень приятные RSpec и RR.
              0
              Обычные юниттестовые фреймворки. Каких миллион под любой язык. А учитывая то, что у нас даже не о юнит-тестах речь идет… В любом случае, выбор языка был сделан из практических соображений — питон народ в среднем знает лучше.
            0
            Да, как правильно написали выше, Руби команде дольше пришлось бы осваивать.
            0
            Что за библиотека то?

            Only users with full accounts can post comments. Log in, please.