qubithub 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {qubithub-0.2.0 → qubithub-0.2.1}/.gitignore +12 -0
  2. {qubithub-0.2.0 → qubithub-0.2.1}/CHANGELOG.md +15 -2
  3. {qubithub-0.2.0 → qubithub-0.2.1}/PKG-INFO +2 -2
  4. {qubithub-0.2.0 → qubithub-0.2.1}/pyproject.toml +8 -1
  5. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/__init__.py +1 -1
  6. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/async_client.py +10 -6
  7. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/auth.py +1 -1
  8. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/cli.py +15 -27
  9. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/repo.py +8 -11
  10. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_async_client.py +6 -2
  11. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_auth.py +23 -0
  12. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_cli.py +63 -67
  13. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_client_contracts.py +2 -6
  14. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_repo.py +18 -48
  15. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_repo_operations.py +8 -3
  16. {qubithub-0.2.0 → qubithub-0.2.1}/LICENSE +0 -0
  17. {qubithub-0.2.0 → qubithub-0.2.1}/README.md +0 -0
  18. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/_transport.py +0 -0
  19. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/client.py +0 -0
  20. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/display.py +0 -0
  21. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/exceptions.py +0 -0
  22. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/generated/__init__.py +0 -0
  23. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/models.py +0 -0
  24. {qubithub-0.2.0 → qubithub-0.2.1}/qubithub/py.typed +0 -0
  25. {qubithub-0.2.0 → qubithub-0.2.1}/tests/conftest.py +0 -0
  26. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_exceptions.py +0 -0
  27. {qubithub-0.2.0 → qubithub-0.2.1}/tests/test_models.py +0 -0
@@ -342,6 +342,11 @@ ui/.env.production.local
342
342
  !ui/src/lib/
343
343
 
344
344
  # Docs site (Starlight)
345
+ # Any node_modules, at any depth. The root .gitignore previously listed only
346
+ # `docs-site/node_modules/`, and `ui/` is covered by its own .gitignore — so a
347
+ # REPO-ROOT node_modules (created by running `bunx vitest` from the root rather
348
+ # than from ui/) was tracked, and a `git add -A` committed a vitest cache file.
349
+ node_modules/
345
350
  docs-site/node_modules/
346
351
  docs-site/dist/
347
352
  docs-site/.astro/
@@ -371,3 +376,10 @@ docs-site/.astro/
371
376
  # accidental in-repo placements as a safety net.
372
377
  migration-backup/
373
378
  /migration-backup/
379
+ .ds-sync/
380
+ ds-bundle/
381
+ .design-sync/.cache/
382
+ .design-sync/learnings/
383
+ .design-sync/node_modules
384
+ ui/.ds-entry.tsx
385
+ ui/.ds-styles.css
@@ -6,7 +6,19 @@ documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
- ## [0.2.0] — unreleased
9
+ ## [0.2.1] — unreleased
10
+
11
+ ### Fixed
12
+ - **Python 3.10:** `qubithub circuits push` and `circuits metadata` crashed with
13
+ `ModuleNotFoundError: No module named 'tomllib'` on reading `qubithub.toml` —
14
+ so a manifest `circuits init` had just written could not be published.
15
+ `tomllib` is standard library only from 3.11; the package now falls back to
16
+ `tomli` (already a declared dependency on 3.10). Python 3.11–3.13 were not
17
+ affected.
18
+
19
+ No other behaviour changed since 0.2.0 (the rest of the diff is formatting).
20
+
21
+ ## [0.2.0] — 2026-05-22
10
22
 
11
23
  First functional release of `qubithub` — the Python SDK and CLI for
