-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
generateApiComponents.ts
and tests (#1074)
Part of #1008 This PR is a precursor of #1026, and it creates a new script named `generateApiComponents.ts` with some tests. This script will have all the logic to generate the MDX components, but, in the meantime, this PR adds some functions that can be tested independently by just receiving, as parameters, a list of props or HTML code. It also copies over the functions `prepareGitHubLink` and `findByText` from `processHtml.ts`
- Loading branch information
1 parent
f3a730c
commit 3f0855d
Showing
7 changed files
with
420 additions
and
143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
// 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 { | ||
ComponentProps, | ||
prepareGitHubLink, | ||
htmlSignatureToMd, | ||
addExtraSignatures, | ||
createOpeningTag, | ||
} from "./generateApiComponents"; | ||
import { APOSTROPHE_HEX_CODE } from "../stringUtils"; | ||
import { CheerioDoc } from "../testUtils"; | ||
|
||
const RAW_SIGNATURE_EXAMPLE = `<span class='sig-prename descclassname'><span class='pre'>Estimator.</span></span><span class='sig-name descname'><span class='pre'>run</span></span><span class='sig-paren'>(</span><em class='sig-param'><span class='n'><span class='pre'>circuits</span></span></em>, <em class='sig-param'><span class='n'><span class='pre'>observables</span></span></em>, <em class='sig-param'><span class='n'><span class='pre'>parameter_values</span></span><span class='o'><span class='pre'>=</span></span><span class='default_value'><span class='pre'>None</span></span></em>, <em class='sig-param'><span class='o'><span class='pre'>**</span></span><span class='n'><span class='pre'>kwargs</span></span></em><span class='sig-paren'>)</span></dt>`; | ||
|
||
test("htmlSignatureToMd", async () => { | ||
const result = await htmlSignatureToMd(RAW_SIGNATURE_EXAMPLE); | ||
expect(result).toEqual( | ||
`Estimator.run(circuits, observables, parameter_values=None, **kwargs)`, | ||
); | ||
}); | ||
|
||
describe("addExtraSignatures()", () => { | ||
test("Function with overload signatures", () => { | ||
const componentProps = { | ||
id: "qiskit_ibm_runtime.Estimator.run", | ||
rawSignature: "first signature", | ||
}; | ||
const extraRawSignatures = [ | ||
{ | ||
id: "qiskit_ibm_runtime.Estimator.run", | ||
rawSignature: "second signature", | ||
}, | ||
]; | ||
|
||
const resultExpected = { | ||
id: "qiskit_ibm_runtime.Estimator.run", | ||
rawSignature: "first signature", | ||
extraRawSignatures: ["second signature"], | ||
}; | ||
addExtraSignatures(componentProps, extraRawSignatures); | ||
expect(componentProps).toEqual(resultExpected); | ||
}); | ||
|
||
test("Function without overload signatures", () => { | ||
const componentProps = { | ||
id: "qiskit_ibm_runtime.Estimator.run", | ||
rawSignature: "first signature", | ||
}; | ||
const extraRawSignatures: ComponentProps[] = []; | ||
|
||
addExtraSignatures(componentProps, extraRawSignatures); | ||
expect(componentProps).toEqual(componentProps); | ||
}); | ||
}); | ||
|
||
describe("createOpeningTag()", () => { | ||
test("Create Function tag with some props", async () => { | ||
const componentProps = { | ||
id: "qiskit_ibm_runtime.Estimator.run", | ||
name: "run", | ||
rawSignature: RAW_SIGNATURE_EXAMPLE, | ||
}; | ||
|
||
const tag = await createOpeningTag("Function", componentProps); | ||
expect(tag).toEqual(`<Function | ||
id='qiskit_ibm_runtime.Estimator.run' | ||
name='run' | ||
attributeTypeHint='undefined' | ||
attributeValue='undefined' | ||
github='undefined' | ||
signature='Estimator.run(circuits, observables, parameter_values=None, **kwargs)' | ||
extraSignatures='[]' | ||
> | ||
`); | ||
}); | ||
|
||
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(`<Function | ||
id='qiskit_ibm_runtime.Estimator.run' | ||
name='run' | ||
attributeTypeHint='undefined' | ||
attributeValue='undefined' | ||
github='undefined' | ||
signature='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}]' | ||
> | ||
`); | ||
}); | ||
|
||
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(`<Attribute | ||
id='qiskit.circuit.QuantumCircuit.instance' | ||
name='instance' | ||
attributeTypeHint='str | None' | ||
attributeValue='None' | ||
github='undefined' | ||
signature='' | ||
extraSignatures='[]' | ||
> | ||
`); | ||
}); | ||
|
||
test("Create Class tag without props", async () => { | ||
const componentProps = { | ||
id: "qiskit.circuit.Sampler", | ||
}; | ||
|
||
const tag = await createOpeningTag("Class", componentProps); | ||
expect(tag).toEqual(`<Class | ||
id='qiskit.circuit.Sampler' | ||
name='undefined' | ||
attributeTypeHint='undefined' | ||
attributeValue='undefined' | ||
github='undefined' | ||
signature='' | ||
extraSignatures='[]' | ||
> | ||
`); | ||
}); | ||
}); | ||
|
||
describe("prepareGitHubLink()", () => { | ||
test("no link", () => { | ||
const html = `<span class="pre">None</span><span class="sig-paren">)</span><a class="headerlink" href="#qiskit_ibm_runtime.IBMBackend" title="Link to this definition">#</a>`; | ||
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 = CheerioDoc.load( | ||
`<span class="pre">None</span><span class="sig-paren">)</span><a class="reference internal" href="https://ibm.com/my_link"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#qiskit_ibm_runtime.IBMBackend" title="Link to this definition">#</a>`, | ||
); | ||
const result = prepareGitHubLink(doc.$main, false); | ||
expect(result).toEqual(`https://ibm.com/my_link`); | ||
doc.expectHtml( | ||
`<span class="pre">None</span><span class="sig-paren">)</span><a class="headerlink" href="#qiskit_ibm_runtime.IBMBackend" title="Link to this definition">#</a>`, | ||
); | ||
}); | ||
|
||
test("link from sphinx.ext.linkcode", () => { | ||
const doc = CheerioDoc.load( | ||
`<span class="pre">None</span><span class="sig-paren">)</span><a class="reference external" href="https://github.com/Qiskit/qiskit/blob/stable/1.0/qiskit/utils/deprecation.py#L24-L101"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#qiskit_ibm_runtime.IBMBackend" title="Link to this definition">#</a>`, | ||
); | ||
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( | ||
`<span class="pre">None</span><span class="sig-paren">)</span><a class="headerlink" href="#qiskit_ibm_runtime.IBMBackend" title="Link to this definition">#</a>`, | ||
); | ||
}); | ||
|
||
test("method link only used when it has line numbers", () => { | ||
const withLinesDoc = CheerioDoc.load( | ||
`<span class="sig-paren">)</span><a class="reference external" href="https://github.com/Qiskit/qiskit-ibm-provider/tree/stable/0.10/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py#L91-L117"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#qiskit_ibm_provider.transpiler.passes.scheduling.PadDelay.run" title="Link to this definition">¶</a>`, | ||
); | ||
const withoutLinesDoc = CheerioDoc.load( | ||
`<span class="sig-paren">)</span><a class="reference external" href="https://github.com/Qiskit/qiskit-ibm-provider/tree/stable/0.10/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#qiskit_ibm_provider.transpiler.passes.scheduling.PadDelay.run" title="Link to this definition">¶</a>`, | ||
); | ||
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 = `<span class="sig-paren">)</span><a class="headerlink" href="#qiskit_ibm_provider.transpiler.passes.scheduling.PadDelay.run" title="Link to this definition">¶</a>`; | ||
withLinesDoc.expectHtml(strippedHtml); | ||
withoutLinesDoc.expectHtml(strippedHtml); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// 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<string> { | ||
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 ?? []) { | ||
extraSignatures.push( | ||
`${APOSTROPHE_HEX_CODE}${await htmlSignatureToMd( | ||
sig!, | ||
)}${APOSTROPHE_HEX_CODE}`, | ||
); | ||
} | ||
|
||
return `<${tagName} | ||
id='${props.id}' | ||
name='${props.name}' | ||
attributeTypeHint='${attributeTypeHint}' | ||
attributeValue='${attributeValue}' | ||
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<any>, | ||
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` | ||
*/ | ||
export function findByText( | ||
$: CheerioAPI, | ||
$main: Cheerio<any>, | ||
selector: string, | ||
text: string, | ||
): Cheerio<any> { | ||
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<string> { | ||
if (!signatureHtml) { | ||
return ""; | ||
} | ||
|
||
const html = `<code>${signatureHtml}</code>`; | ||
const file = await unified() | ||
.use(rehypeParse) | ||
.use(rehypeRemark) | ||
.use(remarkStringify) | ||
.process(html); | ||
|
||
return String(file) | ||
.replaceAll("\n", "") | ||
.replaceAll("'", APOSTROPHE_HEX_CODE) | ||
.replace(/^`/, "") | ||
.replace(/`$/, ""); | ||
} |
Oops, something went wrong.