Документация Engee

5. Создание пакетов

Создание файлов для пакета

Пакет PkgTemplates предлагает простой, повторяемый и настраиваемый способ генерации файлов для нового пакета. Он также может генерировать файлы, необходимые для документации, CI и т. д. PkgTemplates рекомендуется использовать для создания новых пакетов вместо того, чтобы использовать минимальную функциональность pkg> generate, описанную ниже.

Чтобы сгенерировать минимальное количество файлов для нового пакета, используйте pkg> generate.

(@v1.8) pkg> generate HelloWorld

Будет создан новый проект HelloWorld в одноименном подкаталоге, содержащий следующие файлы (визуализируемые с помощью внешней команды tree):

shell> tree HelloWorld/
HelloWorld/
├── Project.toml
└── src
    └── HelloWorld.jl

2 directories, 2 files

Файл Project.toml содержит имя пакета, его уникальный идентификатор UUID, версию, авторов и потенциальные зависимости:

name = "HelloWorld"
uuid = "b4cd1eb8-1e24-11e8-3319-93036a3eb9f3"
version = "0.1.0"
authors = ["Some One <someone@email.com>"]

[deps]

Вот содержимое src/HelloWorld.jl:

module HelloWorld

greet() = print("Hello World!")

end # модуль

Теперь мы можем активировать проект, указав путь к каталогу, в котором он установлен, и загрузить пакет:

pkg> activate ./HelloWorld

julia> import HelloWorld

julia> HelloWorld.greet()
Hello World!

Для удобства в остальной части руководства мы будем входить в каталог проекта:

julia> cd("HelloWorld")

Добавление зависимостей в проект

Допустим, мы хотим использовать в проекте пакет Random стандартной библиотеки и зарегистрированный пакет JSON. Мы просто добавляем (add) эти пакеты (обратите внимание, что в приглашении теперь отображается имя только что сгенерированного проекта, так как мы его активировали (activate)):

(HelloWorld) pkg> add Random JSON
   Resolving package versions...
    Updating `~/HelloWorld/Project.toml`
  [682c06a0] + JSON v0.21.3
  [9a3f8284] + Random
    Updating `~/HelloWorld/Manifest.toml`
  [682c06a0] + JSON v0.21.3
  [69de0a69] + Parsers v2.4.0
  [ade2ca70] + Dates
 ...

И Random, и JSON были добавлены в файл Project.toml проекта, а результирующие зависимости были добавлены в файл Manifest.toml. Сопоставитель установил каждый пакет с максимально возможной версией, соблюдая при этом совместимость, которую каждый пакет обеспечивает для своих зависимостей.

Теперь мы можем использовать в проекте и Random, и JSON. Изменив src/HelloWorld.jl на

module HelloWorld

import Random
import JSON

greet() = print("Hello World!")
greet_alien() = print("Hello ", Random.randstring(8))

end # модуль

и перезагрузив пакет, можно вызвать новую функцию greet_alien, использующую Random:

julia> HelloWorld.greet_alien()
Hello aT157rHV

Определение общедоступного API

Если вы хотите, чтобы ваш пакет был полезен для других пакетов и чтобы пользователи могли легко выполнять обновления до новых выходящих версий вашего пакета, важно документировать, какое поведение будет оставаться неизменным в обновлениях.

Если не указано иное, общедоступный API вашего пакета определяется как все поведение, описываемое вами в отношении общедоступных символов. Общедоступный символ — это символ, который экспортируется из вашего пакета с помощью ключевого слова export или помечается как общедоступный с помощью ключевого слова public. При изменении поведения чего-то, что ранее было общедоступным, так что новая версия больше не соответствует спецификациям, представленным в старой версии, необходимо изменить номер версии пакета в соответствии с вариантом Julia в SemVer. Если вы хотите включить в свой общедоступный API символ, не экспортируя его в глобальное пространство имен пользователей, которые вызывают using YourPackage, этот символ следует пометить как общедоступный с помощью public that_symbol. Символы, помеченные как общедоступные с помощью ключевого слова public, являются такими же общедоступными, как и те, что помечены как общедоступные с помощью ключевого слова export, но когда пользователи вызывают using YourPackage, им все равно придется уточнять доступ к этим символам с помощью YourPackage.that_symbol.

Допустим, мы хотим, чтобы частью общедоступного API была функция greet, но не функция greet_alien. Мы можем написать следующее и выпустить это как версию 1.0.0.

module HelloWorld

export greet

import Random
import JSON

"Writes a friendly message."
greet() = print("Hello World!")

"Greet an alien by a randomly generated name."
greet_alien() = print("Hello ", Random.randstring(8))

end # модуль

Затем, если изменить greet на

"Writes a friendly message that is exactly three words long."
greet() = print("Hello Lovely World!")

Мы выпустим новую версию как 1.1.0. Это не критическое изменение, поскольку новая реализация соответствует старой документации, но она привносит новую особенность — сообщение должно состоять из трех слов.

Позже мы, возможно, захотим изменить greet_alien на

"Greet an alien by a the name of \\\"Zork\\\"."
greet_alien() = print("Hello Zork")

А также экспортировать ее, изменив

export greet

на

export greet, greet_alien

Мы должны выпустить эту новую версию как 1.2.0, потому что она добавляет новую возможность greet_alien в общедоступный API. Даже если greet_alien была документирована ранее и новая версия не соответствует старой документации, это не является нарушением, потому что старая документация не была прикреплена к символу, который экспортировался в то время, поэтому документация не применяется в выпущенных версиях.

Однако если теперь мы захотим изменить greet на

"Writes a friendly message that is exactly four words long."
greet() = print("Hello very lovely world")

нам нужно будет выпустить новую версию как 2.0.0. В версии 1.1.0 мы указали, что приветствие должно состоять из трех слов, а поскольку greet была экспортирована, это описание также применимо ко всем последующим версиям до следующего важного выпуска. Поскольку эта новая версия не соответствует старой спецификации, она должна быть помечена как критическое изменение.

Имейте в виду, что доступно буквально неограниченное количество номеров версий. Можно использовать много номеров (например, версия 6.62.8).

Добавление шага сборки в пакет

Шаг сборки выполняется при первой установке пакета или при явном вызове с помощью build. Пакет собирается при выполнении файла deps/build.jl.

julia> mkpath("deps");

julia> write("deps/build.jl",
             """
             println("I am being built...")
             """);

(HelloWorld) pkg> build
  Building HelloWorld → `deps/build.log`
 Resolving package versions...

julia> print(readchomp("deps/build.log"))
I am being built...

Если шаг сборки завершается сбоем, выходные данные шага сборки выводятся на консоль

julia> write("deps/build.jl",
             """
             error("Ooops")
             """);

(HelloWorld) pkg> build
    Building HelloWorld → `~/HelloWorld/deps/build.log`
ERROR: Error building `HelloWorld`:
ERROR: LoadError: Ooops
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] top-level scope
   @ ~/HelloWorld/deps/build.jl:1
 [3] include(fname::String)
   @ Base.MainInclude ./client.jl:476
 [4] top-level scope
   @ none:5
in expression starting at /home/kc/HelloWorld/deps/build.jl:1

На шаге сборки, как правило, не должны создаваться или изменяться какие-либо файлы в каталоге пакета. Если вам нужно сохранить некоторые файлы с шага сборки, используйте пакет Scratch.jl.

Добавление тестов в пакет

При тестировании пакета выполняется файл test/runtests.jl:

julia> mkpath("test");

julia> write("test/runtests.jl",
             """
             println("Testing...")
             """);

(HelloWorld) pkg> test
   Testing HelloWorld
 Resolving package versions...
Testing...
   Testing HelloWorld tests passed

Тесты выполняются в новом процессе Julia, в котором доступен сам пакет и все специфические для теста зависимости (см. ниже).

Тесты, как правило, не должны создавать или изменять какие-либо файлы в каталоге пакета. Если вам нужно сохранить некоторые файлы с шага сборки, используйте пакет Scratch.jl.

Зависимости, характерные для теста

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

Зависимости, характерные для теста на основе target

При использовании этого метода добавления характерных для теста зависимостей пакеты добавляются в раздел [extras] и целевой объект теста, например чтобы добавить Markdown и Test в качестве тестовых зависимостей, добавьте следующее в файл Project.toml:

[extras]
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Markdown", "Test"]

Обратите внимание, что поддерживаются только целевые объекты test и build, последний из которых (не рекомендуется) можно использовать для любых скриптов deps/build.jl.

Альтернативный подход: зависимости, характерные для теста на основе файла test/Project.toml

Точное взаимодействие между файлами Project.toml, test/Project.toml и соответствующими им файлами Manifest.toml не до конца проработано и может быть изменено в будущих версиях. Поэтому старый метод добавления зависимостей, характерных для теста, описанный в предыдущем разделе, будет поддерживаться во всех выпусках Julia 1.X.

В Julia 1.2 и более поздних версиях зависимости тестов могут быть объявлены в test/Project.toml. При выполнении тестов Pkg автоматически объединит этот пакет и пакет Projects для создания тестовой среды.

Если test/Project.toml не существует, Pkg будет использовать зависимости, характерные для теста на основе target.

Чтобы добавить зависимость, характерную для теста, то есть зависимость, которая доступна только при тестировании, достаточно добавить эту зависимость в проект test/Project.toml. Это можно сделать из REPL Pkg, активировав эту среду, а затем использовать add обычным образом. Давайте добавим стандартную библиотеку Test в качестве зависимости теста:

