diff --git a/.gitignore b/.gitignore index 1df4005..363d9ff 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ test_* .vscode/settings.json *.psd todo +scratch autodocstring-*.vsix diff --git a/.vscode/Below is a step-by-step guide on how to .py b/.vscode/Below is a step-by-step guide on how to .py new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 992ae76..9466320 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,11 @@ "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", "dev": true }, + "@types/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ==" + }, "@types/mocha": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", @@ -327,6 +332,11 @@ "wrap-ansi": "^7.0.0" } }, + "clone": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.1.19.tgz", + "integrity": "sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU=" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -651,6 +661,11 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, + "is-equal": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-equal/-/is-equal-0.1.0.tgz", + "integrity": "sha1-/Pbg/cncqaTUdouuc7jNqls/q24=" + }, "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", @@ -705,10 +720,28 @@ "argparse": "^2.0.1" } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", + "jsdiff": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsdiff/-/jsdiff-1.1.1.tgz", + "integrity": "sha1-6qbnwW259kuXnSj5hvocpFeyIuU=", + "requires": { + "is-equal": "~0.1.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "locate-path": { @@ -941,6 +974,34 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "reverse-mustache": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/reverse-mustache/-/reverse-mustache-1.9.0.tgz", + "integrity": "sha1-bRwp8ssPXTF5avk4B1ClHrtQIwY=", + "requires": { + "clone": "~0.1.11", + "mustache": "~2.3.0", + "pathval": "~0.1.1" + }, + "dependencies": { + "mustache": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", + "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==" + }, + "pathval": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-0.1.1.tgz", + "integrity": "sha1-CPkRzcqczllCiA2ngXvAtyO2bYI=" + } + } + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index 4f64f7c..53b8cfc 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,10 @@ { "command": "autoDocstring.generateDocstring", "title": "Generate Docstring" + }, + { + "command": "autoDocstring.updateDocstring", + "title": "Update Docstring [beta]" } ], "keybindings": [ diff --git a/src/constants.ts b/src/constants.ts index 95a23c8..f44ec57 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,3 +5,4 @@ export const extensionRoot: ExtensionRoot = { path: "" }; export const debug = false; export const extensionID = "autoDocstring"; export const generateDocstringCommand = "autoDocstring.generateDocstring"; +export const updateDocstringCommand = "autoDocstring.updateDocstring"; diff --git a/src/docstring/index.ts b/src/docstring/index.ts index ad7bd54..06e4383 100644 --- a/src/docstring/index.ts +++ b/src/docstring/index.ts @@ -1,2 +1,3 @@ export { DocstringFactory } from "./docstring_factory"; export { getTemplate } from "./get_template"; +export { parseDocstring } from "./parse_docstring"; diff --git a/src/docstring/parse_docstring.ts b/src/docstring/parse_docstring.ts new file mode 100644 index 0000000..07bdb85 --- /dev/null +++ b/src/docstring/parse_docstring.ts @@ -0,0 +1,220 @@ +// import { reverseMustache } from "reverse-mustache"; +// const reverseMustache = require("reverse-mustache"); +import { diffLines } from "Diff"; +import { Argument, DocstringParts } from "../docstring_parts"; + +// Need to deal with +// 1. multiline regex +// 2. indentation +// 3. description +// 4. check crlf +// 5. sections in different orders +// 6. A section is not there + +export function parseDocstring(oldDocstring: string, template: string) { + const docstringLines = oldDocstring.split("\n"); + const templateLines = template.split("\n"); + + const argRegex = getTemplateBlockRegex(template, "args"); + const kwargRegex = getTemplateBlockRegex(template, "kwargs"); + const exceptionRegex = getTemplateBlockRegex(template, "exceptions"); + const returnRegex = getTemplateBlockRegex(template, "returns"); + const yieldRegex = getTemplateBlockRegex(template, "yields"); + + console.log(yieldRegex); + + const argRegexLineCount = argRegex.toString().split("\\n").length; + const kwargRegexLineCount = kwargRegex.toString().split("\\n").length; + const exceptionRegexLineCount = exceptionRegex.toString().split("\\n").length; + const returnRegexLineCount = returnRegex.toString().split("\\n").length; + const yieldRegexLineCount = yieldRegex.toString().split("\\n").length; + + const docstringParts = emptyDocstringParts(); + + let j = 0; + for (let i = 0; i < templateLines.length; i++) { + // Advance docstring and template if lines match + if (templateLines[i] == docstringLines[j]) { + j++; + continue; + } + + // Start parsing args if the start arg block is reached + if (/{{#args}}/.test(templateLines[i])) { + // Keep parsing arg lines until lines no longer match arg regex or they match kwarg pattern + while (j <= docstringLines.length) { + const lines = docstringLines.slice(j, j + argRegexLineCount).join("\n"); + + if (!argRegex.test(lines) || kwargRegex.test(lines)) { + break; + } + + const match = lines.match(argRegex); + docstringParts.args.push({ + var: match.groups.var, + type: match.groups.type, + }); + + j += argRegexLineCount; + } + + // Advance to end of arg block + while (!/{{\/args}}/.test(templateLines[i]) && i < templateLines.length) { + i++; + } + } + + // Start parsing kwargs if the start kwarg block is reached + if (/{{#kwargs}}/.test(templateLines[i])) { + while (j <= docstringLines.length) { + const lines = docstringLines.slice(j, j + kwargRegexLineCount).join("\n"); + + if (!kwargRegex.test(lines)) { + break; + } + + const match = lines.match(kwargRegex); + docstringParts.kwargs.push({ + var: match.groups.var, + type: match.groups.type, + default: match.groups.default, + }); + + j += kwargRegexLineCount; + } + + // Advance to end of kwarg block + while (!/{{\/kwargs}}/.test(templateLines[i]) && i < templateLines.length) { + i++; + } + } + + if (/{{#exceptions}}/.test(templateLines[i])) { + while (j <= docstringLines.length) { + const lines = docstringLines.slice(j, j + exceptionRegexLineCount).join("\n"); + + if (!exceptionRegex.test(lines)) { + break; + } + + const match = lines.match(exceptionRegex); + docstringParts.exceptions.push({ + type: match.groups.type, + }); + + j += exceptionRegexLineCount; + } + + // Advance to end of kwarg block + while (!/{{\/exceptions}}/.test(templateLines[i]) && i < templateLines.length) { + i++; + } + } + + if (/{{#returns}}/.test(templateLines[i])) { + while (j <= docstringLines.length) { + const lines = docstringLines.slice(j, j + returnRegexLineCount).join("\n"); + console.log(lines); + if (!returnRegex.test(lines)) { + break; + } + + const match = lines.match(returnRegex); + docstringParts.returns = { + type: match.groups.type, + }; + + j += returnRegexLineCount; + } + + // Advance to end of kwarg block + while (!/{{\/returns}}/.test(templateLines[i]) && i < templateLines.length) { + i++; + } + } + + if (/{{#yields}}/.test(templateLines[i])) { + while (j <= docstringLines.length) { + const lines = docstringLines.slice(j, j + yieldRegexLineCount).join("\n"); + console.log(lines); + if (!yieldRegex.test(lines)) { + break; + } + + const match = lines.match(yieldRegex); + docstringParts.yields = { + type: match.groups.type, + }; + + j += yieldRegexLineCount; + } + + // Advance to end of kwarg block + while (!/{{\/yields}}/.test(templateLines[i]) && i < templateLines.length) { + i++; + } + } + } + + return docstringParts; +} + +/** Gets the docstring template section for the given block type */ +function getTemplateBlockRegex(template: string, blockName: string): RegExp { + const block = getTemplateBlock(template, blockName); + + // Escape all characters that can be misidentified in regex + let blockRegex = escapeRegExp(block); + + // Replace all tags with named regex capture groups + blockRegex = replaceTags(blockRegex, "{{var}}", "\\w+", "var"); + blockRegex = replaceTags(blockRegex, "{{typePlaceholder}}", "[\\w\\[\\], ]+", "type"); + blockRegex = replaceTags(blockRegex, "{{descriptionPlaceholder}}", ".*", "description"); + blockRegex = replaceTags(blockRegex, "{{&default}}", ".+", "default"); + blockRegex = replaceTags(blockRegex, "{{type}}", "\\w+", "type"); + + // Escape newlines and handle possible indentation + blockRegex = blockRegex.trim(); + blockRegex = blockRegex.replace(/\n/g, "\\n\\s*"); + + return new RegExp(blockRegex.trim()); +} + +function getTemplateBlock(template: string, block: string): string { + const blockStartTag = `{{#${block}}}`; + const blockEndTag = `{{/${block}}}`; + const blockRegex = new RegExp(blockStartTag + "(.*)" + blockEndTag, "s"); + + const match = template.match(blockRegex); + let pattern = match[1]; + + return pattern; +} + +function replaceTags(str: string, tag: string, pattern: string, captureGroupName: string): string { + tag = escapeRegExp(tag); + str = str.replace(tag, `(?<${captureGroupName}>${pattern})`); + + // Replace remaining matches with non capturing pattern + while (new RegExp(escapeRegExp(tag)).test(str)) { + str = str.replace(tag, pattern); + } + + return str; +} + +function escapeRegExp(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +} + +function emptyDocstringParts(): DocstringParts { + return { + name: "", + args: [], + kwargs: [], + decorators: [], + exceptions: [], + yields: null, + returns: null, + }; +} diff --git a/src/extension.ts b/src/extension.ts index a436bf1..b9d78f7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,12 @@ import * as vs from "vscode"; import { AutoDocstring } from "./generate_docstring"; import { docstringIsClosed, validDocstringPrefix } from "./parse"; -import { extensionRoot, generateDocstringCommand, extensionID } from "./constants"; +import { + extensionRoot, + generateDocstringCommand, + extensionID, + updateDocstringCommand, +} from "./constants"; import { getStackTrace } from "./telemetry"; import { logInfo, logError } from "./logger"; @@ -20,23 +25,56 @@ export function activate(context: vs.ExtensionContext): void { logError(error + "\n\t" + getStackTrace(error)); } }), + + vs.commands.registerCommand(updateDocstringCommand, () => { + const editor = vs.window.activeTextEditor; + const autoDocstring = new AutoDocstring(editor); + + try { + return autoDocstring.updateDocstring(); + } catch (error) { + logError(error + "\n\t" + getStackTrace(error)); + } + }), + + vs.languages.registerCompletionItemProvider( + "python", + { + provideCompletionItems: ( + document: vs.TextDocument, + position: vs.Position, + _: vs.CancellationToken, + ) => { + if (validEnterActivation(document, position)) { + return [new AutoDocstringCompletionItem(document, position)]; + } + }, + }, + '"', + "'", + "#", + ), ); - ['python', 'starlark'].map((language) => { + ["python", "starlark"].map((language) => { context.subscriptions.push( vs.languages.registerCompletionItemProvider( language, { - provideCompletionItems: (document: vs.TextDocument, position: vs.Position, _: vs.CancellationToken) => { + provideCompletionItems: ( + document: vs.TextDocument, + position: vs.Position, + _: vs.CancellationToken, + ) => { if (validEnterActivation(document, position)) { return [new AutoDocstringCompletionItem(document, position)]; } }, }, - "\"", + '"', "'", "#", - ) + ), ); }); @@ -46,7 +84,7 @@ export function activate(context: vs.ExtensionContext): void { /** * This method is called when the extension is deactivated */ -export function deactivate() { } +export function deactivate() {} /** * Checks that the preceding characters of the position is a valid docstring prefix diff --git a/src/generate_docstring.ts b/src/generate_docstring.ts index 1094b3a..8d50d6a 100644 --- a/src/generate_docstring.ts +++ b/src/generate_docstring.ts @@ -2,7 +2,7 @@ import * as path from "path"; import * as vs from "vscode"; import { DocstringFactory } from "./docstring/docstring_factory"; import { getCustomTemplate, getTemplate } from "./docstring/get_template"; -import { getDocstringIndentation, getDefaultIndentation, parse } from "./parse"; +import { getDocstringIndentation, getDefaultIndentation, parse, getDocstring } from "./parse"; import { extensionID } from "./constants"; import { logDebug, logInfo } from "./logger"; import { docstringPartsToString } from "./docstring_parts"; @@ -43,6 +43,18 @@ export class AutoDocstring { return success; } + public updateDocstring() { + const position = this.editor.selection.active; + const document = this.editor.document.getText(); + logInfo(`Updating Docstring at line: ${position.line}`); + + let existingDocstring = getDocstring(document, position.line); + + // existingDocstring = normalize; + + logInfo(existingDocstring); + } + private generateDocstringSnippet(document: string, position: vs.Position): vs.SnippetString { const config = this.getConfig(); diff --git a/src/parse/docstring/get_docstring.ts b/src/parse/docstring/get_docstring.ts new file mode 100644 index 0000000..9ac8be9 --- /dev/null +++ b/src/parse/docstring/get_docstring.ts @@ -0,0 +1,15 @@ +import { getDocstringStartIndex, getDocstringEndIndex } from "./get_docstring_range"; + +export function getDocstring(document: string, lineNum: number): string { + const lines = document.split("\n"); + + const startIndex = getDocstringStartIndex(lines, lineNum); + const endIndex = getDocstringEndIndex(lines, lineNum); + + if (startIndex == undefined || endIndex == undefined) { + return ""; + } + + const docstringLines = lines.slice(startIndex, endIndex + 1); + return docstringLines.join("\n"); +} diff --git a/src/parse/docstring/get_docstring_range.ts b/src/parse/docstring/get_docstring_range.ts new file mode 100644 index 0000000..647f94a --- /dev/null +++ b/src/parse/docstring/get_docstring_range.ts @@ -0,0 +1,61 @@ +// export function getDocstringRange(document: string, lineNum: number): ; + +export function getDocstringStartIndex(lines: string[], lineNum: number) { + const docstringStartPattern = /^\s*("""|''')/; + + if (lineNum == 0) { + return 0; + } + + // If the starting line contains only docstring quotes and the previous + // line is not the function definition we can assume that we are at the + // end quotes of the docstring and should shift our position back 1 + if (isOnlyDocstringQuotes(lines[lineNum]) && !isFunctionDefinition(lines[lineNum - 1])) { + lineNum -= 1; + } + + while (lineNum >= 0) { + const line = lines[lineNum]; + if (docstringStartPattern.test(line)) { + return lineNum; + } + lineNum--; + } + + return undefined; +} + +export function getDocstringEndIndex(lines: string[], lineNum: number) { + const docstringEndPattern = /("""|''')\s*$/; + + if (lineNum >= lines.length - 1) { + return lineNum; + } + + // If the starting line contains only docstring quotes and the previous + // line is the function definition we can assume that we are at the + // start quotes of the docstring and should shift our position forward 1 + if (isOnlyDocstringQuotes(lines[lineNum]) && isFunctionDefinition(lines[lineNum - 1])) { + lineNum += 1; + } + + while (lineNum < lines.length) { + const line = lines[lineNum]; + if (docstringEndPattern.test(line)) { + return lineNum; + } + lineNum++; + } + + return undefined; +} + +/** Line contains only a docstring opening or closing quotes */ +function isOnlyDocstringQuotes(line: string): boolean { + return /^\s*("""|''')\s*$/.test(line); +} + +/** Line contains the last part of a function definition */ +function isFunctionDefinition(line: string): boolean { + return /(:|(?=[#'"\n]))/.test(line); +} diff --git a/src/parse/docstring/normalize_docstring.ts b/src/parse/docstring/normalize_docstring.ts new file mode 100644 index 0000000..8e55b27 --- /dev/null +++ b/src/parse/docstring/normalize_docstring.ts @@ -0,0 +1,42 @@ +import { blankLine } from "../utilities"; + +export function normalizeDocstring(docstring: string): string { + let lines = docstring.split("\n"); + + lines = normalizeDocstringIndentation(lines); + lines = removeDocstringQuotes(lines); + + return lines.join("\n"); +} + +/** Removes docstring block indentation while keeping relative internal + * documentation consistent + */ +function normalizeDocstringIndentation(lines: string[]): string[] { + const indentationPattern = /^\s*/; + + let minimumIndentation = " ".repeat(50); + for (const line of lines) { + if (blankLine(line)) { + continue; + } + + const match = indentationPattern.exec(line); + + if (match[0].length < minimumIndentation.length) { + minimumIndentation = match[0]; + } + } + + const minimumIndentationPattern = new RegExp("^" + minimumIndentation); + lines = lines.map((line) => line.replace(minimumIndentationPattern, "")); + + return lines; +} + +/** Remove opening and closing docstring quotes */ +function removeDocstringQuotes(lines: string[]): string[] { + lines = lines.map((line) => line.replace(/^\s*("""|''')/, "")); + lines = lines.map((line) => line.replace(/("""|''')\s*$/, "")); + return lines; +} diff --git a/src/parse/index.ts b/src/parse/index.ts index 915625c..0543d9f 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -9,3 +9,4 @@ export { parse } from "./parse"; export { parseParameters } from "./parse_parameters"; export { tokenizeDefinition } from "./tokenize_definition"; export { getDefaultIndentation } from "./utilities"; +export { getDocstring } from "./docstring/get_docstring"; diff --git a/src/parse/valid_docstring_prefix.ts b/src/parse/valid_docstring_prefix.ts index d955fa0..cc1a98e 100644 --- a/src/parse/valid_docstring_prefix.ts +++ b/src/parse/valid_docstring_prefix.ts @@ -8,7 +8,7 @@ export function validDocstringPrefix( charPosition: number, quoteStyle: string, ): boolean { - const lines = document.split(/\r?\n/); + const lines = document.split("\n"); const line = lines[linePosition]; const prefix = line.slice(0, charPosition + 1); diff --git a/src/test/docstring/parse_docstring.spec.ts b/src/test/docstring/parse_docstring.spec.ts new file mode 100644 index 0000000..dcf6706 --- /dev/null +++ b/src/test/docstring/parse_docstring.spec.ts @@ -0,0 +1,146 @@ +import chai = require("chai"); +import "mocha"; +import dedent from "ts-dedent"; + +import { parseDocstring, getTemplate } from "../../docstring"; + +chai.config.truncateThreshold = 0; +const expect = chai.expect; + +it.only("Full google docstring", () => { + const fullGoogleDocstring = dedent` + Args: + arg1 ([type]): An argument. It is named arg1 + arg2 (Dict[str, int]): This is also an argument => a good one! + kwarg1 (int, optional): a kwarg this time. A really good one. Defaults to 1. + + Raises: + FileExistsError: Oh nej! + KeyError: bad things! + + Returns: + [type]: [description] + + Yields: + [type]: [description] + """`; + + // const template = getTemplate("google"); + // parseDocstring(googleDocstring, template); + + // parseDocstring("world", "{{#place}}{{name}}{{/place}}"); + const docstring = parseDocstring(fullGoogleDocstring, googleTemplate); + + expect(docstring).to.eql({ + name: "", + args: [{ var: "arg1", type: "[type]" }, { var: "arg2" }], + kwargs: [{ var: "kwarg1", type: "int", default: "1" }], + decorators: [], + exceptions: [{ type: "FileExistsError" }, { type: "KeyError" }], + yields: { type: "[type]" }, + returns: { type: "[type]" }, + }); +}); + +it("parses a google docstring with no args", () => { + const noArgsGoogleDocstring = dedent` + Args: + kwarg1 (int, optional): a kwarg this time. A really good one. Defaults to 1. + + Raises: + FileExistsError: Oh nej! + KeyError: bad things! + + Returns: + [type]: [description] + + Yields: + [type]: [description] + """`; + + const docstring = parseDocstring(noArgsGoogleDocstring, googleTemplate); + + expect(docstring).to.eql({ + name: "", + args: [], + kwargs: [{ var: "kwarg1", type: "int", default: "1" }], + decorators: [], + exceptions: [{ type: "FileExistsError" }, { type: "KeyError" }], + yields: { type: "[type]" }, + returns: { type: "[type]" }, + }); +}); + +it("sphinx", () => { + // const template = getTemplate("google"); + // parseDocstring(googleDocstring, template); + + // parseDocstring("world", "{{#place}}{{name}}{{/place}}"); + parseDocstring(fullSphinxDocstring, sphinxTemplate); +}); + +const googleTemplate = ` +{{#parametersExist}} +Args: +{{#args}} + {{var}} ({{typePlaceholder}}): {{descriptionPlaceholder}} +{{/args}} +{{#kwargs}} + {{var}} ({{typePlaceholder}}, optional): {{descriptionPlaceholder}}. Defaults to {{&default}}. +{{/kwargs}} +{{/parametersExist}} + +{{#exceptionsExist}} +Raises: +{{#exceptions}} + {{type}}: {{descriptionPlaceholder}} +{{/exceptions}} +{{/exceptionsExist}} + +{{#returnsExist}} +Returns: +{{#returns}} + {{typePlaceholder}}: {{descriptionPlaceholder}} +{{/returns}} +{{/returnsExist}} + +{{#yieldsExist}} +Yields: +{{#yields}} + {{typePlaceholder}}: {{descriptionPlaceholder}} +{{/yields}} +{{/yieldsExist}}`; + +const fullSphinxDocstring = ` +:param arg1: [description] +:type arg1: [type] +:param arg2: [description] +:type arg2: [type] +:param kwarg1: [description], defaults to 1 +:type kwarg1: int, optional +:raises FileExistsError: [description] +:return: [description] +:rtype: [type] +:yield: [description] +:rtype: [type]`; + +const sphinxTemplate = ` +{{#args}} +:param {{var}}: {{descriptionPlaceholder}} +:type {{var}}: {{typePlaceholder}} +{{/args}} +{{#kwargs}} +:param {{var}}: {{descriptionPlaceholder}}, defaults to {{&default}} +:type {{var}}: {{typePlaceholder}}, optional +{{/kwargs}} +{{#exceptions}} +:raises {{type}}: {{descriptionPlaceholder}} +{{/exceptions}} +{{#returns}} +:return: {{descriptionPlaceholder}} +:rtype: {{typePlaceholder}} +{{/returns}} +{{#yields}} +:yield: {{descriptionPlaceholder}} +:rtype: {{typePlaceholder}} +{{/yields}}`; diff --git a/src/test/parse/docstring/get_docstring_test.spec.ts b/src/test/parse/docstring/get_docstring_test.spec.ts new file mode 100644 index 0000000..5fc043f --- /dev/null +++ b/src/test/parse/docstring/get_docstring_test.spec.ts @@ -0,0 +1,164 @@ +import chai = require("chai"); +import "mocha"; + +import { getDocstring } from "../../../parse/docstring/get_docstring"; + +chai.config.truncateThreshold = 0; +const expect = chai.expect; + +describe("getDocstring()", () => { + it("should return the lines with the docstring the linePosition is focused on with quotes removed", () => { + const result = getDocstring(oneLineDocstring, 4); + + expect(result).to.equal(' """A docstring"""'); + }); + + it("should preserve the relative indentation", () => { + const result = getDocstring(largeDocstring, 4); + + expect(result).to.equal( + [ + ' """[summary]', + "", + " Args:", + " a ([type]): [description]", + " b (int, optional): [description]. Defaults to 1.", + "", + " Returns:", + " Tuple[int, d]: [description]", + ' """', + ].join("\n"), + ); + }); + + it("should work from linePosition at the beginning or end of the docstring", () => { + for (const line of [4, 5, 6, 7, 8]) { + const result = getDocstring(smallDocstring, line); + + expect(result).to.equal( + [ + ' """', + " [summary]", + "", + " [description]", + " [more description]", + ' """', + ].join("\n"), + "Line: " + line, + ); + } + }); + + it("should handle single quotes", () => { + const result = getDocstring(singleQuoteDocstring, 4); + + expect(result).to.equal(" '''A docstring'''"); + }); + + it("should return an empty string if a start and end could not be found", () => { + const result = getDocstring(noDocstring, 4); + + expect(result).to.equal(""); + }); +}); + +const oneLineDocstring = ` + return 3 + +def basic_function(param1, param2 = abc): + """A docstring""" + print("HELLO WORLD") + try: + something() + except Error: + raise SomethingWentWrong + return 3 + +def something_else(): +`; + +const largeDocstring = ` +Something Else + +def gap_function(): + """[summary] + + Args: + a ([type]): [description] + b (int, optional): [description]. Defaults to 1. + + Returns: + Tuple[int, d]: [description] + """ + print('HELLO WORLD') + + print('HELLO AGAIN') + +Something Else +`; +const smallDocstring = ` +Something Else + +def gap_function(): + """ + [summary] + + [description] + [more description] + """ + print('HELLO WORLD') + + print('HELLO AGAIN') + +Something Else +`; + +const indentedDocstring = ` +Something Else + + def gap_function(): + """[summary] + + Args: + a ([type]): [description] + b (int, optional): [description]. Defaults to 1. + + Returns: + Tuple[int, d]: [description] + """ + print('HELLO WORLD') + + print('HELLO AGAIN') + +Something Else +`; + +const singleQuoteDocstring = ` + return 3 + +def basic_function(param1, param2 = abc): + '''A docstring''' + print("HELLO WORLD") + try: + something() + except Error: + raise SomethingWentWrong + return 3 + +def something_else(): +`; + +const noDocstring = ` + return 3 + +def basic_function(param1, param2 = abc): + + print("HELLO WORLD") + try: + something() + except Error: + raise SomethingWentWrong + return 3 + +def something_else(): +`; diff --git a/src/test/parse/docstring/normalize_docstring_test.spec.ts b/src/test/parse/docstring/normalize_docstring_test.spec.ts new file mode 100644 index 0000000..c28165e --- /dev/null +++ b/src/test/parse/docstring/normalize_docstring_test.spec.ts @@ -0,0 +1,84 @@ +import chai = require("chai"); +import "mocha"; + +import { normalizeDocstring } from "../../../parse/docstring/normalize_docstring"; + +chai.config.truncateThreshold = 0; +const expect = chai.expect; + +describe("normalizeDocstring()", () => { + it("should remove indentation and quotes of a one line docstring", () => { + const result = normalizeDocstring(oneLineDocstring); + + expect(result).to.equal("A docstring"); + }); + + it("should preserve the relative indentation", () => { + const result = normalizeDocstring(largeDocstring); + + expect(result).to.equal( + [ + "[summary]", + "", + "Args:", + " a ([type]): [description]", + " b (int, optional): [description]. Defaults to 1.", + "", + "Returns:", + " Tuple[int, d]: [description]", + "", + ].join("\n"), + ); + }); + + it("should remove the minimum indentation", () => { + const result = normalizeDocstring(indentedDocstring); + + expect(result).to.equal( + [ + "[summary]", + "", + "Args:", + " a ([type]): [description]", + " b (int, optional): [description]. Defaults to 1.", + "", + "Returns:", + " Tuple[int, d]: [description]", + "", + ].join("\n"), + ); + }); + + it("should handle single quotes", () => { + const result = normalizeDocstring(singleQuoteDocstring); + + expect(result).to.equal("A docstring"); + }); +}); + +const oneLineDocstring = `\ + """A docstring"""`; + +const largeDocstring = `\ + """[summary] + + Args: + a ([type]): [description] + b (int, optional): [description]. Defaults to 1. + + Returns: + Tuple[int, d]: [description] + """`; + +const indentedDocstring = `\ + """[summary] + + Args: + a ([type]): [description] + b (int, optional): [description]. Defaults to 1. + + Returns: + Tuple[int, d]: [description] + """`; + +const singleQuoteDocstring = `'''A docstring'''`;