diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28a78a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.vsix diff --git a/extension.js b/extension.js index deb44f5..88a7dcd 100644 --- a/extension.js +++ b/extension.js @@ -1,5 +1,6 @@ const vscode = require('vscode'); const path = require('path'); +const { createDecoratorHoverProvider } = require('./src/hoverProvider'); let sharedTerminal = null; @@ -67,7 +68,13 @@ function activate(context) { () => runPythonCommand('spin_func') ); - context.subscriptions.push(runCmd, spinCmd); + // Register the hover provider for Metaflow decorators in Python files + const hoverProvider = vscode.languages.registerHoverProvider( + { language: 'python', scheme: 'file' }, + createDecoratorHoverProvider() + ); + + context.subscriptions.push(runCmd, spinCmd, hoverProvider); } function deactivate() { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6f71cf4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1190 @@ +{ + "name": "metaflow-dev", + "version": "0.0.7", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "metaflow-dev", + "version": "0.0.7", + "devDependencies": { + "mocha": "^11.7.5" + }, + "engines": { + "vscode": "^1.80.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 31a7c2f..8017bec 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,11 @@ "displayName": "Run and Spin Metaflow", "version": "0.0.7", "publisher": "local", - "engines": { "vscode": "^1.80.0" }, + "engines": { + "vscode": "^1.80.0" + }, "activationEvents": [ + "onLanguage:python", "onCommand:extension.runPythonFunction", "onCommand:extension.spinPythonFunction" ], @@ -36,5 +39,11 @@ "when": "editorLangId == python" } ] + }, + "scripts": { + "test": "mocha test/**/*.test.js" + }, + "devDependencies": { + "mocha": "^11.7.5" } } diff --git a/src/decorators.js b/src/decorators.js new file mode 100644 index 0000000..6e70015 --- /dev/null +++ b/src/decorators.js @@ -0,0 +1,240 @@ +/** + * Metaflow decorator metadata for hover documentation. + * + * Each entry contains: + * - description: short summary of what the decorator does + * - params: array of { name, type, default, description } + * - docUrl: link to the official Metaflow docs page + * - level: "step" or "flow" + */ +const DECORATOR_INFO = { + step: { + description: 'Marks a method in a FlowSpec as a Metaflow Step. Must be placed closest to the method (before other decorators).', + params: [ + { name: 'f', type: 'Callable', default: '—', description: 'Function to make into a Metaflow Step.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/step', + level: 'step' + }, + + retry: { + description: 'Specifies the number of times the task corresponding to a step needs to be retried. Useful for handling transient errors such as networking issues.', + params: [ + { name: 'times', type: 'int', default: '3', description: 'Number of times to retry this task.' }, + { name: 'minutes_between_retries', type: 'int', default: '2', description: 'Number of minutes between retries.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/retry', + level: 'step' + }, + + catch: { + description: 'Specifies that the step will succeed under all circumstances. Creates an optional artifact containing the caught exception.', + params: [ + { name: 'var', type: 'str', default: 'None', description: 'Name of the artifact in which to store the caught exception.' }, + { name: 'print_exception', type: 'bool', default: 'True', description: 'Determines whether the exception is printed to stdout when caught.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/catch', + level: 'step' + }, + + timeout: { + description: 'Specifies a timeout for your step. Useful if the step may hang indefinitely. All values are added together.', + params: [ + { name: 'seconds', type: 'int', default: '0', description: 'Number of seconds to wait prior to timing out.' }, + { name: 'minutes', type: 'int', default: '0', description: 'Number of minutes to wait prior to timing out.' }, + { name: 'hours', type: 'int', default: '0', description: 'Number of hours to wait prior to timing out.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/timeout', + level: 'step' + }, + + resources: { + description: 'Specifies the resources needed when executing this step. Use with @batch or @kubernetes to specify requirements independently of the compute layer.', + params: [ + { name: 'cpu', type: 'int', default: '1', description: 'Number of CPUs required for this step.' }, + { name: 'gpu', type: 'int', default: 'None', description: 'Number of GPUs required for this step.' }, + { name: 'memory', type: 'int', default: '4096', description: 'Memory size (in MB) required for this step.' }, + { name: 'disk', type: 'int', default: 'None', description: 'Disk size (in MB) required for this step. Only applies on Kubernetes.' }, + { name: 'shared_memory', type: 'int', default: 'None', description: 'Size (in MiB) of the /dev/shm volume for this step.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/resources', + level: 'step' + }, + + environment: { + description: 'Specifies environment variables to be set prior to the execution of a step.', + params: [ + { name: 'vars', type: 'Dict[str, str]', default: '{}', description: 'Dictionary of environment variables to set.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/environment', + level: 'step' + }, + + card: { + description: 'Creates a human-readable report (a Metaflow Card) after this step completes. Multiple @card decorators can be added to a step.', + params: [ + { name: 'type', type: 'str', default: "'default'", description: 'Card type.' }, + { name: 'id', type: 'str', default: 'None', description: 'If multiple cards are present, use this id to identify this card.' }, + { name: 'options', type: 'Dict[str, Any]', default: '{}', description: 'Options passed to the card. Contents depend on the card type.' }, + { name: 'timeout', type: 'int', default: '45', description: 'Interrupt reporting if it takes more than this many seconds.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/card', + level: 'step' + }, + + secrets: { + description: 'Specifies secrets to be retrieved and injected as environment variables prior to the execution of a step.', + params: [ + { name: 'sources', type: 'List[Union[str, Dict]]', default: '[]', description: 'List of secret specs, defining how the secrets are to be retrieved.' }, + { name: 'role', type: 'str', default: 'None', description: 'Role to use for fetching secrets.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/secrets', + level: 'step' + }, + + kubernetes: { + description: 'Specifies that this step should execute on Kubernetes.', + params: [ + { name: 'cpu', type: 'int', default: '1', description: 'Number of CPUs required for this step.' }, + { name: 'memory', type: 'int', default: '4096', description: 'Memory size (in MB) required for this step.' }, + { name: 'disk', type: 'int', default: '10240', description: 'Disk size (in MB) required for this step.' }, + { name: 'image', type: 'str', default: 'None', description: 'Docker image to use when launching on Kubernetes.' }, + { name: 'namespace', type: 'str', default: 'METAFLOW_KUBERNETES_NAMESPACE', description: 'Kubernetes namespace to use when launching pod.' }, + { name: 'gpu', type: 'int', default: 'None', description: 'Number of GPUs required for this step.' }, + { name: 'gpu_vendor', type: 'str', default: 'KUBERNETES_GPU_VENDOR', description: 'The vendor of the GPUs to be used.' }, + { name: 'service_account', type: 'str', default: 'METAFLOW_KUBERNETES_SERVICE_ACCOUNT', description: 'Kubernetes service account to use.' }, + { name: 'node_selector', type: 'Union[Dict, str]', default: 'None', description: 'Kubernetes node selector(s) to apply to the pod.' }, + { name: 'tolerations', type: 'List[Dict]', default: '[]', description: 'Kubernetes tolerations to use when launching pod.' }, + { name: 'shared_memory', type: 'int', default: 'None', description: 'Shared memory size (in MiB) required for this step.' }, + { name: 'port', type: 'int', default: 'None', description: 'Port number to specify in the Kubernetes job object.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/kubernetes', + level: 'step' + }, + + batch: { + description: 'Specifies that this step should execute on AWS Batch.', + params: [ + { name: 'cpu', type: 'int', default: '1', description: 'Number of CPUs required for this step.' }, + { name: 'gpu', type: 'int', default: '0', description: 'Number of GPUs required for this step.' }, + { name: 'memory', type: 'int', default: '4096', description: 'Memory size (in MB) required for this step.' }, + { name: 'image', type: 'str', default: 'None', description: 'Docker image to use when launching on AWS Batch.' }, + { name: 'queue', type: 'str', default: 'METAFLOW_BATCH_JOB_QUEUE', description: 'AWS Batch Job Queue to submit the job to.' }, + { name: 'iam_role', type: 'str', default: 'METAFLOW_ECS_S3_ACCESS_IAM_ROLE', description: 'AWS IAM role that the container uses to access cloud resources.' }, + { name: 'execution_role', type: 'str', default: 'METAFLOW_ECS_FARGATE_EXECUTION_ROLE', description: 'AWS IAM role for triggering Fargate tasks.' }, + { name: 'shared_memory', type: 'int', default: 'None', description: 'Size (in MiB) of the /dev/shm volume for this step.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/batch', + level: 'step' + }, + + conda: { + description: 'Specifies the Conda environment for the step. Augments attributes set in @conda_base.', + params: [ + { name: 'packages', type: 'Dict[str, str]', default: '{}', description: 'Packages to use for this step. Key is package name, value is the version.' }, + { name: 'libraries', type: 'Dict[str, str]', default: '{}', description: 'Supported for backward compatibility. packages takes precedence.' }, + { name: 'python', type: 'str', default: 'None', description: "Version of Python to use, e.g. '3.7.4'. None uses the current interpreter version." }, + { name: 'disabled', type: 'bool', default: 'False', description: 'If set to True, disables @conda.' } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/conda', + level: 'step' + }, + + pypi: { + description: 'Specifies the PyPI packages for the step. Augments attributes set in @pypi_base.', + params: [ + { name: 'packages', type: 'Dict[str, str]', default: '{}', description: 'Packages to use for this step. Key is package name, value is the version.' }, + { name: 'python', type: 'str', default: 'None', description: "Version of Python to use, e.g. '3.7.4'. None uses the current interpreter version." } + ], + docUrl: 'https://docs.metaflow.org/api/step-decorators/pypi', + level: 'step' + }, + + parallel: { + description: 'Enables parallel execution of a step across multiple nodes. Use with `num_parallel` in `self.next()` to specify the number of parallel tasks.', + params: [], + docUrl: 'https://docs.metaflow.org/scaling/remote-tasks/introduction', + level: 'step' + }, + + pytorch_parallel: { + description: 'Enables distributed PyTorch training across multiple nodes. Sets up the PyTorch distributed environment automatically. Extends @parallel.', + params: [ + { name: 'master_port', type: 'int', default: 'None', description: 'Port to use for the PyTorch distributed master process.' } + ], + docUrl: 'https://docs.metaflow.org/scaling/remote-tasks/introduction', + level: 'step' + }, + + // --- Flow-level decorators --- + + conda_base: { + description: 'Specifies the Conda environment for all steps of the flow.', + params: [ + { name: 'packages', type: 'Dict[str, str]', default: '{}', description: 'Packages to use for this flow. Key is package name, value is the version.' }, + { name: 'libraries', type: 'Dict[str, str]', default: '{}', description: 'Supported for backward compatibility. packages takes precedence.' }, + { name: 'python', type: 'str', default: 'None', description: "Version of Python to use, e.g. '3.7.4'. None uses the current interpreter version." }, + { name: 'disabled', type: 'bool', default: 'False', description: 'If set to True, disables Conda.' } + ], + docUrl: 'https://docs.metaflow.org/api/flow-decorators/conda_base', + level: 'flow' + }, + + pypi_base: { + description: 'Specifies the PyPI packages for all steps of the flow. Analogous to @conda_base but for PyPI.', + params: [ + { name: 'packages', type: 'Dict[str, str]', default: '{}', description: 'Packages to use for this flow. Key is package name, value is the version.' }, + { name: 'python', type: 'str', default: 'None', description: "Version of Python to use, e.g. '3.7.4'. None uses the current interpreter version." } + ], + docUrl: 'https://docs.metaflow.org/api/flow-decorators', + level: 'flow' + }, + + project: { + description: 'Specifies what flows belong to the same project. Creates a project-specific namespace for all flows with the same name.', + params: [ + { name: 'name', type: 'str', default: '—', description: 'Project name. Must be unique among all projects using the same production scheduler. Only lowercase alphanumeric and underscores.' }, + { name: 'branch', type: 'str', default: 'None', description: 'The branch to use. Defaults to user. unless production is True.' }, + { name: 'production', type: 'bool', default: 'False', description: 'Whether or not the branch is the production branch.' } + ], + docUrl: 'https://docs.metaflow.org/api/flow-decorators/project', + level: 'flow' + }, + + schedule: { + description: 'Specifies the times when the flow should be run on a production scheduler.', + params: [ + { name: 'hourly', type: 'bool', default: 'False', description: 'Run the workflow hourly.' }, + { name: 'daily', type: 'bool', default: 'True', description: 'Run the workflow daily.' }, + { name: 'weekly', type: 'bool', default: 'False', description: 'Run the workflow weekly.' }, + { name: 'cron', type: 'str', default: 'None', description: 'Run the workflow at a custom Cron schedule.' }, + { name: 'timezone', type: 'str', default: 'None', description: 'Timezone on which the schedule runs (IANA format).' } + ], + docUrl: 'https://docs.metaflow.org/api/flow-decorators/schedule', + level: 'flow' + }, + + trigger: { + description: 'Specifies the event(s) that this flow depends on.', + params: [ + { name: 'event', type: 'Union[str, Dict]', default: 'None', description: 'Event dependency for this flow.' }, + { name: 'events', type: 'List[Union[str, Dict]]', default: '[]', description: 'Events dependency for this flow.' }, + { name: 'options', type: 'Dict[str, Any]', default: '{}', description: 'Backend-specific configuration for tuning eventing behavior.' } + ], + docUrl: 'https://docs.metaflow.org/api/flow-decorators/trigger', + level: 'flow' + }, + + trigger_on_finish: { + description: 'Specifies the flow(s) that this flow depends on. Triggers when upstream runs finish.', + params: [ + { name: 'flow', type: 'Union[str, Dict]', default: 'None', description: 'Upstream flow dependency for this flow.' }, + { name: 'flows', type: 'List[Union[str, Dict]]', default: '[]', description: 'Upstream flow dependencies for this flow.' }, + { name: 'options', type: 'Dict[str, Any]', default: '{}', description: 'Backend-specific configuration for tuning eventing behavior.' } + ], + docUrl: 'https://docs.metaflow.org/api/flow-decorators/trigger_on_finish', + level: 'flow' + } +}; + +module.exports = { DECORATOR_INFO }; diff --git a/src/hoverProvider.js b/src/hoverProvider.js new file mode 100644 index 0000000..6736d3c --- /dev/null +++ b/src/hoverProvider.js @@ -0,0 +1,78 @@ +const vscode = require('vscode'); +const { DECORATOR_INFO } = require('./decorators'); + +/** + * Regex to match a Metaflow decorator on the current line. + * Captures the decorator name (e.g. "retry", "conda_base", "trigger_on_finish"). + */ +const DECORATOR_REGEX = /@([a-zA-Z_][a-zA-Z0-9_]*)/; + +/** + * Builds a Markdown tooltip for the given decorator info. + */ +function buildHoverMarkdown(name, info) { + const md = new vscode.MarkdownString('', true); + md.isTrusted = true; + md.supportHtml = true; + + // Header + md.appendMarkdown(`### \`@${name}\` — ${info.level === 'flow' ? 'Flow' : 'Step'} Decorator\n\n`); + + // Description + md.appendMarkdown(`${info.description}\n\n`); + + // Parameter table + if (info.params && info.params.length > 0) { + md.appendMarkdown(`| Parameter | Type | Default | Description |\n`); + md.appendMarkdown(`|:---|:---|:---|:---|\n`); + for (const p of info.params) { + // Escape pipes inside values + const desc = p.description.replace(/\|/g, '\\|'); + const type = p.type.replace(/\|/g, '\\|'); + md.appendMarkdown(`| \`${p.name}\` | \`${type}\` | \`${p.default}\` | ${desc} |\n`); + } + md.appendMarkdown('\n'); + } + + // Link to docs + md.appendMarkdown(`[📖 View full documentation](${info.docUrl})\n`); + + return md; +} + +/** + * Returns a HoverProvider for Metaflow decorators in Python files. + */ +function createDecoratorHoverProvider() { + return { + provideHover(document, position, _token) { + const line = document.lineAt(position.line); + const lineText = line.text; + + // Find all decorator matches on this line + const globalRegex = /@([a-zA-Z_][a-zA-Z0-9_]*)/g; + let match; + while ((match = globalRegex.exec(lineText)) !== null) { + const decoratorName = match[1]; + const startCol = match.index; // position of '@' + const endCol = match.index + match[0].length; + + // Check if cursor is on this decorator token + if (position.character >= startCol && position.character <= endCol) { + const info = DECORATOR_INFO[decoratorName]; + if (info) { + const range = new vscode.Range( + position.line, startCol, + position.line, endCol + ); + return new vscode.Hover(buildHoverMarkdown(decoratorName, info), range); + } + } + } + + return null; + } + }; +} + +module.exports = { createDecoratorHoverProvider, buildHoverMarkdown }; diff --git a/test/decorators.test.js b/test/decorators.test.js new file mode 100644 index 0000000..e327db1 --- /dev/null +++ b/test/decorators.test.js @@ -0,0 +1,99 @@ +const assert = require('assert'); +const { DECORATOR_INFO } = require('../src/decorators'); + +describe('DECORATOR_INFO', () => { + + const expectedStepDecorators = [ + 'step', 'retry', 'catch', 'timeout', 'resources', + 'environment', 'card', 'secrets', 'kubernetes', 'batch', + 'conda', 'pypi', 'parallel', 'pytorch_parallel' + ]; + + const expectedFlowDecorators = [ + 'conda_base', 'pypi_base', 'project', 'schedule', + 'trigger', 'trigger_on_finish' + ]; + + it('should export a non-empty object', () => { + assert.ok(DECORATOR_INFO); + assert.ok(typeof DECORATOR_INFO === 'object'); + assert.ok(Object.keys(DECORATOR_INFO).length > 0); + }); + + it('should contain all expected step-level decorators', () => { + for (const name of expectedStepDecorators) { + assert.ok(DECORATOR_INFO[name], `Missing step decorator: ${name}`); + assert.strictEqual(DECORATOR_INFO[name].level, 'step', `${name} should be level "step"`); + } + }); + + it('should contain all expected flow-level decorators', () => { + for (const name of expectedFlowDecorators) { + assert.ok(DECORATOR_INFO[name], `Missing flow decorator: ${name}`); + assert.strictEqual(DECORATOR_INFO[name].level, 'flow', `${name} should be level "flow"`); + } + }); + + it('every decorator should have description, params, docUrl, and level', () => { + for (const [name, info] of Object.entries(DECORATOR_INFO)) { + assert.ok(typeof info.description === 'string' && info.description.length > 0, + `${name}: description must be a non-empty string`); + assert.ok(Array.isArray(info.params), + `${name}: params must be an array`); + assert.ok(typeof info.docUrl === 'string' && info.docUrl.startsWith('https://'), + `${name}: docUrl must be a valid https URL`); + assert.ok(info.level === 'step' || info.level === 'flow', + `${name}: level must be "step" or "flow"`); + } + }); + + it('every param should have name, type, default, and description', () => { + for (const [decName, info] of Object.entries(DECORATOR_INFO)) { + for (const param of info.params) { + assert.ok(typeof param.name === 'string' && param.name.length > 0, + `${decName}: param name must be a non-empty string`); + assert.ok(typeof param.type === 'string' && param.type.length > 0, + `${decName}.${param.name}: type must be a non-empty string`); + assert.ok(typeof param.default === 'string', + `${decName}.${param.name}: default must be a string`); + assert.ok(typeof param.description === 'string' && param.description.length > 0, + `${decName}.${param.name}: description must be a non-empty string`); + } + } + }); + + it('@retry should have times and minutes_between_retries params', () => { + const retry = DECORATOR_INFO.retry; + const paramNames = retry.params.map(p => p.name); + assert.ok(paramNames.includes('times')); + assert.ok(paramNames.includes('minutes_between_retries')); + assert.strictEqual(retry.params.find(p => p.name === 'times').default, '3'); + assert.strictEqual(retry.params.find(p => p.name === 'minutes_between_retries').default, '2'); + }); + + it('@kubernetes should have cpu, memory, disk, image params', () => { + const k8s = DECORATOR_INFO.kubernetes; + const paramNames = k8s.params.map(p => p.name); + assert.ok(paramNames.includes('cpu')); + assert.ok(paramNames.includes('memory')); + assert.ok(paramNames.includes('disk')); + assert.ok(paramNames.includes('image')); + }); + + it('@batch should have cpu, gpu, memory, image, queue params', () => { + const b = DECORATOR_INFO.batch; + const paramNames = b.params.map(p => p.name); + assert.ok(paramNames.includes('cpu')); + assert.ok(paramNames.includes('gpu')); + assert.ok(paramNames.includes('memory')); + assert.ok(paramNames.includes('image')); + assert.ok(paramNames.includes('queue')); + }); + + it('docUrl should point to the correct Metaflow docs page', () => { + assert.ok(DECORATOR_INFO.retry.docUrl.includes('/step-decorators/retry')); + assert.ok(DECORATOR_INFO.schedule.docUrl.includes('/flow-decorators/schedule')); + assert.ok(DECORATOR_INFO.kubernetes.docUrl.includes('/step-decorators/kubernetes')); + assert.ok(DECORATOR_INFO.project.docUrl.includes('/flow-decorators/project')); + }); +}); diff --git a/test/hoverProvider.test.js b/test/hoverProvider.test.js new file mode 100644 index 0000000..e51976c --- /dev/null +++ b/test/hoverProvider.test.js @@ -0,0 +1,213 @@ +const assert = require('assert'); +const { DECORATOR_INFO } = require('../src/decorators'); + +// --------------------------------------------------------------------------- +// Lightweight mocks for the vscode module so we can require hoverProvider.js +// outside of a real VS Code host. +// --------------------------------------------------------------------------- +const vscodeStub = { + MarkdownString: class MarkdownString { + constructor(value = '', supportThemeIcons = false) { + this._value = value; + this.isTrusted = false; + this.supportHtml = false; + } + appendMarkdown(md) { this._value += md; return this; } + get value() { return this._value; } + }, + Range: class Range { + constructor(startLine, startChar, endLine, endChar) { + this.start = { line: startLine, character: startChar }; + this.end = { line: endLine, character: endChar }; + } + }, + Hover: class Hover { + constructor(contents, range) { + this.contents = contents; + this.range = range; + } + } +}; + +// Patch require so that `require('vscode')` returns our stub. +const Module = require('module'); +const originalResolveFilename = Module._resolveFilename; +Module._resolveFilename = function (request, parent, isMain, options) { + if (request === 'vscode') return request; + return originalResolveFilename.call(this, request, parent, isMain, options); +}; +const originalLoad = Module._cache; +require.cache['vscode'] = { id: 'vscode', filename: 'vscode', loaded: true, exports: vscodeStub }; + +// Now we can safely require the hover provider: +const { createDecoratorHoverProvider, buildHoverMarkdown } = require('../src/hoverProvider'); + +// --------------------------------------------------------------------------- +// Helper: build a fake document + position for the hover provider +// --------------------------------------------------------------------------- +function makeFakeDoc(lines) { + return { + lineAt(lineNum) { + return { text: lines[lineNum] || '' }; + } + }; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- +describe('buildHoverMarkdown', () => { + + it('should produce markdown containing the decorator name', () => { + const md = buildHoverMarkdown('retry', DECORATOR_INFO.retry); + assert.ok(md.value.includes('@retry')); + }); + + it('should include "Step Decorator" for step-level decorators', () => { + const md = buildHoverMarkdown('retry', DECORATOR_INFO.retry); + assert.ok(md.value.includes('Step Decorator')); + }); + + it('should include "Flow Decorator" for flow-level decorators', () => { + const md = buildHoverMarkdown('schedule', DECORATOR_INFO.schedule); + assert.ok(md.value.includes('Flow Decorator')); + }); + + it('should include the description text', () => { + const md = buildHoverMarkdown('timeout', DECORATOR_INFO.timeout); + assert.ok(md.value.includes(DECORATOR_INFO.timeout.description)); + }); + + it('should render a parameter table with all params', () => { + const md = buildHoverMarkdown('kubernetes', DECORATOR_INFO.kubernetes); + for (const p of DECORATOR_INFO.kubernetes.params) { + assert.ok(md.value.includes(p.name), + `Parameter "${p.name}" should appear in the tooltip`); + } + }); + + it('should contain a link to the documentation', () => { + const md = buildHoverMarkdown('batch', DECORATOR_INFO.batch); + assert.ok(md.value.includes(DECORATOR_INFO.batch.docUrl)); + assert.ok(md.value.includes('View full documentation')); + }); + + it('should handle decorators with no params (e.g. @parallel)', () => { + const md = buildHoverMarkdown('parallel', DECORATOR_INFO.parallel); + assert.ok(md.value.includes('@parallel')); + // Should NOT contain a table header when there are zero params + assert.ok(!md.value.includes('| Parameter |'), + '@parallel has no params so there should be no parameter table'); + }); +}); + +describe('createDecoratorHoverProvider – provideHover', () => { + + const provider = createDecoratorHoverProvider(); + + it('should return a Hover for a known decorator', () => { + const doc = makeFakeDoc([' @retry(times=5)']); + const pos = { line: 0, character: 5 }; // on "retry" + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover, 'Expected a Hover object'); + assert.ok(hover.contents.value.includes('@retry')); + }); + + it('should return null for an unknown decorator', () => { + const doc = makeFakeDoc([' @unknown_decorator']); + const pos = { line: 0, character: 5 }; + const hover = provider.provideHover(doc, pos, null); + assert.strictEqual(hover, null); + }); + + it('should return null when cursor is not on a decorator', () => { + const doc = makeFakeDoc(['def start(self):']); + const pos = { line: 0, character: 4 }; + const hover = provider.provideHover(doc, pos, null); + assert.strictEqual(hover, null); + }); + + it('should return a Hover when cursor is exactly on the @ symbol', () => { + const doc = makeFakeDoc(['@retry']); + const pos = { line: 0, character: 0 }; // on '@' + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover, 'Hovering on @ should still show the decorator tooltip'); + }); + + it('should return a Hover when cursor is on the last character of the name', () => { + const doc = makeFakeDoc(['@retry']); + const pos = { line: 0, character: 5 }; // on 'y' + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover); + }); + + it('should return null when cursor is past the decorator name', () => { + const doc = makeFakeDoc(['@retry(times=5)']); + const pos = { line: 0, character: 7 }; // on '(' — past the name + const hover = provider.provideHover(doc, pos, null); + assert.strictEqual(hover, null); + }); + + it('should handle indented decorators', () => { + const doc = makeFakeDoc([' @kubernetes(cpu=2)']); + const pos = { line: 0, character: 10 }; // on 'k' + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover); + assert.ok(hover.contents.value.includes('@kubernetes')); + }); + + it('should handle @conda_base (underscore in name)', () => { + const doc = makeFakeDoc(['@conda_base(python="3.9")']); + const pos = { line: 0, character: 5 }; // on 'a' in conda_base + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover); + assert.ok(hover.contents.value.includes('@conda_base')); + assert.ok(hover.contents.value.includes('Flow Decorator')); + }); + + it('should handle @trigger_on_finish', () => { + const doc = makeFakeDoc(["@trigger_on_finish(flow='UpstreamFlow')"]); + const pos = { line: 0, character: 5 }; + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover); + assert.ok(hover.contents.value.includes('@trigger_on_finish')); + }); + + it('should handle @parallel (no-param decorator)', () => { + const doc = makeFakeDoc(['@parallel']); + const pos = { line: 0, character: 3 }; + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover); + assert.ok(hover.contents.value.includes('@parallel')); + }); + + it('should handle @pytorch_parallel', () => { + const doc = makeFakeDoc(['@pytorch_parallel']); + const pos = { line: 0, character: 5 }; + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover); + assert.ok(hover.contents.value.includes('@pytorch_parallel')); + }); + + it('should provide correct range covering only the decorator token', () => { + const doc = makeFakeDoc([' @resources(cpu=4, memory=8192)']); + const pos = { line: 0, character: 8 }; + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover); + // '@resources' starts at col 4, ends at col 14 + assert.strictEqual(hover.range.start.character, 4); + assert.strictEqual(hover.range.end.character, 14); + }); + + it('should work for every decorator in DECORATOR_INFO', () => { + for (const name of Object.keys(DECORATOR_INFO)) { + const line = `@${name}`; + const doc = makeFakeDoc([line]); + const pos = { line: 0, character: 1 }; // on first char of name + const hover = provider.provideHover(doc, pos, null); + assert.ok(hover, `Hovering on @${name} should return a Hover`); + assert.ok(hover.contents.value.includes(`@${name}`), + `Tooltip for @${name} should contain the name`); + } + }); +}); diff --git a/test/test_hover_flow.py b/test/test_hover_flow.py new file mode 100644 index 0000000..a3a253b --- /dev/null +++ b/test/test_hover_flow.py @@ -0,0 +1,50 @@ +""" +Sample Metaflow flow to test decorator hover documentation. +Open this file in VS Code with the extension loaded and hover over the decorators. +""" +from metaflow import FlowSpec, step, retry, catch, timeout, resources, card, batch, kubernetes, conda, pypi, environment, secrets, schedule, project, trigger, trigger_on_finish, conda_base, pypi_base, parallel, pytorch_parallel + +@schedule(weekly=True) +@project(name='test_project') +@trigger(event='my_event') +@trigger_on_finish(flow='UpstreamFlow') +@conda_base(libraries={'pandas': '1.3.0'}) +class TestHoverFlow(FlowSpec): + + @environment(vars={'MY_VAR': 'hello'}) + @retry(times=5, minutes_between_retries=1) + @catch(var='error_info', print_exception=True) + @timeout(hours=1, minutes=30) + @resources(cpu=4, memory=8192, gpu=1) + @card(type='default') + @secrets(sources=['my-secret']) + @kubernetes(cpu=2, memory=4096, image='my-image:latest') + @batch(cpu=4, gpu=1, memory=16384, queue='my-queue') + @conda(packages={'numpy': '1.21.0'}) + @pypi(packages={'requests': '2.26.0'}) + @step + def start(self): + """ + Hover over each decorator above to see its documentation tooltip! + """ + self.next(self.parallel_step, num_parallel=2) + + @parallel + @step + def parallel_step(self): + """ + This step runs in parallel. Hover over @parallel to see its docs. + """ + self.next(self.join_step) + + @step + def join_step(self, inputs): + self.next(self.end) + + @step + def end(self): + pass + + +if __name__ == '__main__': + TestHoverFlow()