Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions +matnwb/+extension/+internal/isCatalogExtension.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function tf = isCatalogExtension(extensionNames)
% isCatalogExtension - Check whether extensions are in the extension catalog.
%
% tf = matnwb.extension.internal.isCatalogExtension(extensionNames)
% returns a logical array that is true for each name in extensionNames
% that is an extension in the Neurodata Extensions Catalog.
%
% matnwb.extension.CatalogExtension lists the catalog as of the last
% update of matnwb. An extension added to the catalog since then is
% found in the online catalog, which is read only when CatalogExtension
% lacks a name.

arguments
extensionNames string
end

tf = ismember(extensionNames, matnwb.extension.CatalogExtension.listNames());
if ~all(tf)
onlineCatalog = matnwb.extension.listExtensions();
tf = ismember(extensionNames, onlineCatalog.name);
end
end
11 changes: 8 additions & 3 deletions +matnwb/+extension/+internal/listPypiVersions.m
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
function versions = listPypiVersions(packageName)
function versions = listPypiVersions(packageName, options)
% listPypiVersions - List the released versions of a package on PyPI.
%
% versions = matnwb.extension.internal.listPypiVersions(packageName)
% returns the versions of the package as a string array, or an empty
% array when PyPI has no package with that name.
%
% versions = matnwb.extension.internal.listPypiVersions(packageName, Timeout=seconds)
% waits at most the given number of seconds for PyPI to answer. The
% default is 5 seconds.
%
% The JSON form of the PyPI simple index lists versions as an array.
% The release list of the JSON API is keyed by version, which webread
% would turn into struct field names.

arguments
packageName (1,1) string
options.Timeout (1,1) double {mustBePositive} = 5
end

