diff --git a/.github/workflows/package-pydra.yml b/.github/workflows/package-pydra.yml new file mode 100644 index 0000000000..5c4ff9011b --- /dev/null +++ b/.github/workflows/package-pydra.yml @@ -0,0 +1,173 @@ +#This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +# For deployment, it will be necessary to create a PyPI API token and store it as a secret +# https://docs.github.com/en/actions/reference/encrypted-secrets + +name: Pydra + +on: + push: + branches: [ master ] + tags: [ '*' ] + pull_request: + branches: + - master + - dev + +jobs: + + build: + + runs-on: ubuntu-latest + + env: + CFLAGS: -Werror + QT_SELECT: qt6 + SCCACHE_GHA_ENABLED: "true" + SCCACHE_CACHE_SIZE: "2G" + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + + - name: Determine MRtrix3 version + run: echo "MRTRIX3_VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV + + - name: install dependencies + run: | + sudo apt-get update + sudo apt-get install clang qt6-base-dev libglvnd-dev libeigen3-dev zlib1g-dev libfftw3-dev ninja-build + + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + + - name: Get CMake + uses: lukka/get-cmake@latest + with: + cmakeVersion: '3.16.3' + + - name: Print CMake version + run: cmake --version + + - name: configure + run: > + cmake + -B build + -G Ninja + -D CMAKE_BUILD_TYPE=Release + -D MRTRIX_BUILD_TESTS=ON + -D MRTRIX_STL_DEBUGGING=ON + -D MRTRIX_WARNINGS_AS_ERRORS=ON + -D CMAKE_C_COMPILER=clang + -D CMAKE_CXX_COMPILER=clang++ + -D CMAKE_INSTALL_PREFIX=$(pwd)/install + + - name: Build Mrtrix + run: cmake --build build + + - name: Install Mrtrix + run: cmake --install build + + - name: Set PATH Variable + run: echo "PATH=$PATH:$(pwd)/install/bin" >> $GITHUB_ENV + + - name: Set LD_LIBRARY_PATH Variable + run: echo "LD_LIBRARY_PATH=$(pwd)/install/lib" >> $GITHUB_ENV + + - name: Set up Python + uses: actions/setup-python@v2 + + - name: Install Python build dependencies + run: | + python -m pip install --upgrade pip + + - name: Clone Pydra-MRtrix3 package + run: git clone https://github.com/nipype/pydra-mrtrix3.git pydra + + - name: Install pydra-auto-gen requirements + run: | + pip install -e ./pydra/fileformats-medimage-mrtrix3 + pip install -e ./pydra/fileformats-medimage-mrtrix3-extras + pip install -e ./pydra[dev] + + - name: Generate task specifications + run: > + ./pydra/generate.py + $(pwd)/install/bin + $(pwd)/pydra/src + $MRTRIX3_VERSION + --log-errors + --latest + + - name: Upload MRtrix3 install + uses: actions/upload-artifact@v2 + with: + name: MRtrix3 + path: mrtrix3/install + + - name: Upload auto-gen pydra + uses: actions/upload-artifact@v2 + with: + name: AutoGen + path: pydra/tasks/mrtrix3/$MRTRIX3_VERSION + + - name: Write version file + run: echo $MRTRIX3_VERSION > mrtrix3_version.txt + + - name: Upload version file + uses: actions/upload-artifact@v2 + with: + name: VersionFile + path: pydra/tasks/mrtrix3/$MRTRIX3_VERSION + + test: + needs: [build] + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, 3.11] + defaults: + run: + shell: bash -l {0} + steps: + + - name: Download version file + uses: actions/download-artifact@v2 + with: + name: VersionFile + path: mrtrix3_version.txt + + - name: Extract Mrtrix version + run: echo "MRTRIX3_VERSION=$(cat mrtrix3_version.txt)" >> $GITHUB_ENV + + - uses: actions/checkout@v2 + - name: Install Minconda + uses: conda-incubator/setup-miniconda@v2 + with: + auto-activate-base: true + activate-environment: "" + - name: Install MRtrix via Conda + run: | + conda install -c mrtrix3 mrtrix3 + mrconvert --version + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + - name: Install task package + run: | + pip install pydra/fileformats-medimage-mrtrix pydra/fileformats-medimage-mrtrix-extras '.[test]' + python -c "import pydra.tasks.mrtrix3 as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" + python -c "import pydra as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" + - name: Test with pytest + run: | + pytest -sv --doctest-modules pydra/tasks/mrtrix3 \ + --cov pydra.tasks.mrtrix3 --cov-report xml + - uses: codecov/codecov-action@v1 + if: ${{ always() }} + diff --git a/.gitignore b/.gitignore index 58b50c3e38..e92902dbde 100644 --- a/.gitignore +++ b/.gitignore @@ -11,12 +11,22 @@ *.img *.hdr *.rste -/python/lib/mrtrix3/_version.py +*.venv +config +build.* +/configure.log +/build.log +/core/version.cpp +/src/exec_version.cpp +/src/project_version.h +/lib/mrtrix3/_version.py +/test/src/project_version.h /scripts/mrtrix_bash_completion /dev/ /compiled_docs/ /.vscode/ .cproject +.DS_store .idea .project .settings @@ -32,4 +42,12 @@ .check_syntax.tmp .check_syntax2.tmp build/ -CMakeLists.txt.user \ No newline at end of file +CMakeLists.txt.user +*.venv +__pycache__ +.DS_Store +_version.py +/interfaces/pydra/src/pydra/tasks/mrtrix3/v3_0 +/interfaces/pydra/**/dist/** +.pytest_cache + diff --git a/cmd/amp2response.cpp b/cmd/amp2response.cpp index 30ef5bac9c..07fbc36532 100644 --- a/cmd/amp2response.cpp +++ b/cmd/amp2response.cpp @@ -57,7 +57,7 @@ void usage() { ARGUMENTS + Argument ("amps", "the amplitudes image").type_image_in() + Argument ("mask", "the mask containing the voxels from which to estimate the response function").type_image_in() - + Argument ("directions", "a 4D image containing the estimated fibre directions").type_image_in() + + Argument ("directions_image", "a 4D image containing the estimated fibre directions").type_image_in() + Argument ("response", "the output zonal spherical harmonic coefficients").type_file_out(); OPTIONS diff --git a/core/app.cpp b/core/app.cpp index a4e9b811a3..d624b6e698 100644 --- a/core/app.cpp +++ b/core/app.cpp @@ -794,6 +794,404 @@ std::string restructured_text_usage() { return s; } +std::string pydra_usage() { + + std::string CMD_PREFIXES[] = {"Fivett", "Afd", "Amp", "Connectome", "Dcm", "Dir", "Dwi", "Fixel", + "Fod", "Label", "Mask", "Mesh", "Mr", "Mt", "Peaks", "Sh", + "Tck", "Tensor", "Transform", "Tsf", "Voxel", "Vector", "Warp"}; + + auto convert_to_pascal_case = [&](const std::string &input) { + std::string result; + bool capitalizeNext = true; + for (char c : input) { + if (!result.size() && c == '5') { + result += "Five"; // handle 5tt prefixes so they don't create invalid Python identifiers + capitalizeNext = false; + } else if (std::isalpha(c) && capitalizeNext) { + result += std::toupper(c); + capitalizeNext = false; + } else { + result += c; + if (c == '2') + capitalizeNext = true; + for (const std::string &prefix : CMD_PREFIXES) { + if (result == prefix) { + capitalizeNext = true; + break; + } + } + } + } + return result; + }; + + std::string name_string = convert_to_pascal_case(NAME); + // Check whether name starts with 5tt and escape the name if so + + if (name_string.length() > 3) { + std::string prefix = name_string.substr(0, 3); + if (!prefix.compare("5tt")) { + name_string = "Fivett" + name_string.substr(3, name_string.length()); + } + } + + std::string base_indent(" "); + std::string indent = base_indent + " "; + std::string md_indent = indent + " "; + + std::string PYTHON_KEYWORDS[] = { + "and", "as", "assert", "break", "class", "continue", "def", "del", "elif", + "else", "except", "False", "finally", "for", "from", "global", "if", "import", + "in", "is", "lambda", "None", "nonlocal", "not", "or", "pass", "raise", + "return", "True", "try", "while", "with", "yield", "container", "image", "container_xargs"}; + + // Add import lines + std::string s = + std::string("# Auto-generated from MRtrix C++ command with '__print_usage_pydra__' secret option\n\n"); + s += "import typing as ty \n"; + s += "from pathlib import Path # noqa: F401\n"; + s += "from fileformats.generic import File, Directory # noqa: F401\n"; + s += "from fileformats.medimage_mrtrix3 import ImageIn, ImageOut, Tracks # noqa: F401\n"; + s += "from pydra.engine import specs, ShellCommandTask\n"; + + auto escape_id = [&](const std::string &id) { + std::string escaped = id; + // Replace any spaces and periods with underscores + std::replace(escaped.begin(), escaped.end(), ' ', '_'); + std::replace(escaped.begin(), escaped.end(), '.', '_'); + // Append any Python keywords with an underscore + bool is_keyword = std::any_of(std::begin(PYTHON_KEYWORDS), + std::end(PYTHON_KEYWORDS), + [&id](const std::string &kword) { return kword == id; }); + if (is_keyword) + escaped += "_"; + + return escaped; + }; + + auto format_type = [&](const ArgType &type, bool for_output = false) { + switch (type) { + case Undefined: + return "ty.Any"; + case Text: + return "str"; + case Boolean: + return "bool"; + case Integer: + return "int"; + case Float: + return "float"; + case ArgFileIn: + return "File"; + case ArgFileOut: + if (for_output) + return "File"; + else + return "Path"; + case ArgDirectoryIn: + return "Directory"; + case ArgDirectoryOut: + if (for_output) + return "Directory"; + else + return "Path"; + case Choice: + return "str"; + case ImageIn: + return "ImageIn"; + case ImageOut: + if (for_output) + return "ImageOut"; + else + return "Path"; + case IntSeq: + return "ty.List[int]"; + case FloatSeq: + return "ty.List[float]"; + case TracksIn: + return "Tracks"; + case TracksOut: + return "Tracks"; + case Various: + return "ty.Any"; + default: + assert(0); + } + return "not-reached"; + }; + + auto format_option_type = [&](const Option &opt, bool for_output = false) { + std::string f; + bool is_multi = (opt.flags & AllowMultiple) && (!opt.size() || opt[0].type != ArgFileOut); + if (is_multi) { + f += "specs.MultiInputObj["; + } + if (!opt.size()) { + f += "bool"; + } else if (opt.size() == 1) { + f += format_type(opt[0].type, for_output); + } else { + f += "ty.Tuple["; + for (size_t a = 0; a < opt.size(); ++a) { + f += format_type(opt[0].type, for_output); + if (a != opt.size() - 1) { + f += ", "; + } + } + f += "]"; + } + if (is_multi) { + f += "]"; + } + return f; + }; + + auto format_choices = [&](const Argument &arg) { + std::string f = md_indent + "\"allowed_values\": ["; + const char *const *choices = arg.limits.choices; + f += std::string("\"") + choices[0] + "\""; + for (int i = 0; choices[i]; ++i) { + f += std::string(", \"") + choices[i] + "\""; + } + f += "],\n"; + return f; + }; + + auto format_output_template = [&](const std::string &id, const ArgType &type) { + std::string tmpl(id); + if (type == ImageOut) { + tmpl += ".mif"; + } else if (type == TracksOut) { + tmpl += ".tck"; + } else if (type == ArgFileOut) { + tmpl += ".txt"; + } + // TODO: Add special cases for file-out based on the 'id' where the extension + // is something else. + return tmpl; + }; + + auto format_output_templates = [&](const std::string &id, const Option &opt) { + if (opt.size() == 1) + return "\"" + format_output_template(id, opt[0].type) + "\""; + std::string tmpl = "("; + for (size_t i = 0; i < opt.size(); ++i) + tmpl += "\"" + format_output_template(id + MR::str(i), opt[i].type) + "\","; + tmpl += ")"; + return tmpl; + }; + + auto format_option = [&](const Option &opt) { + std::string f = base_indent + "(\n"; + // Print name of field + f += indent + "\"" + escape_id(opt.id) + "\",\n"; + bool is_output_file = false; + std::string type_string = format_option_type(opt); + bool is_multi = type_string.length() > 19 && type_string.substr(0, 19) == "specs.MultiInputObj"; + if (opt.size() && (opt[0].type == ImageOut || opt[0].type == ArgFileOut || opt[0].type == ArgDirectoryOut)) { + is_output_file = true; + if (!is_multi) + type_string = "ty.Union[" + type_string + ", bool]"; + } + // Print type + f += indent + type_string + ",\n"; + if (is_output_file && !is_multi) + f += indent + "False,\n"; + f += indent + "{\n"; + // Print metadata fields + f += md_indent + "\"argstr\": \"-" + opt.id + "\",\n"; + if (is_output_file) { + f += md_indent + "\"output_file_template\": " + format_output_templates(escape_id(opt.id), opt) + ",\n"; + } + f += md_indent + "\"help_string\": \"\"\"" + opt.desc + "\"\"\",\n"; + if (!(opt.flags & Optional) && !is_output_file) { + f += md_indent + "\"mandatory\": True,\n"; + } + if (opt.size() == 1 && (opt[0].type == IntSeq || opt[0].type == FloatSeq)) { + f += md_indent + "\"sep\": \",\",\n"; + } + if (opt.size() == 1 && opt[0].type == Choice) { + f += format_choices(opt[0]); + } + f += indent + "},\n" + base_indent + "),\n"; + return f; + }; + + auto format_arg_name = [&](const Argument &arg) { + std::string id = arg.id; + std::string arg_name; + if (id == "input" && (arg.type == ImageIn || arg.type == ArgFileIn)) + arg_name = "in_file"; + else if (id == "input" && arg.type == ArgDirectoryIn) + arg_name = "in_dir"; + else if (id == "output" && (arg.type == ImageOut || arg.type == ArgFileOut)) + arg_name = "out_file"; + else if (id == "output" && arg.type == ArgDirectoryOut) + arg_name = "out_dir"; + else + arg_name = escape_id(arg.id); + return arg_name; + }; + + // Print out input spec + s += "\n\ninput_fields = [\n\n" + base_indent + "# Arguments\n"; + for (size_t i = 0; i < ARGUMENTS.size(); ++i) { + + bool is_multi = (ARGUMENTS[i].flags & AllowMultiple) && (ARGUMENTS[i].type != ArgFileOut); + s += base_indent + "(\n"; + // Print name of field + std::string arg_name = format_arg_name(ARGUMENTS[i]); + s += indent + "\"" + arg_name + "\",\n"; + // Print type + s += indent; + if (is_multi) { + s += "specs.MultiInputObj["; + } + s += format_type(ARGUMENTS[i].type); + if (is_multi) { + s += "]"; + } + s += +",\n" + indent + "{\n"; + // Print metadata fields + s += md_indent + "\"argstr\": \"\",\n"; + s += md_indent + "\"position\": " + std::to_string(i) + ",\n"; + bool output_type = false; + if (ARGUMENTS[i].type == ImageOut || ARGUMENTS[i].type == ArgFileOut || ARGUMENTS[i].type == ArgDirectoryOut) { + s += md_indent + "\"output_file_template\": \"" + format_output_template(arg_name, ARGUMENTS[i].type) + "\",\n"; + output_type = true; + } + s += md_indent + "\"help_string\": \"\"\"" + ARGUMENTS[i].desc + "\"\"\",\n"; + if (!(ARGUMENTS[i].flags & Optional) && !output_type) { + s += md_indent + "\"mandatory\": True,\n"; + } + if (ARGUMENTS[i].type == Choice) { + s += format_choices(ARGUMENTS[i]); + } + s += indent + "},\n" + base_indent + "),\n"; + } + + std::vector group_names; + for (size_t i = 0; i < OPTIONS.size(); ++i) { + if (std::find(group_names.begin(), group_names.end(), OPTIONS[i].name) == group_names.end()) + group_names.push_back(OPTIONS[i].name); + } + for (size_t i = 0; i < group_names.size(); ++i) { + size_t n = i; + while (OPTIONS[n].name != group_names[i]) + ++n; + if (OPTIONS[n].name != std::string("OPTIONS")) + s += std::string("\n") + base_indent + "# " + OPTIONS[n].name + " Option Group\n"; + while (n < OPTIONS.size()) { + if (OPTIONS[n].name == group_names[i]) { + for (size_t o = 0; o < OPTIONS[n].size(); ++o) { + s += format_option(OPTIONS[n][o]); + } + } + ++n; + } + } + + s += "\n" + base_indent + "# Standard options\n"; + for (size_t i = 0; i < __standard_options.size(); ++i) + s += format_option(__standard_options[i]); + + s += "]\n\n"; + + s += name_string + "InputSpec = specs.SpecInfo(name='" + name_string + + "Input', fields=input_fields, bases=(specs.ShellSpec,))\n\n\n"; + + s += "output_fields = [\n"; + + for (size_t i = 0; i < ARGUMENTS.size(); ++i) { + if (ARGUMENTS[i].type == ImageOut || ARGUMENTS[i].type == ArgFileOut || ARGUMENTS[i].type == ArgDirectoryOut) { + bool is_multi = ARGUMENTS[i].flags & AllowMultiple; + s += base_indent + "(\n"; + // Print name of field + s += indent + "\"" + format_arg_name(ARGUMENTS[i]) + "\",\n"; + // Print type + std::string type_string; + if (is_multi) + type_string += "ty.List["; + type_string += format_type(ARGUMENTS[i].type, true); + if (is_multi) + type_string += "]"; + s += indent + type_string + ",\n"; + s += indent + "{\n"; + s += md_indent + "\"help_string\": \"\"\"" + ARGUMENTS[i].desc + "\"\"\",\n"; + s += indent + "},\n" + base_indent + "),\n"; + } + } + + for (size_t i = 0; i < group_names.size(); ++i) { + size_t n = i; + while (OPTIONS[n].name != group_names[i]) + ++n; + while (n < OPTIONS.size()) { + if (OPTIONS[n].name == group_names[i]) { + for (size_t o = 0; o < OPTIONS[n].size(); ++o) { + bool is_output_file = false; + for (size_t j = 0; j < OPTIONS[n][o].size(); ++j) { + if (OPTIONS[n][o][j].type == ImageOut || OPTIONS[n][o][j].type == ArgFileOut || + OPTIONS[n][o][j].type == ArgDirectoryOut) { + is_output_file = true; + break; + } + } + if (is_output_file) { + s += base_indent + "(\n"; + // Print name of field + s += indent + "\"" + OPTIONS[n][o].id + "\",\n"; + // Print type + s += indent + format_option_type(OPTIONS[n][o], true) + ",\n"; + s += indent + "{\n"; + s += md_indent + "\"help_string\": \"\"\"" + OPTIONS[n][o].desc + "\"\"\",\n"; + s += indent + "},\n" + base_indent + "),\n"; + } + } + } + ++n; + } + } + + s += "]\n"; + s += name_string + "OutputSpec = specs.SpecInfo(name='" + name_string + + "Output', fields=output_fields, bases=(specs.ShellOutSpec,))\n\n\n"; + + // Create actual class + s += "class " + name_string + "(ShellCommandTask):\n"; + s += " \"\"\""; + // Add description + if (DESCRIPTION.size()) { + for (size_t i = 0; i < DESCRIPTION.size(); ++i) + s += base_indent + std::string(DESCRIPTION[i]) + "\n\n"; + } + + if (EXAMPLES.size()) { + s += "\n" + base_indent + "Example usages\n" + base_indent + "--------------\n\n"; + for (size_t i = 0; i < EXAMPLES.size(); ++i) { + s += "\n" + base_indent + EXAMPLES[i].title + ":\n\n"; + s += indent + "`$ " + EXAMPLES[i].code + "`\n\n"; + if (EXAMPLES[i].description.size()) + s += indent + EXAMPLES[i].description + "\n"; + s += "\n"; + } + } + + s += "\n" + base_indent + "References\n" + base_indent + "----------\n\n"; + for (size_t i = 0; i < REFERENCES.size(); ++i) + s += indent + REFERENCES[i] + "\n\n"; + s += indent + MRTRIX_CORE_REFERENCE + "\n\n"; + + s += "\n" + base_indent + "MRtrix\n" + base_indent + "------" + "\n\n" + indent + "Version:" + mrtrix_version + + ", built " + build_date + "\n\n" + indent + "Author: " + AUTHOR + "\n\n" + indent + "Copyright: " + COPYRIGHT; + s += " \"\"\"\n"; + s += " executable = \"" + name_string + "\"\n"; + s += " input_spec = " + name_string + "InputSpec\n"; + s += " output_spec = " + name_string + "OutputSpec\n\n"; + + return s; +} + const Option *match_option(const char *arg) { if (consume_dash(arg) && *arg && !isdigit(*arg) && *arg != '.') { while (consume_dash(arg)) @@ -888,6 +1286,10 @@ void parse_special_options() { print(restructured_text_usage()); throw 0; } + if (strcmp(argv[1], "__print_usage_pydra__") == 0) { + print(pydra_usage()); + throw 0; + } if (strcmp(argv[1], "__print_synopsis__") == 0) { print(SYNOPSIS); throw 0; diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 7f64a031df..a555fba24d 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -13,9 +13,18 @@ # # For more details, see http://www.mrtrix.org/. -import argparse, inspect, math, os, random, shlex, shutil, signal, string, subprocess, sys, textwrap, time +import argparse, inspect, math, os, random, shlex, shutil, signal, string, subprocess, sys, textwrap, time, re +import typing as ty +from keyword import kwlist as PYTHON_KEYWORDS +try: + import black.parsing +except ImportError: + HAVE_BLACK = False +else: + HAVE_BLACK = True from mrtrix3 import ANSI, CONFIG, MRtrixError, setup_ansi from mrtrix3 import utils # Needed at global level + from ._version import __version__ @@ -144,6 +153,9 @@ def _execute(module): #pylint: disable=unused-variable elif sys.argv[-1] == '__print_usage_rst__': CMDLINE.print_usage_rst() sys.exit(0) + elif sys.argv[-1] == '__print_usage_pydra__': + CMDLINE.print_usage_pydra() + sys.exit(0) # Do the main command-line input parsing ARGS = CMDLINE.parse_args() @@ -1093,6 +1105,188 @@ def print_group_options(group): for alg in self._subparsers._group_actions[0].choices: subprocess.call ([ sys.executable, os.path.realpath(sys.argv[0]), alg, '__print_usage_rst__' ]) + + def print_usage_pydra(self): + + if self._subparsers: + + if len(sys.argv) == 3: + for alg in self._subparsers._group_actions[0].choices: + if alg == sys.argv[-2]: + self._subparsers._group_actions[0].choices[alg].print_usage_pydra() + return + self.error('Invalid subparser nominated: ' + sys.argv[-2]) + assert len(sys.argv) == 2 + sys.stdout.write(",".join(self._subparsers._group_actions[0].choices)) + sys.stdout.flush() + return + + def get_arg_metadata(arg): + metadata = { + "help_string": arg.help, + } + if arg.choices: + metadata["allowed_values"] = list(arg.choices) + if arg.required: + metadata["mandatory"] = True + return metadata + + def parse_type(type_): + if type_: + return f"#{type_.__name__}#" + return ty.Any + + def escape_id(id_: str) -> str: + if id_ == "input": + escaped = "in_file" + elif id_ == "output": + escaped = "out_file" + elif id_ in list(PYTHON_KEYWORDS) + ["container", "image", "container_xargs"]: + escaped = id_ + "_" + else: + escaped = id_ + escaped = escaped.replace(".", '_') + return escaped + + inputs = [] + input_names = [a.dest for a in self._positionals._group_actions] + output_names = [] + for pos, arg in enumerate(self._positionals._group_actions): + metadata = get_arg_metadata(arg) + metadata["position"] = pos + metadata["argstr"] = "" + arg_id = escape_id(arg.dest) + if arg.type: + type_ = parse_type(arg.type) + elif arg_id == "input": + type_ = "#FsObject#" + elif arg_id == "output": + type_ = "#Path#" + output_names.append(arg_id) + else: + type_ = ty.Any + if arg_id == "output" and "input" in input_names: + metadata["output_file_template"] = "output_{input}" + metadata.pop("mandatory", None) + inputs.append( + ( + arg_id, + type_, + metadata, + ) + ) + for group in reversed(self._action_groups): + for option in group._group_actions: + if option.dest in input_names: + continue + if isinstance(option, argparse._StoreTrueAction): + type_ = "#bool#" + else: + type_ = parse_type(option.type) + if isinstance(option, argparse._AppendAction): + type_ = f"#specs.MultiInputObj[{type_}]#" + metadata = get_arg_metadata(option) + metadata["argstr"] = "-" + option.dest + inputs.append( + ( + escape_id(option.dest), + type_, + metadata, + ) + ) + # Replace # escapes + inputs_str = re.sub(r"'#([a-zA-Z0-9\._\[\]]+)#'", r"\1", str(inputs)) + + outputs = [] + for arg in self._positionals._group_actions: + arg_id = escape_id(arg.dest) + if arg_id in output_names: + metadata = get_arg_metadata(arg) + if arg.type: + type_ = arg.type + else: + type_ = "#FsObject#" + outputs.append(( + ( + arg_id, + type_, + metadata, + ) + ) + ) + outputs_str = re.sub(r"'#([a-zA-Z0-9_\[\]]+)#'", r"\1", str(outputs)) + + def cmd_to_task_name(cmd_name: str) -> str: + """Get Task class name from cmd name""" + if cmd_name == "population_template": + return "PopulationTemplate" + task_name = cmd_name.replace(" ", "_") + if task_name[0] == "5": + task_name = "five" + task_name[1:] + cmd_prefixes = [ + "fivett", "afd", "amp", "connectome", "dcm", "dir", "dwi", + "fixel", "fod", "label", "mask", "mesh", "mr", "mt", + "peaks", "response", "sh", "tck", "transform", "tsf", "voxel", "vector" + ] + # convert to PascalCase + task_name = "".join( + g.capitalize() + for g in re.match(rf"({'|'.join(cmd_prefixes)})(2?)([^_]+)(_?)(.*)", task_name).groups() + ) + return task_name + + task_name = cmd_to_task_name(self.prog) + + text = ( + "# Auto-generated by mrtrix3/app.py:print_usage_pydra()\n\n" + "import typing\n" + "from pathlib import Path # noqa: F401\n" + "from fileformats.generic import FsObject, File, Directory # noqa: F401\n" + "from fileformats.medimage_mrtrix3 import Tracks, ImageIn, ImageOut # noqa: F401\n" + "from pydra.engine.task import ShellCommandTask \n" + "from pydra.engine import specs\n" + ) + + text += f"input_fields = {inputs_str}\n" + text += f"{task_name}InputSpec = specs.SpecInfo(name='{task_name}Input', fields=input_fields, bases=(specs.ShellSpec,))\n\n" + text += f"output_fields = {outputs_str}\n" + text += f"{task_name}OutputSpec = specs.SpecInfo(name='{task_name}Output', fields=output_fields, bases=(specs.ShellOutSpec,))\n\n" + text += f"class {task_name}(ShellCommandTask):\n" + indent = " " + text += indent + "\"\"\"\n" + text += indent + (self.description if self.description else "") + text += indent + "References\n" + text += indent + "----------\n\n" + for ref in self._citation_list: + ref_text = indent + "* " + if ref[0]: + ref_text += ref[0] + ': ' + ref_text += ref[1] + text += ref_text + '\n\n' + text += indent + _MRTRIX3_CORE_REFERENCE + '\n\n' + text += indent + '--------------\n\n\n\n' + text += indent + '**Author:** ' + self._author + '\n\n' + text += indent + '**Copyright:** ' + self._copyright + '\n\n' + text += indent + "\"\"\"\n" + text += f" input_spec = {task_name}InputSpec\n" + text += f" output_spec = {task_name}OutputSpec\n" + if " " in self.prog: + executable = tuple(self.prog.split(" ")) + else: + executable = self.prog + text += f" executable={executable!r}\n\n" + + if HAVE_BLACK: + try: + text = black.format_file_contents( + text, fast=False, mode=black.FileMode() + ) + except black.parsing.InvalidInput: + pass + sys.stdout.write(text) + sys.stdout.flush() + + def print_version(self): text = '== ' + self.prog + ' ' + (self._git_version if self._is_project else __version__) + ' ==\n' if self._is_project: