-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add generateApiComponents.ts
and tests
#1074
Changes from all commits
ce2f582
dc0f12f
5d14f12
77a99f3
1118c9a
9c5e815
27cffc9
069585c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
}); | ||
}); |
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 ?? []), | ||
]; | ||
Comment on lines
+117
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Also can There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In reality,
This is a good idea. I think we could remove the |
||
} | ||
|
||
/** | ||
* 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(/`$/, ""); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big deal if this is going to be annoying to implement, but you can switch processHtml.md to use this now, along with
findByText
. That way we don't duplicate. And you could move the tests too if it's too hard.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it probably would be good to do this if isn't too much of a pain because it may take a few days to land the follow up PR since it's blocked by the Docker image being updated for staging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!