simpleIndexUrl = sprintf("https://pypi.org/simple/%s/", packageName);
options = weboptions("ContentType", "json", ...
requestOptions = weboptions("ContentType", "json", "Timeout", options.Timeout, ...
"HeaderFields", ["Accept", "application/vnd.pypi.simple.v1+json"]);
try
index = webread(simpleIndexUrl, options);
index = webread(simpleIndexUrl, requestOptions);
versions = string(index.versions);
catch ME
if strcmp(ME.identifier, "MATLAB:webservices:HTTP404StatusCodeError")
Expand Down
42 changes: 42 additions & 0 deletions +matnwb/+extension/+internal/listVersionChoices.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function versions = listVersionChoices(extensionName)
% listVersionChoices - List the releases of an extension for code suggestions.
%
% versions = matnwb.extension.internal.listVersionChoices(extensionName)
% returns the released versions of an extension on PyPI as a cell array
% of character vectors. resources/functionSignatures.json uses it to
% suggest the version argument of nwbInstallExtension after the user has
% typed the extension name.
%
% MATLAB evaluates the function while the user types, so it never
% throws: it returns an empty cell array when extensionName is not one
% name or PyPI does not answer within two seconds. Results are kept for
% the MATLAB session, so each extension is looked up once.

% Suggestions wait for this function, so a slow answer would stall typing.
timeoutSeconds = 2;

persistent versionsByName
if isempty(versionsByName)
versionsByName = containers.Map();
end

versions = {};
if ~(ischar(extensionName) || (isstring(extensionName) && isscalar(extensionName)))
return
end
extensionName = char(extensionName);

if isKey(versionsByName, extensionName)
versions = versionsByName(extensionName);
return
end

try
versions = cellstr(matnwb.extension.internal.listPypiVersions( ...
extensionName, "Timeout", timeoutSeconds));
catch
% No network or no answer from PyPI: offer no suggestions this time.
return
end
versionsByName(extensionName) = versions;
end
70 changes: 70 additions & 0 deletions +matnwb/+extension/CatalogExtension.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
classdef CatalogExtension
% CatalogExtension - Extensions in the Neurodata Extensions Catalog.
%
% Each member is an extension listed in the Neurodata Extensions Catalog
% (https://nwb-extensions.github.io). The member name is the extension
% name as a MATLAB identifier, and the Name property holds the extension
% name, such as "ndx-miniscope".
%
% nwbInstallExtension accepts these names, and MATLAB suggests them when
% typing its first argument.
%
% This file is generated by tools/maintenance/matnwb_createNwbInstallExtension
% from resources/function_templates/CatalogExtension.txt. The
% update_extension_list workflow regenerates it from the catalog.
%
% Example:
% names = matnwb.extension.CatalogExtension.listNames()

properties (SetAccess = immutable)
% Name - Name of the extension in the catalog
Name (1,1) string
end

methods
function obj = CatalogExtension(name)
obj.Name = name;
end
end

methods (Static)
function names = listNames()
% listNames - List the names of the extensions in the catalog.
members = enumeration("matnwb.extension.CatalogExtension");
names = reshape([members.Name], 1, []);
end
end

enumeration
ndx_miniscope ("ndx-miniscope")
ndx_simulation_output ("ndx-simulation-output")
ndx_ecog ("ndx-ecog")
ndx_fret ("ndx-fret")
ndx_icephys_meta ("ndx-icephys-meta")
ndx_events ("ndx-events")
ndx_nirs ("ndx-nirs")
ndx_hierarchical_behavioral_data ("ndx-hierarchical-behavioral-data")
ndx_sound ("ndx-sound")
ndx_extract ("ndx-extract")
ndx_photometry ("ndx-photometry")
ndx_acquisition_module ("ndx-acquisition-module")
ndx_odor_metadata ("ndx-odor-metadata")
ndx_whisk ("ndx-whisk")
ndx_ecg ("ndx-ecg")
ndx_franklab_novela ("ndx-franklab-novela")
ndx_photostim ("ndx-photostim")
ndx_multichannel_volume ("ndx-multichannel-volume")
ndx_depth_moseq ("ndx-depth-moseq")
ndx_probeinterface ("ndx-probeinterface")
ndx_dbs ("ndx-dbs")
ndx_hed ("ndx-hed")
ndx_ophys_devices ("ndx-ophys-devices")
ndx_microscopy ("ndx-microscopy")
ndx_pose ("ndx-pose")
ndx_fiber_photometry ("ndx-fiber-photometry")
ndx_binned_spikes ("ndx-binned-spikes")
ndx_wearables ("ndx-wearables")
ndx_fscv ("ndx-fscv")
ndx_rate_maps ("ndx-rate-maps")
end
end
19 changes: 11 additions & 8 deletions +matnwb/+extension/installExtension.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ function installExtension(extensionName, version, options)
% matnwb.extension.installExtension(extensionName, version) installs the
% given release of the extension, for example "0.2.1".
%
% extensionName must be an extension in the catalog. It is checked
% against matnwb.extension.CatalogExtension, and against the online
% catalog when CatalogExtension does not list it, so an extension added
% to the catalog after this version of matnwb can be installed.
%
% Releases are downloaded from the Python Package Index (PyPI), where
% NWB extensions are published with their specification files. Downloads
% are kept in the "NWB-Extension-Source" folder in the user path.
Expand All @@ -28,14 +33,12 @@ function installExtension(extensionName, version, options)
downloadFolder = fullfile(userpath, "NWB-Extension-Source");
if ~isfolder(downloadFolder); mkdir(downloadFolder); end

T = matnwb.extension.listExtensions();
isMatch = T.name == extensionName;

extensionList = join( compose(" %s", [T.name]), newline );
assert( ...
any(isMatch), ...
'NWB:InstallExtension:ExtensionNotFound', ...
'Extension "%s" was not found in the extension catalog:\n', extensionList)
if ~matnwb.extension.internal.isCatalogExtension(extensionName)
catalogNames = matnwb.extension.CatalogExtension.listNames();
error('NWB:InstallExtension:ExtensionNotFound', ...
'Extension "%s" was not found in the extension catalog. Use one of:\n\n%s\n', ...
extensionName, join(compose(" %s", catalogNames), newline))
end

matnwb.extension.internal.installRelease( ...
extensionName, version, options.savedir, downloadFolder)
Expand Down
35 changes: 35 additions & 0 deletions +tests/+unit/InstallExtensionTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,41 @@ function testReplacingDependencyNamesInstalledDependents(testCase)
testCase.verifySubstring(output, 'may not work with version 0.2.0: ndx-fiber-photometry')
end

function testVersionChoicesListReleases(testCase)
versions = matnwb.extension.internal.listVersionChoices("ndx-miniscope");
testCase.verifyClass(versions, 'cell')
testCase.verifyTrue(ismember('0.2.2', versions))
end

function testVersionChoicesAreEmptyWithoutOneKnownName(testCase)
testCase.verifyEmpty(matnwb.extension.internal.listVersionChoices(["ndx-miniscope", "ndx-ecog"]))
testCase.verifyEmpty(matnwb.extension.internal.listVersionChoices("ndx-no-such-extension-exists"))
testCase.verifyEmpty(matnwb.extension.internal.listVersionChoices(42))
end

function testCatalogExtensionListsNames(testCase)
names = matnwb.extension.CatalogExtension.listNames();
testCase.verifyClass(names, 'string')
testCase.verifyTrue(ismember("ndx-miniscope", names))
testCase.verifyEqual(matnwb.extension.CatalogExtension.ndx_miniscope.Name, "ndx-miniscope")
end

function testUnknownExtensionNameFails(testCase)
testCase.verifyError(@() nwbInstallExtension("ndx-not-in-the-catalog"), ...
'NWB:InstallExtension:UnknownExtension')
end

function testIsCatalogExtension(testCase)
tf = matnwb.extension.internal.isCatalogExtension(["ndx-miniscope", "ndx-not-in-the-catalog"]);
testCase.verifyEqual(tf, [true, false])
end

function testInstallExtensionRejectsNameOutsideCatalog(testCase)
testCase.verifyError( ...
@() matnwb.extension.installExtension("ndx-not-in-the-catalog"), ...
'NWB:InstallExtension:ExtensionNotFound')
end

function testInstallExtensionWithoutWheel(testCase)
% ndx-ecg is published on PyPI as a source distribution only.
testCase.addTeardown(@() testCase.clearExtension("ndx-ecg"))
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/update_extension_list.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ jobs:
ref: refs/heads/main
ssh-key: ${{ secrets.DEPLOY_KEY }}

- name: Update extension list in nwbInstallExtensions
- name: Update extension list in nwbInstallExtension and CatalogExtension
uses: matlab-actions/run-command@v3
with:
command: |
addpath(genpath("tools"));
matnwb_createNwbInstallExtension();

- name: Commit the updated nwbInstallExtension function
- name: Commit the updated extension list
run: |
set -e # Exit script on error
git config user.name "${{ github.workflow }} by ${{ github.actor }}"
git config user.email "<>"

if [[ -n $(git status --porcelain nwbInstallExtension.m) ]]; then
git add nwbInstallExtension.m
if [[ -n $(git status --porcelain nwbInstallExtension.m +matnwb/+extension/CatalogExtension.m) ]]; then
git add nwbInstallExtension.m +matnwb/+extension/CatalogExtension.m
git commit -m "Update list of extensions in nwbInstallExtension"
git push
else
Expand Down
52 changes: 17 additions & 35 deletions nwbInstallExtension.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,45 +58,12 @@ function nwbInstallExtension(extensionNames, version, options)
% matnwb.extension.listExtensions, matnwb.extension.installExtension

arguments
extensionNames (1,:) string {mustBeMember(extensionNames, [...
"ndx-miniscope", ...
"ndx-simulation-output", ...
"ndx-ecog", ...
"ndx-fret", ...
"ndx-icephys-meta", ...
"ndx-events", ...
"ndx-nirs", ...
"ndx-hierarchical-behavioral-data", ...
"ndx-sound", ...
"ndx-extract", ...
"ndx-photometry", ...
"ndx-acquisition-module", ...
"ndx-odor-metadata", ...
"ndx-whisk", ...
"ndx-ecg", ...
"ndx-franklab-novela", ...
"ndx-photostim", ...
"ndx-multichannel-volume", ...
"ndx-depth-moseq", ...
"ndx-probeinterface", ...
"ndx-dbs", ...
"ndx-hed", ...
"ndx-ophys-devices", ...
"ndx-microscopy", ...
"ndx-pose", ...
"ndx-fiber-photometry", ...
"ndx-binned-spikes", ...
"ndx-wearables", ...
"ndx-fscv", ...
"ndx-rate-maps" ...
] ...
)} = []
extensionNames (1,:) string {mustBeCatalogExtension} = []
version (1,1) string = "latest"
options.savedir (1,1) string = misc.getMatnwbDir()
end
if isempty(extensionNames)
T = matnwb.extension.listExtensions();
extensionList = join( compose(" %s", [T.name]), newline );
extensionList = join( compose(" %s", matnwb.extension.CatalogExtension.listNames()), newline );
error('NWB:InstallExtension:MissingArgument', ...
'Please specify the name of an extension. Available extensions:\n\n%s\n', extensionList)
else
Expand All @@ -108,3 +75,18 @@ function nwbInstallExtension(extensionNames, version, options)
end
end
end

function mustBeCatalogExtension(extensionNames)
% mustBeCatalogExtension - Validate that names are extensions in the catalog.
%
% Names missing from matnwb.extension.CatalogExtension are looked up in
% the online catalog, so extensions added to the catalog after this
% version of matnwb are accepted.
unknownNames = extensionNames(~matnwb.extension.internal.isCatalogExtension(extensionNames));
if ~isempty(unknownNames)
catalogNames = matnwb.extension.CatalogExtension.listNames();
error('NWB:InstallExtension:UnknownExtension', ...
'Unknown extension: %s. Use one of the extensions in the catalog:\n\n%s\n', ...
strjoin(unknownNames, ', '), join(compose(" %s", catalogNames), newline))
end
end
9 changes: 9 additions & 0 deletions resources/functionSignatures.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@
}
]
},
"nwbInstallExtension":
{
"inputs":
[
{"name":"extensionNames", "kind":"required", "type":"choices=matnwb.extension.CatalogExtension.listNames()", "purpose":"Name of an extension in the Neurodata Extensions Catalog"},
{"name":"version", "kind":"ordered", "type":"choices=matnwb.extension.internal.listVersionChoices(extensionNames)", "purpose":"Release of the extension to install, such as \"0.2.1\""},
{"name":"savedir", "kind":"namevalue", "type":"folder", "purpose":"Output folder for generated classes"}
]
},
"nwbRead":
{
"inputs":
Expand Down
41 changes: 41 additions & 0 deletions resources/function_templates/CatalogExtension.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
classdef CatalogExtension
% CatalogExtension - Extensions in the Neurodata Extensions Catalog.
%
% Each member is an extension listed in the Neurodata Extensions Catalog
% (https://nwb-extensions.github.io). The member name is the extension
% name as a MATLAB identifier, and the Name property holds the extension
% name, such as "ndx-miniscope".
%
% nwbInstallExtension accepts these names, and MATLAB suggests them when
% typing its first argument.
%
% This file is generated by tools/maintenance/matnwb_createNwbInstallExtension
% from resources/function_templates/CatalogExtension.txt. The
% update_extension_list workflow regenerates it from the catalog.
%
% Example:
% names = matnwb.extension.CatalogExtension.listNames()

properties (SetAccess = immutable)
% Name - Name of the extension in the catalog
Name (1,1) string
end

methods
function obj = CatalogExtension(name)
obj.Name = name;
end
end

methods (Static)
function names = listNames()
% listNames - List the names of the extensions in the catalog.
members = enumeration("matnwb.extension.CatalogExtension");
names = reshape([members.Name], 1, []);
end
end

enumeration
{{enumerationMembers}}
end
end
Loading
Loading