12
24
  [QubitHub](https://qubithub.co), developer infrastructure for quantum computing.
@@ -53,4 +65,5 @@ which has been yanked from PyPI.
53
65
  - Supports Qiskit, Cirq, and PennyLane circuits executed against QubitHub's managed
54
66
  simulators.
55
67
 
56
- [0.2.0]: https://github.com/quantputation/qubithub/releases/tag/sdk-v0.2.0
68
+ [0.2.1]: https://pypi.org/project/qubithub/0.2.1/
69
+ [0.2.0]: https://pypi.org/project/qubithub/0.2.0/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: qubithub
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python SDK and CLI for QubitHub — developer infrastructure for quantum computing
5
5
  Project-URL: Homepage, https://qubithub.co
6
6
  Project-URL: Documentation, https://docs.qubithub.co
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "qubithub"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Python SDK and CLI for QubitHub — developer infrastructure for quantum computing"
9
9
  authors = [{ name = "QubitHub", email = "hi@qubithub.co" }]
10
10
  license = "Apache-2.0"
@@ -56,6 +56,13 @@ addopts = "-q"
56
56
  target-version = "py310"
57
57
  line-length = 100
58
58
 
59
+ [tool.ruff.format]
60
+ # README.md is this package's PyPI front page, and its examples are laid out
61
+ # by hand. ruff >=0.16 formats Python inside Markdown code blocks, so without
62
+ # this the `ruff format --check` gate reflows them — and, since `ruff` is
63
+ # unpinned here, the gate's verdict would depend on which ruff CI resolved.
64
+ exclude = ["*.md"]
65
+
59
66
  [tool.ruff.lint]
60
67
  select = ["E", "F", "I", "W"]
61
68
  ignore = ["E501"]
@@ -26,7 +26,7 @@ from .models import (
26
26
  User,
27
27
  )
28
28
 
29
- __version__ = "0.2.0"
29
+ __version__ = "0.2.1"
30
30
  __all__ = [
31
31
  "AsyncQubitHubClient",
32
32
  "QubitHubClient",
@@ -89,9 +89,7 @@ class AsyncQubitHubClient:
89
89
  elif auto_login:
90
90
  self._needs_auto_login = True
91
91
 
92
- self._http = httpx.AsyncClient(
93
- base_url=self.base_url, headers=headers, timeout=timeout
94
- )
92
+ self._http = httpx.AsyncClient(base_url=self.base_url, headers=headers, timeout=timeout)
95
93
 
96
94
  async def __aenter__(self) -> AsyncQubitHubClient:
97
95
  return self
@@ -154,7 +152,9 @@ class AsyncQubitHubClient:
154
152
 
155
153
  async def login(self, email: str, password: str) -> AuthResponse:
156
154
  """Log in, store credentials, and return AuthResponse."""
157
- data = await self._request("POST", "/auth/login", json={"email": email, "password": password})
155
+ data = await self._request(
156
+ "POST", "/auth/login", json={"email": email, "password": password}
157
+ )
158
158
  resp = AuthResponse(**data)
159
159
 
160
160
  self._http.headers["Authorization"] = f"Bearer {resp.access_token}"
@@ -242,7 +242,9 @@ class AsyncQubitHubClient:
242
242
  async def fork_circuit(self, circuit_id: str, name: str | None = None) -> CircuitDetail:
243
243
  """Fork a circuit. Optionally provide a custom name."""
244
244
  body = {"name": name} if name else {}
245
- return CircuitDetail(**await self._request("POST", f"/circuits/{circuit_id}/fork", json=body))
245
+ return CircuitDetail(
246
+ **await self._request("POST", f"/circuits/{circuit_id}/fork", json=body)
247
+ )
246
248
 
247
249
  async def list_circuit_files(
248
250
  self,
@@ -369,7 +371,9 @@ class AsyncQubitHubClient:
369
371
 
370
372
  async def create_api_key(self, req: APIKeyCreateRequest) -> APIKeyCreateResponse:
371
373
  """Create a new API key (plaintext shown once)."""
372
- return APIKeyCreateResponse(**await self._request("POST", "/api-keys", json=req.model_dump()))
374
+ return APIKeyCreateResponse(
375
+ **await self._request("POST", "/api-keys", json=req.model_dump())
376
+ )
373
377
 
374
378
  async def list_api_keys(self) -> APIKeyListResponse:
375
379
  """List user's API keys."""
@@ -264,6 +264,6 @@ def resolve_web_url(api_base_url: str, *, strip_api_prefix: bool = True) -> str:
264
264
 
265
265
  netloc = parts.netloc
266
266
  if strip_api_prefix and netloc.startswith("api."):
267
- netloc = netloc[len("api."):]
267
+ netloc = netloc[len("api.") :]
268
268
 
269
269
  return f"{parts.scheme}://{netloc}"
@@ -245,9 +245,7 @@ def _resolve_web_origin(api_base_url: str, override: str | None) -> str:
245
245
  would see a Python traceback instead of a CLI error.
246
246
  """
247
247
  if override:
248
- return auth_module.resolve_web_url(
249
- override, strip_api_prefix=False
250
- ).rstrip("/")
248
+ return auth_module.resolve_web_url(override, strip_api_prefix=False).rstrip("/")
251
249
  return auth_module.resolve_web_url(api_base_url).rstrip("/")
252
250
 
253
251
 
@@ -303,9 +301,7 @@ def _fail_clone_probe(repo_name: str, probe: repo.GitProxyProbe) -> None:
303
301
 
304
302
  @app.command()
305
303
  def clone(
306
- repo_name: Annotated[
307
- str, typer.Argument(help="Repository name (e.g., qubithub/bell-state)")
308
- ],
304
+ repo_name: Annotated[str, typer.Argument(help="Repository name (e.g., qubithub/bell-state)")],
309
305
  directory: Annotated[
310
306
  str | None, typer.Argument(help="Target directory (default: circuit name)")
311
307
  ] = None,
@@ -348,14 +344,10 @@ def clone(
348
344
  target = Path(directory) if directory else Path(slug)
349
345
  if target.exists():
350
346
  if not target.is_dir():
351
- display.display_error(
352
- f"Target '{target}' exists and is not a directory."
353
- )
347
+ display.display_error(f"Target '{target}' exists and is not a directory.")
354
348
  raise typer.Exit(1)
355
349
  if any(target.iterdir()):
356
- display.display_error(
357
- f"Directory '{target}' already exists and is not empty."
358
- )
350
+ display.display_error(f"Directory '{target}' already exists and is not empty.")
359
351
  raise typer.Exit(1)
360
352
 
361
353
  api_base_url = _client_kwargs.get("base_url") or "https://qubithub.co/api"
@@ -629,14 +621,10 @@ def _http_clone(repo_name: str, directory: str | None) -> None:
629
621
 
630
622
  if target.exists():
631
623
  if not target.is_dir():
632
- display.display_error(
633
- f"Target '{target}' exists and is not a directory."
634
- )
624
+ display.display_error(f"Target '{target}' exists and is not a directory.")
635
625
  raise typer.Exit(1)
636
626
  if any(target.iterdir()):
637
- display.display_error(
638
- f"Directory '{target}' already exists and is not empty."
639
- )
627
+ display.display_error(f"Directory '{target}' already exists and is not empty.")
640
628
  raise typer.Exit(1)
641
629
 
642
630
  # Get file list — deduplicate by name, keeping the highest version.
@@ -784,7 +772,9 @@ def circuits_push(
784
772
 
785
773
  circuit_name = name or metadata.get("name")
786
774
  if not circuit_name:
787
- display.display_error("Circuit name required (--name or 'name' in qubithub.toml/metadata.json)")
775
+ display.display_error(
776
+ "Circuit name required (--name or 'name' in qubithub.toml/metadata.json)"
777
+ )
788
778
  raise typer.Exit(1)
789
779
 
790
780
  try:
@@ -838,7 +828,9 @@ def circuits_push(
838
828
  commit_msg = message or "Push circuit files"
839
829
  response: dict[str, Any] | None = None
840
830
  if len(file_pairs) > 1:
841
- response = client.upload_circuit_files(circuit_id, file_pairs, commit_message=commit_msg)
831
+ response = client.upload_circuit_files(
832
+ circuit_id, file_pairs, commit_message=commit_msg
833
+ )
842
834
  elif file_pairs:
843
835
  local, name = file_pairs[0]
844
836
  client.upload_circuit_file(
@@ -876,8 +868,7 @@ def circuits_push(
876
868
 
877
869
  if errors and not uploaded:
878
870
  display.display_error(
879
- f"All {len(errors)} file(s) failed to upload to"
880
- f" {circuit.repo_name or circuit_name}"
871
+ f"All {len(errors)} file(s) failed to upload to {circuit.repo_name or circuit_name}"
881
872
  )
882
873
  raise typer.Exit(1)
883
874
  elif errors:
@@ -951,9 +942,7 @@ def circuits_init(
951
942
  @circuits_app.command("metadata")
952
943
  def circuits_metadata(
953
944
  directory: str = typer.Argument(".", help="Circuit directory"),
954
- output_format: str = typer.Option(
955
- "json", "--format", "-f", help="Output format: json or toml"
956
- ),
945
+ output_format: str = typer.Option("json", "--format", "-f", help="Output format: json or toml"),
957
946
  ):
958
947
  """Show parsed circuit metadata from qubithub.toml.
959
948
 
@@ -1101,8 +1090,7 @@ def _validate_config_key(key: str) -> None:
1101
1090
  """Reject config keys that would corrupt TOML structure (dots, brackets, etc.)."""
1102
1091
  if not _VALID_CONFIG_KEY.match(key):
1103
1092
  raise typer.BadParameter(
1104
- f"Invalid config key '{key}'. "
1105
- "Keys must contain only letters, digits, and underscores."
1093
+ f"Invalid config key '{key}'. Keys must contain only letters, digits, and underscores."
1106
1094
  )
1107
1095
 
1108
1096
 
@@ -27,6 +27,11 @@ from urllib.parse import urlsplit
27
27
 
28
28
  import httpx
29
29
 
30
+ try:
31
+ import tomllib # Python 3.11+
32
+ except ModuleNotFoundError:
33
+ import tomli as tomllib # type: ignore[no-redef]
34
+
30
35
  # ── Constants ────────────────────────────────────────────────────
31
36
 
32
37
  REQUIRED_FILES = {"README.md", "circuit.py", "qubithub.toml"}
@@ -278,8 +283,6 @@ def validate_directory(path: Path) -> ValidationResult:
278
283
 
279
284
  def _validate_toml(toml_path: Path, errors: list[str], warnings: list[str]) -> None:
280
285
  """Validate a qubithub.toml file using the TOML manifest parser."""
281
- import tomllib
282
-
283
286
  try:
284
287
  raw = tomllib.loads(toml_path.read_text())
285
288
  except Exception as e:
@@ -305,8 +308,7 @@ def _validate_toml(toml_path: Path, errors: list[str], warnings: list[str]) -> N
305
308
  difficulty = project.get("difficulty")
306
309
  if difficulty and difficulty not in VALID_DIFFICULTIES:
307
310
  warnings.append(
308
- f"Unknown difficulty '{difficulty}'. "
309
- f"Expected: {', '.join(sorted(VALID_DIFFICULTIES))}"
311
+ f"Unknown difficulty '{difficulty}'. Expected: {', '.join(sorted(VALID_DIFFICULTIES))}"
310
312
  )
311
313
 
312
314
  qubits = project.get("qubits")
@@ -314,9 +316,7 @@ def _validate_toml(toml_path: Path, errors: list[str], warnings: list[str]) -> N
314
316
  errors.append("qubithub.toml: project.qubits must be a positive integer")
315
317
 
316
318
 
317
- def _validate_metadata_json(
318
- metadata_path: Path, errors: list[str], warnings: list[str]
319
- ) -> None:
319
+ def _validate_metadata_json(metadata_path: Path, errors: list[str], warnings: list[str]) -> None:
320
320
  """Validate a metadata.json file (legacy format, backwards compat)."""
321
321
  try:
322
322
  metadata = json.loads(metadata_path.read_text())
@@ -341,8 +341,7 @@ def _validate_metadata_json(
341
341
  difficulty = metadata.get("difficulty")
342
342
  if difficulty and difficulty not in VALID_DIFFICULTIES:
343
343
  warnings.append(
344
- f"Unknown difficulty '{difficulty}'. "
345
- f"Expected: {', '.join(sorted(VALID_DIFFICULTIES))}"
344
+ f"Unknown difficulty '{difficulty}'. Expected: {', '.join(sorted(VALID_DIFFICULTIES))}"
346
345
  )
347
346
 
348
347
  qubits = metadata.get("qubits")
@@ -369,8 +368,6 @@ def load_metadata(path: Path) -> dict:
369
368
 
370
369
  def _load_metadata_from_toml(toml_path: Path) -> dict:
371
370
  """Load metadata from qubithub.toml and return as a metadata dict."""
372
- import tomllib
373
-
374
371
  raw = tomllib.loads(toml_path.read_text())
375
372
  project = raw.get("project", {})
376
373
 
@@ -265,6 +265,7 @@ async def test_timeout_raises_network_error():
265
265
  with pytest.raises(QubitHubError) as exc_info:
266
266
  await client.me()
267
267
  from qubithub.exceptions import NetworkError
268
+
268
269
  assert isinstance(exc_info.value, NetworkError)
269
270
  assert exc_info.value.cause is not None
270
271
  assert "timed out" in exc_info.value.message
@@ -281,6 +282,7 @@ async def test_connection_error_raises_network_error():
281
282
  with pytest.raises(QubitHubError) as exc_info:
282
283
  await client.me()
283
284
  from qubithub.exceptions import NetworkError
285
+
284
286
  assert isinstance(exc_info.value, NetworkError)
285
287
 
286
288
 
@@ -324,11 +326,13 @@ def test_async_client_has_same_public_methods():
324
326
  from qubithub.client import QubitHubClient
325
327
 
326
328
  sync_methods = {
327
- m for m in dir(QubitHubClient)
329
+ m
330
+ for m in dir(QubitHubClient)
328
331
  if not m.startswith("_") and callable(getattr(QubitHubClient, m))
329
332
  }
330
333
  async_methods = {
331
- m for m in dir(AsyncQubitHubClient)
334
+ m
335
+ for m in dir(AsyncQubitHubClient)
332
336
  if not m.startswith("_") and callable(getattr(AsyncQubitHubClient, m))
333
337
  }
334
338
  # aclose is async-only, close is sync-only — both expected
@@ -187,6 +187,29 @@ def test_refresh_access_token_success(monkeypatch):
187
187
  assert creds["refresh_token"] == "same_rt"
188
188
 
189
189
 
190
+ def test_refresh_access_token_stores_the_rotated_refresh_token(monkeypatch):
191
+ """The API rotates refresh tokens: the presented one is retired, and
192
+ presenting it again after a 30 s grace window revokes the whole session.
193
+ The SDK must keep the successor, never the token it sent."""
194
+
195
+ def mock_post(url, **kwargs):
196
+ assert kwargs["json"] == {"refresh_token": "retired_rt"}
197
+ return httpx.Response(
198
+ 200,
199
+ json={
200
+ "access_token": "new_at",
201
+ "refresh_token": "successor_rt",
202
+ "token_type": "bearer",
203
+ },
204
+ )
205
+
206
+ monkeypatch.setattr(httpx, "post", mock_post)
207
+ auth.store_tokens(access_token="old_at", refresh_token="retired_rt")
208
+
209
+ assert auth.refresh_access_token("http://test", "retired_rt") == "new_at"
210
+ assert auth.load_credentials()["refresh_token"] == "successor_rt"
211
+
212
+
190
213
  def test_refresh_access_token_failure(monkeypatch):
191
214
  """Failed refresh returns None."""
192
215
 
@@ -10,9 +10,15 @@ Tests all commands by mocking the QubitHubClient layer, verifying:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import contextlib
13
14
  import importlib.metadata
14
15
  import json
16
+ import os
17
+ import shutil
15
18
  import subprocess
19
+ import tempfile
20
+ from collections.abc import Iterator
21
+ from pathlib import Path
16
22
  from unittest.mock import MagicMock, patch
17
23
 
18
24
  import pytest
@@ -40,6 +46,27 @@ from qubithub.models import (
40
46
 
41
47
  runner = CliRunner()
42
48
 
49
+
50
+ @contextlib.contextmanager
51
+ def isolated_filesystem() -> Iterator[Path]:
52
+ """Run the block with the working directory set to a fresh temp dir.
53
+
54
+ Replaces ``CliRunner.isolated_filesystem``. typer 0.26.0 vendored click as
55
+ ``typer._click`` and dropped click from its dependencies; the vendored test
56
+ runner does not carry that helper, so ``typer.testing.CliRunner`` no longer
57
+ has it. Click 8.3 had deprecated it in any case, pointing at
58
+ ``tempfile.TemporaryDirectory`` — which is what this uses.
59
+ """
60
+ previous = os.getcwd()
61
+ target = tempfile.mkdtemp()
62
+ os.chdir(target)
63
+ try:
64
+ yield Path(target)
65
+ finally:
66
+ os.chdir(previous)
67
+ shutil.rmtree(target, ignore_errors=True)
68
+
69
+
43
70
  # ── Fixtures ─────────────────────────────────────────────────────
44
71
 
45
72
 
@@ -452,7 +479,7 @@ class TestClone:
452
479
  cli.auth_module, "load_credentials", lambda: {"access_token": "jwt-abc"}
453
480
  )
454
481
 
455
- with runner.isolated_filesystem():
482
+ with isolated_filesystem():
456
483
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
457
484
 
458
485
  assert result.exit_code == 0, result.output
@@ -520,7 +547,7 @@ class TestClone:
520
547
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
521
548
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
522
549
 
523
- with runner.isolated_filesystem():
550
+ with isolated_filesystem():
524
551
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
525
552
 
526
553
  assert result.exit_code == 0, result.output
@@ -537,11 +564,9 @@ class TestClone:
537
564
  monkeypatch.setattr(cli.repo, "is_git_available", lambda: True)
538
565
  monkeypatch.setattr(cli.repo, "git_clone", fake_git_clone)
539
566
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
540
- monkeypatch.setattr(
541
- cli.auth_module, "load_credentials", lambda: {"api_key": "qak_abc"}
542
- )
567
+ monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {"api_key": "qak_abc"})
543
568
 
544
- with runner.isolated_filesystem():
569
+ with isolated_filesystem():
545
570
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
546
571
 
547
572
  assert result.exit_code == 0, result.output
@@ -560,7 +585,7 @@ class TestClone:
560
585
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
561
586
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
562
587
 
563
- with runner.isolated_filesystem():
588
+ with isolated_filesystem():
564
589
  result = runner.invoke(
565
590
  cli.app,
566
591
  [
@@ -609,9 +634,7 @@ class TestClone:
609
634
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
610
635
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
611
636
 
612
- result = runner.invoke(
613
- cli.app, ["--format", "json", "clone", "qubithub/bell-state"]
614
- )
637
+ result = runner.invoke(cli.app, ["--format", "json", "clone", "qubithub/bell-state"])
615
638
 
616
639
  assert result.exit_code == 128
617
640
  payload = json.loads(result.output)
@@ -646,9 +669,7 @@ class TestClone:
646
669
  assert result.exit_code == 1
647
670
  assert called["git"] is False
648
671
 
649
- def test_clone_uses_explicit_token_flag_over_stored_creds(
650
- self, mock_client, monkeypatch
651
- ):
672
+ def test_clone_uses_explicit_token_flag_over_stored_creds(self, mock_client, monkeypatch):
652
673
  """``--token`` from the global flag wins over any stored credentials."""
653
674
  captured: dict = {}
654
675
 
@@ -659,16 +680,14 @@ class TestClone:
659
680
  monkeypatch.setattr(cli.repo, "is_git_available", lambda: True)
660
681
  monkeypatch.setattr(cli.repo, "git_clone", fake_git_clone)
661
682
  # Stored creds should be IGNORED in favour of the explicit flag
662
- monkeypatch.setattr(
663
- cli.auth_module, "resolve_token", lambda _u: "stored-jwt-stale"
664
- )
683
+ monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: "stored-jwt-stale")
665
684
  monkeypatch.setattr(
666
685
  cli.auth_module,
667
686
  "load_credentials",
668
687
  lambda: {"api_key": "stored-key-stale"},
669
688
  )
670
689
 
671
- with runner.isolated_filesystem():
690
+ with isolated_filesystem():
672
691
  result = runner.invoke(
673
692
  cli.app, ["--token", "explicit-jwt", "clone", "qubithub/bell-state"]
674
693
  )
@@ -676,9 +695,7 @@ class TestClone:
676
695
  assert result.exit_code == 0, result.output
677
696
  assert captured["token"] == "explicit-jwt"
678
697
 
679
- def test_clone_uses_explicit_api_key_flag_when_no_token(
680
- self, mock_client, monkeypatch
681
- ):
698
+ def test_clone_uses_explicit_api_key_flag_when_no_token(self, mock_client, monkeypatch):
682
699
  """``--api-key`` is honoured when no ``--token`` is given."""
683
700
  captured: dict = {}
684
701
 
@@ -691,7 +708,7 @@ class TestClone:
691
708
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
692
709
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
693
710
 
694
- with runner.isolated_filesystem():
711
+ with isolated_filesystem():
695
712
  result = runner.invoke(
696
713
  cli.app,
697
714
  ["--api-key", "qak_explicit", "clone", "qubithub/bell-state"],
@@ -700,9 +717,7 @@ class TestClone:
700
717
  assert result.exit_code == 0, result.output
701
718
  assert captured["token"] == "qak_explicit"
702
719
 
703
- def test_clone_explicit_api_key_beats_explicit_token(
704
- self, mock_client, monkeypatch
705
- ):
720
+ def test_clone_explicit_api_key_beats_explicit_token(self, mock_client, monkeypatch):
706
721
  """Within the explicit-CLI tier, api_key wins — matches QubitHubClient."""
707
722
  captured: dict = {}
708
723
 
@@ -715,12 +730,14 @@ class TestClone:
715
730
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
716
731
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
717
732
 
718
- with runner.isolated_filesystem():
733
+ with isolated_filesystem():
719
734
  result = runner.invoke(
720
735
  cli.app,
721
736
  [
722
- "--token", "tok-explicit",
723
- "--api-key", "qak-explicit",
737
+ "--token",
738
+ "tok-explicit",
739
+ "--api-key",
740
+ "qak-explicit",
724
741
  "clone",
725
742
  "qubithub/bell-state",
726
743
  ],
@@ -745,16 +762,14 @@ class TestClone:
745
762
  monkeypatch.setattr(cli.repo, "is_git_available", lambda: True)
746
763
  monkeypatch.setattr(cli.repo, "git_clone", fake_git_clone)
747
764
  # Both stored: stale JWT + recent api_key
748
- monkeypatch.setattr(
749
- cli.auth_module, "resolve_token", lambda _u: "stale-jwt"
750
- )
765
+ monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: "stale-jwt")
751
766
  monkeypatch.setattr(
752
767
  cli.auth_module,
753
768
  "load_credentials",
754
769
  lambda: {"access_token": "stale-jwt", "api_key": "qak-recent"},
755
770
  )
756
771
 
757
- with runner.isolated_filesystem():
772
+ with isolated_filesystem():
758
773
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
759
774
 
760
775
  assert result.exit_code == 0, result.output
@@ -769,9 +784,7 @@ class TestClone:
769
784
  "ht!tp://qubithub.co", # invalid scheme chars (urlsplit accepts but no host parse)
770
785
  ],
771
786
  )
772
- def test_clone_rejects_invalid_git_base_url(
773
- self, mock_client, monkeypatch, bad_override
774
- ):
787
+ def test_clone_rejects_invalid_git_base_url(self, mock_client, monkeypatch, bad_override):
775
788
  """Bad ``--git-base-url`` produces a clean ConfigurationError, not a traceback."""
776
789
  monkeypatch.setattr(cli.repo, "is_git_available", lambda: True)
777
790
  # Should never be invoked
@@ -781,7 +794,7 @@ class TestClone:
781
794
  lambda **_: pytest.fail("git_clone should not run on bad override"),
782
795
  )
783
796
 
784
- with runner.isolated_filesystem():
797
+ with isolated_filesystem():
785
798
  result = runner.invoke(
786
799
  cli.app,
787
800
  ["clone", "--git-base-url", bad_override, "qubithub/bell-state"],
@@ -792,8 +805,7 @@ class TestClone:
792
805
  assert "Traceback" not in result.output
793
806
  # Either the configuration error message OR a clean exit with example URL
794
807
  assert (
795
- "must include scheme and host" in result.output
796
- or "Invalid web origin" in result.output
808
+ "must include scheme and host" in result.output or "Invalid web origin" in result.output
797
809
  )
798
810
 
799
811
  def test_clone_rejects_file_as_target(self, mock_client, monkeypatch, tmp_path):
@@ -809,9 +821,7 @@ class TestClone:
809
821
  existing_file = tmp_path / "bell-state"
810
822
  existing_file.write_text("not a directory")
811
823
 
812
- result = runner.invoke(
813
- cli.app, ["clone", "qubithub/bell-state", str(existing_file)]
814
- )
824
+ result = runner.invoke(cli.app, ["clone", "qubithub/bell-state", str(existing_file)])
815
825
 
816
826
  assert result.exit_code == 1
817
827
  assert "not a directory" in result.output
@@ -831,9 +841,7 @@ class TestClone:
831
841
  assert result.exit_code == 0
832
842
  assert called["value"] is True
833
843
 
834
- def test_clone_fails_loud_on_server_404_when_git_available(
835
- self, mock_client, monkeypatch
836
- ):
844
+ def test_clone_fails_loud_on_server_404_when_git_available(self, mock_client, monkeypatch):
837
845
  """Proxy 404 (disabled / not git-backed) → fail loud, never clone.
838
846
 
839
847
  The whole point of Phase 2: a server-side fault must not
@@ -860,16 +868,14 @@ class TestClone:
860
868
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
861
869
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
862
870
 
863
- with runner.isolated_filesystem():
871
+ with isolated_filesystem():
864
872
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
865
873
 
866
874
  assert result.exit_code == 1
867
875
  assert "does not support git clone" in result.output
868
876
  assert "--no-git" in result.output
869
877
 
870
- def test_clone_fails_loud_on_non_git_200_response(
871
- self, mock_client, monkeypatch
872
- ):
878
+ def test_clone_fails_loud_on_non_git_200_response(self, mock_client, monkeypatch):
873
879
  """A 200 non-git response (SPA fallthrough) fails loud — and the
874
880
  message never cites a contradictory 'HTTP 200'."""
875
881
  monkeypatch.setattr(cli.repo, "is_git_available", lambda: True)
@@ -888,7 +894,7 @@ class TestClone:
888
894
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
889
895
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
890
896
 
891
- with runner.isolated_filesystem():
897
+ with isolated_filesystem():
892
898
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
893
899
 
894
900
  assert result.exit_code == 1
@@ -915,7 +921,7 @@ class TestClone:
915
921
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
916
922
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
917
923
 
918
- with runner.isolated_filesystem():
924
+ with isolated_filesystem():
919
925
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
920
926
 
921
927
  assert result.exit_code == 1
@@ -923,9 +929,7 @@ class TestClone:
923
929
  assert "transient" in result.output
924
930
  assert "--no-git" in result.output
925
931
 
926
- def test_clone_falls_back_silently_when_git_missing(
927
- self, mock_client, monkeypatch
928
- ):
932
+ def test_clone_falls_back_silently_when_git_missing(self, mock_client, monkeypatch):
929
933
  """git absent → HTTP fallback WITHOUT probing the proxy.
930
934
 
931
935
  The probe distinguishes server faults from user-environment
@@ -938,9 +942,7 @@ class TestClone:
938
942
 
939
943
  def counting_probe(*a, **kw):
940
944
  probe_calls["n"] += 1
941
- return cli.repo.GitProxyProbe(
942
- status=cli.repo.PROXY_SUPPORTED, status_code=200
943
- )
945
+ return cli.repo.GitProxyProbe(status=cli.repo.PROXY_SUPPORTED, status_code=200)
944
946
 
945
947
  monkeypatch.setattr(cli.repo, "probe_git_proxy", counting_probe)
946
948
 
@@ -978,7 +980,7 @@ class TestClone:
978
980
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
979
981
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
980
982
 
981
- with runner.isolated_filesystem():
983
+ with isolated_filesystem():
982
984
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
983
985
 
984
986
  assert result.exit_code == 0, result.output
@@ -1002,19 +1004,15 @@ class TestClone:
1002
1004
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
1003
1005
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
1004
1006
 
1005
- with runner.isolated_filesystem():
1006
- result = runner.invoke(
1007
- cli.app, ["--format", "json", "clone", "qubithub/bell-state"]
1008
- )
1007
+ with isolated_filesystem():
1008
+ result = runner.invoke(cli.app, ["--format", "json", "clone", "qubithub/bell-state"])
1009
1009
 
1010
1010
  assert result.exit_code == 1
1011
1011
  payload = json.loads(result.output)
1012
1012
  assert payload["error"] == "git_proxy_unsupported"
1013
1013
  assert payload["status_code"] == 404
1014
1014
 
1015
- def test_probe_avoids_extra_request_on_happy_path(
1016
- self, mock_client, monkeypatch
1017
- ):
1015
+ def test_probe_avoids_extra_request_on_happy_path(self, mock_client, monkeypatch):
1018
1016
  """Happy path probes exactly once — one quick check, then clone.
1019
1017
 
1020
1018
  Pins the contract that the SDK never double-counts requests: a
@@ -1027,9 +1025,7 @@ class TestClone:
1027
1025
 
1028
1026
  def counting_probe(*a, **kw):
1029
1027
  probe_calls["n"] += 1
1030
- return cli.repo.GitProxyProbe(
1031
- status=cli.repo.PROXY_SUPPORTED, status_code=200
1032
- )
1028
+ return cli.repo.GitProxyProbe(status=cli.repo.PROXY_SUPPORTED, status_code=200)
1033
1029
 
1034
1030
  monkeypatch.setattr(cli.repo, "probe_git_proxy", counting_probe)
1035
1031
 
@@ -1043,7 +1039,7 @@ class TestClone:
1043
1039
  monkeypatch.setattr(cli.auth_module, "resolve_token", lambda _u: None)
1044
1040
  monkeypatch.setattr(cli.auth_module, "load_credentials", lambda: {})
1045
1041
 
1046
- with runner.isolated_filesystem():
1042
+ with isolated_filesystem():
1047
1043
  result = runner.invoke(cli.app, ["clone", "qubithub/bell-state"])
1048
1044
 
1049
1045
  assert result.exit_code == 0, result.output
@@ -662,13 +662,9 @@ def test_clients_default_to_prod_api_base_url():
662
662
  from qubithub.async_client import AsyncQubitHubClient
663
663
 
664
664
  expected = "https://qubithub.co/api"
665
+ assert inspect.signature(QubitHubClient.__init__).parameters["base_url"].default == expected
665
666
  assert (
666
- inspect.signature(QubitHubClient.__init__).parameters["base_url"].default
667
- == expected
668
- )
669
- assert (
670
- inspect.signature(AsyncQubitHubClient.__init__).parameters["base_url"].default
671
- == expected
667
+ inspect.signature(AsyncQubitHubClient.__init__).parameters["base_url"].default == expected
672
668
  )
673
669
  # And the default flows through to the live client attribute.
674
670
  assert QubitHubClient(auto_login=False).base_url == expected
@@ -31,6 +31,11 @@ from qubithub.repo import (
31
31
  validate_directory,
32
32
  )
33
33
 
34
+ try:
35
+ import tomllib # Python 3.11+
36
+ except ModuleNotFoundError:
37
+ import tomli as tomllib # type: ignore[no-redef]
38
+
34
39
  # ── Validation ───────────────────────────────────────────────────
35
40
 
36
41
 
@@ -167,8 +172,6 @@ class TestInitCircuit:
167
172
  assert not (target / "metadata.json").exists()
168
173
 
169
174
  def test_toml_content(self, tmp_path):
170
- import tomllib
171
-
172
175
  target = tmp_path / "bell-state"
173
176
  init_circuit(target, "bell-state", framework="qiskit", qubits=2)
174
177
  parsed = tomllib.loads((target / "qubithub.toml").read_text())
@@ -180,8 +183,6 @@ class TestInitCircuit:
180
183
  assert parsed["project"]["entry_point"] == "circuit.py"
181
184
 
182
185
  def test_custom_title(self, tmp_path):
183
- import tomllib
184
-
185
186
  target = tmp_path / "my-circuit"
186
187
  init_circuit(target, "my-circuit", title="My Custom Circuit")
187
188
  parsed = tomllib.loads((target / "qubithub.toml").read_text())
@@ -320,16 +321,12 @@ class TestCheckFrameworkVersion:
320
321
  assert check_framework_version({"framework_version": "1.3.0"}) == []
321
322
 
322
323
  def test_matching_version_returns_empty(self, monkeypatch):
323
- monkeypatch.setattr(
324
- "qubithub.repo.get_installed_framework_version", lambda _fw: "1.3.2"
325
- )
324
+ monkeypatch.setattr("qubithub.repo.get_installed_framework_version", lambda _fw: "1.3.2")
326
325
  warnings = check_framework_version({"framework": "qiskit", "framework_version": "1.3.0"})
327
326
  assert warnings == []
328
327
 
329
328
  def test_mismatch_warns(self, monkeypatch):
330
- monkeypatch.setattr(
331
- "qubithub.repo.get_installed_framework_version", lambda _fw: "2.0.1"
332
- )
329
+ monkeypatch.setattr("qubithub.repo.get_installed_framework_version", lambda _fw: "2.0.1")
333
330
  warnings = check_framework_version({"framework": "qiskit", "framework_version": "1.3.0"})
334
331
  assert len(warnings) == 1
335
332
  assert "mismatch" in warnings[0].lower()
@@ -337,18 +334,14 @@ class TestCheckFrameworkVersion:
337
334
  assert "2.0" in warnings[0]
338
335
 
339
336
  def test_not_installed_warns_with_install_hint(self, monkeypatch):
340
- monkeypatch.setattr(
341
- "qubithub.repo.get_installed_framework_version", lambda _fw: None
342
- )
337
+ monkeypatch.setattr("qubithub.repo.get_installed_framework_version", lambda _fw: None)
343
338
  warnings = check_framework_version({"framework": "cirq", "framework_version": "1.4.0"})
344
339
  assert len(warnings) == 1
345
340
  assert "not installed" in warnings[0].lower()
346
341
  assert "pip install cirq-core" in warnings[0]
347
342
 
348
343
  def test_unknown_framework_warns(self, monkeypatch):
349
- monkeypatch.setattr(
350
- "qubithub.repo.get_installed_framework_version", lambda _fw: None
351
- )
344
+ monkeypatch.setattr("qubithub.repo.get_installed_framework_version", lambda _fw: None)
352
345
  warnings = check_framework_version(
353
346
  {"framework": "unknown-fw", "framework_version": "1.0.0"}
354
347
  )
@@ -357,17 +350,13 @@ class TestCheckFrameworkVersion:
357
350
 
358
351
  def test_same_major_different_patch_no_warning(self, monkeypatch):
359
352
  """Patch-level differences should NOT trigger a warning."""
360
- monkeypatch.setattr(
361
- "qubithub.repo.get_installed_framework_version", lambda _fw: "1.3.5"
362
- )
353
+ monkeypatch.setattr("qubithub.repo.get_installed_framework_version", lambda _fw: "1.3.5")
363
354
  warnings = check_framework_version({"framework": "qiskit", "framework_version": "1.3.0"})
364
355
  assert warnings == []
365
356
 
366
357
  def test_pre_release_matches(self, monkeypatch):
367
358
  """Pre-release installed version should match the declared major.minor."""
368
- monkeypatch.setattr(
369
- "qubithub.repo.get_installed_framework_version", lambda _fw: "1.3.0rc1"
370
- )
359
+ monkeypatch.setattr("qubithub.repo.get_installed_framework_version", lambda _fw: "1.3.0rc1")
371
360
  warnings = check_framework_version({"framework": "qiskit", "framework_version": "1.3.0"})
372
361
  assert warnings == []
373
362
 
@@ -536,9 +525,7 @@ class TestBuildGitCloneURL:
536
525
  assert url == "https://qubithub.co/qubithub/bell-state.git"
537
526
 
538
527
  def test_preserves_port_for_localhost(self):
539
- url = build_git_clone_url(
540
- "http://localhost:8000", "alice", "demo", with_username=False
541
- )
528
+ url = build_git_clone_url("http://localhost:8000", "alice", "demo", with_username=False)
542
529
  assert url == "http://localhost:8000/alice/demo.git"
543
530
 
544
531
  def test_invalid_origin_raises(self):
@@ -707,7 +694,6 @@ class TestGitClone:
707
694
  askpass_paths.append(env["GIT_ASKPASS"])
708
695
  raise subprocess.CalledProcessError(returncode=128, cmd=cmd)
709
696
 
710
-
711
697
  with patch("qubithub.repo.subprocess.run", side_effect=fake_run):
712
698
  with pytest.raises(subprocess.CalledProcessError):
713
699
  git_clone(
@@ -731,7 +717,6 @@ class TestGitClone:
731
717
  (Path(cmd[-1]) / "partial.txt").write_text("oops")
732
718
  raise subprocess.CalledProcessError(returncode=128, cmd=cmd)
733
719
 
734
-
735
720
  with patch("qubithub.repo.subprocess.run", side_effect=fake_run):
736
721
  with pytest.raises(subprocess.CalledProcessError):
737
722
  git_clone(
@@ -753,7 +738,6 @@ class TestGitClone:
753
738
  def fake_run(cmd, env, check, **_kwargs):
754
739
  raise subprocess.CalledProcessError(returncode=128, cmd=cmd)
755
740
 
756
-
757
741
  with patch("qubithub.repo.subprocess.run", side_effect=fake_run):
758
742
  with pytest.raises(subprocess.CalledProcessError):
759
743
  git_clone(
@@ -780,10 +764,7 @@ class TestResolveWebURL:
780
764
  def test_strips_api_prefix_for_staging(self):
781
765
  from qubithub.auth import resolve_web_url
782
766
 
783
- assert (
784
- resolve_web_url("https://api.staging.qubithub.co")
785
- == "https://staging.qubithub.co"
786
- )
767
+ assert resolve_web_url("https://api.staging.qubithub.co") == "https://staging.qubithub.co"
787
768
 
788
769
  def test_preserves_port(self):
789
770
  from qubithub.auth import resolve_web_url
@@ -798,10 +779,7 @@ class TestResolveWebURL:
798
779
  def test_drops_path_query_fragment(self):
799
780
  from qubithub.auth import resolve_web_url
800
781
 
801
- assert (
802
- resolve_web_url("https://api.qubithub.co/v2?x=1#frag")
803
- == "https://qubithub.co"
804
- )
782
+ assert resolve_web_url("https://api.qubithub.co/v2?x=1#frag") == "https://qubithub.co"
805
783
 
806
784
  def test_missing_scheme_raises(self):
807
785
  from qubithub.auth import resolve_web_url
@@ -832,9 +810,7 @@ class TestResolveWebURL:
832
810
  from qubithub.auth import resolve_web_url
833
811
 
834
812
  assert (
835
- resolve_web_url(
836
- "https://example.com/foo?x=1#frag", strip_api_prefix=False
837
- )
813
+ resolve_web_url("https://example.com/foo?x=1#frag", strip_api_prefix=False)
838
814
  == "https://example.com"
839
815
  )
840
816
 
@@ -865,9 +841,7 @@ class TestProbeGitProxy:
865
841
  monkeypatch,
866
842
  response=httpx.Response(
867
843
  200,
868
- headers={
869
- "content-type": "application/x-git-upload-pack-advertisement"
870
- },
844
+ headers={"content-type": "application/x-git-upload-pack-advertisement"},
871
845
  ),
872
846
  )
873
847
  probe = probe_git_proxy("https://qubithub.co", "qubithub", "bell-state")
@@ -892,9 +866,7 @@ class TestProbeGitProxy:
892
866
  """A 200 HTML body is an SPA catch-all, not a real git endpoint."""
893
867
  self._patch_get(
894
868
  monkeypatch,
895
- response=httpx.Response(
896
- 200, headers={"content-type": "text/html; charset=utf-8"}
897
- ),
869
+ response=httpx.Response(200, headers={"content-type": "text/html; charset=utf-8"}),
898
870
  )
899
871
  probe = probe_git_proxy("https://self-hosted.example.com", "x", "y")
900
872
  assert probe.status == PROXY_UNSUPPORTED
@@ -930,9 +902,7 @@ class TestProbeGitProxy:
930
902
  monkeypatch,
931
903
  response=httpx.Response(
932
904
  200,
933
- headers={
934
- "content-type": "application/x-git-upload-pack-advertisement"
935
- },
905
+ headers={"content-type": "application/x-git-upload-pack-advertisement"},
936
906
  ),
937
907
  )
938
908
  probe_git_proxy("https://qubithub.co", "qubithub", "bell-state")
@@ -20,6 +20,11 @@ from qubithub.client import QubitHubClient
20
20
  from qubithub.exceptions import APIError, QubitHubError
21
21
  from qubithub.models import CircuitDetail, User
22
22
 
23
+ try:
24
+ import tomllib # Python 3.11+
25
+ except ModuleNotFoundError:
26
+ import tomli as tomllib # type: ignore[no-redef]
27
+
23
28
  runner = CliRunner()
24
29
 
25
30
 
@@ -450,7 +455,6 @@ class TestInitCLI:
450
455
  monkeypatch.chdir(tmp_path)
451
456
  result = runner.invoke(app, ["circuits", "init", "my-circuit", "--framework", "cirq"])
452
457
  assert result.exit_code == 0
453
- import tomllib
454
458
  parsed = tomllib.loads((tmp_path / "my-circuit" / "qubithub.toml").read_text())
455
459
  assert parsed["project"]["framework"] == "cirq"
456
460
 
@@ -480,7 +484,6 @@ class TestInitCLI:
480
484
  monkeypatch.chdir(tmp_path)
481
485
  result = runner.invoke(app, ["circuits", "init", "my-circuit", "--qubits", "4"])
482
486
  assert result.exit_code == 0
483
- import tomllib
484
487
  parsed = tomllib.loads((tmp_path / "my-circuit" / "qubithub.toml").read_text())
485
488
  assert parsed["project"]["qubits"] == 4
486
489
 
@@ -600,7 +603,9 @@ class TestPushCLI:
600
603
  assert result.exit_code == 0
601
604
  # Check that commit message was passed to batch upload call
602
605
  mock_client.upload_circuit_files.assert_called_once()
603
- assert mock_client.upload_circuit_files.call_args.kwargs["commit_message"] == "Initial commit"
606
+ assert (
607
+ mock_client.upload_circuit_files.call_args.kwargs["commit_message"] == "Initial commit"
608
+ )
604
609
 
605
610
  def test_push_not_found_without_create(self, mock_client, tmp_path, monkeypatch):
606
611
  monkeypatch.chdir(tmp_path)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes