From ce2f582ee2cade8d5d1fe88f7fdbbef65aa0b53a Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Fri, 22 Mar 2024 18:43:02 +0100 Subject: [PATCH 1/8] Add generateApiComponents.ts and tests --- scripts/lib/api/generateApiComponents.test.ts | 206 ++++++++++++++++++ scripts/lib/api/generateApiComponents.ts | 138 ++++++++++++ scripts/lib/stringUtils.ts | 2 + 3 files changed, 346 insertions(+) create mode 100644 scripts/lib/api/generateApiComponents.test.ts create mode 100644 scripts/lib/api/generateApiComponents.ts diff --git a/scripts/lib/api/generateApiComponents.test.ts b/scripts/lib/api/generateApiComponents.test.ts new file mode 100644 index 00000000000..94524f05678 --- /dev/null +++ b/scripts/lib/api/generateApiComponents.test.ts @@ -0,0 +1,206 @@ +// This code is a Qiskit project. +// +// (C) Copyright IBM 2024. +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +import { expect, test } from "@jest/globals"; +import { CheerioAPI, Cheerio, load as cheerioLoad } from "cheerio"; + +import { + ComponentProps, + prepareGitHubLink, + htmlSignatureToMd, + addExtraSignatures, + createOpeningTag, +} from "./generateApiComponents"; + +class Doc { + readonly $: CheerioAPI; + readonly $main: Cheerio; + + constructor($: CheerioAPI, $main: Cheerio) { + this.$ = $; + this.$main = $main; + } + + static load(html: string): Doc { + const $ = cheerioLoad(`
${html}
`); + return new Doc($, $("[role='main']")); + } + + html(): string { + return this.$main.html()!.trim(); + } + + expectHtml(expected: string): void { + expect(this.html()).toEqual(expected.trim()); + } +} + +test("htmlSignatureToMd", async () => { + const html = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; + const result = await htmlSignatureToMd(html); + expect(result).toEqual( + `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) → ForLoopContext[source]`, + ); +}); + +describe("addExtraSignatures()", () => { + test("Function with overload signatures", () => { + const rawSignature = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; + const overloadRawSignature = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: QuantumCircuit, qubits: Sequence[Qubit | QuantumRegister | int | slice | Sequence[Qubit | int]], clbits: Sequence[Clbit | ClassicalRegister | int | slice | Sequence[Clbit | int]], *, label: str | None) InstructionSet`; + const componentProps = { + id: "qiskit.circuit.QuantumCircuit.for_loop", + rawSignature, + }; + const extraRawSignatures = [ + { + id: "qiskit.circuit.QuantumCircuit.for_loop", + rawSignature: overloadRawSignature, + }, + ]; + + const resultExpected = { + id: "qiskit.circuit.QuantumCircuit.for_loop", + rawSignature, + extraRawSignatures: [overloadRawSignature], + }; + addExtraSignatures(componentProps, extraRawSignatures); + expect(componentProps).toEqual(resultExpected); + }); + + test("Function without overload signatures", () => { + const rawSignature = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; + const componentProps = { + id: "qiskit.circuit.QuantumCircuit.for_loop", + rawSignature, + }; + const extraRawSignatures: ComponentProps[] = []; + + addExtraSignatures(componentProps, extraRawSignatures); + expect(componentProps).toEqual(componentProps); + }); +}); + +describe("createOpeningTag()", () => { + test("Create Function tag with some props", async () => { + const rawSignature = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; + const componentProps = { + id: "qiskit.circuit.QuantumCircuit.for_loop", + name: "for_loop", + rawSignature, + }; + + const tag = await createOpeningTag("Function", componentProps); + expect(tag).toEqual(` + `); + }); + + test("Create Attribute tag with default value and type hint", async () => { + const componentProps = { + id: "qiskit.circuit.QuantumCircuit.instance", + name: "instance", + attributeTypeHint: "str | None", + attributeValue: "None", + }; + + const tag = await createOpeningTag("Attribute", componentProps); + expect(tag).toEqual(` + `); + }); + + test("Create tag without props", async () => { + const componentProps = { + id: "qiskit.circuit.Sampler", + }; + + const tag = await createOpeningTag("Class", componentProps); + expect(tag).toEqual(` + `); + }); +}); + +describe("prepareGitHubLink()", () => { + test("no link", () => { + const html = `None)#`; + const doc = Doc.load(html); + const result = prepareGitHubLink(doc.$main, false); + expect(result).toEqual(undefined); + doc.expectHtml(html); + }); + + test("link from sphinx.ext.viewcode", () => { + const doc = Doc.load( + `None)[source]#`, + ); + const result = prepareGitHubLink(doc.$main, false); + expect(result).toEqual(`https://ibm.com/my_link`); + doc.expectHtml( + `None)#`, + ); + }); + + test("link from sphinx.ext.linkcode", () => { + const doc = Doc.load( + `None)[source]#`, + ); + const result = prepareGitHubLink(doc.$main, false); + expect(result).toEqual( + `https://github.com/Qiskit/qiskit/blob/stable/1.0/qiskit/utils/deprecation.py#L24-L101`, + ); + doc.expectHtml( + `None)#`, + ); + }); + + test("method link only used when it has line numbers", () => { + const withLinesDoc = Doc.load( + `)[source]`, + ); + const withoutLinesDoc = Doc.load( + `)[source]`, + ); + const withLinesResult = prepareGitHubLink(withLinesDoc.$main, true); + const withoutLinesResult = prepareGitHubLink(withoutLinesDoc.$main, true); + + expect(withLinesResult).toEqual( + `https://github.com/Qiskit/qiskit-ibm-provider/tree/stable/0.10/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py#L91-L117`, + ); + expect(withoutLinesResult).toEqual(undefined); + + const strippedHtml = `)`; + withLinesDoc.expectHtml(strippedHtml); + withoutLinesDoc.expectHtml(strippedHtml); + }); +}); diff --git a/scripts/lib/api/generateApiComponents.ts b/scripts/lib/api/generateApiComponents.ts new file mode 100644 index 00000000000..0f1b991b7db --- /dev/null +++ b/scripts/lib/api/generateApiComponents.ts @@ -0,0 +1,138 @@ +// This code is a Qiskit project. +// +// (C) Copyright IBM 2024. +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +import { CheerioAPI, Cheerio } from "cheerio"; +import { unified } from "unified"; +import rehypeParse from "rehype-parse"; +import rehypeRemark from "rehype-remark"; +import remarkStringify from "remark-stringify"; + +import { APOSTROPHE_HEX_CODE } from "../stringUtils"; + +export type ComponentProps = { + id: string; + name?: string; + attributeTypeHint?: string; + attributeValue?: string; + githubSourceLink?: string; + rawSignature?: string; + extraRawSignatures?: string[]; +}; + +// ------------------------------------------------------------------ +// Generate MDX components +// ------------------------------------------------------------------ + +/** + * Creates the opening tag of the API components. The function sets all possible + * props values even if they are empty or undefined. All the props without value + * will be removed when generating the markdown file in `htmlToMd.ts`. + */ +export async function createOpeningTag( + tagName: string, + props: ComponentProps, +): Promise { + const type = props.attributeTypeHint?.replaceAll("'", APOSTROPHE_HEX_CODE); + const value = props.attributeValue?.replaceAll("'", APOSTROPHE_HEX_CODE); + const signature = await htmlSignatureToMd(props.rawSignature!); + const extraSignatures: string[] = []; + for (const sig of props.extraRawSignatures ?? []) { + extraSignatures.push( + `${APOSTROPHE_HEX_CODE}${await htmlSignatureToMd( + sig!, + )}${APOSTROPHE_HEX_CODE}`, + ); + } + + return `<${tagName} + id='${props.id}' + name='${props.name}' + attributeTypeHint='${type}' + attributeValue='${value}' + github='${props.githubSourceLink}' + signature='${signature}' + extraSignatures='[${extraSignatures.join(", ")}]' + > + `; +} + +/** + * Removes the original link from sphinx.ext.viewcode and returns the HTML string for our own link. + * + * This returns the HTML string, rather than directly inserting into the HTML, because the insertion + * logic is most easily handled by the calling code. + * + * This function works the same regardless of whether the Sphinx build used `sphinx.ext.viewcode` + * or `sphinx.ext.linkcode` because they have the same HTML structure. + * + * If the link corresponds to a method, we only return a link if it has line numbers included, + * which implies that the link came from `sphinx.ext.linkcode` rather than `sphinx.ext.viewcode`. + * That's because the owning class will already have a link to the relevant file; it's + * noisy and not helpful to repeat the same link without line numbers for the individual methods. + */ +export function prepareGitHubLink( + $child: Cheerio, + isMethod: boolean, +): string | undefined { + const originalLink = $child.find(".viewcode-link").closest("a"); + if (originalLink.length === 0) { + return undefined; + } + const href = originalLink.attr("href")!; + originalLink.first().remove(); + return !isMethod || href.includes(".py#") ? href : undefined; +} + +/** + * Find the element that both matches the `selector` and whose content is the same as `text` + */ +function findByText( + $: CheerioAPI, + $main: Cheerio, + selector: string, + text: string, +): Cheerio { + return $main.find(selector).filter((i, el) => $(el).text().trim() === text); +} + +export function addExtraSignatures( + componentProps: ComponentProps, + extraRawSignatures: ComponentProps[], +): void { + componentProps.extraRawSignatures = [ + ...extraRawSignatures.flatMap((sigProps) => sigProps.rawSignature ?? []), + ]; +} + +/** + * Converts a given HTML into markdown + */ +export async function htmlSignatureToMd( + signatureHtml: string, +): Promise { + if (!signatureHtml) { + return ""; + } + + const html = `

${signatureHtml}

`; + const file = await unified() + .use(rehypeParse) + .use(rehypeRemark) + .use(remarkStringify) + .process(html); + + return String(file) + .replaceAll("\n", "") + .replaceAll("'", APOSTROPHE_HEX_CODE) + .replace(/^`/, "") + .replace(/`$/, ""); +} diff --git a/scripts/lib/stringUtils.ts b/scripts/lib/stringUtils.ts index 531c1eb196d..e43d551364f 100644 --- a/scripts/lib/stringUtils.ts +++ b/scripts/lib/stringUtils.ts @@ -12,6 +12,8 @@ import { last, split } from "lodash"; +export const APOSTROPHE_HEX_CODE = "'"; + export function removePart(text: string, separator: string, matcher: string[]) { return text .split(separator) From dc0f12f9dcaaa4d7073016c5433fc8b748e3c9fd Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Fri, 22 Mar 2024 19:02:53 +0100 Subject: [PATCH 2/8] reduce HTML --- scripts/lib/api/generateApiComponents.test.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/lib/api/generateApiComponents.test.ts b/scripts/lib/api/generateApiComponents.test.ts index 94524f05678..978b87af4dc 100644 --- a/scripts/lib/api/generateApiComponents.test.ts +++ b/scripts/lib/api/generateApiComponents.test.ts @@ -45,30 +45,30 @@ class Doc { } test("htmlSignatureToMd", async () => { - const html = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; + const html = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; const result = await htmlSignatureToMd(html); expect(result).toEqual( - `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) → ForLoopContext[source]`, + `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`, ); }); describe("addExtraSignatures()", () => { test("Function with overload signatures", () => { - const rawSignature = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; - const overloadRawSignature = `for_loop
(indexset: Iterable[int], loop_parameter: Parameter | None, body: QuantumCircuit, qubits: Sequence[Qubit | QuantumRegister | int | slice | Sequence[Qubit | int]], clbits: Sequence[Clbit | ClassicalRegister | int | slice | Sequence[Clbit | int]], *, label: str | None) InstructionSet`; + const rawSignature = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; + const overloadRawSignature = `Estimator.run(circuits, parameter_values=None, **kwargs)`; const componentProps = { - id: "qiskit.circuit.QuantumCircuit.for_loop", + id: "qiskit_ibm_runtime.Estimator.run", rawSignature, }; const extraRawSignatures = [ { - id: "qiskit.circuit.QuantumCircuit.for_loop", + id: "qiskit_ibm_runtime.Estimator.run", rawSignature: overloadRawSignature, }, ]; const resultExpected = { - id: "qiskit.circuit.QuantumCircuit.for_loop", + id: "qiskit_ibm_runtime.Estimator.run", rawSignature, extraRawSignatures: [overloadRawSignature], }; @@ -77,9 +77,9 @@ describe("addExtraSignatures()", () => { }); test("Function without overload signatures", () => { - const rawSignature = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; + const rawSignature = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; const componentProps = { - id: "qiskit.circuit.QuantumCircuit.for_loop", + id: "qiskit_ibm_runtime.Estimator.run", rawSignature, }; const extraRawSignatures: ComponentProps[] = []; @@ -91,21 +91,21 @@ describe("addExtraSignatures()", () => { describe("createOpeningTag()", () => { test("Create Function tag with some props", async () => { - const rawSignature = `for_loop(indexset: Iterable[int], loop_parameter: Parameter | None, body: None, qubits: None, clbits: None, *, label: str | None) ForLoopContext[source]`; + const rawSignature = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; const componentProps = { - id: "qiskit.circuit.QuantumCircuit.for_loop", - name: "for_loop", + id: "qiskit_ibm_runtime.Estimator.run", + name: "run", rawSignature, }; const tag = await createOpeningTag("Function", componentProps); expect(tag).toEqual(` `); @@ -132,7 +132,7 @@ describe("createOpeningTag()", () => { `); }); - test("Create tag without props", async () => { + test("Create Class tag without props", async () => { const componentProps = { id: "qiskit.circuit.Sampler", }; From 5d14f124f34491e75c11b95a74e7878af46ea2b4 Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Fri, 22 Mar 2024 19:06:14 +0100 Subject: [PATCH 3/8] Simplify tests --- scripts/lib/api/generateApiComponents.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/scripts/lib/api/generateApiComponents.test.ts b/scripts/lib/api/generateApiComponents.test.ts index 978b87af4dc..2339496cc1a 100644 --- a/scripts/lib/api/generateApiComponents.test.ts +++ b/scripts/lib/api/generateApiComponents.test.ts @@ -54,33 +54,30 @@ test("htmlSignatureToMd", async () => { describe("addExtraSignatures()", () => { test("Function with overload signatures", () => { - const rawSignature = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; - const overloadRawSignature = `Estimator.run(circuits, parameter_values=None, **kwargs)`; const componentProps = { id: "qiskit_ibm_runtime.Estimator.run", - rawSignature, + rawSignature: "first signature", }; const extraRawSignatures = [ { id: "qiskit_ibm_runtime.Estimator.run", - rawSignature: overloadRawSignature, + rawSignature: "second signature", }, ]; const resultExpected = { id: "qiskit_ibm_runtime.Estimator.run", - rawSignature, - extraRawSignatures: [overloadRawSignature], + rawSignature: "first signature", + extraRawSignatures: ["second signature"], }; addExtraSignatures(componentProps, extraRawSignatures); expect(componentProps).toEqual(resultExpected); }); test("Function without overload signatures", () => { - const rawSignature = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; const componentProps = { id: "qiskit_ibm_runtime.Estimator.run", - rawSignature, + rawSignature: "first signature", }; const extraRawSignatures: ComponentProps[] = []; From 77a99f3ab07ea6fdc7cfe03881a2482660289a39 Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Fri, 22 Mar 2024 19:14:52 +0100 Subject: [PATCH 4/8] add new test --- scripts/lib/api/generateApiComponents.test.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/scripts/lib/api/generateApiComponents.test.ts b/scripts/lib/api/generateApiComponents.test.ts index 2339496cc1a..3c23bcb912d 100644 --- a/scripts/lib/api/generateApiComponents.test.ts +++ b/scripts/lib/api/generateApiComponents.test.ts @@ -44,9 +44,10 @@ class Doc { } } +const RAW_SIGNATURE_EXAMPLE = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; + test("htmlSignatureToMd", async () => { - const html = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; - const result = await htmlSignatureToMd(html); + const result = await htmlSignatureToMd(RAW_SIGNATURE_EXAMPLE); expect(result).toEqual( `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`, ); @@ -88,11 +89,10 @@ describe("addExtraSignatures()", () => { describe("createOpeningTag()", () => { test("Create Function tag with some props", async () => { - const rawSignature = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; const componentProps = { id: "qiskit_ibm_runtime.Estimator.run", name: "run", - rawSignature, + rawSignature: RAW_SIGNATURE_EXAMPLE, }; const tag = await createOpeningTag("Function", componentProps); @@ -108,6 +108,27 @@ describe("createOpeningTag()", () => { `); }); + test("Create Function tag with overloaded signatures", async () => { + const componentProps = { + id: "qiskit_ibm_runtime.Estimator.run", + name: "run", + rawSignature: RAW_SIGNATURE_EXAMPLE, + extraRawSignatures: [RAW_SIGNATURE_EXAMPLE, RAW_SIGNATURE_EXAMPLE], + }; + + const tag = await createOpeningTag("Function", componentProps); + expect(tag).toEqual(` + `); + }); + test("Create Attribute tag with default value and type hint", async () => { const componentProps = { id: "qiskit.circuit.QuantumCircuit.instance", From 1118c9ad3ea8b6ba5a10efb3e154fca740721332 Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Mon, 25 Mar 2024 12:15:56 +0100 Subject: [PATCH 5/8] partial feedback --- scripts/lib/api/generateApiComponents.test.ts | 3 ++- scripts/lib/api/generateApiComponents.ts | 16 +++++++++++----- scripts/lib/stringUtils.test.ts | 6 ++++++ scripts/lib/stringUtils.ts | 4 ++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/lib/api/generateApiComponents.test.ts b/scripts/lib/api/generateApiComponents.test.ts index 3c23bcb912d..03945d2b3e9 100644 --- a/scripts/lib/api/generateApiComponents.test.ts +++ b/scripts/lib/api/generateApiComponents.test.ts @@ -20,6 +20,7 @@ import { addExtraSignatures, createOpeningTag, } from "./generateApiComponents"; +import { APOSTROPHE_HEX_CODE } from "../stringUtils"; class Doc { readonly $: CheerioAPI; @@ -124,7 +125,7 @@ describe("createOpeningTag()", () => { attributeValue='undefined' github='undefined' signature='Estimator.run(circuits, observables, parameter_values=None, **kwargs)' - extraSignatures='['Estimator.run(circuits, observables, parameter_values=None, **kwargs)', 'Estimator.run(circuits, observables, parameter_values=None, **kwargs)']' + extraSignatures='[${APOSTROPHE_HEX_CODE}Estimator.run(circuits, observables, parameter_values=None, **kwargs)${APOSTROPHE_HEX_CODE}, ${APOSTROPHE_HEX_CODE}Estimator.run(circuits, observables, parameter_values=None, **kwargs)${APOSTROPHE_HEX_CODE}]' > `); }); diff --git a/scripts/lib/api/generateApiComponents.ts b/scripts/lib/api/generateApiComponents.ts index 0f1b991b7db..8d5398ad409 100644 --- a/scripts/lib/api/generateApiComponents.ts +++ b/scripts/lib/api/generateApiComponents.ts @@ -41,8 +41,14 @@ export async function createOpeningTag( tagName: string, props: ComponentProps, ): Promise { - const type = props.attributeTypeHint?.replaceAll("'", APOSTROPHE_HEX_CODE); - const value = props.attributeValue?.replaceAll("'", APOSTROPHE_HEX_CODE); + const attributeTypeHint = props.attributeTypeHint?.replaceAll( + "'", + APOSTROPHE_HEX_CODE, + ); + const attributeValue = props.attributeValue?.replaceAll( + "'", + APOSTROPHE_HEX_CODE, + ); const signature = await htmlSignatureToMd(props.rawSignature!); const extraSignatures: string[] = []; for (const sig of props.extraRawSignatures ?? []) { @@ -56,8 +62,8 @@ export async function createOpeningTag( return `<${tagName} id='${props.id}' name='${props.name}' - attributeTypeHint='${type}' - attributeValue='${value}' + attributeTypeHint='${attributeTypeHint}' + attributeValue='${attributeValue}' github='${props.githubSourceLink}' signature='${signature}' extraSignatures='[${extraSignatures.join(", ")}]' @@ -123,7 +129,7 @@ export async function htmlSignatureToMd( return ""; } - const html = `

${signatureHtml}

`; + const html = `${signatureHtml}`; const file = await unified() .use(rehypeParse) .use(rehypeRemark) diff --git a/scripts/lib/stringUtils.test.ts b/scripts/lib/stringUtils.test.ts index 3b42d11cef3..15a3cd71c87 100644 --- a/scripts/lib/stringUtils.test.ts +++ b/scripts/lib/stringUtils.test.ts @@ -17,6 +17,7 @@ import { removePrefix, removeSuffix, getLastPartFromFullIdentifier, + capitalize, } from "./stringUtils"; test("removePart()", () => { @@ -49,3 +50,8 @@ test("getLastPartFromFullIdentifier", () => { expect(getLastPartFromFullIdentifier("my_software")).toEqual("my_software"); expect(getLastPartFromFullIdentifier("")).toEqual(""); }); + +test("capitalize()", () => { + const input = "hello world!"; + expect(capitalize(input)).toEqual("Hello world!"); +}); diff --git a/scripts/lib/stringUtils.ts b/scripts/lib/stringUtils.ts index e43d551364f..d30e9562ed9 100644 --- a/scripts/lib/stringUtils.ts +++ b/scripts/lib/stringUtils.ts @@ -38,3 +38,7 @@ export function removeSuffix(text: string, suffix: string) { export function getLastPartFromFullIdentifier(fullIdentifierName: string) { return last(split(fullIdentifierName, "."))!; } + +export function capitalize(text: string) { + return text.charAt(0).toUpperCase() + text.slice(1); +} From 9c5e81584532bb9895a75d684f9909b363b208f4 Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Mon, 25 Mar 2024 12:21:13 +0100 Subject: [PATCH 6/8] add CheerioDoc --- scripts/lib/api/generateApiComponents.test.ts | 35 ++------- scripts/lib/api/processHtml.test.ts | 75 +++++++------------ scripts/lib/testUtils.ts | 36 +++++++++ 3 files changed, 68 insertions(+), 78 deletions(-) create mode 100644 scripts/lib/testUtils.ts diff --git a/scripts/lib/api/generateApiComponents.test.ts b/scripts/lib/api/generateApiComponents.test.ts index 03945d2b3e9..d4e3d8e2290 100644 --- a/scripts/lib/api/generateApiComponents.test.ts +++ b/scripts/lib/api/generateApiComponents.test.ts @@ -11,7 +11,6 @@ // that they have been altered from the originals. import { expect, test } from "@jest/globals"; -import { CheerioAPI, Cheerio, load as cheerioLoad } from "cheerio"; import { ComponentProps, @@ -21,29 +20,7 @@ import { createOpeningTag, } from "./generateApiComponents"; import { APOSTROPHE_HEX_CODE } from "../stringUtils"; - -class Doc { - readonly $: CheerioAPI; - readonly $main: Cheerio; - - constructor($: CheerioAPI, $main: Cheerio) { - this.$ = $; - this.$main = $main; - } - - static load(html: string): Doc { - const $ = cheerioLoad(`
${html}
`); - return new Doc($, $("[role='main']")); - } - - html(): string { - return this.$main.html()!.trim(); - } - - expectHtml(expected: string): void { - expect(this.html()).toEqual(expected.trim()); - } -} +import { CheerioDoc } from "../testUtils"; const RAW_SIGNATURE_EXAMPLE = `Estimator.run(circuits, observables, parameter_values=None, **kwargs)`; @@ -173,14 +150,14 @@ describe("createOpeningTag()", () => { describe("prepareGitHubLink()", () => { test("no link", () => { const html = `None)#`; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); const result = prepareGitHubLink(doc.$main, false); expect(result).toEqual(undefined); doc.expectHtml(html); }); test("link from sphinx.ext.viewcode", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( `None)[source]#`, ); const result = prepareGitHubLink(doc.$main, false); @@ -191,7 +168,7 @@ describe("prepareGitHubLink()", () => { }); test("link from sphinx.ext.linkcode", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( `None)[source]#`, ); const result = prepareGitHubLink(doc.$main, false); @@ -204,10 +181,10 @@ describe("prepareGitHubLink()", () => { }); test("method link only used when it has line numbers", () => { - const withLinesDoc = Doc.load( + const withLinesDoc = CheerioDoc.load( `)[source]`, ); - const withoutLinesDoc = Doc.load( + const withoutLinesDoc = CheerioDoc.load( `)[source]`, ); const withLinesResult = prepareGitHubLink(withLinesDoc.$main, true); diff --git a/scripts/lib/api/processHtml.test.ts b/scripts/lib/api/processHtml.test.ts index 8a6de44bab0..62992a06965 100644 --- a/scripts/lib/api/processHtml.test.ts +++ b/scripts/lib/api/processHtml.test.ts @@ -10,7 +10,6 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -import { CheerioAPI, Cheerio, load as cheerioLoad } from "cheerio"; import { describe, expect, test } from "@jest/globals"; import { @@ -30,33 +29,11 @@ import { processMembersAndSetMeta, } from "./processHtml"; import { Metadata } from "./Metadata"; - -class Doc { - readonly $: CheerioAPI; - readonly $main: Cheerio; - - constructor($: CheerioAPI, $main: Cheerio) { - this.$ = $; - this.$main = $main; - } - - static load(html: string): Doc { - const $ = cheerioLoad(`
${html}
`); - return new Doc($, $("[role='main']")); - } - - html(): string { - return this.$main.html()!.trim(); - } - - expectHtml(expected: string): void { - expect(this.html()).toEqual(expected.trim()); - } -} +import { CheerioDoc } from "../testUtils"; describe("loadImages()", () => { test("normal file", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( `Logo`, ); const images = loadImages(doc.$, doc.$main, "/my-images", false); @@ -76,7 +53,7 @@ describe("loadImages()", () => { }); test("release note", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( ``, ); const images = loadImages(doc.$, doc.$main, "/my-images/0.45", true); @@ -91,7 +68,7 @@ describe("loadImages()", () => { }); test("handleSphinxDesignCards()", () => { - const doc = Doc.load(` + const doc = CheerioDoc.load(`
Account initialization @@ -143,13 +120,13 @@ test("handleSphinxDesignCards()", () => { }); test("renameAllH1s()", () => { - const doc = Doc.load(`

Release Notes!!!

0.45.0

`); + const doc = CheerioDoc.load(`

Release Notes!!!

0.45.0

`); renameAllH1s(doc.$, "New Title"); doc.expectHtml(`

New Title

0.45.0

`); }); test("removeHtmlExtensionsInRelativeLinks()", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( ``, ); removeHtmlExtensionsInRelativeLinks(doc.$, doc.$main); @@ -159,7 +136,7 @@ test("removeHtmlExtensionsInRelativeLinks()", () => { }); test("removeDownloadSourceCode()", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( `

(Source code)

../_images/converters-1.png @@ -174,7 +151,7 @@ test("removeDownloadSourceCode()", () => { }); test("removePermalinks()", () => { - const doc = Doc.load(`Link + const doc = CheerioDoc.load(`Link Link Link Link @@ -185,7 +162,7 @@ test("removePermalinks()", () => { }); test("removeColonSpans()", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( `
Parameters:
`, ); removeColonSpans(doc.$main); @@ -194,7 +171,7 @@ test("removeColonSpans()", () => { describe("removeMatplotlibFigCaptions()", () => { test("removes
", () => { - const doc = Doc.load(` + const doc = CheerioDoc.load(`
../_images/fake_provider-1_00.png
@@ -230,7 +207,7 @@ describe("removeMatplotlibFigCaptions()", () => { }); test("removes
", () => { - const doc = Doc.load(` + const doc = CheerioDoc.load(`
../_images/qiskit-transpiler-passes-DynamicalDecoupling-1_00.png

@@ -276,14 +253,14 @@ describe("removeMatplotlibFigCaptions()", () => {

My caption

`; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); removeMatplotlibFigCaptions(doc.$main); doc.expectHtml(html); }); }); test("addLanguageClassToCodeBlocks()", () => { - const doc1 = Doc.load(`

Circuit symbol:

+ const doc1 = CheerioDoc.load(`

Circuit symbol:

     ┌──────────┐
     q_0: ┤ U(ϴ,φ,λ) ├
         └──────────┘
@@ -301,7 +278,7 @@ test("addLanguageClassToCodeBlocks()", () => {
     
`); - const doc2 = Doc.load(`
+ const doc2 = CheerioDoc.load(`
from qiskit_ibm_runtime.options import Options
 
@@ -324,7 +301,7 @@ test("addLanguageClassToCodeBlocks()", () => {
 
 test("replaceSourceLinksWithGitHub()", () => {
   // Assumes that removeHtmlExtensionsInRelativeLinks() has already removed .html from the URL.
-  const doc = Doc.load(
+  const doc = CheerioDoc.load(
     `
     
     `,
@@ -343,7 +320,7 @@ test("replaceSourceLinksWithGitHub()", () => {
 });
 
 test("convertRubricsToHeaders()", () => {
-  const doc = Doc.load(`

Example

+ const doc = CheerioDoc.load(`

Example

Examples

References

Reference

@@ -366,7 +343,7 @@ describe("maybeSetModuleMetadata()", () => { test("not a module", () => { const html = `

Hello

`; const meta: Metadata = {}; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); maybeSetModuleMetadata(doc.$, doc.$main, meta); doc.expectHtml(html); expect(meta).toEqual({}); @@ -374,7 +351,7 @@ describe("maybeSetModuleMetadata()", () => { const checkModuleFound = (html: string, name: string): void => { const meta: Metadata = {}; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); maybeSetModuleMetadata(doc.$, doc.$main, meta); doc.expectHtml(html); expect(meta).toEqual({ @@ -409,14 +386,14 @@ describe("maybeSetModuleMetadata()", () => { describe("prepareGitHubLink()", () => { test("no link", () => { const html = `None)#`; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); const result = prepareGitHubLink(doc.$main, false); expect(result).toEqual(""); doc.expectHtml(html); }); test("link from sphinx.ext.viewcode", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( `None)[source]#`, ); const result = prepareGitHubLink(doc.$main, false); @@ -429,7 +406,7 @@ describe("prepareGitHubLink()", () => { }); test("link from sphinx.ext.linkcode", () => { - const doc = Doc.load( + const doc = CheerioDoc.load( `None)[source]#`, ); const result = prepareGitHubLink(doc.$main, false); @@ -442,10 +419,10 @@ describe("prepareGitHubLink()", () => { }); test("method link only used when it has line numbers", () => { - const withLinesDoc = Doc.load( + const withLinesDoc = CheerioDoc.load( `)[source]`, ); - const withoutLinesDoc = Doc.load( + const withoutLinesDoc = CheerioDoc.load( `)[source]`, ); const withLinesResult = prepareGitHubLink(withLinesDoc.$main, true); @@ -476,7 +453,7 @@ describe("processMembersAndSetMeta()", () => {
  • copy_operations – Deep copy the operation objects in the QuantumCircuit for the output DAGCircuit.

  • `; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); const meta: Metadata = {}; processMembersAndSetMeta(doc.$, doc.$main, meta); doc.expectHtml(`

    Circuit Converters

    @@ -523,7 +500,7 @@ backends may not have this attribute.

    `; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); const meta: Metadata = {}; processMembersAndSetMeta(doc.$, doc.$main, meta); doc.expectHtml(`

    least_busy

    @@ -583,7 +560,7 @@ minimal install. You can read more about those, and ways to check for their pre particular error, which subclasses both QiskitError and the Python built-in ImportError.

    `; - const doc = Doc.load(html); + const doc = CheerioDoc.load(html); const meta: Metadata = {}; processMembersAndSetMeta(doc.$, doc.$main, meta); doc.expectHtml(`
    diff --git a/scripts/lib/testUtils.ts b/scripts/lib/testUtils.ts new file mode 100644 index 00000000000..d3a8469fcb0 --- /dev/null +++ b/scripts/lib/testUtils.ts @@ -0,0 +1,36 @@ +// This code is a Qiskit project. +// +// (C) Copyright IBM 2024. +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +import { CheerioAPI, Cheerio, load as cheerioLoad } from "cheerio"; + +export class CheerioDoc { + readonly $: CheerioAPI; + readonly $main: Cheerio; + + constructor($: CheerioAPI, $main: Cheerio) { + this.$ = $; + this.$main = $main; + } + + static load(html: string): CheerioDoc { + const $ = cheerioLoad(`
    ${html}
    `); + return new CheerioDoc($, $("[role='main']")); + } + + html(): string { + return this.$main.html()!.trim(); + } + + expectHtml(expected: string): void { + expect(this.html()).toEqual(expected.trim()); + } +} From 27cffc9719b79faa46565b9e7621f792acaca46e Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Mon, 25 Mar 2024 15:24:24 +0100 Subject: [PATCH 7/8] deduplicate code --- scripts/lib/api/generateApiComponents.ts | 2 +- scripts/lib/api/processHtml.test.ts | 57 ------------------------ scripts/lib/api/processHtml.ts | 57 +++++------------------- 3 files changed, 11 insertions(+), 105 deletions(-) diff --git a/scripts/lib/api/generateApiComponents.ts b/scripts/lib/api/generateApiComponents.ts index 8d5398ad409..8e43bba2772 100644 --- a/scripts/lib/api/generateApiComponents.ts +++ b/scripts/lib/api/generateApiComponents.ts @@ -101,7 +101,7 @@ export function prepareGitHubLink( /** * Find the element that both matches the `selector` and whose content is the same as `text` */ -function findByText( +export function findByText( $: CheerioAPI, $main: Cheerio, selector: string, diff --git a/scripts/lib/api/processHtml.test.ts b/scripts/lib/api/processHtml.test.ts index 62992a06965..d73db331d9d 100644 --- a/scripts/lib/api/processHtml.test.ts +++ b/scripts/lib/api/processHtml.test.ts @@ -25,7 +25,6 @@ import { removeMatplotlibFigCaptions, replaceViewcodeLinksWithGitHub, convertRubricsToHeaders, - prepareGitHubLink, processMembersAndSetMeta, } from "./processHtml"; import { Metadata } from "./Metadata"; @@ -383,62 +382,6 @@ describe("maybeSetModuleMetadata()", () => { }); }); -describe("prepareGitHubLink()", () => { - test("no link", () => { - const html = `None)#`; - const doc = CheerioDoc.load(html); - const result = prepareGitHubLink(doc.$main, false); - expect(result).toEqual(""); - doc.expectHtml(html); - }); - - test("link from sphinx.ext.viewcode", () => { - const doc = CheerioDoc.load( - `None)[source]#`, - ); - const result = prepareGitHubLink(doc.$main, false); - expect(result).toEqual( - ` GitHub`, - ); - doc.expectHtml( - `None)#`, - ); - }); - - test("link from sphinx.ext.linkcode", () => { - const doc = CheerioDoc.load( - `None)[source]#`, - ); - const result = prepareGitHubLink(doc.$main, false); - expect(result).toEqual( - ` GitHub`, - ); - doc.expectHtml( - `None)#`, - ); - }); - - test("method link only used when it has line numbers", () => { - const withLinesDoc = CheerioDoc.load( - `)[source]`, - ); - const withoutLinesDoc = CheerioDoc.load( - `)[source]`, - ); - const withLinesResult = prepareGitHubLink(withLinesDoc.$main, true); - const withoutLinesResult = prepareGitHubLink(withoutLinesDoc.$main, true); - - expect(withLinesResult).toEqual( - ` GitHub`, - ); - expect(withoutLinesResult).toEqual(""); - - const strippedHtml = `)`; - withLinesDoc.expectHtml(strippedHtml); - withoutLinesDoc.expectHtml(strippedHtml); - }); -}); - describe("processMembersAndSetMeta()", () => { test("function with added heading", () => { const html = `

    Circuit Converters

    diff --git a/scripts/lib/api/processHtml.ts b/scripts/lib/api/processHtml.ts index 543b2f61f16..217933568f2 100644 --- a/scripts/lib/api/processHtml.ts +++ b/scripts/lib/api/processHtml.ts @@ -15,6 +15,7 @@ import { CheerioAPI, Cheerio, load } from "cheerio"; import { Image } from "./HtmlToMdResult"; import { Metadata, ApiType } from "./Metadata"; import { getLastPartFromFullIdentifier } from "../stringUtils"; +import { findByText, prepareGitHubLink } from "./generateApiComponents"; export type ProcessedHtml = { html: string; @@ -314,28 +315,31 @@ function processMember( apiType: string, id: string, ) { - const githubSourceLink = prepareGitHubLink($child, apiType === "method"); + const githubUrl = prepareGitHubLink($child, apiType === "method"); + const gitHubSourceLink = githubUrl + ? ` GitHub` + : ""; findByText($, $main, "em.property", apiType).remove(); if (apiType == "class") { - return `

    ${$child.html()}${githubSourceLink}

    `; + return `

    ${$child.html()}${gitHubSourceLink}

    `; } if (apiType == "property") { - return processProperty($child, $dl, priorApiType, id, githubSourceLink); + return processProperty($child, $dl, priorApiType, id, gitHubSourceLink); } if (apiType == "method") { - return processMethod($, $child, $dl, priorApiType, id, githubSourceLink); + return processMethod($, $child, $dl, priorApiType, id, gitHubSourceLink); } if (apiType == "attribute") { - return processAttribute($child, $dl, priorApiType, id, githubSourceLink); + return processAttribute($child, $dl, priorApiType, id, gitHubSourceLink); } if (apiType === "function" || apiType === "exception") { - return processFunctionOrException($child, $dl, id, githubSourceLink); + return processFunctionOrException($child, $dl, id, gitHubSourceLink); } throw new Error(`Unhandled Python type: ${apiType}`); @@ -448,35 +452,6 @@ function processFunctionOrException( return `

    ${apiName}

    ${descriptionHtml}`; } -/** - * Removes the original link from sphinx.ext.viewcode and returns the HTML string for our own link. - * - * This returns the HTML string, rather than directly inserting into the HTML, because the insertion - * logic is most easily handled by the calling code. - * - * This function works the same regardless of whether the Sphinx build used `sphinx.ext.viewcode` - * or `sphinx.ext.linkcode` because they have the same HTML structure. - * - * If the link corresponds to a method, we only return a link if it has line numbers included, - * which implies that the link came from `sphinx.ext.linkcode` rather than `sphinx.ext.viewcode`. - * That's because the owning class will already have a link to the relevant file; it's - * noisy and not helpful to repeat the same link without line numbers for the individual methods. - */ -export function prepareGitHubLink( - $child: Cheerio, - isMethod: boolean, -): string { - const originalLink = $child.find(".viewcode-link").closest("a"); - if (originalLink.length === 0) { - return ""; - } - const href = originalLink.attr("href")!; - originalLink.first().remove(); - return !isMethod || href.includes(".py#") - ? ` GitHub` - : ""; -} - export function maybeSetModuleMetadata( $: CheerioAPI, $main: Cheerio, @@ -531,18 +506,6 @@ export function updateModuleHeadings( }); } -/** - * Find the element that both matches the `selector` and whose content is the same as `text` - */ -function findByText( - $: CheerioAPI, - $main: Cheerio, - selector: string, - text: string, -): Cheerio { - return $main.find(selector).filter((i, el) => $(el).text().trim() === text); -} - function getApiType($dl: Cheerio): ApiType | undefined { for (const className of [ "function", From 069585c71578fe452a818a746d67f51e3ef6f860 Mon Sep 17 00:00:00 2001 From: Arnau Casau Date: Mon, 25 Mar 2024 15:25:28 +0100 Subject: [PATCH 8/8] fix github name --- scripts/lib/api/processHtml.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/lib/api/processHtml.ts b/scripts/lib/api/processHtml.ts index 217933568f2..c5e41c7e36f 100644 --- a/scripts/lib/api/processHtml.ts +++ b/scripts/lib/api/processHtml.ts @@ -316,30 +316,30 @@ function processMember( id: string, ) { const githubUrl = prepareGitHubLink($child, apiType === "method"); - const gitHubSourceLink = githubUrl + const githubSourceLink = githubUrl ? ` GitHub` : ""; findByText($, $main, "em.property", apiType).remove(); if (apiType == "class") { - return `

    ${$child.html()}${gitHubSourceLink}

    `; + return `

    ${$child.html()}${githubSourceLink}

    `; } if (apiType == "property") { - return processProperty($child, $dl, priorApiType, id, gitHubSourceLink); + return processProperty($child, $dl, priorApiType, id, githubSourceLink); } if (apiType == "method") { - return processMethod($, $child, $dl, priorApiType, id, gitHubSourceLink); + return processMethod($, $child, $dl, priorApiType, id, githubSourceLink); } if (apiType == "attribute") { - return processAttribute($child, $dl, priorApiType, id, gitHubSourceLink); + return processAttribute($child, $dl, priorApiType, id, githubSourceLink); } if (apiType === "function" || apiType === "exception") { - return processFunctionOrException($child, $dl, id, gitHubSourceLink); + return processFunctionOrException($child, $dl, id, githubSourceLink); } throw new Error(`Unhandled Python type: ${apiType}`);