From dfd136784c3526ce291cac3536943d4cc7091d5b Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sun, 26 Apr 2026 07:54:25 -0700 Subject: [PATCH 1/5] Surface private module docstrings in pdoc output pdoc skips module-level docstrings from private submodules when symbols are re-exported via __init__.py. Compose __doc__ dynamically from the existing submodule docstrings so pdoc renders the exception hierarchy, extension examples, gate parameter conventions, and usage examples that were previously invisible in the generated documentation. --- ionq_core/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ionq_core/__init__.py b/ionq_core/__init__.py index fbee532..d659148 100644 --- a/ionq_core/__init__.py +++ b/ionq_core/__init__.py @@ -1,8 +1,6 @@ # Copyright 2026 IonQ, Inc. # SPDX-License-Identifier: Apache-2.0 -"""A client library for accessing IonQ Cloud Platform API""" - from ._exceptions import ( APIConnectionError, APIError, @@ -63,3 +61,15 @@ "wait_for_job", "zz_matrix", ) + +from . import _exceptions, _extensions, _gates, _pagination, _polling, _session + +__doc__ = "\n\n".join([ + "A client library for accessing IonQ Cloud Platform API", + f"## Exceptions\n\n{_exceptions.__doc__}", + f"## Extensions\n\n{_extensions.__doc__}", + f"## Native Gates\n\n{_gates.__doc__}", + f"## Job Polling\n\n{_polling.__doc__}", + f"## Pagination\n\n{_pagination.__doc__}", + f"## Sessions\n\n{_session.__doc__}", +]) From 93ed6aa3cafaab3d036854940ce434ad2a708b64 Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sun, 26 Apr 2026 07:55:52 -0700 Subject: [PATCH 2/5] Add CLA contact information to CONTRIBUTING.md Closes #20 --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c88033e..3d907df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,10 @@ uvx openapi-python-client==0.28.3 generate \ - CI must pass before merging (lint, tests, type check, generated code staleness check). - The generated code staleness check on PRs verifies that `ionq_core/` matches what the generator produces. If it fails, regenerate and commit the result. +## Contributor License Agreement + +To receive IonQ's CLA, please contact @mjk or email [opensource@ionq.com](mailto:opensource@ionq.com). + ## License By submitting a pull request, you represent that you have the right to license your From 1b1d96299db1994c95f4df7e419e65c581cba2e1 Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sun, 26 Apr 2026 07:59:12 -0700 Subject: [PATCH 3/5] Surface private module docstrings in pdoc output Update the package_init template to compose __doc__ dynamically from submodule docstrings instead of using a static literal. This way pdoc renders the exception hierarchy, extension examples, gate parameter conventions, and usage examples that were previously invisible. --- custom-templates/package_init.py.jinja | 14 ++++++++++++-- ionq_core/__init__.py | 20 +++++++++++--------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/custom-templates/package_init.py.jinja b/custom-templates/package_init.py.jinja index c602cf0..74cc2a9 100644 --- a/custom-templates/package_init.py.jinja +++ b/custom-templates/package_init.py.jinja @@ -1,5 +1,3 @@ -{% from "helpers.jinja" import safe_docstring %} -{{ safe_docstring(package_description) }} from ._exceptions import ( APIConnectionError, APIError, @@ -60,3 +58,15 @@ __all__ = ( "wait_for_job", "zz_matrix", ) + +from . import _exceptions, _extensions, _gates, _pagination, _polling, _session + +__doc__ = "\n\n".join([ + "{{ package_description }}", + f"## Exceptions\n\n{_exceptions.__doc__}", + f"## Extensions\n\n{_extensions.__doc__}", + f"## Native Gates\n\n{_gates.__doc__}", + f"## Job Polling\n\n{_polling.__doc__}", + f"## Pagination\n\n{_pagination.__doc__}", + f"## Sessions\n\n{_session.__doc__}", +]) diff --git a/ionq_core/__init__.py b/ionq_core/__init__.py index d659148..be3688a 100644 --- a/ionq_core/__init__.py +++ b/ionq_core/__init__.py @@ -64,12 +64,14 @@ from . import _exceptions, _extensions, _gates, _pagination, _polling, _session -__doc__ = "\n\n".join([ - "A client library for accessing IonQ Cloud Platform API", - f"## Exceptions\n\n{_exceptions.__doc__}", - f"## Extensions\n\n{_extensions.__doc__}", - f"## Native Gates\n\n{_gates.__doc__}", - f"## Job Polling\n\n{_polling.__doc__}", - f"## Pagination\n\n{_pagination.__doc__}", - f"## Sessions\n\n{_session.__doc__}", -]) +__doc__ = "\n\n".join( + [ + "A client library for accessing IonQ Cloud Platform API", + f"## Exceptions\n\n{_exceptions.__doc__}", + f"## Extensions\n\n{_extensions.__doc__}", + f"## Native Gates\n\n{_gates.__doc__}", + f"## Job Polling\n\n{_polling.__doc__}", + f"## Pagination\n\n{_pagination.__doc__}", + f"## Sessions\n\n{_session.__doc__}", + ] +) From a8acae61c86ea5c50abd8b1ad5a5beebcd0e8da7 Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:13:11 -0700 Subject: [PATCH 4/5] Make public-API modules public, remove __doc__ workaround Rename _exceptions, _extensions, _gates, _pagination, _polling, and _session to drop the underscore prefix. These modules all export user-facing symbols and were only private by convention. Making them public lets pdoc render their module-level docstrings naturally, eliminating the dynamic __doc__ composition workaround entirely. _transport.py stays private as it contains only internal plumbing. --- CONTRIBUTING.md | 12 ++++---- custom-templates/package_init.py.jinja | 26 ++++++----------- ionq_core/__init__.py | 32 +++++++-------------- ionq_core/_transport.py | 2 +- ionq_core/{_exceptions.py => exceptions.py} | 0 ionq_core/{_extensions.py => extensions.py} | 0 ionq_core/{_gates.py => gates.py} | 0 ionq_core/ionq_client.py | 2 +- ionq_core/{_pagination.py => pagination.py} | 2 +- ionq_core/{_polling.py => polling.py} | 2 +- ionq_core/{_session.py => session.py} | 2 +- tests/integration/test_sessions.py | 2 +- tests/integration/test_usage.py | 2 +- tests/test_exceptions.py | 2 +- tests/test_extensions.py | 4 +-- tests/test_gates.py | 2 +- tests/test_polling.py | 12 ++++---- tests/test_session.py | 4 +-- tests/test_transport.py | 2 +- 19 files changed, 45 insertions(+), 65 deletions(-) rename ionq_core/{_exceptions.py => exceptions.py} (100%) rename ionq_core/{_extensions.py => extensions.py} (100%) rename ionq_core/{_gates.py => gates.py} (100%) rename ionq_core/{_pagination.py => pagination.py} (99%) rename ionq_core/{_polling.py => polling.py} (99%) rename ionq_core/{_session.py => session.py} (99%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d907df..2bf4971 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,11 +31,13 @@ Most of the code in `ionq_core/` is **auto-generated** from the IonQ OpenAPI spe **Hand-written (edit freely):** - `ionq_core/__init__.py` -- public API exports - `ionq_core/ionq_client.py` -- IonQClient convenience wrapper -- `ionq_core/_exceptions.py` -- exception hierarchy -- `ionq_core/_extensions.py` -- extension API for downstream SDKs -- `ionq_core/_transport.py` -- retry transport -- `ionq_core/_pagination.py` -- pagination helpers -- `ionq_core/_polling.py` -- job polling helpers +- `ionq_core/exceptions.py` -- exception hierarchy +- `ionq_core/extensions.py` -- extension API for downstream SDKs +- `ionq_core/_transport.py` -- retry transport (internal) +- `ionq_core/pagination.py` -- pagination helpers +- `ionq_core/polling.py` -- job polling helpers +- `ionq_core/gates.py` -- native gate matrices +- `ionq_core/session.py` -- session lifecycle manager - `tests/` -- all tests ## Regenerating the client diff --git a/custom-templates/package_init.py.jinja b/custom-templates/package_init.py.jinja index 74cc2a9..e532b23 100644 --- a/custom-templates/package_init.py.jinja +++ b/custom-templates/package_init.py.jinja @@ -1,4 +1,6 @@ -from ._exceptions import ( +{% from "helpers.jinja" import safe_docstring %} +{{ safe_docstring(package_description) }} +from .exceptions import ( APIConnectionError, APIError, APITimeoutError, @@ -10,16 +12,16 @@ from ._exceptions import ( RateLimitError, ServerError, ) -from ._extensions import AsyncEventHook, ClientExtension, EventHook -from ._gates import gpi2_matrix, gpi_matrix, ms_matrix, zz_matrix -from ._pagination import aiter_jobs, aiter_session_jobs, iter_jobs, iter_session_jobs -from ._polling import ( +from .extensions import AsyncEventHook, ClientExtension, EventHook +from .gates import gpi2_matrix, gpi_matrix, ms_matrix, zz_matrix +from .pagination import aiter_jobs, aiter_session_jobs, iter_jobs, iter_session_jobs +from .polling import ( JobFailedError, JobTimeoutError, async_wait_for_job, wait_for_job, ) -from ._session import SessionManager +from .session import SessionManager from .client import AuthenticatedClient, Client from .ionq_client import IonQClient, __version__ from .types import UNSET, Unset @@ -58,15 +60,3 @@ __all__ = ( "wait_for_job", "zz_matrix", ) - -from . import _exceptions, _extensions, _gates, _pagination, _polling, _session - -__doc__ = "\n\n".join([ - "{{ package_description }}", - f"## Exceptions\n\n{_exceptions.__doc__}", - f"## Extensions\n\n{_extensions.__doc__}", - f"## Native Gates\n\n{_gates.__doc__}", - f"## Job Polling\n\n{_polling.__doc__}", - f"## Pagination\n\n{_pagination.__doc__}", - f"## Sessions\n\n{_session.__doc__}", -]) diff --git a/ionq_core/__init__.py b/ionq_core/__init__.py index be3688a..b05def5 100644 --- a/ionq_core/__init__.py +++ b/ionq_core/__init__.py @@ -1,7 +1,10 @@ # Copyright 2026 IonQ, Inc. # SPDX-License-Identifier: Apache-2.0 -from ._exceptions import ( +"""A client library for accessing IonQ Cloud Platform API""" + +from .client import AuthenticatedClient, Client +from .exceptions import ( APIConnectionError, APIError, APITimeoutError, @@ -13,18 +16,17 @@ RateLimitError, ServerError, ) -from ._extensions import AsyncEventHook, ClientExtension, EventHook -from ._gates import gpi2_matrix, gpi_matrix, ms_matrix, zz_matrix -from ._pagination import aiter_jobs, aiter_session_jobs, iter_jobs, iter_session_jobs -from ._polling import ( +from .extensions import AsyncEventHook, ClientExtension, EventHook +from .gates import gpi2_matrix, gpi_matrix, ms_matrix, zz_matrix +from .ionq_client import IonQClient, __version__ +from .pagination import aiter_jobs, aiter_session_jobs, iter_jobs, iter_session_jobs +from .polling import ( JobFailedError, JobTimeoutError, async_wait_for_job, wait_for_job, ) -from ._session import SessionManager -from .client import AuthenticatedClient, Client -from .ionq_client import IonQClient, __version__ +from .session import SessionManager from .types import UNSET, Unset __all__ = ( @@ -61,17 +63,3 @@ "wait_for_job", "zz_matrix", ) - -from . import _exceptions, _extensions, _gates, _pagination, _polling, _session - -__doc__ = "\n\n".join( - [ - "A client library for accessing IonQ Cloud Platform API", - f"## Exceptions\n\n{_exceptions.__doc__}", - f"## Extensions\n\n{_extensions.__doc__}", - f"## Native Gates\n\n{_gates.__doc__}", - f"## Job Polling\n\n{_polling.__doc__}", - f"## Pagination\n\n{_pagination.__doc__}", - f"## Sessions\n\n{_session.__doc__}", - ] -) diff --git a/ionq_core/_transport.py b/ionq_core/_transport.py index e342cfc..5720d75 100644 --- a/ionq_core/_transport.py +++ b/ionq_core/_transport.py @@ -16,7 +16,7 @@ import httpx from httpx_retries import Retry, RetryTransport -from ._exceptions import APIConnectionError, APITimeoutError, raise_for_status +from .exceptions import APIConnectionError, APITimeoutError, raise_for_status RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({429, 500, 502, 503, *range(520, 530)}) """HTTP status codes that trigger an automatic retry.""" diff --git a/ionq_core/_exceptions.py b/ionq_core/exceptions.py similarity index 100% rename from ionq_core/_exceptions.py rename to ionq_core/exceptions.py diff --git a/ionq_core/_extensions.py b/ionq_core/extensions.py similarity index 100% rename from ionq_core/_extensions.py rename to ionq_core/extensions.py diff --git a/ionq_core/_gates.py b/ionq_core/gates.py similarity index 100% rename from ionq_core/_gates.py rename to ionq_core/gates.py diff --git a/ionq_core/ionq_client.py b/ionq_core/ionq_client.py index ab371dc..c80036c 100644 --- a/ionq_core/ionq_client.py +++ b/ionq_core/ionq_client.py @@ -17,9 +17,9 @@ import httpx -from ._extensions import ClientExtension, HookTransport from ._transport import DEFAULT_MAX_RETRIES, RETRYABLE_STATUS_CODES, build_transport from .client import AuthenticatedClient +from .extensions import ClientExtension, HookTransport try: __version__ = _pkg_version("ionq-core") diff --git a/ionq_core/_pagination.py b/ionq_core/pagination.py similarity index 99% rename from ionq_core/_pagination.py rename to ionq_core/pagination.py index 8669d20..1c9f5fa 100644 --- a/ionq_core/_pagination.py +++ b/ionq_core/pagination.py @@ -23,8 +23,8 @@ from collections.abc import AsyncIterator, Callable, Iterator from typing import TYPE_CHECKING, Any -from ._exceptions import IonQError from .api.default import get_jobs, get_session_jobs +from .exceptions import IonQError from .types import UNSET, Unset if TYPE_CHECKING: diff --git a/ionq_core/_polling.py b/ionq_core/polling.py similarity index 99% rename from ionq_core/_polling.py rename to ionq_core/polling.py index c93aa1e..bdf8959 100644 --- a/ionq_core/_polling.py +++ b/ionq_core/polling.py @@ -27,8 +27,8 @@ import time from typing import TYPE_CHECKING -from ._exceptions import IonQError from .api.default import get_job +from .exceptions import IonQError from .types import Unset if TYPE_CHECKING: diff --git a/ionq_core/_session.py b/ionq_core/session.py similarity index 99% rename from ionq_core/_session.py rename to ionq_core/session.py index fd13f64..d2df148 100644 --- a/ionq_core/_session.py +++ b/ionq_core/session.py @@ -30,8 +30,8 @@ import logging from typing import TYPE_CHECKING -from ._exceptions import IonQError from .api.default import create_session, end_session, get_session +from .exceptions import IonQError from .models.create_session_request import CreateSessionRequest from .models.session_cost_limit import SessionCostLimit from .models.session_settings_request import SessionSettingsRequest diff --git a/tests/integration/test_sessions.py b/tests/integration/test_sessions.py index 22c293a..bd3b291 100644 --- a/tests/integration/test_sessions.py +++ b/tests/integration/test_sessions.py @@ -3,7 +3,7 @@ import pytest from ionq_core import SessionManager -from ionq_core._exceptions import NotFoundError +from ionq_core.exceptions import NotFoundError from ionq_core.api.default import get_session_jobs, get_sessions pytestmark = pytest.mark.integration diff --git a/tests/integration/test_usage.py b/tests/integration/test_usage.py index 545cc6e..32dad09 100644 --- a/tests/integration/test_usage.py +++ b/tests/integration/test_usage.py @@ -4,7 +4,7 @@ import pytest -from ionq_core._exceptions import PermissionDeniedError +from ionq_core.exceptions import PermissionDeniedError from ionq_core.api.usage import get_usages pytestmark = pytest.mark.integration diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index bfa0726..f8ece26 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,6 +1,6 @@ import pytest -from ionq_core._exceptions import ( +from ionq_core.exceptions import ( APIError, AuthenticationError, BadRequestError, diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 63e182e..fbb9a34 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -4,8 +4,8 @@ import pytest from ionq_core import ClientExtension, EventHook, IonQClient -from ionq_core._exceptions import APIError, NotFoundError -from ionq_core._extensions import ( +from ionq_core.exceptions import APIError, NotFoundError +from ionq_core.extensions import ( AsyncEventHook, HookTransport, ) diff --git a/tests/test_gates.py b/tests/test_gates.py index 6f12623..c74eadc 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -2,7 +2,7 @@ import pytest -from ionq_core._gates import gpi2_matrix, gpi_matrix, ms_matrix, zz_matrix +from ionq_core.gates import gpi2_matrix, gpi_matrix, ms_matrix, zz_matrix _PHIS = [0, 0.1, 0.25, 0.5, 0.73] diff --git a/tests/test_polling.py b/tests/test_polling.py index 420c3e6..6ca6bc2 100644 --- a/tests/test_polling.py +++ b/tests/test_polling.py @@ -2,8 +2,8 @@ import pytest -from ionq_core._exceptions import IonQError -from ionq_core._polling import JobFailedError, JobTimeoutError, async_wait_for_job, wait_for_job +from ionq_core.exceptions import IonQError +from ionq_core.polling import JobFailedError, JobTimeoutError, async_wait_for_job, wait_for_job from tests.conftest import make_job_json _real_sleep = asyncio.sleep @@ -17,7 +17,7 @@ def test_already_completed(self, httpx_mock, auth_client): assert wait_for_job(auth_client, "j1", timeout=5).status == "completed" def test_polls_until_completed(self, httpx_mock, auth_client, monkeypatch): - monkeypatch.setattr("ionq_core._polling.time.sleep", lambda _: None) + monkeypatch.setattr("ionq_core.polling.time.sleep", lambda _: None) for s in ("submitted", "ready", "completed"): httpx_mock.add_response(json=make_job_json("j1", s)) assert wait_for_job(auth_client, "j1", poll_interval=0.01, timeout=10).status == "completed" @@ -42,7 +42,7 @@ def test_none_response_raises(self, httpx_mock, auth_client): wait_for_job(auth_client, "j1", timeout=5) def test_timeout_raises(self, httpx_mock, auth_client, monkeypatch): - monkeypatch.setattr("ionq_core._polling.time.sleep", lambda _: None) + monkeypatch.setattr("ionq_core.polling.time.sleep", lambda _: None) httpx_mock.add_response(json=make_job_json("j1", "submitted"), is_reusable=True) with pytest.raises(JobTimeoutError, match="j1"): wait_for_job(auth_client, "j1", poll_interval=0.01, timeout=0.0) @@ -69,13 +69,13 @@ async def test_none_response_raises(self, httpx_mock, auth_client): await async_wait_for_job(auth_client, "j1", timeout=5) async def test_timeout_raises(self, httpx_mock, auth_client, monkeypatch): - monkeypatch.setattr("ionq_core._polling.asyncio.sleep", lambda _: _real_sleep(0)) + monkeypatch.setattr("ionq_core.polling.asyncio.sleep", lambda _: _real_sleep(0)) httpx_mock.add_response(json=make_job_json("j1", "submitted"), is_reusable=True) with pytest.raises(JobTimeoutError, match="j1"): await async_wait_for_job(auth_client, "j1", poll_interval=0.01, timeout=0.0) async def test_polls_until_completed(self, httpx_mock, auth_client, monkeypatch): - monkeypatch.setattr("ionq_core._polling.asyncio.sleep", lambda _: _real_sleep(0)) + monkeypatch.setattr("ionq_core.polling.asyncio.sleep", lambda _: _real_sleep(0)) for s in ("submitted", "ready", "completed"): httpx_mock.add_response(json=make_job_json("j1", s)) result = await async_wait_for_job(auth_client, "j1", poll_interval=0.01, timeout=10) diff --git a/tests/test_session.py b/tests/test_session.py index 38ad7a3..8dd710d 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -3,8 +3,8 @@ import httpx import pytest -from ionq_core._exceptions import IonQError -from ionq_core._session import SessionManager +from ionq_core.exceptions import IonQError +from ionq_core.session import SessionManager _BASE = "https://test.invalid/v0.4" diff --git a/tests/test_transport.py b/tests/test_transport.py index 1355d58..e9f5d71 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1,7 +1,7 @@ import httpx import pytest -from ionq_core._exceptions import ( +from ionq_core.exceptions import ( APIConnectionError, APITimeoutError, AuthenticationError, From a695ce0c822da5e94b8c84c4400c757f50e21e2c Mon Sep 17 00:00:00 2001 From: Spencer Churchill <25377399+splch@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:15:08 -0700 Subject: [PATCH 5/5] Fix import sorting in test files after module rename --- tests/integration/test_sessions.py | 2 +- tests/integration/test_usage.py | 2 +- tests/test_extensions.py | 2 +- tests/test_transport.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_sessions.py b/tests/integration/test_sessions.py index bd3b291..15a074c 100644 --- a/tests/integration/test_sessions.py +++ b/tests/integration/test_sessions.py @@ -3,8 +3,8 @@ import pytest from ionq_core import SessionManager -from ionq_core.exceptions import NotFoundError from ionq_core.api.default import get_session_jobs, get_sessions +from ionq_core.exceptions import NotFoundError pytestmark = pytest.mark.integration diff --git a/tests/integration/test_usage.py b/tests/integration/test_usage.py index 32dad09..85ffaa4 100644 --- a/tests/integration/test_usage.py +++ b/tests/integration/test_usage.py @@ -4,8 +4,8 @@ import pytest -from ionq_core.exceptions import PermissionDeniedError from ionq_core.api.usage import get_usages +from ionq_core.exceptions import PermissionDeniedError pytestmark = pytest.mark.integration diff --git a/tests/test_extensions.py b/tests/test_extensions.py index fbb9a34..e7d3075 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -4,12 +4,12 @@ import pytest from ionq_core import ClientExtension, EventHook, IonQClient +from ionq_core._transport import ErrorRaisingTransport from ionq_core.exceptions import APIError, NotFoundError from ionq_core.extensions import ( AsyncEventHook, HookTransport, ) -from ionq_core._transport import ErrorRaisingTransport _BACKENDS_URL = "https://api.ionq.co/v0.4/backends" diff --git a/tests/test_transport.py b/tests/test_transport.py index e9f5d71..8e38971 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1,6 +1,7 @@ import httpx import pytest +from ionq_core._transport import ErrorRaisingTransport, build_transport from ionq_core.exceptions import ( APIConnectionError, APITimeoutError, @@ -10,7 +11,6 @@ RateLimitError, ServerError, ) -from ionq_core._transport import ErrorRaisingTransport, build_transport _URL = "https://api.ionq.co/v0.4/backends"