From 2f21b3b743d9b0c7034f20aa387dc8aa03d7b0d9 Mon Sep 17 00:00:00 2001 From: Arnau Casau <47946624+arnaucasau@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:50 +0200 Subject: [PATCH] API generation script uses MDX components (#1026) Part of #1008 This PR teaches our API generation script to generate MDX components that will define the styling of the classes, properties, attributes, methods, functions, and exceptions. The script will generate custom tags to wrap all the content and store useful information as props. This is an example of git diff for an Attribute: ```diff - ### dtm - Return the system time resolution of output signals :returns: The output signal timestep in seconds. :rtype: dtm + ### dtm + + Return the system time resolution of output signals :returns: The output signal timestep in seconds. :rtype: dtm + ``` The script continues generating headers for every apiType that needs it, but this will be removed in a follow-up. ### Details The PR adds a new script named `generateMdxComponents.ts` that will generate all the components. The main function is `processMdxComponent` where we prepare the necessary props for each component and then return an open tag and a closed tag ready for its use. To process the new tags, new handlers have been added to `htmlToMd.ts`. --------- Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> --- .../conversionPipeline.test.ts.snap | 286 ++++++++---------- scripts/lib/api/generateApiComponents.test.ts | 6 +- scripts/lib/api/generateApiComponents.ts | 243 ++++++++++++++- scripts/lib/api/htmlToMd.test.ts | 122 ++++---- scripts/lib/api/htmlToMd.ts | 74 ++++- scripts/lib/api/mergeClassMembers.test.ts | 75 ++--- scripts/lib/api/mergeClassMembers.ts | 37 ++- scripts/lib/api/processHtml.test.ts | 35 +-- scripts/lib/api/processHtml.ts | 203 +++---------- 9 files changed, 617 insertions(+), 464 deletions(-) diff --git a/scripts/lib/api/__snapshots__/conversionPipeline.test.ts.snap b/scripts/lib/api/__snapshots__/conversionPipeline.test.ts.snap index 1186fa329d1..a81d590e2b0 100644 --- a/scripts/lib/api/__snapshots__/conversionPipeline.test.ts.snap +++ b/scripts/lib/api/__snapshots__/conversionPipeline.test.ts.snap @@ -54,123 +54,111 @@ python_api_name: api_example.Electron # Electron - + + Bases: \`object\` -\`api_example.Electron(size=None, name=None)\` [GitHub](https://github.com/Qiskit/qiskit_sphinx_theme/tree/stable/0.1/api_example/electron.py "view source code") + A representation of an electron. -Bases: \`object\` + **Examples** -A representation of an electron. + \`\`\`python + from api_example import Electron -**Examples** + ELECTRON = Electron(size="2GB", name="QuantumComputing") + \`\`\` -\`\`\`python -from api_example import Electron + ### size -ELECTRON = Electron(size="2GB", name="QuantumComputing") -\`\`\` + + How big the Electron is. - + **Type** -### size + str + -How big the Electron is. + ### name -**Type** + + What the Electron is called. + -str + Create an electron. - + **Parameters** -### name + * **size** (*str*) – How big should this thing be? + * **name** (*str*) – The name we’ll call the electron. Nicknames preferred. -What the Electron is called. + **Raises** -Create an electron. + **ValueError** – You did something wrong -**Parameters** + ## Attributes -* **size** (*str*) – How big should this thing be? -* **name** (*str*) – The name we’ll call the electron. Nicknames preferred. + ### CLASS\\_ATTRIBUTE -**Raises** + -**ValueError** – You did something wrong + ### charge -## Attributes + - + ### mass -### CLASS\\_ATTRIBUTE + + The mass of the electron. + -\`= re.compile('abc')\` + ### really\\_long\\_named\\_attribute\\_that\\_probably\\_does\\_not\\_fit\\_nicely - + + A bit too verbose if you ask me. + -### charge + ## Methods - + ### compute\\_momentum -### mass + + Compute the electron’s velocity. -The mass of the electron. + **Parameters** - + **velocity** (*float*) – The electron’s velocity -### really\\_long\\_named\\_attribute\\_that\\_probably\\_does\\_not\\_fit\\_nicely + **Returns** -A bit too verbose if you ask me. + The computed momentum. -## Methods + **Raises** -### compute\\_momentum + **ValueError** – You did something wrong - + **Return type** -\`compute_momentum(velocity)\` + float + -Compute the electron’s velocity. + ### method\\_with\\_a\\_reallyyreallreallyreallyreallyreallyreallreallyreallyreallyreally\\_long\\_title -**Parameters** + + blahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblah + -**velocity** (*float*) – The electron’s velocity + ### overloaded\\_func -**Returns** + + This is meant to test out [https://github.com/Qiskit/qiskit\\_sphinx\\_theme/pull/319](https://github.com/Qiskit/qiskit_sphinx_theme/pull/319). -The computed momentum. + **Parameters** -**Raises** - -**ValueError** – You did something wrong - -**Return type** - -float - -### method\\_with\\_a\\_reallyyreallreallyreallyreallyreallyreallreallyreallyreallyreally\\_long\\_title - - - -\`method_with_a_reallyyreallreallyreallyreallyreallyreallreallyreallyreallyreally_long_title()\` - -blahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblah - -### overloaded\\_func - - - -\`overloaded_func(arg1: tuple[str, str], arg2: list[str], arg3: int, arg4: Electron) → None\` - -\`overloaded_func(arg1: tuple[int, int], arg2: list[int], arg3: bool, arg4: set[Electron]) → None\` - -This is meant to test out [https://github.com/Qiskit/qiskit\\_sphinx\\_theme/pull/319](https://github.com/Qiskit/qiskit_sphinx_theme/pull/319). - -**Parameters** - -* **arg1** – Tuples ftw! -* **arg2** – But lists are more flexy. -* **arg3** – Primitive values are good too. -* **arg4** – Recursionnnnn. + * **arg1** – Tuples ftw! + * **arg2** – But lists are more flexy. + * **arg3** – Primitive values are good too. + * **arg4** – Recursionnnnn. + + " `; @@ -188,24 +176,22 @@ python_api_name: api_example.my_function # api\\_example.my\\_function - - -\`api_example.my_function(input1, input2, input3=None, **kwargs)\` [GitHub](https://github.com/Qiskit/qiskit_sphinx_theme/tree/stable/0.1/api_example.py "view source code") + + A function that does awesome stuff. -A function that does awesome stuff. + **Returns** -**Returns** + Did the function work. -Did the function work. + **Raises** -**Raises** + * **ValueError** – If the inputs are not the correct values. + * **TypeError** – If the inputs are not strings. -* **ValueError** – If the inputs are not the correct values. -* **TypeError** – If the inputs are not strings. + **Return type** -**Return type** - -int + int + " `; @@ -227,94 +213,82 @@ This is a page to test out how we render classes and functions included inline i Every time you use this program, you’ll want to create an instance of [\`api_example.inline_classes.SimpleInlineClass\`](#api_example.inline_classes.SimpleInlineClass "api_example.inline_classes.SimpleInlineClass"). It has a simple interface: - - -\`api_example.inline_classes.SimpleInlineClass(arg1)\` [GitHub](https://github.com/Qiskit/qiskit_sphinx_theme/tree/stable/0.1/api_example/inline_classes.py "view source code") + + This is a simple class that does not have any methods or attributes. -This is a simple class that does not have any methods or attributes. - -It only has the class description and constructor. Many classes in Qiskit docs are simple like this. + It only has the class description and constructor. Many classes in Qiskit docs are simple like this. + It can be useful to use free functions rather than the class: ### -\`api_example.my_function(input1, input2, input3=None, **kwargs)\` [GitHub](https://github.com/Qiskit/qiskit_sphinx_theme/tree/stable/0.1/api_example.py "view source code") - -A function that does awesome stuff. + + A function that does awesome stuff. -**Returns** + **Returns** -Did the function work. + Did the function work. -**Raises** + **Raises** -* **ValueError** – If the inputs are not the correct values. -* **TypeError** – If the inputs are not strings. + * **ValueError** – If the inputs are not the correct values. + * **TypeError** – If the inputs are not strings. -**Return type** + **Return type** -int + int + Sometimes, you even need to use a really complex class! - - -\`api_example.inline_classes.InlineClassWithMethods\` [GitHub](https://github.com/Qiskit/qiskit_sphinx_theme/tree/stable/0.1/api_example/inline_classes.py "view source code") - -Bases: \`object\` - -This class is more involved. - -Note how the methods and attributes are rendered and indented when this class is inlined on the docs page. + + Bases: \`object\` -## Attributes + This class is more involved. - + Note how the methods and attributes are rendered and indented when this class is inlined on the docs page. -### CLASS\\_ATTRIBUTE + ## Attributes -\`str\` + ### CLASS\\_ATTRIBUTE -\`= 'An important part of any API.'\` + - + ### interest\\_rate -### interest\\_rate + -## Methods + ## Methods -### method1 + ### method1 - + + A simple method. -\`method1()\` + **Return type** -A simple method. + int + -**Return type** + ### method2 -int + + A method with a lot of args! -### method2 + This method will use a Hamiltonian to reach quantum advantage. Hamilton: great play & the secret to quantum computing. What a polymath. - + **Parameters** -\`method2(arg1, arg2, description)\` + * **arg1** (*int | float*) – All numbers accepted. + * **arg2** (*list\\[*[*InlineClassWithMethods*](#api_example.inline_classes.InlineClassWithMethods "api_example.inline_classes.InlineClassWithMethods")*]*) – A list of other instances, although these will be discarded. + * **description** (*str*) – If your description is too boring or too cryptic, this program will crash your computer. -A method with a lot of args! + **Return type** -This method will use a Hamiltonian to reach quantum advantage. Hamilton: great play & the secret to quantum computing. What a polymath. - -**Parameters** - -* **arg1** (*int | float*) – All numbers accepted. -* **arg2** (*list\\[*[*InlineClassWithMethods*](#api_example.inline_classes.InlineClassWithMethods "api_example.inline_classes.InlineClassWithMethods")*]*) – A list of other instances, although these will be discarded. -* **description** (*str*) – If your description is too boring or too cryptic, this program will crash your computer. - -**Return type** - -tuple\\[int, [*InlineClassWithMethods*](#api_example.inline_classes.InlineClassWithMethods "api_example.inline_classes.InlineClassWithMethods")] + tuple\\[int, [*InlineClassWithMethods*](#api_example.inline_classes.InlineClassWithMethods "api_example.inline_classes.InlineClassWithMethods")] + + ### Warning: exceptions @@ -322,11 +296,9 @@ The above APIs might raise an exception! Be careful! ### CustomException - - -\`api_example.inline_classes.CustomException\` [GitHub](https://github.com/Qiskit/qiskit_sphinx_theme/tree/stable/0.1/api_example/inline_classes.py "view source code") - -See how exceptions render too. + + See how exceptions render too. + ## Other prose @@ -373,22 +345,22 @@ Testing internal references… [\`Electron.compute_momentum()\`](api_example.Ele ### -\`api_example.my_function(input1, input2, input3=None, **kwargs)\` [GitHub](https://github.com/Qiskit/qiskit_sphinx_theme/tree/stable/0.1/api_example.py "view source code") - -A function that does awesome stuff. + + A function that does awesome stuff. -**Returns** + **Returns** -Did the function work. + Did the function work. -**Raises** + **Raises** -* **ValueError** – If the inputs are not the correct values. -* **TypeError** – If the inputs are not strings. + * **ValueError** – If the inputs are not the correct values. + * **TypeError** – If the inputs are not strings. -**Return type** + **Return type** -int + int + " `; diff --git a/scripts/lib/api/generateApiComponents.test.ts b/scripts/lib/api/generateApiComponents.test.ts index d4e3d8e2290..c37c91eee05 100644 --- a/scripts/lib/api/generateApiComponents.test.ts +++ b/scripts/lib/api/generateApiComponents.test.ts @@ -79,6 +79,7 @@ describe("createOpeningTag()", () => { name='run' attributeTypeHint='undefined' attributeValue='undefined' + isDedicatedPage='undefined' github='undefined' signature='Estimator.run(circuits, observables, parameter_values=None, **kwargs)' extraSignatures='[]' @@ -100,9 +101,10 @@ describe("createOpeningTag()", () => { name='run' attributeTypeHint='undefined' attributeValue='undefined' + isDedicatedPage='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}]' + extraSignatures='["Estimator.run(circuits, observables, parameter_values=None, **kwargs)", "Estimator.run(circuits, observables, parameter_values=None, **kwargs)"]' > `); }); @@ -121,6 +123,7 @@ describe("createOpeningTag()", () => { name='instance' attributeTypeHint='str | None' attributeValue='None' + isDedicatedPage='undefined' github='undefined' signature='' extraSignatures='[]' @@ -139,6 +142,7 @@ describe("createOpeningTag()", () => { name='undefined' attributeTypeHint='undefined' attributeValue='undefined' + isDedicatedPage='undefined' github='undefined' signature='' extraSignatures='[]' diff --git a/scripts/lib/api/generateApiComponents.ts b/scripts/lib/api/generateApiComponents.ts index 8e43bba2772..e97b78b973d 100644 --- a/scripts/lib/api/generateApiComponents.ts +++ b/scripts/lib/api/generateApiComponents.ts @@ -10,24 +10,253 @@ // 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 { CheerioAPI, Cheerio, Element } 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"; +import { ApiType } from "./Metadata"; +import { + getLastPartFromFullIdentifier, + APOSTROPHE_HEX_CODE, +} from "../stringUtils"; export type ComponentProps = { - id: string; + id?: string; name?: string; attributeTypeHint?: string; attributeValue?: string; githubSourceLink?: string; rawSignature?: string; extraRawSignatures?: string[]; + isDedicatedPage?: boolean; }; +const APITYPE_TO_TAG: Record = { + class: "class", + exception: "class", + attribute: "attribute", + property: "attribute", + function: "function", + method: "function", +}; + +export async function processMdxComponent( + $: CheerioAPI, + $main: Cheerio, + signatures: Cheerio[], + $dl: Cheerio, + priorApiType: ApiType | undefined, + apiType: ApiType, + id: string, +): Promise<[string, string]> { + const tagName = APITYPE_TO_TAG[apiType]; + + const $firstSignature = signatures.shift()!; + const componentProps = prepareProps( + $, + $main, + $firstSignature, + $dl, + priorApiType, + apiType, + id, + ); + + const extraProps = signatures.flatMap( + ($overloadedSignature) => + prepareProps($, $main, $overloadedSignature, $dl, apiType, apiType, id) ?? + [], + ); + addExtraSignatures(componentProps, extraProps); + + return [await createOpeningTag(tagName, componentProps), ``]; +} + +// ------------------------------------------------------------------ +// Prepare props for MDX components +// ------------------------------------------------------------------ + +function prepareProps( + $: CheerioAPI, + $main: Cheerio, + $child: Cheerio, + $dl: Cheerio, + priorApiType: ApiType | undefined, + apiType: ApiType, + id: string, +): ComponentProps { + const preparePropsPerApiType: Record ComponentProps> = { + class: () => prepareClassProps($child, githubSourceLink, id), + property: () => + preparePropertyProps($child, $dl, priorApiType, githubSourceLink, id), + method: () => + prepareMethodProps($, $child, $dl, priorApiType, githubSourceLink, id), + attribute: () => + prepareAttributeProps($, $child, $dl, priorApiType, githubSourceLink, id), + function: () => + prepareFunctionOrExceptionProps($, $child, $dl, id, githubSourceLink), + exception: () => + prepareFunctionOrExceptionProps($, $child, $dl, id, githubSourceLink), + }; + + const githubSourceLink = prepareGitHubLink($child, apiType === "method"); + findByText($, $main, "em.property", apiType).remove(); + + if (!(apiType in preparePropsPerApiType)) { + throw new Error(`Unhandled Python type: ${apiType}`); + } + + return preparePropsPerApiType[apiType](); +} + +function prepareClassProps( + $child: Cheerio, + githubSourceLink: string | undefined, + id: string, +): ComponentProps { + return { + id, + rawSignature: $child.html()!, + githubSourceLink, + }; +} + +function preparePropertyProps( + $child: Cheerio, + $dl: Cheerio, + priorApiType: string | undefined, + githubSourceLink: string | undefined, + id: string, +): ComponentProps { + const rawSignature = $child.find("em").text()?.replace(/^:\s+/, ""); + const props = { + id, + name: getLastPartFromFullIdentifier(id), + rawSignature, + githubSourceLink, + }; + + if (!priorApiType && id) { + $dl.siblings("h1").text(getLastPartFromFullIdentifier(id)); + return { + ...props, + isDedicatedPage: true, + }; + } + + return props; +} + +function prepareMethodProps( + $: CheerioAPI, + $child: Cheerio, + $dl: Cheerio, + priorApiType: string | undefined, + githubSourceLink: string | undefined, + id: string, +): ComponentProps { + const props = { + id, + name: getLastPartFromFullIdentifier(id), + rawSignature: $child.html()!, + githubSourceLink, + }; + + if (id) { + if (!priorApiType) { + $dl.siblings("h1").text(getLastPartFromFullIdentifier(id)); + return { + ...props, + isDedicatedPage: true, + }; + } else if ($child.attr("id")) { + $(`

${getLastPartFromFullIdentifier(id)}

`).insertBefore($dl); + } + } + return props; +} + +function prepareAttributeProps( + $: CheerioAPI, + $child: Cheerio, + $dl: Cheerio, + priorApiType: string | undefined, + githubSourceLink: string | undefined, + id: string, +): ComponentProps { + if (!priorApiType) { + if (id) { + $dl.siblings("h1").text(getLastPartFromFullIdentifier(id)); + } + const rawSignature = $child.find("em").text()?.replace(/^:\s+/, ""); + return { + id, + rawSignature, + githubSourceLink, + isDedicatedPage: true, + }; + } + + // Else, the attribute is embedded on the class + const text = $child.text(); + + // Index of the default value of the attribute + let equalIndex = text.indexOf("="); + if (equalIndex == -1) { + equalIndex = text.length; + } + // Index of the attribute's type. The type should be + // found before the default value + let colonIndex = text.slice(0, equalIndex).indexOf(":"); + if (colonIndex == -1) { + colonIndex = text.length; + } + + $(`

${getLastPartFromFullIdentifier(id)}

`).insertBefore($dl); + // The attributes have the following shape: name [: type] [= value] + const name = text.slice(0, Math.min(colonIndex, equalIndex)).trim(); + const attributeTypeHint = text + .slice(Math.min(colonIndex + 1, equalIndex), equalIndex) + .trim(); + const attributeValue = text.slice(equalIndex, text.length).trim(); + + return { + id, + name, + attributeTypeHint, + attributeValue, + }; +} + +function prepareFunctionOrExceptionProps( + $: CheerioAPI, + $child: Cheerio, + $dl: Cheerio, + id: string, + githubSourceLink: string | undefined, +): ComponentProps { + const props = { + id, + name: getLastPartFromFullIdentifier(id), + rawSignature: $child.html()!, + githubSourceLink, + }; + + const pageHeading = $dl.siblings("h1").text(); + if (id.endsWith(pageHeading) && pageHeading != "") { + // Page is already dedicated to apiType; no heading needed + return { + ...props, + isDedicatedPage: true, + }; + } + $(`

${getLastPartFromFullIdentifier(id)}

`).insertBefore($dl); + + return props; +} + // ------------------------------------------------------------------ // Generate MDX components // ------------------------------------------------------------------ @@ -52,11 +281,7 @@ export async function createOpeningTag( 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}`, - ); + extraSignatures.push(`"${await htmlSignatureToMd(sig!)}"`); } return `<${tagName} @@ -64,6 +289,7 @@ export async function createOpeningTag( name='${props.name}' attributeTypeHint='${attributeTypeHint}' attributeValue='${attributeValue}' + isDedicatedPage='${props.isDedicatedPage}' github='${props.githubSourceLink}' signature='${signature}' extraSignatures='[${extraSignatures.join(", ")}]' @@ -139,6 +365,7 @@ export async function htmlSignatureToMd( return String(file) .replaceAll("\n", "") .replaceAll("'", APOSTROPHE_HEX_CODE) + .replaceAll('"', '\\"') .replace(/^`/, "") .replace(/`$/, ""); } diff --git a/scripts/lib/api/htmlToMd.test.ts b/scripts/lib/api/htmlToMd.test.ts index c77c95580f6..cd6188ab541 100644 --- a/scripts/lib/api/htmlToMd.test.ts +++ b/scripts/lib/api/htmlToMd.test.ts @@ -173,42 +173,38 @@ Can be either (1) a dictionary mapping XX angle values to fidelity at that angle ).toMatchInlineSnapshot(` "# DAGCircuit - + + Bases: \`object\` - \`qiskit.dagcircuit.DAGCircuit\` [GitHub](https://github.com/Qiskit/qiskit-ibm-runtime/tree/0.9.2/qiskit/dagcircuit/dagcircuit.py "view source code") + Quantum circuit as a directed acyclic graph. - Bases: \`object\` + ## Attributes - Quantum circuit as a directed acyclic graph. + ### ancillas - ## Attributes + + Returns a list of ancilla bits in the order that the registers were added. + - + ## Methods - ### ancillas + ### add\\_calibration - Returns a list of ancilla bits in the order that the registers were added. + + Register a low-level, custom pulse definition for the given gate. - ## Methods + **Parameters** - ### add\\_calibration + * **gate** (*Union\\[*[*Gate*](qiskit.circuit.Gate#qiskit.circuit.Gate "qiskit.circuit.Gate")*, str]*) – Gate information. + * **qubits** (*Union\\[int, Tuple\\[int]]*) – List of qubits to be measured. + * **schedule** ([*Schedule*](qiskit.pulse.Schedule#qiskit.pulse.Schedule "qiskit.pulse.Schedule")) – Schedule information. + * **params** (*Optional\\[List\\[Union\\[float,* [*Parameter*](qiskit.circuit.Parameter#qiskit.circuit.Parameter "qiskit.circuit.Parameter")*]]]*) – A list of parameters. - + **Raises** - \`add_calibration(gate, qubits, schedule, params=None)\` - - Register a low-level, custom pulse definition for the given gate. - - **Parameters** - - * **gate** (*Union\\[*[*Gate*](qiskit.circuit.Gate#qiskit.circuit.Gate "qiskit.circuit.Gate")*, str]*) – Gate information. - * **qubits** (*Union\\[int, Tuple\\[int]]*) – List of qubits to be measured. - * **schedule** ([*Schedule*](qiskit.pulse.Schedule#qiskit.pulse.Schedule "qiskit.pulse.Schedule")) – Schedule information. - * **params** (*Optional\\[List\\[Union\\[float,* [*Parameter*](qiskit.circuit.Parameter#qiskit.circuit.Parameter "qiskit.circuit.Parameter")*]]]*) – A list of parameters. - - **Raises** - - **Exception** – if the gate is of type string and params is None. + **Exception** – if the gate is of type string and params is None. + + " `); }); @@ -389,21 +385,21 @@ for execution on present day noisy quantum systems.

).toMatchInlineSnapshot(` "In addition to the public abstract methods, subclasses should also implement the following private methods: - \`classmethod _default_options()\` - - Return the default options + + Return the default options - This method will return a [\`qiskit.providers.Options\`](qiskit.providers.Options#qiskit.providers.Options "qiskit.providers.Options") subclass object that will be used for the default options. These should be the default parameters to use for the options of the backend. + This method will return a [\`qiskit.providers.Options\`](qiskit.providers.Options#qiskit.providers.Options "qiskit.providers.Options") subclass object that will be used for the default options. These should be the default parameters to use for the options of the backend. - **Returns** + **Returns** - **A options object with** + **A options object with** - default values set + default values set - **Return type** + **Return type** - [qiskit.providers.Options](qiskit.providers.Options#qiskit.providers.Options "qiskit.providers.Options") + [qiskit.providers.Options](qiskit.providers.Options#qiskit.providers.Options "qiskit.providers.Options") + " `); }); @@ -683,11 +679,9 @@ for execution on present day noisy quantum systems.

"isReleaseNotes": false, "markdown": "# Estimator - - - \`SamplerExample(circuits=None, parameters=None) \`[GitHub](https://github.com/Qiskit/qiskit-ibm-runtime/tree/0.9.2/qiskit_ibm_runtime/sampler.py "view source code") - - Class for interacting with Qiskit Runtime Sampler primitive service. + + Class for interacting with Qiskit Runtime Sampler primitive service. + ", "meta": { "apiName": "qiskit_ibm_runtime.Sampler", @@ -737,11 +731,9 @@ for execution on present day noisy quantum systems.

"isReleaseNotes": false, "markdown": "# circuits - - - \`tuple[qiskit.circuit.quantumcircuit.QuantumCircuit, ...]\` - - Quantum circuits that represents quantum states. + + Quantum circuits that represents quantum states. + ", "meta": { "apiName": "qiskit_ibm_runtime.Estimator.circuits", @@ -770,11 +762,9 @@ for execution on present day noisy quantum systems.

"isReleaseNotes": false, "markdown": "# run - - - \`Estimator.run(circuits, observables, parameter_values=None, **kwargs)\` - - Submit a request to the estimator primitive program. + + Submit a request to the estimator primitive program. + ", "meta": { "apiName": "qiskit_ibm_runtime.Estimator.run", @@ -803,9 +793,7 @@ for execution on present day noisy quantum systems.

"isReleaseNotes": false, "markdown": "# callback - - - \`Optional[Callable] = None\` + ", "meta": { "apiName": "qiskit_ibm_runtime.options.EnvironmentOptions.callback", @@ -853,21 +841,19 @@ By default this is sys.stdout.

# job\\_monitor - - - \`job_monitor(job, interval=None, output=<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>)\` [GitHub](https://github.com/Qiskit/qiskit-ibm-runtime/tree/0.9.2/qiskit_ibm_provider/job/job_monitor.py "view source code") + + Monitor the status of an \`IBMJob\` instance. - Monitor the status of an \`IBMJob\` instance. - - **Parameters** + **Parameters** - * **job** (\`IBMJob\`) – Job to monitor. - * **interval** (\`Optional\`\\[\`float\`]) – Time interval between status queries. - * **output** (\`TextIO\`) – The file like object to write status messages to. By default this is sys.stdout. + * **job** (\`IBMJob\`) – Job to monitor. + * **interval** (\`Optional\`\\[\`float\`]) – Time interval between status queries. + * **output** (\`TextIO\`) – The file like object to write status messages to. By default this is sys.stdout. - **Return type** + **Return type** - \`None\` + \`None\` + ", "meta": { "apiName": "qiskit_ibm_provider.job.job_monitor", @@ -904,13 +890,11 @@ By default this is sys.stdout.

# IBMJobError - - - \`IBMJobError(*message)\` [GitHub](https://github.com/Qiskit/qiskit-ibm-runtime/tree/0.9.2/qiskit_ibm_provider/job/exceptions.py "view source code") - - Base class for errors raised by the job modules. + + Base class for errors raised by the job modules. - Set the error message. + Set the error message. + ", "meta": { "apiName": "qiskit_ibm_provider.job.IBMJobError", diff --git a/scripts/lib/api/htmlToMd.ts b/scripts/lib/api/htmlToMd.ts index 98a747c3b73..a826912114a 100644 --- a/scripts/lib/api/htmlToMd.ts +++ b/scripts/lib/api/htmlToMd.ts @@ -27,7 +27,7 @@ import { Emphasis, Root, Content } from "mdast"; import { processHtml } from "./processHtml"; import { HtmlToMdResult } from "./HtmlToMdResult"; import { Metadata } from "./Metadata"; -import { removePrefix, removeSuffix } from "../stringUtils"; +import { removePrefix, removeSuffix, capitalize } from "../stringUtils"; import { remarkStringifyOptions } from "./commonParserConfig"; export async function sphinxHtmlToMarkdown(options: { @@ -37,7 +37,7 @@ export async function sphinxHtmlToMarkdown(options: { determineGithubUrl: (fileName: string) => string; releaseNotesTitle: string; }): Promise { - const processedHtml = processHtml(options); + const processedHtml = await processHtml(options); const markdown = await generateMarkdownFile( processedHtml.html, processedHtml.meta, @@ -123,6 +123,15 @@ function prepareHandlers(meta: Metadata): Record { return defaultHandlers.div(h, node); }, + class(h, node: any): any { + return buildApiComponent(h, node); + }, + function(h, node: any): any { + return buildApiComponent(h, node); + }, + attribute(h, node: any): any { + return buildApiComponent(h, node); + }, }; return handlers; @@ -301,3 +310,64 @@ function buildMathExpression(node: any, type: "math" | "inlineMath"): any { } return { type: type, value }; } + +function buildApiComponent(h: H, node: any): any { + const componentName = capitalize(node.tagName); + + const hastTree = { + type: "mdxJsxFlowElement", + name: componentName, + attributes: [], + children: all(h, node), + }; + + maybeAddAttribute(hastTree, "id", node.properties.id); + maybeAddAttribute(hastTree, "name", node.properties.name); + maybeAddAttribute( + hastTree, + "attributeTypeHint", + node.properties.attributetypehint, + ); + maybeAddAttribute(hastTree, "attributeValue", node.properties.attributevalue); + maybeAddExpressionAttribute( + hastTree, + "isDedicatedPage", + node.properties.isdedicatedpage, + ); + maybeAddAttribute(hastTree, "github", node.properties.github); + maybeAddAttribute(hastTree, "signature", node.properties.signature); + maybeAddExpressionAttribute( + hastTree, + "extraSignatures", + node.properties.extrasignatures, + ); + + return hastTree; +} + +function maybeAddAttribute(hastTree: any, name: string, value: string): void { + if (value && value.trim() && value != "undefined") { + hastTree.attributes.push({ + type: "mdxJsxAttribute", + name, + value, + }); + } +} + +function maybeAddExpressionAttribute( + hastTree: any, + name: string, + value: string, +): void { + if (value && value != "undefined" && value != "[]") { + hastTree.attributes.push({ + type: "mdxJsxAttribute", + name, + value: { + type: "mdxJsxAttributeValueExpression", + value, + }, + }); + } +} diff --git a/scripts/lib/api/mergeClassMembers.test.ts b/scripts/lib/api/mergeClassMembers.test.ts index 32ace2a5dd5..300186c1c02 100644 --- a/scripts/lib/api/mergeClassMembers.test.ts +++ b/scripts/lib/api/mergeClassMembers.test.ts @@ -19,17 +19,19 @@ describe("mergeClassMembers", () => { test("merge class members", async () => { const results: HtmlToMdResultWithUrl[] = [ { - markdown: `## Attributes + markdown: ` + ## Attributes -| | | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -| [\`RuntimeOptions.backend\`](qiskit_ibm_runtime.RuntimeOptions.backend#qiskit_ibm_runtime.RuntimeOptions.backend "qiskit_ibm_runtime.RuntimeOptions.backend") | | + | | | + | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | + | [\`RuntimeOptions.backend\`](qiskit_ibm_runtime.RuntimeOptions.backend#qiskit_ibm_runtime.RuntimeOptions.backend "qiskit_ibm_runtime.RuntimeOptions.backend") | | -## Methods + ## Methods -| | | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | -| [\`RuntimeOptions.validate\`](qiskit_ibm_runtime.RuntimeOptions.validate#qiskit_ibm_runtime.RuntimeOptions.validate "qiskit_ibm_runtime.RuntimeOptions.validate")(channel) | Validate options. |`, + | | | + | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | + | [\`RuntimeOptions.validate\`](qiskit_ibm_runtime.RuntimeOptions.validate#qiskit_ibm_runtime.RuntimeOptions.validate "qiskit_ibm_runtime.RuntimeOptions.validate")(channel) | Validate options. | +`, meta: { apiType: "class", apiName: "RuntimeOptions", @@ -40,8 +42,7 @@ describe("mergeClassMembers", () => { }, { markdown: `# RuntimeOptions.backend - -\`Optional[str] = None\` + `, meta: { apiType: "attribute", @@ -54,7 +55,7 @@ describe("mergeClassMembers", () => { { markdown: `# RuntimeOptions.circuits -\`Optional[str] = None\` + `, meta: { apiType: "property", @@ -68,21 +69,21 @@ describe("mergeClassMembers", () => { markdown: ` # RuntimeOptions.validate -\`RuntimeOptions.validate(channel)\` + + Validate options. -Validate options. + * Parameters: -* Parameters: + **channel** (\`str\`) – channel type. - **channel** (\`str\`) – channel type. + * Raises: -* Raises: + **IBMInputValueError** – If one or more option is invalid. - **IBMInputValueError** – If one or more option is invalid. + * Return type: -* Return type: - - \`None\` + \`None\` + `, meta: { apiType: "method", @@ -96,35 +97,37 @@ Validate options. const merged = await mergeClassMembers(results); expect(merged.find((item) => item.meta.apiType === "class")?.markdown) .toMatchInlineSnapshot(` - "## Attributes - - ### RuntimeOptions.backend + " + ## Attributes - \`Optional[str] = None\` + ### RuntimeOptions.backend - ### RuntimeOptions.circuits + - \`Optional[str] = None\` + ### RuntimeOptions.circuits - ## Methods + - ### RuntimeOptions.validate + ## Methods - \`RuntimeOptions.validate(channel)\` + ### RuntimeOptions.validate - Validate options. + + Validate options. - * Parameters: + * Parameters: - **channel** (\`str\`) – channel type. + **channel** (\`str\`) – channel type. - * Raises: + * Raises: - **IBMInputValueError** – If one or more option is invalid. + **IBMInputValueError** – If one or more option is invalid. - * Return type: + * Return type: - \`None\` + \`None\` + + " `); expect(merged.length).toEqual(1); diff --git a/scripts/lib/api/mergeClassMembers.ts b/scripts/lib/api/mergeClassMembers.ts index 78193fc3f9f..5198745b099 100644 --- a/scripts/lib/api/mergeClassMembers.ts +++ b/scripts/lib/api/mergeClassMembers.ts @@ -18,6 +18,7 @@ import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import remarkStringify from "remark-stringify"; import { Content, Root } from "mdast"; +import { MdxJsxFlowElement, MdxJsxAttribute } from "mdast-util-mdx-jsx"; import { visit } from "unist-util-visit"; import { HtmlToMdResultWithUrl } from "./HtmlToMdResult"; @@ -62,17 +63,32 @@ export async function mergeClassMembers( .use(remarkGfm) .use(remarkMath) .use(() => { - return async (tree: Root) => { - for (const node of tree.children) { + return async (root: Root) => { + // The attribute and method's section can be found under the class component + const mdxClassElement = root.children.find( + (node): node is MdxJsxFlowElement => + node.type == "mdxJsxFlowElement" && node.name == "Class", + ); + + if (!mdxClassElement) { + return; + } + + for (const node of mdxClassElement.children) { await replaceMembersAfterTitle( - tree, + mdxClassElement, node, "Attributes", attributesAndProps, ); - await replaceMembersAfterTitle(tree, node, "Methods", methods); await replaceMembersAfterTitle( - tree, + mdxClassElement, + node, + "Methods", + methods, + ); + await replaceMembersAfterTitle( + mdxClassElement, node, "Methods Defined Here", methods, @@ -94,7 +110,7 @@ export async function mergeClassMembers( } async function replaceMembersAfterTitle( - tree: Root, + tree: MdxJsxFlowElement, node: Content, title: string, members: HtmlToMdResultWithUrl[], @@ -136,6 +152,15 @@ async function parseMarkdownIncreasingHeading( visit(root, "heading", (node: any) => { node.depth = node.depth + depthIncrease; }); + visit(root, "mdxJsxFlowElement", (node: any) => { + // We are inlining functions and attributes and thus we need to remove + // the `isDedicatedPage` prop + if (node.name == "Attribute" || node.name == "Function") { + node.attributes = node.attributes.filter( + (attr: MdxJsxAttribute) => attr.name != "isDedicatedPage", + ); + } + }); }); const root = pipeline.parse(md); diff --git a/scripts/lib/api/processHtml.test.ts b/scripts/lib/api/processHtml.test.ts index d73db331d9d..c977250f891 100644 --- a/scripts/lib/api/processHtml.test.ts +++ b/scripts/lib/api/processHtml.test.ts @@ -383,7 +383,7 @@ describe("maybeSetModuleMetadata()", () => { }); describe("processMembersAndSetMeta()", () => { - test("function with added heading", () => { + test("function with added heading", async () => { const html = `

Circuit Converters

@@ -398,10 +398,10 @@ describe("processMembersAndSetMeta()", () => { `; const doc = CheerioDoc.load(html); const meta: Metadata = {}; - processMembersAndSetMeta(doc.$, doc.$main, meta); + await processMembersAndSetMeta(doc.$, doc.$main, meta); doc.expectHtml(`

Circuit Converters

-

circuit_to_dag

-qiskit.converters.circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None) GitHub

+

circuit_to_dag

+

Build a DAGCircuit object from a QuantumCircuit.

Parameters:
@@ -409,14 +409,15 @@ describe("processMembersAndSetMeta()", () => {
  • circuit – the input circuit.

  • copy_operations – Deep copy the operation objects in the QuantumCircuit for the output DAGCircuit.

  • -
    `); +
    +`); expect(meta).toEqual({ apiType: "function", apiName: "qiskit.converters.circuit_to_dag", }); }); - test("function without added heading", () => { + test("function without added heading", async () => { const html = `

    least_busy

    @@ -445,10 +446,10 @@ backends may not have this attribute.

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

    least_busy

    -

    -least_busy(backends) GitHub

    +
    +

    Return the least busy backend from a list.

    Return the least busy available backend for those that have a pending_jobs in their status. Note that local @@ -469,14 +470,15 @@ backends may not have this attribute.

    does not have the pending_jobs attribute in its status.

    -
    `); + +`); expect(meta).toEqual({ apiType: "function", apiName: "qiskit_ibm_provider.least_busy", }); }); - test("exception with added heading", () => { + test("exception with added heading", async () => { const html = `

    Top-level exceptions (qiskit.exceptions)

    All Qiskit-related errors raised by Qiskit are subclasses of the base:

    @@ -486,7 +488,6 @@ backends may not have this attribute.

    Base class for errors raised by Qiskit.

    Set the error message.

    -

    Note

    Errors that are just general programming errors, such as incorrect typing, may still raise @@ -505,16 +506,16 @@ particular error, which subclasses both

    Top-level exceptions (qiskit.exceptions)

    All Qiskit-related errors raised by Qiskit are subclasses of the base:

    -

    QiskitError

    -qiskit.exceptions.QiskitError(*message) GitHub

    +

    QiskitError

    +

    Base class for errors raised by Qiskit.

    Set the error message.

    -
    - +
    +

    Note

    Errors that are just general programming errors, such as incorrect typing, may still raise diff --git a/scripts/lib/api/processHtml.ts b/scripts/lib/api/processHtml.ts index c5e41c7e36f..8daa34709ec 100644 --- a/scripts/lib/api/processHtml.ts +++ b/scripts/lib/api/processHtml.ts @@ -10,12 +10,11 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -import { CheerioAPI, Cheerio, load } from "cheerio"; +import { CheerioAPI, Cheerio, load, Element } from "cheerio"; import { Image } from "./HtmlToMdResult"; import { Metadata, ApiType } from "./Metadata"; -import { getLastPartFromFullIdentifier } from "../stringUtils"; -import { findByText, prepareGitHubLink } from "./generateApiComponents"; +import { processMdxComponent } from "./generateApiComponents"; export type ProcessedHtml = { html: string; @@ -24,13 +23,13 @@ export type ProcessedHtml = { isReleaseNotes: boolean; }; -export function processHtml(options: { +export async function processHtml(options: { html: string; fileName: string; imageDestination: string; determineGithubUrl: (fileName: string) => string; releaseNotesTitle: string; -}): ProcessedHtml { +}): Promise { const { html, fileName, @@ -61,7 +60,7 @@ export function processHtml(options: { preserveMathBlockWhitespace($, $main); const meta: Metadata = {}; - processMembersAndSetMeta($, $main, meta); + await processMembersAndSetMeta($, $main, meta); maybeSetModuleMetadata($, $main, meta); if (meta.apiType === "module") { updateModuleHeadings($, $main, meta); @@ -260,11 +259,11 @@ export function removeColonSpans($main: Cheerio): void { $main.find(".colon").remove(); } -export function processMembersAndSetMeta( +export async function processMembersAndSetMeta( $: CheerioAPI, $main: Cheerio, meta: Metadata, -): void { +): Promise { let continueMapMembers = true; while (continueMapMembers) { // members can be recursive, so we need to pick elements one by one @@ -283,173 +282,41 @@ export function processMembersAndSetMeta( const id = $dl.find("dt").attr("id") || ""; const apiType = getApiType($dl); - const replacement = $dl - .children() - .toArray() - .map((child) => { - const $child = $(child); - if (child.name !== "dt" || !apiType) { - return `

    ${$child.html()}
    `; - } - - const priorApiType = meta.apiType; - if (!priorApiType) { - meta.apiType = apiType; - meta.apiName = id; - } - - return processMember($, $main, $child, $dl, priorApiType, apiType, id); - }) - .join("\n"); - - $dl.replaceWith(`
    ${replacement}
    `); - } -} - -function processMember( - $: CheerioAPI, - $main: Cheerio, - $child: Cheerio, - $dl: Cheerio, - priorApiType: string | undefined, - apiType: string, - id: string, -) { - const githubUrl = prepareGitHubLink($child, apiType === "method"); - const githubSourceLink = githubUrl - ? ` GitHub` - : ""; - - findByText($, $main, "em.property", apiType).remove(); - - if (apiType == "class") { - return `

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

    `; - } - - if (apiType == "property") { - return processProperty($child, $dl, priorApiType, id, githubSourceLink); - } - - if (apiType == "method") { - return processMethod($, $child, $dl, priorApiType, id, githubSourceLink); - } - - if (apiType == "attribute") { - return processAttribute($child, $dl, priorApiType, id, githubSourceLink); - } - - if (apiType === "function" || apiType === "exception") { - return processFunctionOrException($child, $dl, id, githubSourceLink); - } - - throw new Error(`Unhandled Python type: ${apiType}`); -} - -function processProperty( - $child: Cheerio, - $dl: Cheerio, - priorApiType: string | undefined, - id: string, - githubSourceLink: string, -) { - if (!priorApiType && id) { - $dl.siblings("h1").text(getLastPartFromFullIdentifier(id)); - } - - const signature = $child.find("em").text()?.replace(/^:\s+/, ""); - if (signature.trim().length === 0) return; - return `

    ${signature}${githubSourceLink}

    `; -} - -function processMethod( - $: CheerioAPI, - $child: Cheerio, - $dl: Cheerio, - priorApiType: string | undefined, - id: string, - githubSourceLink: string, -) { - if (id) { + const priorApiType = meta.apiType; if (!priorApiType) { - $dl.siblings("h1").text(getLastPartFromFullIdentifier(id)); - } else if (!$child.attr("id")) { - // Overload methods have more than one
    tag, but only the first one - // contains an id. - return `

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

    `; - } else { - // Inline methods - $(`

    ${getLastPartFromFullIdentifier(id)}

    `).insertBefore($dl); + meta.apiType = apiType; + meta.apiName = id; } - } - return `

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

    `; -} + const bodyElements: string[] = []; + const signatures: Cheerio[] = []; -function processAttribute( - $child: Cheerio, - $dl: Cheerio, - priorApiType: string | undefined, - id: string, - githubSourceLink: string, -) { - if (!priorApiType) { - if (id) { - $dl.siblings("h1").text(getLastPartFromFullIdentifier(id)); + for (const child of $dl.children().toArray()) { + const $child = $(child); + if (child.name !== "dt" || !apiType) { + bodyElements.push(`
    ${$child.html()}
    `); + continue; + } + signatures.push($child); } - const signature = $child.find("em").text()?.replace(/^:\s+/, ""); - if (signature.trim().length === 0) return; - return `

    ${signature}${githubSourceLink}

    `; - } - - // Else, the attribute is embedded on the class - const text = $child.text(); - - // Index of the default value of the attribute - let equalIndex = text.indexOf("="); - if (equalIndex == -1) { - equalIndex = text.length; - } - // Index of the attribute's type. The type should be - // found before the default value - let colonIndex = text.slice(0, equalIndex).indexOf(":"); - if (colonIndex == -1) { - colonIndex = text.length; - } - - // The attributes have the following shape: name [: type] [= value] - const name = text.slice(0, Math.min(colonIndex, equalIndex)).trim(); - const type = text - .slice(Math.min(colonIndex + 1, equalIndex), equalIndex) - .trim(); - const value = text.slice(equalIndex, text.length).trim(); - - const output = [`

    ${name}

    `]; - if (type) { - output.push(`

    ${type}

    `); - } - if (value) { - output.push(`

    ${value}

    `); - } - return output.join("\n"); -} - -function processFunctionOrException( - $child: Cheerio, - $dl: Cheerio, - id: string, - githubSourceLink: string, -) { - const descriptionHtml = `

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

    `; - - const pageHeading = $dl.siblings("h1").text(); - if (id.endsWith(pageHeading) && pageHeading != "") { - // Page is already dedicated to apiType; no heading needed - return descriptionHtml; + if (signatures.length == 0) { + $dl.replaceWith(`
    ${bodyElements.join("\n")}
    `); + } else { + const [openTag, closeTag] = await processMdxComponent( + $, + $main, + signatures, + $dl, + priorApiType, + apiType!, + id, + ); + $dl.replaceWith( + `
    ${openTag}\n${bodyElements.join("\n")}\n${closeTag}
    `, + ); + } } - - const apiName = id.split(".").slice(-1)[0]; - return `

    ${apiName}

    ${descriptionHtml}`; } export function maybeSetModuleMetadata(