diff --git a/__init__.py b/__init__.py index 41c6fba7..7482e606 100644 --- a/__init__.py +++ b/__init__.py @@ -120,6 +120,7 @@ except Exception as e: logger = get_logger("prompt_manager.init") logger.error(f"Failed to register API routes: {e}") except Exception: + # logging failed too; a report must never stop the node loading pass # Shared database, set up with the monitoring system below. Stays None when that @@ -411,6 +412,7 @@ except Exception as e: f"Failed to register the exit signal: {e}" ) except Exception: + # logging failed too; a report must never stop the node loading pass try: @@ -428,6 +430,7 @@ except Exception as e: f"Failed to register the background work teardown: {e}" ) except Exception: + # logging failed too; a report must never stop the node loading pass __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/database/library_move.py b/database/library_move.py index 91dc7274..c104ac15 100644 --- a/database/library_move.py +++ b/database/library_move.py @@ -590,6 +590,7 @@ class _Move: try: os.rmdir(self.plan.target_dir) except OSError: + # not empty or already gone: the undo leaves it alone pass diff --git a/database/managed_connection.py b/database/managed_connection.py index fb71fc6d..49e58390 100644 --- a/database/managed_connection.py +++ b/database/managed_connection.py @@ -35,38 +35,51 @@ from __future__ import annotations import contextlib import logging +import operator +import sys import threading import time import weakref -from typing import Iterable, Iterator, List, Optional +from collections import deque +from typing import Any, Callable, Iterable, Iterator, List, Optional import sqlite3 CLOSED_MESSAGE = "Cannot operate on a closed database." # Rows fetched per step when a cursor is iterated: the connection is marked # in use once per chunk instead of once per row (iteration costs about what -# a plain cursor costs). A loop that stops early leaves up to this many rows -# fetched; ``fetchone``/``fetchmany`` after a partial loop skip them. +# a plain cursor costs). Prefetched rows belong to the cursor so iteration +# and explicit fetches always advance the same position. ITER_CHUNK = 128 class ManagedCursor(sqlite3.Cursor): """A cursor whose calls into SQLite count as its connection being in use.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._pending = deque() + self._closed = False + def execute(self, sql, parameters=(), /): - conn = self.connection - conn._enter() - try: - return super().execute(sql, parameters) - finally: - conn._leave() + return self._run_statement(super().execute, sql, parameters) def executemany(self, sql, seq_of_parameters, /): + return self._run_statement(super().executemany, sql, seq_of_parameters) + + def _run_statement(self, operation: Callable, *args: Any) -> ManagedCursor: conn = self.connection conn._enter() + description = self.description + completed = False try: - return super().executemany(sql, seq_of_parameters) + result = operation(*args) + completed = True + return result finally: + # Argument parsing can fail before SQLite replaces the old statement. + if completed or self.description is not description: + self._pending.clear() conn._leave() def executescript(self, sql_script, /): @@ -81,6 +94,8 @@ class ManagedCursor(sqlite3.Cursor): conn = self.connection conn._enter() try: + if self._pending and not self._closed: + return self._pending.popleft() return super().fetchone() finally: conn._leave() @@ -89,7 +104,20 @@ class ManagedCursor(sqlite3.Cursor): conn = self.connection conn._enter() try: - return super().fetchmany(*args, **kwargs) + # Let SQLite validate the original arguments, including version-specific + # handling of zero/negative sizes, before touching prefetched rows. + rows = super().fetchmany(*args, **kwargs) + if not self._pending: + return rows + size = operator.index( + kwargs.get("size", args[0] if args else self.arraysize) + ) + # Python 3.12 reads all rows for size=0; 3.13+ reads none. + if size == 0 and sys.version_info >= (3, 13): + return rows + self._pending.extend(rows) + count = len(self._pending) if size <= 0 else min(size, len(self._pending)) + return [self._pending.popleft() for _ in range(count)] finally: conn._leave() @@ -97,25 +125,31 @@ class ManagedCursor(sqlite3.Cursor): conn = self.connection conn._enter() try: - return super().fetchall() + rows = super().fetchall() + pending = list(self._pending) + self._pending.clear() + return pending + rows finally: conn._leave() def __iter__(self) -> Iterator: - return self._chunks() - - def _chunks(self) -> Iterator: - while True: - rows = self.fetchmany(ITER_CHUNK) - if not rows: - return - yield from rows + return self def __next__(self): conn = self.connection + pending = self._pending + # Buffered Python rows need no SQLite lock, but retirement and cursor + # close must still stop an iterator obtained before either operation. + if conn._retired or self._closed: + raise sqlite3.ProgrammingError(CLOSED_MESSAGE) + if pending: + return pending.popleft() conn._enter() try: - return super().__next__() + pending.extend(super().fetchmany(ITER_CHUNK)) + if not pending: + raise StopIteration + return pending.popleft() finally: conn._leave() @@ -126,7 +160,10 @@ class ManagedCursor(sqlite3.Cursor): except sqlite3.ProgrammingError: return super().close() # the connection is closed or closing try: - return super().close() + result = super().close() + self._pending = deque() + self._closed = True + return result finally: conn._leave() @@ -293,6 +330,9 @@ class ManagedConnection(sqlite3.Connection): cursors = list(self._cursors) try: for cursor in cursors: + if isinstance(cursor, ManagedCursor): + cursor._pending = deque() + cursor._closed = True with contextlib.suppress(sqlite3.Error): sqlite3.Cursor.close(cursor) super().close() diff --git a/database/operations.py b/database/operations.py index 51607876..2e1dca0b 100644 --- a/database/operations.py +++ b/database/operations.py @@ -34,7 +34,7 @@ from .history_reads import HistoryReadOperations from .health import HealthOperations, iter_consistency_issues from .duplicate_copies import DuplicateCopyOperations from .workflow_storage import WorkflowStorageOperations -from .category_import import CategoryImportOperations, EmptyReplacement # noqa: F401 +from .category_import import CategoryImportOperations from .image_categories import ImageCategoryOperations from .workflow_documents import ( DocumentReader, diff --git a/database/safety_backup.py b/database/safety_backup.py index ad212a14..5ce36619 100644 --- a/database/safety_backup.py +++ b/database/safety_backup.py @@ -46,6 +46,7 @@ def remove_quietly(*paths): if path and os.path.exists(path): os.remove(path) except OSError: + # cleanup after a failure must not replace that failure pass diff --git a/database/vocabulary.py b/database/vocabulary.py index 971112bd..206d13f6 100644 --- a/database/vocabulary.py +++ b/database/vocabulary.py @@ -66,7 +66,6 @@ def _tag_rows(page: str, order: str = "page.id") -> str: ) -_TAG_PAGE = _tag_rows("SELECT id,name FROM tags WHERE id>? ORDER BY id LIMIT ?") # #444 tag sorts: sort -> (key expression on tags/counted, direction). Ties and # "created" follow the id, so every order is total and keyset pages are exact. TAG_SORTS = { diff --git a/py/api/admin.py b/py/api/admin.py index 64768544..3488a056 100644 --- a/py/api/admin.py +++ b/py/api/admin.py @@ -176,7 +176,7 @@ def _physical_file_key(path, stat): volumes on Windows) falls back to the resolved, case-folded path. """ if stat.st_ino: - return ("inode", stat.st_dev, stat.st_ino) + return ("inode", (stat.st_dev, stat.st_ino)) return ("path", os.path.normcase(os.path.realpath(str(path)))) @@ -539,6 +539,7 @@ def _diagnose_output_dirs(configured_dirs, public_path=None): candidates.append(folder_paths.get_output_directory()) except ImportError: + # outside ComfyUI there is no output folder to add pass output_dirs = [] diff --git a/py/api/annotation_suggest.py b/py/api/annotation_suggest.py index 9411640b..bb676d92 100644 --- a/py/api/annotation_suggest.py +++ b/py/api/annotation_suggest.py @@ -13,7 +13,6 @@ import math from ..tagging_defaults import ( # noqa: F401 - re-exported for the route LLM_MODEL_TYPES as LLM_MODELS, MAX_INSTRUCTION_BYTES, - MAX_INSTRUCTION_CHARS, MODEL_TYPES, WD14_MODEL_TYPES as WD14_MODELS, validate_template, diff --git a/py/api/autotag_routes.py b/py/api/autotag_routes.py index 388e5e3a..9e82d30d 100644 --- a/py/api/autotag_routes.py +++ b/py/api/autotag_routes.py @@ -424,6 +424,7 @@ class AutotagRoutesMixin: title = f"Tagging model download · {label}" _download_titles.clear() _download_titles[owner] = title + admitted = False try: job = self._jobs.start( MODEL_DOWNLOAD_JOB_KIND, @@ -439,13 +440,13 @@ class AutotagRoutesMixin: self._run_model_download, service, model_type, owner ), ) - except BaseException as refused: - # JobBusy, or anything else the registry raised: give the gate back. - model_gate.release(owner) - _download_titles.pop(owner, None) - if isinstance(refused, JobBusy): - return None, busy_response(refused) - raise + admitted = True + except JobBusy as refused: + return None, busy_response(refused) + finally: + if not admitted: # JobBusy, or anything else: give the gate back. + model_gate.release(owner) + _download_titles.pop(owner, None) def ended(_task): model_gate.release(owner) @@ -1041,12 +1042,12 @@ class AutotagRoutesMixin: return self._bad_request('existing must be "skip" or "merge"') try: options = self._single_options(data, model_type) + except OptionError as error: + return self._bad_request(str(error)) except (ValueError, TypeError): return self._bad_request( "general_threshold and character_threshold must be numeric" ) - except OptionError as error: - return self._bad_request(str(error)) target, error = await self._select_autotag_target(data) if error is not None: return error diff --git a/py/api/backup_restore.py b/py/api/backup_restore.py index a83ecbfa..ab3d030a 100644 --- a/py/api/backup_restore.py +++ b/py/api/backup_restore.py @@ -971,6 +971,7 @@ class BackupRestoreRoutesMixin: line = restored_line(staged.summary) job.log_line(line) job.update(progress=100, status_line=line) + return None async def _outlive_cancel(future): diff --git a/py/api/duplicate_files.py b/py/api/duplicate_files.py index 34e15ed4..9a844253 100644 --- a/py/api/duplicate_files.py +++ b/py/api/duplicate_files.py @@ -574,12 +574,13 @@ class DuplicateFilesMixin: return _fail_job(job, "sources-changed", SOURCES_CHANGED) ok, backup = await self._safety_backup_step(job, db, backup_on) if not ok: - return + return None try: await self._remove_duplicates(job, db, plan, backup) finally: if backup: await self._prune_backup_pool(job, db) + return None async def _remove_duplicates(self, job, db, plan, backup): targets = plan.payload["targets"] diff --git a/py/api/duplicate_spellings.py b/py/api/duplicate_spellings.py index 7157e994..1e6d2aff 100644 --- a/py/api/duplicate_spellings.py +++ b/py/api/duplicate_spellings.py @@ -27,7 +27,7 @@ def _identity(path): except OSError: return None if info.st_ino: - return ("inode", info.st_dev, info.st_ino) + return ("inode", (info.st_dev, info.st_ino)) return ("path", os.path.normcase(os.path.realpath(str(path)))) diff --git a/py/api/image_delete.py b/py/api/image_delete.py index 862cda26..b81b8497 100644 --- a/py/api/image_delete.py +++ b/py/api/image_delete.py @@ -330,12 +330,13 @@ class ImageDeleteMixin: return _fail_job(job, "sources-changed", SOURCES_CHANGED) ok, backup = await self._safety_backup_step(job, db, backup_on) if not ok: - return + return None try: await self._delete_images(job, db, targets, mode, backup) finally: if backup: await self._prune_backup_pool(job, db) + return None async def _delete_images(self, job, db, targets, mode, backup): job.update(total=len(targets), status_line=TITLES[mode]) diff --git a/py/api/image_tag_jobs.py b/py/api/image_tag_jobs.py index c16751b0..33b679c6 100644 --- a/py/api/image_tag_jobs.py +++ b/py/api/image_tag_jobs.py @@ -531,6 +531,7 @@ class ImageTagJobsMixin: run = AutotagRun( model_type, existing, options, keep_in_memory, backup_on, owner ) + admitted = False try: job = self._jobs.start( AUTOTAG_BATCH_JOB_KIND, @@ -541,11 +542,12 @@ class ImageTagJobsMixin: groups=AUTOTAG_GROUP_NAMES, runner=functools.partial(self._run_autotag_batch, run), ) - except BaseException as refused: - model_gate.release(owner) - if isinstance(refused, JobBusy): - return None, _busy_refusal(refused) - raise + admitted = True + except JobBusy as refused: + return None, _busy_refusal(refused) + finally: + if not admitted: + model_gate.release(owner) job.task.add_done_callback(lambda _task: model_gate.release(owner)) return job, None diff --git a/py/api/images.py b/py/api/images.py index 2ded1782..afa4f319 100644 --- a/py/api/images.py +++ b/py/api/images.py @@ -39,10 +39,8 @@ from .thumbnail_encode import ( PREVIEW_HELD, EncoderClosed, PreviewWriteHeld, - replace_unless_held, run_encoder, sweep_stray_temps, - temp_sibling, thumbnail_frame, writer_settings, write_thumbnail, @@ -339,11 +337,6 @@ def _thumbnail_batch_size(workers): return max(4, workers * 2) -# Preview files are written by thumbnail_encode in the configured format -# (thumbnail_settings, #317); these names stay importable from here. -_temp_sibling = temp_sibling -_replace_unless_held = replace_unless_held - # A preview that could not be replaced (#249 S5 M6). Windows refuses to rename # over a file another process has open (WinError 5 or 32), such as a preview # the gallery is serving; the old preview stays and the file counts as an error. @@ -396,6 +389,7 @@ def _video_placeholder_image(thumbnail_size): font=font, ) except (OSError, AttributeError): + # a placeholder without its label still works pass return img @@ -499,6 +493,7 @@ def _preview_names(folders, folder): except OSError: continue except OSError: + # an unreadable folder lists as empty; nothing there to cache pass folders[folder] = names return names diff --git a/py/api/job_routes.py b/py/api/job_routes.py index 82dcbfcb..ce013446 100644 --- a/py/api/job_routes.py +++ b/py/api/job_routes.py @@ -250,7 +250,7 @@ class JobRoutesMixin: try: await response.write(_summary_event(job)) async with contextlib.aclosing(job.events()) as events: - async for _payload in events: + async for _ in events: await response.write(_summary_event(job)) # The last event is published before the job takes its final state. await response.write(_summary_event(job)) diff --git a/py/api/library_location.py b/py/api/library_location.py index 8c4ec324..17d172c7 100644 --- a/py/api/library_location.py +++ b/py/api/library_location.py @@ -207,6 +207,7 @@ class LibraryLocationRoutesMixin: try: size = os.path.getsize(location.db_path) except OSError: + # no library file yet: report size 0 pass return { "path": self._library_public_path(location.db_path), @@ -373,6 +374,7 @@ class LibraryLocationRoutesMixin: line += ". Logs move at the next restart." job.log_line(line) job.update(progress=100, status_line=line) + return None @staticmethod def _fail_library_move(job, code, message): diff --git a/py/api/log_cleanup.py b/py/api/log_cleanup.py index f6248388..c1c2cd41 100644 --- a/py/api/log_cleanup.py +++ b/py/api/log_cleanup.py @@ -205,3 +205,4 @@ class LogCleanupMixin: line += f", {failed} could not be deleted" job.log_line(line) job.update(processed=len(files), progress=100, status_line=line) + return None diff --git a/py/api/lora_import_routes.py b/py/api/lora_import_routes.py index a09ec689..f6eec8c6 100644 --- a/py/api/lora_import_routes.py +++ b/py/api/lora_import_routes.py @@ -436,7 +436,7 @@ class LoraImportRoutesMixin: job.log_line(f"{CANCELLED} {CACHE_NOTE}") job.update(status_line=CANCELLED) job.finish("cancelled") - return + return None except Exception: self.logger.exception("LoRA import staging failed") return _fail_job(job, "import-failed", f"{FAILED} {CACHE_NOTE}") @@ -455,10 +455,10 @@ class LoraImportRoutesMixin: job.log_line(f"{CANCELLED} {CACHE_NOTE}") job.update(status_line=CANCELLED) job.finish("cancelled") - return + return None ok, _backup = await self._safety_backup_step(job, db, backup_on) if not ok: - return + return None # I1: the files are listed and fingerprinted again right before the # replace, so a file changed, removed or added while the downloads ran # (LoraManager refreshing its CivitAI info) aborts with nothing deleted. @@ -466,6 +466,7 @@ class LoraImportRoutesMixin: return _fail_job(job, "plan-changed", f"{SOURCE_CHANGED} {CACHE_NOTE}") job.update(status_line="Replacing the LoRA collection", progress=95) await self._replace_in_job(job, db, plan, staged) + return None def _count_downloads(self, job, staged): downloaded = staged.stats.get("downloaded", 0) @@ -518,3 +519,4 @@ class LoraImportRoutesMixin: staged.stats.get("download_errors", 0), ) job.update(progress=100, processed=job.total, status_line=line) + return None diff --git a/py/api/lora_integration.py b/py/api/lora_integration.py index 3531d4a2..86370f5b 100644 --- a/py/api/lora_integration.py +++ b/py/api/lora_integration.py @@ -536,6 +536,7 @@ class LoraIntegrationMixin: except asyncio.CancelledError: interrupted = True except Exception: + # the await after this loop raises it for the caller pass if interrupted: raise asyncio.CancelledError diff --git a/py/api/maintenance.py b/py/api/maintenance.py index e7d34020..d93a8973 100644 --- a/py/api/maintenance.py +++ b/py/api/maintenance.py @@ -673,6 +673,7 @@ class MaintenanceRoutesMixin: line = _changed_line(plan.operation, result["changed"], result.get("marks", 0)) job.log_line(line) job.update(progress=100, status_line=f"{line} · {backup_note(backup)}") + return None def _maintenance_rows(self, operation, described): """Outcome rows for the changed records (root-relative paths only).""" diff --git a/py/api/thumbnail_auto.py b/py/api/thumbnail_auto.py index 05af0c2d..5cd662b7 100644 --- a/py/api/thumbnail_auto.py +++ b/py/api/thumbnail_auto.py @@ -533,6 +533,7 @@ class AutoThumbnailer: try: process.kill() except OSError: + # the process has already exited pass if worker is None: return True diff --git a/py/api/thumbnail_encode.py b/py/api/thumbnail_encode.py index bcbee3ef..f7a6ca54 100644 --- a/py/api/thumbnail_encode.py +++ b/py/api/thumbnail_encode.py @@ -158,6 +158,7 @@ def _unlink_quietly(path): try: Path(path).unlink() except FileNotFoundError: + # already gone, which is what was wanted pass except OSError as exc: logger.debug(f"Could not remove temp file {Path(path).name}: {exc}") @@ -221,6 +222,7 @@ def shutdown_encoders(): try: process.kill() except OSError: + # the process has already exited pass @@ -359,10 +361,12 @@ def thumbnail_frame(img, edge): try: img.seek(0) except (EOFError, AttributeError, ValueError): + # not seekable or a single frame: use the current one pass try: img.draft("RGB", (edge, edge)) # JPEG: decode at a reduced scale except (AttributeError, ValueError, OSError): + # draft is only a speedup; the full decode below still works pass frame = img.convert("RGB") if img.mode != "RGB" else img.copy() frame.thumbnail((edge, edge), Image.Resampling.LANCZOS) diff --git a/py/api/workflow_storage.py b/py/api/workflow_storage.py index 753c197b..fde4be76 100644 --- a/py/api/workflow_storage.py +++ b/py/api/workflow_storage.py @@ -275,3 +275,4 @@ class WorkflowStorageRoutesMixin: job.record_counted("changed", 1) job.log_line(line) job.update(progress=100, status_line=line) + return None diff --git a/py/config_store.py b/py/config_store.py index 949423a3..0664f026 100644 --- a/py/config_store.py +++ b/py/config_store.py @@ -109,6 +109,7 @@ def _write_atomically(path: str, payload: bytes) -> None: try: os.unlink(temp_path) except OSError: + # nothing to clean up; the original error is raised below pass raise diff --git a/py/lora_manager_layout.py b/py/lora_manager_layout.py index d05abe59..dba820b3 100644 --- a/py/lora_manager_layout.py +++ b/py/lora_manager_layout.py @@ -642,6 +642,7 @@ def layout_summary(layout: LoraManagerLayout, home: Path) -> Dict: relative = Path(settings_file).relative_to(home) settings_file = os.path.join("~", str(relative)) except ValueError: + # outside the home folder: show the full path pass return { "source": layout.source, diff --git a/py/lora_utils.py b/py/lora_utils.py index da71a3c9..4c8f6de2 100644 --- a/py/lora_utils.py +++ b/py/lora_utils.py @@ -585,6 +585,7 @@ def _read_capped(resp, cap: int) -> Optional[bytes]: if int(declared) > cap: return None except ValueError: + # a bad header says nothing; the read below enforces the cap pass chunks = [] @@ -707,6 +708,7 @@ def _save_atomically(image, local_path: Path) -> None: try: os.remove(temp) except OSError: + # nothing to clean up; the original error is raised below pass raise diff --git a/tests/browser/activity-concept.spec.mjs b/tests/browser/activity-concept.spec.mjs index 9dd4e646..443a6c0e 100644 --- a/tests/browser/activity-concept.spec.mjs +++ b/tests/browser/activity-concept.spec.mjs @@ -6,7 +6,6 @@ import { THEMES, seedConcept, openConcept, openProduction, diffInventory, mutate // app.js 1428–1432) and its backup box with the concept's System `.notice`; the differences are D55. The job view // has no concept counterpart: it is compared with the approved mockup `activity-scan.html` (a local, ignored file), // whose structure and CSS values are restated below (D56). Each comparison is shown to notice a mutation. -const workspace = '/prompt_manager/workspace'; test.beforeEach(async ({ page, request, browserName }) => { await startCoverage(page, browserName); diff --git a/tests/browser/slow_reset_watch.py b/tests/browser/slow_reset_watch.py index e3b453b6..3ff6a85f 100644 --- a/tests/browser/slow_reset_watch.py +++ b/tests/browser/slow_reset_watch.py @@ -109,6 +109,7 @@ class ResetWatch: stream.write(text) stream.flush() except (OSError, ValueError): + # the stream closed; a watcher must not fail the server pass diff --git a/tests/browser/workspace-saved-views.spec.mjs b/tests/browser/workspace-saved-views.spec.mjs index 1b216a4d..69258546 100644 --- a/tests/browser/workspace-saved-views.spec.mjs +++ b/tests/browser/workspace-saved-views.spec.mjs @@ -233,7 +233,7 @@ test('a view stored under older rules still lists and opens with its usable valu // D153 (#447): the workspace no longer offers "Highest rated"; the API still stores such a view as it was saved. test('a view saved under Highest rated opens on Recently added on Images and the Library', async ({ page, request }) => { const images = await create(request, { page: 'images', name: 'Rated cats', state: { ...IMAGE_DEFAULTS, q: 'cat', sort: 'rating' } }); - const library = await create(request, { page: 'library', name: 'Rated dogs', state: { q: 'dog', category: '', sort: 'rating', tags: [], tag_mode: 'all', folder: '' } }); + await create(request, { page: 'library', name: 'Rated dogs', state: { q: 'dog', category: '', sort: 'rating', tags: [], tag_mode: 'all', folder: '' } }); expect((await listed(request)).map(view => view.state.sort)).toEqual(['rating', 'rating']); const listings = []; page.on('request', item => { diff --git a/tests/browser/workspace-sticky-chrome.spec.mjs b/tests/browser/workspace-sticky-chrome.spec.mjs index 88323a0f..eec48a2b 100644 --- a/tests/browser/workspace-sticky-chrome.spec.mjs +++ b/tests/browser/workspace-sticky-chrome.spec.mjs @@ -60,7 +60,7 @@ async function openReview(page, request, items = 30) { const inViewport = (rect, page) => rect.top >= 0 && rect.bottom <= page.viewportSize().height; test.beforeEach(async ({ page, browserName }) => { await startCoverage(page, browserName); }); -test.afterEach(async ({ page, browserName }, testInfo) => { await saveCoverage(page, browserName, testInfo); }); +test.afterEach(async ({ page, browserName }) => { await saveCoverage(page, browserName); }); for (const viewport of [null, PHONE]) { const label = viewport ? ' at 390px' : ''; diff --git a/tests/browser/workspace-system-diagnostics.spec.mjs b/tests/browser/workspace-system-diagnostics.spec.mjs index 2f4cc07a..8f8cf38c 100644 --- a/tests/browser/workspace-system-diagnostics.spec.mjs +++ b/tests/browser/workspace-system-diagnostics.spec.mjs @@ -11,7 +11,6 @@ const REPORT_URL = '/prompt_manager/system/diagnostics'; const LOGS_URL = '/prompt_manager/logs'; const CONFIG_URL = '/prompt_manager/logs/config'; const PREVIEW_URL = '/prompt_manager/maintenance/preview'; -const APPLY_URL = '/prompt_manager/maintenance/apply'; const TEST_LINK_URL = '/prompt_manager/diagnostics/test-link'; const CHANGED = 'The log files changed since the review; nothing was deleted. Review again.'; const KEY = 'sk-s12-browser-SECRET-0123456789'; diff --git a/tests/js/legacy-refusals.test.js b/tests/js/legacy-refusals.test.js index 78d15c9b..23f6cf0e 100644 --- a/tests/js/legacy-refusals.test.js +++ b/tests/js/legacy-refusals.test.js @@ -21,14 +21,6 @@ function jsonResponse(status, body) { json: async () => body }; } -function sseResponse(frames) { - const bytes = new TextEncoder().encode(frames.map(frame => `data: ${JSON.stringify(frame)}\n\n`).join('')); - let sent = false; - return { ok: true, status: 200, body: { getReader: () => ({ - read: async () => (sent ? { done: true } : (sent = true, { done: false, value: bytes })), - }) } }; -} - function load(file, className, fetchUrl, extra = {}, dom = elements()) { const timers = []; const notifications = []; @@ -63,8 +55,6 @@ function load(file, className, fetchUrl, extra = {}, dom = elements()) { return { page, dom, timers, notifications, storage }; } -const BUSY = 'Scan for metadata already running'; - test('admin settings save shows why config.json refused it', async () => { const message = 'config.json cannot be read, so nothing was saved. Fix or remove the file, then save again.'; const { page, notifications } = load('admin.js', 'PromptAdmin', diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index 755a5369..98f424cc 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -2584,7 +2584,7 @@ class TestActivityJobs(AdminAPITestCase): async def test_cancel_after_first_batch_keeps_partial_counts(self): """Review M7: the real scan stops at the batch boundary, keeping batch 1.""" import threading - from unittest import mock + import unittest.mock as mock from py.api import admin diff --git a/tests/test_api_autotag.py b/tests/test_api_autotag.py index 4b009ba8..1ef568b2 100644 --- a/tests/test_api_autotag.py +++ b/tests/test_api_autotag.py @@ -600,6 +600,33 @@ class TestAutotagSingleEdgeCases(AutotagAPITestCase): {"path": str(image), "general_threshold": "high"}, ) self.assertEqual(resp.status, 400) + self.assertEqual( + (await resp.json())["error"], + "general_threshold and character_threshold must be numeric", + ) + + async def test_invalid_single_options_report_the_validation_error(self) -> None: + image = self._make_image(self.output_dir) + cases = ( + ( + {"model_type": "wd14-vit", "general_threshold": 2}, + "general_threshold must be between 0 and 1", + ), + ( + {"model_type": "gguf", "prompt": "x" * 4001}, + "instructions must be at most 4000 characters", + ), + ) + for options, message in cases: + with self.subTest(options=options["model_type"]): + resp = await self._post_json( + "/prompt_manager/autotag/single", + {"path": str(image), **options}, + ) + self.assertEqual(resp.status, 400) + self.assertEqual( + await resp.json(), {"success": False, "error": message} + ) async def test_prompt_lookup_failure_still_returns_tags(self): image = self._make_image(self.output_dir) @@ -1151,7 +1178,9 @@ class TestModelStatus(AutotagAPITestCase): ) # Kept fields legacy pages and the workspace read. for model in models.values(): - self.assertTrue({"name", "description", "size", "downloaded"} <= set(model)) + self.assertLessEqual( + {"name", "description", "size", "downloaded"}, set(model) + ) self.assertTrue(data["model_loaded"]) self.assertEqual(data["loaded_model_type"], "wd14-vit") diff --git a/tests/test_api_core.py b/tests/test_api_core.py index 7d7d1958..96f17862 100644 --- a/tests/test_api_core.py +++ b/tests/test_api_core.py @@ -351,8 +351,6 @@ class TestLongJobLock(unittest.TestCase): self.api = PromptManagerAPI(db=database) def _run(self, coro): - import asyncio - return asyncio.run(coro) def test_second_acquire_is_refused_until_first_releases(self): @@ -825,8 +823,6 @@ class TestInstanceHelpers(FolderPathsFixture): self.addCleanup(setattr, GalleryConfig, "MONITORING_DIRECTORIES", orig_dirs) def _run(self, coro): - import asyncio - return asyncio.run(coro) def test_run_in_executor_with_kwargs(self): diff --git a/tests/test_api_diagnostics_report.py b/tests/test_api_diagnostics_report.py index 8b6ef318..be8b3c7d 100644 --- a/tests/test_api_diagnostics_report.py +++ b/tests/test_api_diagnostics_report.py @@ -16,7 +16,7 @@ import re import sqlite3 import sys import unittest -from unittest import mock +import unittest.mock as mock from unittest.mock import MagicMock _mock_server = MagicMock() diff --git a/tests/test_api_duplicate_safety.py b/tests/test_api_duplicate_safety.py index 8ae50edb..402b48ab 100644 --- a/tests/test_api_duplicate_safety.py +++ b/tests/test_api_duplicate_safety.py @@ -381,7 +381,7 @@ class TestAnotherMount(SafetyTestCase): self.assertTrue(other.is_file() and keeper.is_file()) async def test_a_volume_without_file_ids_is_named_truthfully(self): - keeper, other, patches = self.mount("x.png", "x.png") + self.mount("x.png", "x.png") def zero(path, *args, **kwargs): return _fake_stat(self.real_stat(path), st_dev=0, st_ino=0) diff --git a/tests/test_api_images.py b/tests/test_api_images.py index c1772f66..580fb12a 100644 --- a/tests/test_api_images.py +++ b/tests/test_api_images.py @@ -3713,7 +3713,7 @@ class TestSharedListing(ImageAPITestCase): async def test_a_root_read_again_after_the_roots_change_listens_again(self): make_png(self.output_dir / "a.png") - record = await self._listed() + await self._listed() self.api.reset_gallery_roots_cache() record = await self._listed() # still configured: read and walked self._interval(0) diff --git a/tests/test_api_maintenance.py b/tests/test_api_maintenance.py index 749a3af3..f700aa64 100644 --- a/tests/test_api_maintenance.py +++ b/tests/test_api_maintenance.py @@ -675,7 +675,7 @@ class TestBackupPoolAndDiskSpace(ApplyTestCase): self.assertEqual(len(backups), MAX_SAFETY_BACKUPS) self.assertNotIn(restore_style[0], backups) self.assertNotIn(restore_style[1], backups) - self.assertTrue(set(maintenance_style) <= set(backups)) + self.assertLessEqual(set(maintenance_style), set(backups)) # A restore prunes the same pool, maintenance backups included. self.old_backup("prompts.db.backup_20020101_000000", 3000) _prune_safety_backups(self.api.db.model.db_path) diff --git a/tests/test_browser_fast_reset.py b/tests/test_browser_fast_reset.py index c23fae43..971c40ba 100644 --- a/tests/test_browser_fast_reset.py +++ b/tests/test_browser_fast_reset.py @@ -11,7 +11,7 @@ import time import types import unittest from pathlib import Path -from unittest import mock +import unittest.mock as mock try: from tests.open_handles import assert_closed diff --git a/tests/test_browser_malloc_tuning.py b/tests/test_browser_malloc_tuning.py index d578fc9c..4345f064 100644 --- a/tests/test_browser_malloc_tuning.py +++ b/tests/test_browser_malloc_tuning.py @@ -6,7 +6,7 @@ Large frees then go back to the OS instead of fragmenting per-thread arenas. import ctypes import sys import unittest -from unittest import mock +import unittest.mock as mock from tests.browser import malloc_tuning diff --git a/tests/test_browser_slow_reset_watch.py b/tests/test_browser_slow_reset_watch.py index 569983b8..3d211c65 100644 --- a/tests/test_browser_slow_reset_watch.py +++ b/tests/test_browser_slow_reset_watch.py @@ -46,7 +46,6 @@ class TestResetWatch(unittest.TestCase): self.assertFalse(is_reset_path("/__test__/reset/extra")) def test_a_stalled_reset_reports_state_and_stacks_while_it_waits(self): - stream = io.StringIO() reported = threading.Event() class Stream(io.StringIO): diff --git a/tests/test_bulk_library_delete.py b/tests/test_bulk_library_delete.py index 7fce13e1..7c1aa7b5 100644 --- a/tests/test_bulk_library_delete.py +++ b/tests/test_bulk_library_delete.py @@ -385,8 +385,8 @@ class TestArchivedReaders(LibraryDeleteCase): def setUp(self): super().setUp() - self.kept, _kn, _kp = self.paired("Kept cat", "kept blur") - self.gone, _gn, _gp = self.paired("Gone cat", "gone blur") + self.kept, _, _ = self.paired("Kept cat", "kept blur") + self.gone, _, _ = self.paired("Gone cat", "gone blur") for pid, category, tag in ((self.kept, "keep", "k"), (self.gone, "gone", "g")): self.db.update_prompt_metadata(pid, category=category, tags=[tag]) self.apply([self.gone]) @@ -642,7 +642,7 @@ class TestPairedDelete(LibraryDeleteCase): # The negatives stay, and the image file stays on disk. self.assertTrue((self.root / "owl.png").exists()) texts = [row[0] for row in rows(self.db, "SELECT text FROM prompts")] - self.assertTrue({"dark", "fog", "Bystander"} <= set(texts)) + self.assertLessEqual({"dark", "fog", "Bystander"}, set(texts)) def test_a_negative_used_by_another_positive_is_archived_never_deleted(self): shared = save(self.db, "Shared") diff --git a/tests/test_cached_runs.py b/tests/test_cached_runs.py index bd52416c..ed726220 100644 --- a/tests/test_cached_runs.py +++ b/tests/test_cached_runs.py @@ -14,7 +14,7 @@ import threading import types import unittest from collections import namedtuple -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_database_migrations.py b/tests/test_database_migrations.py index 544d0fb9..47a67b13 100644 --- a/tests/test_database_migrations.py +++ b/tests/test_database_migrations.py @@ -140,8 +140,10 @@ class TestIdempotentMigrations(MigrationTestCase): def test_stale_prompts_new_from_a_failed_rebuild_does_not_block_startup(self): self._legacy_db( [ - "INSERT INTO prompts (text, hash, workflow_name)" - " VALUES ('a', 'h1', 'wf')", + ( + "INSERT INTO prompts (text, hash, workflow_name)" + " VALUES ('a', 'h1', 'wf')" + ), "CREATE TABLE prompts_new (id INTEGER PRIMARY KEY, junk TEXT)", "INSERT INTO prompts_new (junk) VALUES ('leftover')", ] @@ -157,10 +159,14 @@ class TestIdempotentMigrations(MigrationTestCase): # DROP TABLE prompts with foreign keys ON cascades into generated_images self._legacy_db( [ - "INSERT INTO prompts (text, hash, workflow_name)" - " VALUES ('a', 'h1', 'wf')", - "INSERT INTO generated_images (prompt_id, image_path, filename)" - " VALUES (1, '/out/a.png', 'a.png')", + ( + "INSERT INTO prompts (text, hash, workflow_name)" + " VALUES ('a', 'h1', 'wf')" + ), + ( + "INSERT INTO generated_images (prompt_id, image_path, filename)" + " VALUES (1, '/out/a.png', 'a.png')" + ), ] ) @@ -172,11 +178,15 @@ class TestIdempotentMigrations(MigrationTestCase): def test_rebuild_keeps_library_state_rows(self): self._legacy_db( [ - "INSERT INTO prompts (text, hash, workflow_name)" - " VALUES ('a', 'h1', 'wf')", - "CREATE TABLE prompt_library_state (prompt_id INTEGER PRIMARY KEY," - " title TEXT, FOREIGN KEY(prompt_id) REFERENCES prompts(id)" - " ON DELETE CASCADE)", + ( + "INSERT INTO prompts (text, hash, workflow_name)" + " VALUES ('a', 'h1', 'wf')" + ), + ( + "CREATE TABLE prompt_library_state (prompt_id INTEGER PRIMARY KEY," + " title TEXT, FOREIGN KEY(prompt_id) REFERENCES prompts(id)" + " ON DELETE CASCADE)" + ), "INSERT INTO prompt_library_state VALUES (1, 'kept')", ] ) @@ -232,43 +242,59 @@ class TestIdempotentMigrations(MigrationTestCase): byte_id = "6a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d" self._legacy_db( [ - "INSERT INTO prompts (text, hash, workflow_name)" - " VALUES ('a', 'h1', 'wf')", - "CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT," - " name TEXT NOT NULL UNIQUE)", + ( + "INSERT INTO prompts (text, hash, workflow_name)" + " VALUES ('a', 'h1', 'wf')" + ), + ( + "CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT NOT NULL UNIQUE)" + ), "INSERT INTO tags (id, name) VALUES (3, 'img-only')", - "CREATE TABLE media_annotation_metadata (singleton_id INTEGER" - " PRIMARY KEY CHECK(singleton_id=1), schema_version INTEGER NOT" - " NULL CHECK(schema_version=1), image_index_revision INTEGER NOT" - " NULL DEFAULT 0 CHECK(image_index_revision>=0))", + ( + "CREATE TABLE media_annotation_metadata (singleton_id INTEGER" + " PRIMARY KEY CHECK(singleton_id=1), schema_version INTEGER NOT" + " NULL CHECK(schema_version=1), image_index_revision INTEGER NOT" + " NULL DEFAULT 0 CHECK(image_index_revision>=0))" + ), "INSERT INTO media_annotation_metadata VALUES (1, 1, 4)", - "CREATE TABLE media_annotation_files (id TEXT PRIMARY KEY" - " CHECK(length(id)=36), locator TEXT NOT NULL UNIQUE," - " created_at TEXT NOT NULL)", + ( + "CREATE TABLE media_annotation_files (id TEXT PRIMARY KEY" + " CHECK(length(id)=36), locator TEXT NOT NULL UNIQUE," + " created_at TEXT NOT NULL)" + ), f"INSERT INTO media_annotation_files VALUES ('{file_id}'," " '/out/a.png', '2026-10-22T00:00:00.000000+00:00')", - "CREATE TABLE media_annotation_bytes (id TEXT PRIMARY KEY" - " CHECK(length(id)=36), file_id TEXT NOT NULL REFERENCES" - " media_annotation_files(id) ON DELETE CASCADE, sha256 TEXT NOT" - " NULL CHECK(length(sha256)=64 AND sha256 NOT GLOB" - " '*[^0-9a-f]*'), size INTEGER NOT NULL CHECK(size>=0), mtime_ns" - " INTEGER NOT NULL, verified_at TEXT NOT NULL, first_seen_at TEXT" - " NOT NULL, tag_revision INTEGER NOT NULL DEFAULT 0" - " CHECK(tag_revision>=0), UNIQUE(file_id, sha256))", - "CREATE INDEX idx_media_annotation_bytes_fingerprint ON" - " media_annotation_bytes(file_id, size, mtime_ns)", + ( + "CREATE TABLE media_annotation_bytes (id TEXT PRIMARY KEY" + " CHECK(length(id)=36), file_id TEXT NOT NULL REFERENCES" + " media_annotation_files(id) ON DELETE CASCADE, sha256 TEXT NOT" + " NULL CHECK(length(sha256)=64 AND sha256 NOT GLOB" + " '*[^0-9a-f]*'), size INTEGER NOT NULL CHECK(size>=0), mtime_ns" + " INTEGER NOT NULL, verified_at TEXT NOT NULL, first_seen_at TEXT" + " NOT NULL, tag_revision INTEGER NOT NULL DEFAULT 0" + " CHECK(tag_revision>=0), UNIQUE(file_id, sha256))" + ), + ( + "CREATE INDEX idx_media_annotation_bytes_fingerprint ON" + " media_annotation_bytes(file_id, size, mtime_ns)" + ), f"INSERT INTO media_annotation_bytes VALUES ('{byte_id}'," f" '{file_id}', '{'a' * 64}', 3, 1700000000000000000," " '2026-10-22T00:00:00.000000+00:00'," " '2026-10-22T00:00:00.000000+00:00', 2)", - "CREATE TABLE media_image_tags (byte_id TEXT NOT NULL REFERENCES" - " media_annotation_bytes(id) ON DELETE CASCADE, tag_id INTEGER" - " NOT NULL REFERENCES tags(id) ON DELETE CASCADE, position" - " INTEGER NOT NULL CHECK(position>=0), PRIMARY KEY(byte_id," - " tag_id))", + ( + "CREATE TABLE media_image_tags (byte_id TEXT NOT NULL REFERENCES" + " media_annotation_bytes(id) ON DELETE CASCADE, tag_id INTEGER" + " NOT NULL REFERENCES tags(id) ON DELETE CASCADE, position" + " INTEGER NOT NULL CHECK(position>=0), PRIMARY KEY(byte_id," + " tag_id))" + ), "CREATE INDEX idx_media_image_tags_tag ON media_image_tags(tag_id)", - "CREATE UNIQUE INDEX idx_media_image_tags_position ON" - " media_image_tags(byte_id, position)", + ( + "CREATE UNIQUE INDEX idx_media_image_tags_position ON" + " media_image_tags(byte_id, position)" + ), f"INSERT INTO media_image_tags VALUES ('{byte_id}', 3, 0)", ] ) @@ -367,18 +393,28 @@ class TestIdempotentMigrations(MigrationTestCase): def test_image_table_is_rebuilt_with_uniqueness_by_full_path(self): self._legacy_db( [ - "INSERT INTO prompts (text, hash, workflow_name)" - " VALUES ('a', 'h1', 'wf')", + ( + "INSERT INTO prompts (text, hash, workflow_name)" + " VALUES ('a', 'h1', 'wf')" + ), # same basename in two folders: both must survive - "INSERT INTO generated_images (prompt_id, image_path, filename)" - " VALUES (1, '/out/2026-01-01/a.png', 'a.png')", - "INSERT INTO generated_images (prompt_id, image_path, filename)" - " VALUES (1, '/out/2026-01-02/a.png', 'a.png')", + ( + "INSERT INTO generated_images (prompt_id, image_path, filename)" + " VALUES (1, '/out/2026-01-01/a.png', 'a.png')" + ), + ( + "INSERT INTO generated_images (prompt_id, image_path, filename)" + " VALUES (1, '/out/2026-01-02/a.png', 'a.png')" + ), # two spellings of one file: only the newest row survives - "INSERT INTO generated_images (prompt_id, image_path, filename)" - " VALUES (1, '/out/x/../b.png', 'b.png')", - "INSERT INTO generated_images (prompt_id, image_path, filename)" - " VALUES (1, '/out/b.png', 'b.png')", + ( + "INSERT INTO generated_images (prompt_id, image_path, filename)" + " VALUES (1, '/out/x/../b.png', 'b.png')" + ), + ( + "INSERT INTO generated_images (prompt_id, image_path, filename)" + " VALUES (1, '/out/b.png', 'b.png')" + ), ] ) diff --git a/tests/test_diagnostics_config.py b/tests/test_diagnostics_config.py index 2fca7ac4..e3cfb3e1 100644 --- a/tests/test_diagnostics_config.py +++ b/tests/test_diagnostics_config.py @@ -182,7 +182,7 @@ class TestAdminEndpointsUseConfigPath(unittest.TestCase): self.assertIsNone(warning) self.assertEqual(os.path.dirname(snapshot), folder) with open(snapshot, "rb") as handle: - self.assertTrue(handle.read(16) == b"SQLite format 3\x00") + self.assertEqual(handle.read(16), b"SQLite format 3\x00") finally: os.unlink(snapshot) finally: diff --git a/tests/test_execution_history_events.py b/tests/test_execution_history_events.py index 76a1474f..f2742e6b 100644 --- a/tests/test_execution_history_events.py +++ b/tests/test_execution_history_events.py @@ -31,6 +31,20 @@ def message(kind, job_id=JOB, **fields): return (kind, {"prompt_id": job_id, **fields}) +def hash_recording_kind(): + """A str subclass for event kinds, and the list of hashes taken of it.""" + hashed = [] + + class OversizedKind(str): + __eq__ = str.__eq__ + + def __hash__(self): + hashed.append(len(self)) + return str.__hash__(self) + + return OversizedKind, hashed + + def finish(messages, completed=True): return make_finish(SESSION, 0, EPOCH, Status("success", completed, messages), 500) @@ -346,13 +360,10 @@ class ExecutionHistoryEventsTests(unittest.TestCase): self.assertLessEqual(len(encode_command(command)), 2048) def test_oversized_unknown_kind_is_ignored_without_hashing(self): - class OversizedKind(str): - def __hash__(self): - raise AssertionError("Oversized event kind must not be hashed") - + kind_class, hashed = hash_recording_kind() command = finish( [ - message(OversizedKind("x" * 1_000_000)), + message(kind_class("x" * 1_000_000)), message("execution_success", timestamp=300), ] ) @@ -361,19 +372,16 @@ class ExecutionHistoryEventsTests(unittest.TestCase): (result.outcome, result.reason), ("succeeded", "terminal_success") ) self.assertLessEqual(len(encode_command(command)), 2048) + self.assertEqual(hashed, [], "Oversized event kind must not be hashed") def test_encoder_rejects_oversized_kind_without_hashing(self): - class OversizedKind(str): - def __hash__(self): - raise AssertionError("Oversized event kind must not be hashed") - + kind_class, hashed = hash_recording_kind() command = finish([message("execution_success")]) - event = LifecycleEvent( - OversizedKind("x" * 1_000_000), JOB, None, None, None, None - ) + event = LifecycleEvent(kind_class("x" * 1_000_000), JOB, None, None, None, None) with self.assertRaises(ValueError): encode_command( replace( command, evidence=TerminalEvidence((event,), False, False, False) ) ) + self.assertEqual(hashed, [], "Oversized event kind must not be hashed") diff --git a/tests/test_execution_output_capture.py b/tests/test_execution_output_capture.py index 4bf86b9d..423bc26f 100644 --- a/tests/test_execution_output_capture.py +++ b/tests/test_execution_output_capture.py @@ -17,7 +17,7 @@ class Hostile: raise AssertionError("host scalar coercion") def __bool__(self): - raise AssertionError("host truth test") + raise TypeError("host truth test") def __eq__(self, other): raise AssertionError("host equality") diff --git a/tests/test_exit_cancel.py b/tests/test_exit_cancel.py index e3fb5ba5..5eeab498 100644 --- a/tests/test_exit_cancel.py +++ b/tests/test_exit_cancel.py @@ -24,7 +24,7 @@ import tempfile import textwrap import threading import unittest -from unittest import mock +import unittest.mock as mock ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) diff --git a/tests/test_exit_window_threads.py b/tests/test_exit_window_threads.py index 1323f4db..30e39bdd 100644 --- a/tests/test_exit_window_threads.py +++ b/tests/test_exit_window_threads.py @@ -26,7 +26,7 @@ import types import unittest from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from unittest import mock +import unittest.mock as mock from py.api import thumbnail_auto from utils import image_monitor, parallel diff --git a/tests/test_file_prompt.py b/tests/test_file_prompt.py index c4dadfed..8b525659 100644 --- a/tests/test_file_prompt.py +++ b/tests/test_file_prompt.py @@ -7,7 +7,7 @@ I/O: history comes in through ``lookup``. import json import unittest -from unittest import mock +import unittest.mock as mock from database.run_attribution import RecordedOutput from tests.test_pair_scan import flux_graph, sampled_graph diff --git a/tests/test_focused_node.py b/tests/test_focused_node.py index 5e9a2ef1..3e7bbd8f 100644 --- a/tests/test_focused_node.py +++ b/tests/test_focused_node.py @@ -2,7 +2,7 @@ import logging import unittest -from unittest import mock +import unittest.mock as mock from prompt_manager import PromptManager, PromptManagerFocused from utils.prompt_graph import positive_prompt_nodes, run_prompt_nodes diff --git a/tests/test_gallery_listing.py b/tests/test_gallery_listing.py index ca796bfe..0e2ebf7f 100644 --- a/tests/test_gallery_listing.py +++ b/tests/test_gallery_listing.py @@ -312,7 +312,7 @@ class RootContainsTests(unittest.TestCase): root = os.path.normcase(os.path.realpath(tempfile.gettempdir())) inside = os.path.join(root, "x.png") self.assertTrue(root_contains(root, inside)) - self.assertTrue(root_contains(root, inside.upper()) == (os.name == "nt")) + self.assertEqual(root_contains(root, inside.upper()), os.name == "nt") if __name__ == "__main__": diff --git a/tests/test_init_package.py b/tests/test_init_package.py index 43661084..01ab75b5 100644 --- a/tests/test_init_package.py +++ b/tests/test_init_package.py @@ -527,7 +527,7 @@ class TestPackageLoggerIsolation(unittest.TestCase): def log_from_background_thread(): try: logging.getLogger("prompt_manager.late_emitter").warning(message) - except BaseException as error: + except Exception as error: errors.append(error) self.handler.addFilter(pause_before_handler_lock) diff --git a/tests/test_library.py b/tests/test_library.py index 24540f24..e7585639 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -143,8 +143,6 @@ class TestLibrary(unittest.TestCase): self.db.get_library_positive(positive, variant_limit=201) def test_sorts_keysets_survive_anchor_deletion_and_insertions_without_writes(self): - import sqlite3 - a = self.save("Alpha", rating=2, tags=["tag"]) b = self.save("beta", rating=5) c = self.save("charlie") diff --git a/tests/test_library_delete_routes.py b/tests/test_library_delete_routes.py index 1582cbc7..4e3b1bf0 100644 --- a/tests/test_library_delete_routes.py +++ b/tests/test_library_delete_routes.py @@ -297,7 +297,7 @@ class TestLibraryDeleteRoutes(LibraryDeleteRouteCase): ) page = await response.json() self.assertEqual(page["archived_total"], 3) - self.assertTrue(set(item["id"] for item in page["items"]) <= set(kept)) + self.assertLessEqual(set(item["id"] for item in page["items"]), set(kept)) response = await self.client.request( "GET", "/prompt_manager/library?limit=2&shelf=archived" diff --git a/tests/test_library_location_wiring.py b/tests/test_library_location_wiring.py index d2fa31af..a0087b30 100644 --- a/tests/test_library_location_wiring.py +++ b/tests/test_library_location_wiring.py @@ -135,6 +135,7 @@ class TestLogsAndCache(_EnvCase): get_logger_manager()._setup_loggers() except Exception: + # best effort restore in teardown; must not hide the test result pass def test_lora_image_cache_follows_the_data_folder(self): diff --git a/tests/test_library_move.py b/tests/test_library_move.py index 9a450fde..868da97d 100644 --- a/tests/test_library_move.py +++ b/tests/test_library_move.py @@ -18,7 +18,7 @@ import tempfile import threading import types import unittest -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_library_move_review.py b/tests/test_library_move_review.py index 57f61c31..1d1fd60d 100644 --- a/tests/test_library_move_review.py +++ b/tests/test_library_move_review.py @@ -22,7 +22,7 @@ import sqlite3 import sys import threading import unittest -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_local_inspect_parity.py b/tests/test_local_inspect_parity.py index e5ab5dde..e8c45d55 100644 --- a/tests/test_local_inspect_parity.py +++ b/tests/test_local_inspect_parity.py @@ -1112,10 +1112,10 @@ class LocalProjectionParityTests(unittest.TestCase): self.assertEqual(_canonical_bytes(local), _canonical_bytes(server)) seen.update(server["record_formats"].values()) seen.add(server["workflow_graph"]["status"]) - self.assertTrue( - {"reserialized-json", "original-text", "invalid", "limited"} <= seen + self.assertLessEqual( + {"reserialized-json", "original-text", "invalid", "limited"}, seen ) - self.assertTrue({"parsed", "malformed", "limited"} <= seen) + self.assertLessEqual({"parsed", "malformed", "limited"}, seen) def _server_evidence(records, file_name=None): @@ -1287,7 +1287,7 @@ class LocalResolverParityTests(unittest.TestCase): def test_local_roles_match_selection_record_sets(self): answers = self._compare(selection_cases.selection_cases()) reasons = {answer["resolution"]["reason"] for answer in answers.values()} - self.assertTrue( + self.assertLessEqual( { "Every sampler branch uses the same prompt pair", "Multiple sampler branches", @@ -1301,8 +1301,8 @@ class LocalResolverParityTests(unittest.TestCase): "Negative prompt delimiter is absent", "Parameters exceed the length limit", "No A1111 parameters", - } - <= reasons + }, + reasons, ) # A stage reaching only previews is dropped: one sampler is named. self.assertEqual( @@ -1352,7 +1352,7 @@ class LocalResolverParityTests(unittest.TestCase): def test_local_roles_match_golden_record_sets(self): answers = self._compare(resolver_cases.golden_cases()) codes = {answer["resolution"]["reason_code"] for answer in answers.values()} - self.assertTrue( + self.assertLessEqual( { "malformed", "unsupported-node", @@ -1364,16 +1364,16 @@ class LocalResolverParityTests(unittest.TestCase): "graph-ceiling", "no-sampler", None, - } - <= codes + }, + codes, ) sources = { side["source"] for answer in answers.values() for side in answer["prompts"].values() } - self.assertTrue( - {"execution", "graph-inferred", "zeroed-conditioning"} <= sources + self.assertLessEqual( + {"execution", "graph-inferred", "zeroed-conditioning"}, sources ) def test_committed_resolver_vectors_are_current(self): diff --git a/tests/test_lora_database.py b/tests/test_lora_database.py index 648b8213..8a913f0c 100644 --- a/tests/test_lora_database.py +++ b/tests/test_lora_database.py @@ -197,7 +197,7 @@ class TestGetPromptSubfolders(LoraDBTestCase): def test_subfolders_use_forward_slashes_on_every_platform(self): """Clients split folders on "/" whatever separator the OS produced.""" - from unittest import mock + import unittest.mock as mock import database.operations as operations diff --git a/tests/test_lora_manager_layout.py b/tests/test_lora_manager_layout.py index 8af50902..adb0f4f2 100644 --- a/tests/test_lora_manager_layout.py +++ b/tests/test_lora_manager_layout.py @@ -584,9 +584,6 @@ class CentralizedDiscoveryTests(LayoutTestCase): def test_an_unlistable_folder_raises(self): layout = self.load() - def refuse(error): - raise error - def failing_walk(top, onerror=None, followlinks=False): onerror(PermissionError(13, "denied", top)) return iter(()) diff --git a/tests/test_managed_connection.py b/tests/test_managed_connection.py index b33d437a..c5b13542 100644 --- a/tests/test_managed_connection.py +++ b/tests/test_managed_connection.py @@ -15,7 +15,7 @@ import tempfile import threading import unittest import weakref -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -154,6 +154,116 @@ class TestRetire(_Case): class TestCalls(_Case): + def test_iteration_and_fetches_share_one_position(self) -> None: + conn = self.connect() + conn.executemany("INSERT INTO t VALUES (?)", [(x,) for x in range(1, 260)]) + cursor = conn.execute("SELECT x FROM t ORDER BY x") + self.assertIs(iter(cursor), cursor) + self.assertEqual(next(cursor), (0,)) + self.assertEqual(cursor.fetchone(), (1,)) + self.assertEqual(cursor.fetchmany(2), [(2,), (3,)]) + self.assertEqual(next(iter(cursor)), (4,)) + self.assertEqual(cursor.fetchall(), [(x,) for x in range(5, 260)]) + self.assertEqual(list(cursor), []) + + def test_a_partial_loop_can_resume_without_skipping_rows(self) -> None: + cursor = self.connect().execute( + "SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3" + ) + for row in cursor: + self.assertEqual(row, (1,)) + break + self.assertEqual(list(cursor), [(2,), (3,)]) + + def test_an_existing_iterator_refuses_reads_after_retire_or_close(self) -> None: + for retire in (False, True): + with self.subTest(retire=retire): + conn = self.connect() + cursor = conn.execute("SELECT 1 UNION ALL SELECT 2") + iterator = iter(cursor) + self.assertEqual(next(iterator), (1,)) + if retire: + conn.retire() + else: + cursor.close() + with self.assertRaises(sqlite3.ProgrammingError): + next(iterator) + + def test_buffered_fetchmany_matches_sqlite_sizes_and_validation(self) -> None: + conn = self.connect() + conn.executemany("INSERT INTO t VALUES (?)", [(x,) for x in range(1, 260)]) + conn.commit() + with contextlib.closing(sqlite3.connect(self.path)) as plain: + for size in (None, 0, 1, 2, 127, 128, 130, 300, -1): + with self.subTest(size=size): + cursors = [ + c.execute("SELECT x FROM t ORDER BY x") for c in (plain, conn) + ] + for cursor in cursors: + cursor.arraysize = 2 + self.assertEqual(next(iter(cursor)), (0,)) + results = [] + for cursor in cursors: + try: + rows = ( + cursor.fetchmany() + if size is None + else cursor.fetchmany(size) + ) + except ValueError as error: # Negative sizes on Python 3.14+. + rows = type(error) + results.append((rows, cursor.fetchall())) + self.assertEqual(*results) + for args, kwargs in ((("bad",), {}), ((1, 2), {}), ((), {"unknown": 1})): + cursor = conn.execute("SELECT 1 UNION ALL SELECT 2") + self.assertEqual(next(iter(cursor)), (1,)) + with self.assertRaises(TypeError): + cursor.fetchmany(*args, **kwargs) + self.assertEqual(cursor.fetchone(), (2,)) + + def test_reexecuting_a_cursor_matches_sqlite_result_lifetime(self) -> None: + conn = self.connect() + conn.row_factory = sqlite3.Row + for method, args in ( + ("execute", ("SELECT 9 AS x",)), + ("executemany", ("INSERT INTO t VALUES (?)", [(9,)])), + ("executescript", ("SELECT 9 AS x;",)), + ): + with self.subTest(method=method): + cursor = conn.execute("SELECT 1 AS x UNION ALL SELECT 2") + self.assertEqual(next(iter(cursor))["x"], 1) + self.assertIs(getattr(cursor, method)(*args), cursor) + if method == "execute": + self.assertEqual(cursor.fetchone()["x"], 9) + elif method == "executescript": + self.assertEqual(cursor.fetchone()["x"], 2) + self.assertEqual(list(cursor), []) + + def test_failed_execution_matches_sqlite_result_lifetime(self) -> None: + conn = self.connect() + cases = ( + ("execute", (None,)), + ("execute", ("invalid sql",)), + ("execute", ("SELECT ?", ())), + ("executemany", (None, [])), + ("executemany", ("SELECT 9", [])), + ("executescript", (None,)), + ("executescript", ("invalid sql;",)), + ) + with contextlib.closing(sqlite3.connect(self.path)) as plain: + for method, args in cases: + with self.subTest(method=method, args=args): + results = [] + for connection in (plain, conn): + cursor = connection.execute("SELECT 1 UNION ALL SELECT 2") + self.assertEqual(next(iter(cursor)), (1,)) + with self.assertRaises( + sqlite3.Error if isinstance(args[0], str) else TypeError + ): + getattr(cursor, method)(*args) + results.append(cursor.fetchall()) + self.assertEqual(*results) + def test_every_cursor_call_works_and_is_refused_once_retired(self): conn = self.connect() conn.row_factory = sqlite3.Row diff --git a/tests/test_media_capture.py b/tests/test_media_capture.py index f4cc0b53..e4790f1e 100644 --- a/tests/test_media_capture.py +++ b/tests/test_media_capture.py @@ -347,7 +347,7 @@ class CaptureStoreTestCase(unittest.TestCase): with patch.object(CaptureStore, "_read", rewriting): try: cap = store.capture(self.root, "a.png", epoch=EPOCH, scope=SCOPE) - except CaptureChanged: + except capture_module.CaptureChanged: detected = True else: detected = False diff --git a/tests/test_prompt_attribution.py b/tests/test_prompt_attribution.py index 7cf3d101..229ff860 100644 --- a/tests/test_prompt_attribution.py +++ b/tests/test_prompt_attribution.py @@ -14,7 +14,7 @@ import sys import tempfile import threading import unittest -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_prompt_graph.py b/tests/test_prompt_graph.py index 1e6ffdac..1329d08b 100644 --- a/tests/test_prompt_graph.py +++ b/tests/test_prompt_graph.py @@ -4,7 +4,7 @@ import os import sys import time import unittest -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_prompt_pair_resolver.py b/tests/test_prompt_pair_resolver.py index f739d893..2c8f4388 100644 --- a/tests/test_prompt_pair_resolver.py +++ b/tests/test_prompt_pair_resolver.py @@ -9,7 +9,7 @@ import tempfile import unittest from dataclasses import replace from pathlib import Path -from unittest import mock +import unittest.mock as mock from utils import inspection_semantics from utils import prompt_pair_resolver as resolver diff --git a/tests/test_prompt_usage.py b/tests/test_prompt_usage.py index 41bcc7b6..4c85b637 100644 --- a/tests/test_prompt_usage.py +++ b/tests/test_prompt_usage.py @@ -286,7 +286,7 @@ class TestNodeRecordsCachedRunReceipt(UsageTestCase): } def _node(self): - from unittest import mock + import unittest.mock as mock from prompt_manager_text import PromptManagerText diff --git a/tests/test_queue_observers.py b/tests/test_queue_observers.py index e42b520e..44577a75 100644 --- a/tests/test_queue_observers.py +++ b/tests/test_queue_observers.py @@ -13,19 +13,19 @@ class FakeQueue: def __init__(self): self.result = ((0, "job", {}, {}, [], {}), 0) self.calls = [] - self.failure = None + self.failures = () self.done_result = object() def get(self, *args, **kwargs): self.calls.append(("get", args, kwargs)) - if self.failure: - raise self.failure + for failure in self.failures: + raise failure return self.result def task_done(self, *args, **kwargs): self.calls.append(("done", args, kwargs)) - if self.failure: - raise self.failure + for failure in self.failures: + raise failure return self.done_result @@ -46,7 +46,7 @@ class QueueObserversTests(unittest.TestCase): self.assertIsNone(host.get()) self.assertEqual(seen, [original]) for error in (queue.Empty(), RuntimeError("host failure")): - host.failure = error + host.failures = (error,) with self.assertRaises(type(error)) as raised: host.get(timeout=sentinel) self.assertIs(raised.exception, error) @@ -59,7 +59,7 @@ class QueueObserversTests(unittest.TestCase): self.assertIs(host.task_done(0, sentinel, status=sentinel), host.done_result) self.assertEqual(seen, [((0, sentinel), {"status": sentinel})]) error = ValueError("host failure") - host.failure = error + host.failures = (error,) with self.assertRaises(ValueError) as raised: host.task_done(1) self.assertIs(raised.exception, error) diff --git a/tests/test_restore_plans.py b/tests/test_restore_plans.py index f5825169..b9be6087 100644 --- a/tests/test_restore_plans.py +++ b/tests/test_restore_plans.py @@ -357,7 +357,7 @@ class TestRestoreAdmission(RestoreTestCase): gate.set() await health.task - restore, release = self.gated_job("restore", "exclusive", RESTORE_JOB_TITLE) + self.gated_job("restore", "exclusive", RESTORE_JOB_TITLE) resp = await self.client.post("/prompt_manager/system/health") refusal = await resp.json() self.assertEqual((resp.status, refusal["code"]), (409, "busy")) @@ -369,7 +369,7 @@ class TestRestoreAdmission(RestoreTestCase): self.assertEqual(body["running"]["title"], "Move library") self.api._jobs.list()[0].finish() - body = await self.preview(self.upload(["restored"])) + await self.preview(self.upload(["restored"])) self.gated_job("restore", "exclusive", RESTORE_JOB_TITLE) resp = await self.client.post("/prompt_manager/system/library/move") self.assertEqual(resp.status, 409) diff --git a/tests/test_review_image_links_tool.py b/tests/test_review_image_links_tool.py index 0e52fdf0..9d698500 100644 --- a/tests/test_review_image_links_tool.py +++ b/tests/test_review_image_links_tool.py @@ -116,7 +116,7 @@ class ReviewToolTests(ToolFixture): self.assertEqual(code, 0) (verdict,) = json.loads(out.split("--apply, for")[0]) self.assertEqual(verdict["verdict"], "manual") - code, _out = self.run_tool("--ids", str(self.guessed[0]), "--apply") + self.run_tool("--ids", str(self.guessed[0]), "--apply") self.assertEqual(self.links(), before) def test_prompt_selection_and_text_report(self): diff --git a/tests/test_review_inspection.py b/tests/test_review_inspection.py index ea0774a1..e42bd307 100644 --- a/tests/test_review_inspection.py +++ b/tests/test_review_inspection.py @@ -337,7 +337,7 @@ class TestDefaultProfileStillDecides(InspectionTestCase): side_effect=AssertionError("inspection choices were asked"), ), ): - _item, (status, data) = await self._apply("cat") + _, (status, data) = await self._apply("cat") self.assertEqual(status, 200, data) # The saver anchor check (#390): with the file's name, nothing matches. self.assertEqual([c[1] for c in calls], ["two.png"]) @@ -383,7 +383,7 @@ class TestApplyInspectionCandidate(InspectionTestCase): return real(metadata, file_name) with patch.object(review_capture, "resolve_inspection", side_effect=spy): - _item, (status, data) = await self._apply("make it night") + _, (status, data) = await self._apply("make it night") self.assertEqual(status, 200, data) self.assertEqual(len(calls), 1) @@ -566,7 +566,7 @@ class TestVersionChanges(InspectionTestCase): self.assertNotEqual(item["resolver_version"], old["resolver_version"]) self.assertEqual(job["scope"]["review_items"], 1) self.assertEqual(self._pending(), 1) - _item, (status, data) = await self._apply("make it night") + _, (status, data) = await self._apply("make it night") self.assertEqual(status, 200, data) self.assertEqual(self._linked_text(path), "make it night") @@ -885,7 +885,7 @@ class TestInspectionResolverVersion(InspectionTestCase): await self._scan("all") (item,) = self._items() self.assertNotEqual(item["id"], old["id"]) - _item, (status, data) = await self._apply("add a red hat") + _, (status, data) = await self._apply("add a red hat") self.assertEqual(status, 200, data) self.assertEqual(self._linked_text(path), "add a red hat") diff --git a/tests/test_review_saver_anchor.py b/tests/test_review_saver_anchor.py index c61d8112..227cda50 100644 --- a/tests/test_review_saver_anchor.py +++ b/tests/test_review_saver_anchor.py @@ -169,7 +169,7 @@ class TestInspectionItemsAnchor(InspectionTestCase): path = self._write("branch_1_00001_.png", graph=branch_graph(*PROMPTS)) await self._scan() - _item, (status, data) = await self._apply("add a red hat") + _, (status, data) = await self._apply("add a red hat") self.assertEqual(status, 200, data) self.assertEqual(self._linked_text(path), "add a red hat") @@ -316,7 +316,7 @@ class TestDefaultItemsAnchor(InspectionTestCase): with patch.object(review_capture, "_captured_inspection", return_value=None): await self._scan() - _item, (status, data) = await self._apply("cat") + _, (status, data) = await self._apply("cat") self.assertEqual(status, 200, data) self.assertEqual(self._linked_text(path), "cat") diff --git a/tests/test_run_attribution.py b/tests/test_run_attribution.py index 63d2199b..6ecc9868 100644 --- a/tests/test_run_attribution.py +++ b/tests/test_run_attribution.py @@ -7,7 +7,7 @@ import threading import time import unittest from pathlib import Path -from unittest import mock +import unittest.mock as mock from database.run_attribution import ( RecordedOutput, @@ -86,11 +86,10 @@ class RecordedRuns: class TestStoredRunPrompt(RecordedRuns, unittest.TestCase): - setUp = history_tests.HistoryDatabaseTests.setUp close_databases = history_tests.HistoryDatabaseTests.close_databases database = history_tests.HistoryDatabaseTests.database - def setUp(self): # noqa: F811 - the shared temp dir and database list first + def setUp(self): # the shared temp dir and database list first history_tests.HistoryDatabaseTests.setUp(self) self.db = self.database() self.output = str(Path(self.temp.name) / "output") diff --git a/tests/test_save_to_library.py b/tests/test_save_to_library.py index 3d8bee47..0a28a15c 100644 --- a/tests/test_save_to_library.py +++ b/tests/test_save_to_library.py @@ -10,7 +10,7 @@ import os import sys import unittest from pathlib import Path -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_scanned_prompts.py b/tests/test_scanned_prompts.py index c256e6c2..009bb4b7 100644 --- a/tests/test_scanned_prompts.py +++ b/tests/test_scanned_prompts.py @@ -7,7 +7,7 @@ import threading import unittest from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from unittest import mock +import unittest.mock as mock try: from tests.open_handles import assert_closed diff --git a/tests/test_thumbnail_jobs.py b/tests/test_thumbnail_jobs.py index cda1832f..a74680e5 100644 --- a/tests/test_thumbnail_jobs.py +++ b/tests/test_thumbnail_jobs.py @@ -894,7 +894,7 @@ class TestLegacyStreamOnTheJob(ThumbnailJobCase): self.assertEqual(resp.status, 200) await self._wait(first) job = self.api._jobs.active("thumbnails") - _subscribed, state["left"] = self._watch_subscribers(job) + _, state["left"] = self._watch_subscribers(job) resp.close() # the gallery tab went away closed.set() await self._wait(state["left"]) # the server saw the client go diff --git a/tests/test_trash_freedesktop.py b/tests/test_trash_freedesktop.py index 10f88741..22b2ca91 100644 --- a/tests/test_trash_freedesktop.py +++ b/tests/test_trash_freedesktop.py @@ -12,7 +12,7 @@ import stat import sys import tempfile import unittest -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_trash_macos.py b/tests/test_trash_macos.py index 3b2cb185..7ede45c3 100644 --- a/tests/test_trash_macos.py +++ b/tests/test_trash_macos.py @@ -12,7 +12,7 @@ import sys import tempfile import unittest from types import SimpleNamespace -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_utils_logging_config.py b/tests/test_utils_logging_config.py index d64ee51e..4d1cf609 100644 --- a/tests/test_utils_logging_config.py +++ b/tests/test_utils_logging_config.py @@ -998,7 +998,7 @@ class TestConfiguration(IsolatedLoggerTestCase): def log_from_background_thread() -> None: try: self.manager.logger.info(message) - except BaseException as error: + except Exception as error: errors.append(error) # Allow this record through the fixture's normal main-thread filter. diff --git a/tests/test_utils_video_metadata.py b/tests/test_utils_video_metadata.py index f8918780..84222d93 100644 --- a/tests/test_utils_video_metadata.py +++ b/tests/test_utils_video_metadata.py @@ -15,7 +15,7 @@ import subprocess import sys import tempfile import unittest -from unittest import mock +import unittest.mock as mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/tests/test_workspace_system_fixture.py b/tests/test_workspace_system_fixture.py index 23189560..58f68fed 100644 --- a/tests/test_workspace_system_fixture.py +++ b/tests/test_workspace_system_fixture.py @@ -8,7 +8,7 @@ import tempfile import types import unittest from pathlib import Path -from unittest import mock +import unittest.mock as mock from aiohttp import web from aiohttp.test_utils import TestClient, TestServer diff --git a/tools/repair_foreign_links.py b/tools/repair_foreign_links.py index fe75611d..f710999e 100644 --- a/tools/repair_foreign_links.py +++ b/tools/repair_foreign_links.py @@ -506,8 +506,10 @@ def markdown(plan, prompt_texts): "", f"## Relink ({sum(i['action'] == 'relink' for i in rows)})", "", - "| row | file | linked prompt | linked text | target | target hash " - "| target text |", + ( + "| row | file | linked prompt | linked text | target | target hash " + "| target text |" + ), "|---:|---|---:|---|---|---|---|", ] for item in (i for i in rows if i["action"] == "relink"): diff --git a/utils/diagnostics.py b/utils/diagnostics.py index 61172ece..e532e9d3 100644 --- a/utils/diagnostics.py +++ b/utils/diagnostics.py @@ -386,14 +386,9 @@ class GalleryDiagnostics: except ImportError: self.logger.error(" [FAIL] PIL (Pillow): NOT AVAILABLE") - # Check sqlite3 - try: - import sqlite3 - - dependencies["sqlite3"] = True - self.logger.info(f" [PASS] sqlite3: {sqlite3.sqlite_version}") - except ImportError: - self.logger.error(" [FAIL] sqlite3: NOT AVAILABLE") + # sqlite3 is imported at the top of this module, so it is always here. + dependencies["sqlite3"] = True + self.logger.info(f" [PASS] sqlite3: {sqlite3.sqlite_version}") all_deps_ok = all(dependencies.values()) diff --git a/utils/execution_history_writer.py b/utils/execution_history_writer.py index 248b49de..715bfdad 100644 --- a/utils/execution_history_writer.py +++ b/utils/execution_history_writer.py @@ -398,6 +398,7 @@ class HistoryWriter: try: command = self._queue.get_nowait() except queue.Empty: + # no command waiting; the loop below handles command None pass if probe: try: diff --git a/utils/image_monitor.py b/utils/image_monitor.py index 5b016c7b..452bf104 100644 --- a/utils/image_monitor.py +++ b/utils/image_monitor.py @@ -1048,8 +1048,8 @@ class ImageGenerationHandler(FileSystemEventHandler): with Image.open(image_path) as img: file_info["dimensions"] = list(img.size) file_info["format"] = img.format - except Exception: - pass + except Exception as error: + self.logger.debug(f"No dimensions for {image_path}: {error}") return file_info except Exception as e: @@ -1424,8 +1424,8 @@ class ImageMonitor: if self.observer is not None: try: observer_alive = self.observer.is_alive() - except Exception: - pass + except Exception as error: + self.logger.debug(f"Could not read the observer state: {error}") return { "running": self.observer is not None, "monitored_directories": self.monitored_directories, diff --git a/utils/library_location.py b/utils/library_location.py index 4a69b6ab..0e637992 100644 --- a/utils/library_location.py +++ b/utils/library_location.py @@ -466,6 +466,7 @@ def ensure_private_dir(path: str) -> str: try: os.chmod(path, 0o700) except OSError: + # the folder exists; a filesystem without modes keeps its own pass return path diff --git a/utils/media_capture.py b/utils/media_capture.py index b826bc85..c978305b 100644 --- a/utils/media_capture.py +++ b/utils/media_capture.py @@ -181,6 +181,7 @@ def _unlink_quietly(path: str) -> None: try: os.unlink(path) except FileNotFoundError: + # already gone, which is what was wanted pass except OSError as error: logger.warning("Capture file %s could not be removed: %s", path, error) @@ -390,6 +391,7 @@ class CaptureStore: try: os.rmdir(self.directory) except FileNotFoundError: + # already gone, which is what was wanted pass except OSError as error: logger.warning( diff --git a/utils/trash.py b/utils/trash.py index 7fcf5121..309c6207 100644 --- a/utils/trash.py +++ b/utils/trash.py @@ -339,6 +339,7 @@ class FreedesktopBackend: try: os.mkdir(name, 0o700, dir_fd=parent_fd) except FileExistsError: + # the folder already exists; the checks below still vet it pass except OSError as error: raise TrashRefused(NO_TRASH, str(error)) from error diff --git a/utils/trash_macos.py b/utils/trash_macos.py index 4ce422da..0a7644a6 100644 --- a/utils/trash_macos.py +++ b/utils/trash_macos.py @@ -114,6 +114,7 @@ def inside_any_root(path: str, roots) -> bool: if _identity(current) in identities: return True except OSError: + # an unreadable folder cannot be a trash; try its parent pass parent = os.path.dirname(current) if parent == current: diff --git a/utils/workspace_keys.py b/utils/workspace_keys.py index 5087c53b..cdc37ab6 100644 --- a/utils/workspace_keys.py +++ b/utils/workspace_keys.py @@ -88,6 +88,7 @@ def _unlink(path: str) -> None: try: os.unlink(path) except OSError: + # best effort cleanup; the next sweep removes a leftover temp pass @@ -105,6 +106,7 @@ def _sweep_temps(directory: str) -> None: if os.stat(path).st_mtime < cutoff: os.unlink(path) except OSError: + # best effort: a later sweep removes what this one could not pass @@ -116,6 +118,7 @@ def _tighten(path: str) -> None: if stat.S_IMODE(os.stat(path).st_mode) & 0o077: os.chmod(path, 0o600) except OSError: + # best effort: the key still works with its old mode pass diff --git a/web/comfy/prompt_manager.js b/web/comfy/prompt_manager.js index ddd36e5e..6d875c04 100644 --- a/web/comfy/prompt_manager.js +++ b/web/comfy/prompt_manager.js @@ -354,8 +354,6 @@ app.registerExtension({ if (textWidget) { // Store reference to the widget for the closure const widgetRef = textWidget; - const originalSerializeValue = textWidget.serializeValue; - // Override serializeValue - use arrow function to capture widgetRef textWidget.serializeValue = (nodeId, widgetIndex) => { // Always return the current DOM value using our stored reference diff --git a/web/js/admin.js b/web/js/admin.js index 16039261..a3b4b7c4 100644 --- a/web/js/admin.js +++ b/web/js/admin.js @@ -2391,8 +2391,6 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // Extract relative path from the full path // The image_path might be absolute, we need to serve it via the API const path = image.image_path; - // Try to get filename for serving - const filename = image.filename || path.split(/[/\\]/).pop(); // Use the image ID if available, otherwise use the path-based serve endpoint if (image.id) { return ApiClient.resolveUrl(`/prompt_manager/images/${image.id}/file`); diff --git a/web/js/gallery.js b/web/js/gallery.js index acb4b9a5..b889023b 100644 --- a/web/js/gallery.js +++ b/web/js/gallery.js @@ -607,7 +607,6 @@ button: true, backdrop: true, navbar: true, - title: true, // IMPORTANT: Use original images for viewing, not thumbnails url: 'data-original', // Tell ViewerJS to use data-original attribute toolbar: { diff --git a/web/js/workspace/file-inspector.mjs b/web/js/workspace/file-inspector.mjs index 4135152c..97a4f0c6 100644 --- a/web/js/workspace/file-inspector.mjs +++ b/web/js/workspace/file-inspector.mjs @@ -1,7 +1,6 @@ import { createScope, createButton } from '../shared/toolkit/core.js'; import { mountPromptBlocks, promptRoles } from './prompt-blocks.mjs'; import { createInspectionController } from './inspection-state.mjs'; -import { createSendButton } from './prompt-sender.mjs'; import { createWorkspaceIcon } from './icons.mjs'; import { navigateTo } from './state.mjs'; import { showWorkspaceToast } from './toast.mjs'; diff --git a/web/js/workspace/local-resolver-support.mjs b/web/js/workspace/local-resolver-support.mjs index 7e062cdb..aa7e0271 100644 --- a/web/js/workspace/local-resolver-support.mjs +++ b/web/js/workspace/local-resolver-support.mjs @@ -1,7 +1,7 @@ // Tables, evidence reader, bounds and outcome lines for local-resolver.mjs (#249 local Inspect, PR L3 Tasks 8-9): // the constants of utils/prompt_pair_resolver.py and utils/inspection_outcomes.py, utils/pair_execution.py // `read_pair_execution` and the semantics table check of utils/inspection_semantics.py. No DOM. -import { PyFloat, PyValueError, isDict, isList, isInt, pyJsonLoads, pyLen, pySlice, pyStrip, pyTruthy, pyEqualsOne, pyStr, pyGet } from './py-json.mjs'; +import { PyValueError, isDict, isList, isInt, pyJsonLoads, pyLen, pySlice, pyTruthy, pyEqualsOne, pyGet } from './py-json.mjs'; /** The semantics table is unusable: the server's `SemanticsError`. */ export class SemanticsError extends Error { diff --git a/web/js/workspace/local-resolver.mjs b/web/js/workspace/local-resolver.mjs index 435b1a2a..9ce727f7 100644 --- a/web/js/workspace/local-resolver.mjs +++ b/web/js/workspace/local-resolver.mjs @@ -41,7 +41,6 @@ import { UNVERIFIABLE_CODES, Unresolved, ZEROED_SOURCE, - compileSemantics, explained, isLink, nodeRef,