(HelloWorld) pkg> activate ./test
[ Info: activating environment at `~/HelloWorld/test/Project.toml`.

(test) pkg> add Test
 Resolving package versions...
  Updating `~/HelloWorld/test/Project.toml`
  [8dfed614] + Test
  Updating `~/HelloWorld/test/Manifest.toml`
  [...]

Теперь мы можем использовать Test в тестовом скрипте и видим, что он устанавливается во время тестирования:

julia> write("test/runtests.jl",
             """
             using Test
             @test 1 == 1
             """);

(test) pkg> activate .

(HelloWorld) pkg> test
   Testing HelloWorld
 Resolving package versions...
  Updating `/var/folders/64/76tk_g152sg6c6t0b4nkn1vw0000gn/T/tmpPzUPPw/Project.toml`
  [d8327f2a] + HelloWorld v0.1.0 [`~/.julia/dev/Pkg/HelloWorld`]
  [8dfed614] + Test
  Updating `/var/folders/64/76tk_g152sg6c6t0b4nkn1vw0000gn/T/tmpPzUPPw/Manifest.toml`
  [d8327f2a] + HelloWorld v0.1.0 [`~/.julia/dev/Pkg/HelloWorld`]
   Testing HelloWorld tests passed```

Совместимость зависимостей

Каждая зависимость в общем случае должна иметь ограничение по совместимости. Это важная тема, поэтому ей посвящена отдельная глава: Совместимость.

Слабые зависимости

Это несколько расширенное использование Pkg, которое могут пропустить пользователи, впервые знакомящиеся с Julia и пакетами Julia.

Совместимость

Для работы описанной функции требуется Julia версии 1.9 и более поздних версий.

Слабая зависимость — это зависимость, которая не будет автоматически устанавливаться при установке пакета, но вы можете контролировать, какие версии этого пакета могут быть установлены, задав для него возможность совместимости. Они приводятся в файле проекта в разделе [weakdeps]:

[weakdeps]
SomePackage = "b3785f31-9d33-4cdf-bc73-f646780f1739"

[compat]
SomePackage = "1.2"

В настоящее время они используются практически только для «расширений», которые описаны в следующем разделе.

Условная загрузка кода в пакетах (расширения)

Это несколько расширенное использование Pkg, которое могут пропустить пользователи, впервые знакомящиеся с Julia и пакетами Julia.

Совместимость

Для работы описанной функции требуется Julia версии 1.9 и более поздних версий.

Иногда нужно, чтобы два или более пакета хорошо работали вместе, но при этом нежелательно (возможно, из-за увеличения времени загрузки) делать один из них безусловной зависимостью другого. Расширение пакета — это модуль в файле (аналогичный пакету), который автоматически загружается при загрузке другого набора пакетов в Julia. Это очень похоже на функциональность, предоставляемую внешним пакетом Requires.jl, но которая теперь доступна непосредственно в Julia и дает дополнительные преимущества, такие как возможность предварительной компиляции расширения.

Расширения можно применять для пакета построения графиков, который должен строить графики для объектов из множества различных пакетов Julia. Добавление всех этих различных пакетов Julia в качестве зависимостей пакета построения графиков может оказаться дорогостоящим, поскольку они будут загружаться, даже если никогда не будут использоваться. Вместо этого код, необходимый для построения графиков объектов для конкретных пакетов, можно поместить в отдельные файлы (расширения), которые загружаются только при загрузке пакетов, определяющих тип (-ы), графики которых нужно построить.

Ниже приведен пример структурирования кода для случая, когда пакет Plotting хочет отображать объекты, определенные во внешнем пакете Contour. Структура файлов и папок, показанная ниже, находится в пакете Plotting.

Project.toml:

name = "Plotting"
version = "0.1.0"
uuid = "..."

[weakdeps]
Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"

[extensions]
# имя расширения слева

# зависимости расширения, необходимые для загрузки расширения справа

# используйте список для нескольких зависимостей расширения

PlottingContourExt = "Contour"

[compat]
Contour = "0.6.2"

src/Plotting.jl:

module Plotting

function plot(x::Vector)
    # Функциональность для построения здесь вектора
end

end # модуль

ext/PlottingContourExt.jl (может также быть в ext/PlottingContourExt/PlottingContourExt.jl):

module PlottingContourExt # Должно быть то же имя, что и у файла (как и у обычного пакета)

using Plotting, Contour

function Plotting.plot(c::Contour.ContourCollection)
    # Функциональность для построения здесь контура
end

end # модуль

Расширения могут иметь любое произвольное имя (здесь PlottingContourExt), но, скорее всего, хорошей идеей будет использовать нечто похожее на формат этого примера, что делает понятной расширенную функциональность и зависимость расширения.

Пользователь, зависящий только от Plotting, не будет платить за «расширение» внутри модуля PlottingContourExt. Расширение PlottingContourExt загружается и предоставляет новую функциональность только при фактической загрузке пакета Contour.

Если рассматривать PlottingContourExt как совершенно отдельный пакет, можно утверждать, что определение Plotting.plot(c::Contour.ContourCollection) является пиратством типов, поскольку расширению PlottingContourExt не принадлежит ни метод Plotting.plot, ни тип Contour.ContourCollection. Однако для расширений можно считать, что расширению принадлежат методы в его родительском пакете. На самом деле такая форма пиратства является одним из наиболее стандартных вариантов использования расширений.

Совместимость

Часто вы будете помещать зависимости расширения в целевой объект test, чтобы они загружались при выполнении, например, Pkg.test(). В более ранних версиях Julia для этого необходимо также поместить пакет в раздел [extras]. К сожалению, верификатор проекта в старых версиях Julia будет выдавать ошибку, если этого не сделать.

Если вы используете манифест, созданный версией Julia, которая не знает о расширениях, с версией Julia, которая знает о них, расширения не будут загружаться. Это происходит потому, что в манифесте отсутствует некоторая информация, указывающая Julia, когда следует загружать эти пакеты. Поэтому убедитесь, что вы применяете манифест, созданный по крайней мере для той версии Julia, которую используете.

Обратная совместимость

В этом разделе рассматриваются различные методы использования расширений в версиях Julia, которые их поддерживают, при одновременном предоставлении аналогичной функциональности в более старых версиях Julia.

Requires.jl

Этот раздел актуален, если в данный момент вы используете Requires.jl, но хотите перейти на применение расширений (при этом Requires по-прежнему будет использоваться в версиях Julia, которые не поддерживают расширения). Для этого нужно внести следующие изменения (на примере выше):

  • Добавьте в файл пакета следующий код. После этого Requires.jl загружает и вставляет обратный вызов только тогда, когда расширения не поддерживаются

    # Этот символ определен только в версиях Julia, поддерживающих расширения
    if !isdefined(Base, :get_extension)
    using Requires
    end
    
    @static if !isdefined(Base, :get_extension)
    function __init__()
        @require Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" include("../ext/PlottingContourExt.jl")
    end
    end

    или если в вашей функции __init__() есть другие компоненты:

    if !isdefined(Base, :get_extension)
    using Requires
    end
    
    function __init__()
        # Другие начальные функциональные возможности здесь
    
        @static if !isdefined(Base, :get_extension)
            @require Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" include("../ext/PlottingContourExt.jl")
        end
    end
  • Внесите следующее изменение в условно загруженный код:

    isdefined(Base, :get_extension) ? (using Contour) : (using ..Contour)
  • Добавьте Requires к [weakdeps] в файле Project.toml, чтобы он был указан и в [deps], и в [weakdeps]. Julia версии 1.9 и более поздних версий известно, что его не нужно устанавливать как обычную зависимость, в то время как более ранние версии считают его зависимостью.

Теперь пакет должен работать с Requires.jl в версиях Julia, которые существовали до появления расширений, и с расширениями в более новых версиях Julia.

Переход от обычной зависимости к расширению

Этот раздел актуален, если у вас есть обычная зависимость, которую вы хотите перевести в разряд расширений (при этом зависимость должна оставаться обычной зависимостью в версиях Julia, не поддерживающих расширения). Для этого нужно внести следующие изменения (на примере выше):

  • Убедитесь, что пакет находится и в разделе [deps], и в разделе [weakdeps]. Более новые версии Julia будут игнорировать зависимости в [deps], которые также находятся в [weakdeps].

  • Добавьте следующее в основной файл пакета (обычно в самом низу):

    if !isdefined(Base, :get_extension)
      include("../ext/PlottingContourExt.jl")
    end

Использование расширения при поддержке старых версий Julia

В случае если вы хотите использовать расширение (не беспокоясь о том, что оно будет доступно в старых версиях Julia) и при этом поддерживать старые версии Julia, пакеты в разделе [weakdeps] должны быть продублированы в раздел [extras]. Это досадное дублирование, но без этого верификатор проектов в старых версиях Julia будет выдавать ошибку, если найдет пакеты в разделе [compat], которые не указаны в разделе [extras].

Инструкции по именованию пакетов

Имена пакетов должны быть понятны большинству пользователей Julia, даже тем, кто не является экспертом в данной области. Следующие рекомендации относятся к реестру General, но могут быть полезны и для других реестров пакетов.

Поскольку реестр General принадлежит всему сообществу, у пользователей могут быть различные мнения по поводу имени вашего пакета, когда вы его опубликуете, особенно если оно неоднозначно или его можно спутать с чем-то другим, чем оно является на самом деле. Как правило, вам предложат новое название, которое лучше подойдет для вашего пакета.

  1. Старайтесь не использовать жаргон. В частности, используйте аббревиатуры, если только вероятность путаницы минимальна.

    • Можно сказать USA, если речь идет о США.

    • Не следует использовать PMA, даже если вы говорите о позитивном психологическом настрое (positive mental attitude).

  2. Старайтесь не использовать Julia в имени пакета или добавлять Ju в качестве префикса.

    • Из контекста и для ваших пользователей обычно ясно, что пакет — это пакет Julia.

    • Имена пакетов уже имеют расширение .jl, которое указывает пользователям, что Package.jl — это пакет Julia.

    • Наличие «Julia» в имени может означать, что пакет связан с самим языком Julia или одобрен его разработчиками.

  3. Пакеты, которые предоставляют большую часть своей функциональности в связи с новым типом, должны иметь имена во множественном числе.

    • DataFrames предоставляет тип DataFrame.

    • BloomFilters предоставляет тип BloomFilter.

    • Напротив, JuliaParser не предоставляет новый тип, а предоставляет новую функциональность в функции JuliaParser.parse().

  4. Выбирайте ясность, даже если имя кажется вам длинным.

    • RandomMatrices является менее двусмысленным именем, чем RndMat или RMT, хотя последние и короче.

  5. Менее систематическое имя может подойти пакету, который реализует один из нескольких возможных подходов в своей области.

    • В Julia нет ни одного всеобъемлющего пакета для построения графиков. Вместо этого Gadfly, PyPlot, Winston и другие пакеты реализуют уникальный подход, основанный на определенной философии разработки.

    • Напротив, SortingAlgorithms предоставляет последовательный интерфейс для использования многих хорошо известных алгоритмов сортировки.

  6. Пакеты, которые заключают в оболочку внешние библиотеки или программы, должны называться согласно этим библиотекам или программам.

    • CPLEX.jl заключает в оболочку библиотеку CPLEX, которую можно легко найти при поиске в Интернете.

    • MATLAB.jl предоставляет интерфейс для вызова подсистемы MATLAB из Julia.

  7. Старайтесь не использовать для пакета имя, близкое к имени существующего пакета.

    • Websocket — это имя, практически аналогичное WebSockets, что может запутать пользователей. Вместо него используйте новое имя, например SimpleWebsockets.

Регистрация пакетов

Когда пакет готов, его можно зарегистрировать в Общем реестре (см. также раздел вопросов и ответов). Сейчас для отправки пакетов используется Registrator. В дополнение к Registrator TagBot помогает управлять процессом применения тегов к выпускам.

Рекомендации

Пакеты должны избегать изменения своего состояния (записи в файлы внутри каталога пакета). Пакеты, как правило, не должны предполагать, что они находятся в доступном для записи месте (например, если они установлены как часть общесистемного хранилища) или даже в стабильном месте (например, если они включены в образ системы с помощью PackageCompiler.jl). Для поддержки различных случаев использования в экосистеме пакетов Julia разработчики Pkg создали ряд дополнительных пакетов и методов, помогающих авторам пакетов создавать изолированные, неизменяемые и перемещаемые пакеты:

  • Artifacts можно использовать для объединения фрагментов данных вместе с пакетом или даже для их загрузки по требованию. Рекомендуется использовать артефакты вместо попыток открыть файл по пути, например joinpath(@__DIR__, "data", "my_dataset.csv"), так как он является неперемещаемым. Если ваш пакет был предварительно скомпилирован, результат @__DIR__ будет включен в данные предварительно скомпилированного пакета, и если вы решите распространить этот пакет, он попытается загрузить файлы в неправильное расположение. Артефакты можно объединять и затем легко получать к ним доступ с помощью строкового макроса artifact"name".

  • Scratch.jl представляет понятие «вспомогательных пространств», изменяемых контейнеров данных для пакетов. Эти пространства предназначены для кэшей данных, которые полностью управляются пакетом и должны быть удалены при удалении самого пакета. Для важных данных, создаваемых пользователем, пакеты должны продолжать записывать данные по указанному пользователем пути, который не управляется Julia или Pkg.

  • Preferences.jl позволяет пакетам считывать и записывать предпочтения в файл Project.toml верхнего уровня. Эти предпочтения могут быть считаны во время выполнения или компиляции для включения или отключения различных аспектов поведения пакета. Раньше пакеты записывали файлы в собственные каталоги пакетов для записи параметров, заданных пользователем или средой, но с появлением Preferences делать это крайне не рекомендуется.