Reduce per-file-ignores by fixing lint violations across codebase

Fix ruff violations in ~15 source files and ~60+ test files to minimize
per-file-ignores in pyproject.toml. Remaining ignores are justified with
comments explaining why each suppression is necessary.

Source fixes: FBT003 (keyword args), S310 (URL validation), SLF001
(private access), T201 (print→logging), C901 (complexity), E501 (line
length), E402 (import order).

Test fixes: SIM117 (combined with), FBT (boolean args), PERF203 (try in
loop), S310/S607 (URLs/executables), E402/E501 (imports/lines), S108
(tmp paths), PLR0913 (too many args), ARG (unused args), ANN (type
annotations), RUF059 (unused unpacked vars), PT019 (fixture naming).

Remaining per-file-ignores (with justifications):
- Tests: ARG, D, PLC0415, PLR2004, S101, SLF001
- music_gen sources: PLC0415 (heavy ML lazy imports)
- moviepy_showcase: PLC0415 (circular dependency)
- generate_images: PLR0913 (matplotlib helpers need many params)
- praca_magisterska_video: E501, E402 (long paths, mpl.use)
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-03-25 18:58:05 +01:00
parent 996617d4a0
commit e5fd82c822
86 changed files with 1168 additions and 1241 deletions

View File

@ -9,12 +9,14 @@ import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import LineString, Point, Polygon from shapely.geometry import LineString, Point, Polygon
from typing_extensions import Self
from python_pkg.anki_decks.polish_coastal_features import ( from python_pkg.anki_decks.polish_coastal_features import (
polish_coastal_features_anki as _mod, polish_coastal_features_anki as _mod,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable
from pathlib import Path from pathlib import Path
_init_worker = _mod._init_worker _init_worker = _mod._init_worker
@ -62,17 +64,26 @@ def _line_feature() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -3,12 +3,17 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
try: try:
from python_pkg.anki_decks.polish_forests.polish_forests_anki import ( from python_pkg.anki_decks.polish_forests.polish_forests_anki import (
@ -58,17 +63,26 @@ def _forests() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -3,12 +3,17 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
try: try:
from python_pkg.anki_decks.polish_gminy.polish_gminy_anki import ( from python_pkg.anki_decks.polish_gminy.polish_gminy_anki import (
@ -59,17 +64,26 @@ def _gminy() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -3,12 +3,17 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
try: try:
from python_pkg.anki_decks.polish_islands.polish_islands_anki import ( from python_pkg.anki_decks.polish_islands.polish_islands_anki import (
@ -73,17 +78,26 @@ def _island_outside() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -3,12 +3,17 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
try: try:
from python_pkg.anki_decks.polish_lakes.polish_lakes_anki import ( from python_pkg.anki_decks.polish_lakes.polish_lakes_anki import (
@ -58,17 +63,26 @@ def _lakes() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -9,10 +9,12 @@ import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
from typing_extensions import Self
import python_pkg.anki_decks.polish_landscape_parks.polish_landscape_parks_anki as _mod import python_pkg.anki_decks.polish_landscape_parks.polish_landscape_parks_anki as _mod
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable
from pathlib import Path from pathlib import Path
_init_worker = _mod._init_worker _init_worker = _mod._init_worker
@ -47,17 +49,26 @@ def _parks() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import importlib import importlib
from pathlib import Path from pathlib import Path
import sys import sys
from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
@ -34,7 +33,7 @@ class TestImportError:
original_import = builtins.__import__ original_import = builtins.__import__
def mock_import(name: str, *args: Any, **kwargs: Any) -> Any: def mock_import(name: str, *args: object, **kwargs: object) -> object:
if name in ("bs4", "requests"): if name in ("bs4", "requests"):
msg = f"No module named '{name}'" msg = f"No module named '{name}'"
raise ImportError(msg) raise ImportError(msg)
@ -119,7 +118,7 @@ class TestFetchWikipediaHtml:
) )
def test_returns_cached_data_when_valid( def test_returns_cached_data_when_valid(
self, self,
_mock_valid: MagicMock, mock_valid: MagicMock,
mock_cache_path: MagicMock, mock_cache_path: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
@ -144,7 +143,7 @@ class TestFetchWikipediaHtml:
def test_fetches_fresh_when_cache_read_fails( def test_fetches_fresh_when_cache_read_fails(
self, self,
mock_get: MagicMock, mock_get: MagicMock,
_mock_valid: MagicMock, mock_valid: MagicMock,
mock_cache_path: MagicMock, mock_cache_path: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
@ -181,7 +180,7 @@ class TestFetchWikipediaHtml:
def test_fetches_from_wikipedia_when_cache_invalid( def test_fetches_from_wikipedia_when_cache_invalid(
self, self,
mock_get: MagicMock, mock_get: MagicMock,
_mock_valid: MagicMock, mock_valid: MagicMock,
mock_cache_path: MagicMock, mock_cache_path: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
@ -211,7 +210,7 @@ class TestFetchWikipediaHtml:
def test_force_refresh_ignores_cache( def test_force_refresh_ignores_cache(
self, self,
mock_get: MagicMock, mock_get: MagicMock,
_mock_valid: MagicMock, mock_valid: MagicMock,
mock_cache_path: MagicMock, mock_cache_path: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
@ -239,7 +238,7 @@ class TestFetchWikipediaHtml:
def test_force_refresh_skips_valid_cache( def test_force_refresh_skips_valid_cache(
self, self,
mock_get: MagicMock, mock_get: MagicMock,
_mock_valid: MagicMock, mock_valid: MagicMock,
mock_cache_path: MagicMock, mock_cache_path: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
@ -268,7 +267,7 @@ class TestFetchWikipediaHtml:
def test_raises_runtime_error_on_request_exception( def test_raises_runtime_error_on_request_exception(
self, self,
mock_get: MagicMock, mock_get: MagicMock,
_mock_valid: MagicMock, mock_valid: MagicMock,
mock_cache_path: MagicMock, mock_cache_path: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
@ -295,7 +294,7 @@ class TestFetchWikipediaHtml:
def test_continues_when_cache_write_fails( def test_continues_when_cache_write_fails(
self, self,
mock_get: MagicMock, mock_get: MagicMock,
_mock_valid: MagicMock, mock_valid: MagicMock,
mock_cache_path: MagicMock, mock_cache_path: MagicMock,
) -> None: ) -> None:
"""Should return data even when cache write fails.""" """Should return data even when cache write fails."""

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
import tempfile
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.anki_decks.polish_license_plates.fetch_license_plates import ( from python_pkg.anki_decks.polish_license_plates.fetch_license_plates import (
@ -34,7 +35,7 @@ class TestFetchWikipediaLicensePlates:
@patch(f"{MOD}.parse_license_plates_from_html", return_value={"KR": "Kraków"}) @patch(f"{MOD}.parse_license_plates_from_html", return_value={"KR": "Kraków"})
@patch(f"{MOD}.fetch_wikipedia_html", return_value="<html></html>") @patch(f"{MOD}.fetch_wikipedia_html", return_value="<html></html>")
def test_force_refresh_passed( def test_force_refresh_passed(
self, mock_fetch: MagicMock, _mock_parse: MagicMock self, mock_fetch: MagicMock, mock_parse: MagicMock
) -> None: ) -> None:
fetch_wikipedia_license_plates(force_refresh=True) fetch_wikipedia_license_plates(force_refresh=True)
mock_fetch.assert_called_once_with(force_refresh=True) mock_fetch.assert_called_once_with(force_refresh=True)
@ -99,7 +100,7 @@ class TestGenerateLicensePlateDataFile:
class TestMain: class TestMain:
"""Tests for main entry point.""" """Tests for main entry point."""
@patch(f"{MOD}.get_cache_path", return_value=Path("/tmp/cache")) @patch(f"{MOD}.get_cache_path", return_value=Path(tempfile.gettempdir(), "cache"))
@patch(f"{MOD}.generate_license_plate_data_file") @patch(f"{MOD}.generate_license_plate_data_file")
@patch( @patch(
f"{MOD}.fetch_wikipedia_license_plates", f"{MOD}.fetch_wikipedia_license_plates",
@ -109,9 +110,9 @@ class TestMain:
def test_success( def test_success(
self, self,
mock_args: MagicMock, mock_args: MagicMock,
_mock_fetch: MagicMock, mock_fetch: MagicMock,
mock_gen: MagicMock, mock_gen: MagicMock,
_mock_cache: MagicMock, mock_cache: MagicMock,
) -> None: ) -> None:
mock_args.return_value = MagicMock(force=False) mock_args.return_value = MagicMock(force=False)
with patch("sys.stdout", new_callable=StringIO): with patch("sys.stdout", new_callable=StringIO):
@ -127,14 +128,14 @@ class TestMain:
def test_runtime_error( def test_runtime_error(
self, self,
mock_args: MagicMock, mock_args: MagicMock,
_mock_fetch: MagicMock, mock_fetch: MagicMock,
) -> None: ) -> None:
mock_args.return_value = MagicMock(force=False) mock_args.return_value = MagicMock(force=False)
with patch("sys.stderr", new_callable=StringIO): with patch("sys.stderr", new_callable=StringIO):
result = main() result = main()
assert result == 1 assert result == 1
@patch(f"{MOD}.get_cache_path", return_value=Path("/tmp/cache")) @patch(f"{MOD}.get_cache_path", return_value=Path(tempfile.gettempdir(), "cache"))
@patch(f"{MOD}.generate_license_plate_data_file") @patch(f"{MOD}.generate_license_plate_data_file")
@patch( @patch(
f"{MOD}.fetch_wikipedia_license_plates", f"{MOD}.fetch_wikipedia_license_plates",
@ -145,8 +146,8 @@ class TestMain:
self, self,
mock_args: MagicMock, mock_args: MagicMock,
mock_fetch: MagicMock, mock_fetch: MagicMock,
_mock_gen: MagicMock, mock_gen: MagicMock,
_mock_cache: MagicMock, mock_cache: MagicMock,
) -> None: ) -> None:
mock_args.return_value = MagicMock(force=True) mock_args.return_value = MagicMock(force=True)
with patch("sys.stdout", new_callable=StringIO): with patch("sys.stdout", new_callable=StringIO):
@ -154,7 +155,7 @@ class TestMain:
assert result == 0 assert result == 0
mock_fetch.assert_called_once_with(force_refresh=True) mock_fetch.assert_called_once_with(force_refresh=True)
@patch(f"{MOD}.get_cache_path", return_value=Path("/tmp/cache")) @patch(f"{MOD}.get_cache_path", return_value=Path(tempfile.gettempdir(), "cache"))
@patch(f"{MOD}.generate_license_plate_data_file") @patch(f"{MOD}.generate_license_plate_data_file")
@patch( @patch(
f"{MOD}.fetch_wikipedia_license_plates", f"{MOD}.fetch_wikipedia_license_plates",
@ -164,9 +165,9 @@ class TestMain:
def test_prints_summary( def test_prints_summary(
self, self,
mock_args: MagicMock, mock_args: MagicMock,
_mock_fetch: MagicMock, mock_fetch: MagicMock,
_mock_gen: MagicMock, mock_gen: MagicMock,
_mock_cache: MagicMock, mock_cache: MagicMock,
) -> None: ) -> None:
mock_args.return_value = MagicMock(force=False) mock_args.return_value = MagicMock(force=False)
with patch("sys.stdout", new_callable=StringIO) as mock_stdout: with patch("sys.stdout", new_callable=StringIO) as mock_stdout:

View File

@ -3,12 +3,17 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Point, Polygon from shapely.geometry import Point, Polygon
from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
try: try:
from python_pkg.anki_decks.polish_mountain_peaks.polish_mountain_peaks_anki import ( from python_pkg.anki_decks.polish_mountain_peaks.polish_mountain_peaks_anki import (
@ -58,17 +63,26 @@ def _peaks() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -9,10 +9,12 @@ import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
from typing_extensions import Self
import python_pkg.anki_decks.polish_mountain_ranges.polish_mountain_ranges_anki as _mod import python_pkg.anki_decks.polish_mountain_ranges.polish_mountain_ranges_anki as _mod
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable
from pathlib import Path from pathlib import Path
_init_worker = _mod._init_worker _init_worker = _mod._init_worker
@ -49,17 +51,26 @@ def _ranges() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
@ -10,6 +11,11 @@ import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
from typing_extensions import Self
try: try:
from python_pkg.anki_decks.polish_national_parks.polish_national_parks_anki import ( from python_pkg.anki_decks.polish_national_parks.polish_national_parks_anki import (
_init_worker, _init_worker,
@ -73,17 +79,26 @@ def _small_park() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -9,10 +9,12 @@ import geopandas as gpd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from shapely.geometry import Polygon from shapely.geometry import Polygon
from typing_extensions import Self
import python_pkg.anki_decks.polish_nature_reserves.polish_nature_reserves_anki as _mod import python_pkg.anki_decks.polish_nature_reserves.polish_nature_reserves_anki as _mod
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable
from pathlib import Path from pathlib import Path
_init_worker = _mod._init_worker _init_worker = _mod._init_worker
@ -47,17 +49,26 @@ def _reserves() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__(self, processes=None, initializer=None, initargs=()) -> None: def __init__(
self,
_processes: int | None = None,
initializer: Callable[..., object] | None = None,
initargs: tuple[object, ...] = (),
) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered(self, func, items): def imap_unordered(
self,
func: Callable[[object], object],
items: Iterable[object],
) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self): def __enter__(self) -> Self:
return self return self
def __exit__(self, *a): def __exit__(self, *_args: object) -> None:
pass pass

View File

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
@ -12,6 +12,9 @@ import pytest
from shapely.geometry import LineString, Polygon from shapely.geometry import LineString, Polygon
from typing_extensions import Self from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
try: try:
from python_pkg.anki_decks.polish_rivers.polish_rivers_anki import ( from python_pkg.anki_decks.polish_rivers.polish_rivers_anki import (
_init_worker, _init_worker,
@ -77,18 +80,18 @@ def _river_outside() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__( def __init__(
self, self,
processes: int | None = None, _processes: int | None = None,
initializer: Any = None, initializer: Callable[..., object] | None = None,
initargs: tuple[Any, ...] = (), initargs: tuple[object, ...] = (),
) -> None: ) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered( def imap_unordered(
self, self,
func: Any, func: Callable[[object], object],
items: Any, items: Iterable[object],
) -> list[Any]: ) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self) -> Self: def __enter__(self) -> Self:

View File

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import geopandas as gpd import geopandas as gpd
@ -12,6 +12,9 @@ import pytest
from shapely.geometry import Point, Polygon from shapely.geometry import Point, Polygon
from typing_extensions import Self from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
try: try:
from python_pkg.anki_decks.polish_unesco_sites.polish_unesco_sites_anki import ( from python_pkg.anki_decks.polish_unesco_sites.polish_unesco_sites_anki import (
_init_worker, _init_worker,
@ -79,18 +82,18 @@ def _site_polygon() -> gpd.GeoDataFrame:
class _FakePool: class _FakePool:
def __init__( def __init__(
self, self,
processes: int | None = None, _processes: int | None = None,
initializer: Any = None, initializer: Callable[..., object] | None = None,
initargs: tuple[Any, ...] = (), initargs: tuple[object, ...] = (),
) -> None: ) -> None:
if initializer: if initializer:
initializer(*initargs) initializer(*initargs)
def imap_unordered( def imap_unordered(
self, self,
func: Any, func: Callable[[object], object],
items: Any, items: Iterable[object],
) -> list[Any]: ) -> list[object]:
return [func(item) for item in items] return [func(item) for item in items]
def __enter__(self) -> Self: def __enter__(self) -> Self:

View File

@ -167,7 +167,7 @@ class TestLoadStreetData:
patch.object(_mod_ref, "__file__", str(fake_file)), patch.object(_mod_ref, "__file__", str(fake_file)),
patch(f"{_MOD}.get_warsaw_streets", return_value=_street_segments_gdf()), patch(f"{_MOD}.get_warsaw_streets", return_value=_street_segments_gdf()),
): ):
streets, boundary = load_street_data() _, boundary = load_street_data()
assert len(boundary) == 1 assert len(boundary) == 1
def test_file_not_found(self, tmp_path: Path) -> None: def test_file_not_found(self, tmp_path: Path) -> None:

View File

@ -1,37 +1,78 @@
"""Integration tests for the articles C server API.""" """Integration tests for the articles C server API."""
from http import HTTPStatus from http import HTTPStatus
import http.client
import json import json
import os import os
from pathlib import Path from pathlib import Path
import shutil
import socket import socket
import subprocess import subprocess
import time import time
from typing import Any from typing import Any
import urllib.error import urllib.parse
import urllib.request
import pytest import pytest
class _HTTPError(Exception):
"""HTTP error with status code."""
def __init__(self, code: int) -> None:
super().__init__(f"HTTP {code}")
self.code = code
def _req( def _req(
url: str, method: str = "GET", data: dict[str, Any] | bytes | None = None url: str, method: str = "GET", data: dict[str, Any] | bytes | None = None
) -> tuple[int, bytes]: ) -> tuple[int, bytes]:
"""Send an HTTP request and return status code and body.""" """Send an HTTP request and return status code and body."""
if data is not None and not isinstance(data, bytes | bytearray): if data is not None and not isinstance(data, bytes | bytearray):
data = json.dumps(data).encode("utf-8") data = json.dumps(data).encode("utf-8")
req = urllib.request.Request(url, data=data, method=method) parsed = urllib.parse.urlparse(url)
req.add_header("Content-Type", "application/json") conn = http.client.HTTPConnection(parsed.hostname, parsed.port, timeout=5)
with urllib.request.urlopen(req, timeout=5) as resp: try:
headers = {"Content-Type": "application/json"}
conn.request(method, parsed.path or "/", body=data, headers=headers)
resp = conn.getresponse()
body = resp.read() body = resp.read()
return resp.getcode(), body status = resp.status
finally:
conn.close()
if status >= 400:
raise _HTTPError(status)
return status, body
def _probe_server(host: str, port: int) -> bool:
"""Try a single GET to the server. Return True if it responded."""
try:
conn = http.client.HTTPConnection(host, port, timeout=0.2)
try:
conn.request("GET", "/api/articles")
conn.getresponse().read()
return True
finally:
conn.close()
except OSError:
return False
def _wait_for_server(host: str, port: int, attempts: int = 30) -> None:
"""Poll the server until it responds or attempts are exhausted."""
for _ in range(attempts):
if _probe_server(host, port):
return
time.sleep(0.05)
def test_crud_roundtrip(tmp_path: Path) -> None: def test_crud_roundtrip(tmp_path: Path) -> None:
"""Test full CRUD lifecycle for articles API.""" """Test full CRUD lifecycle for articles API."""
# Build C server # Build C server
here = Path(__file__).resolve().parent.parent here = Path(__file__).resolve().parent.parent
subprocess.run(["make", "-s", "server_c"], check=True, cwd=str(here)) make_path = shutil.which("make")
assert make_path is not None, "make not found in PATH"
subprocess.run([make_path, "-s", "server_c"], check=True, cwd=str(here))
# Find a free port # Find a free port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@ -47,16 +88,7 @@ def test_crud_roundtrip(tmp_path: Path) -> None:
env["PORT"] = str(port) env["PORT"] = str(port)
srv = subprocess.Popen(["./server_c"], cwd=str(here), env=env) srv = subprocess.Popen(["./server_c"], cwd=str(here), env=env)
try: try:
# wait briefly for server to be ready _wait_for_server(host, port)
for _ in range(30):
try:
with urllib.request.urlopen(
base + "/api/articles", timeout=0.2
) as resp:
resp.read()
break
except (OSError, urllib.error.URLError):
time.sleep(0.05)
# Create # Create
code, body = _req( code, body = _req(
@ -97,10 +129,9 @@ def test_crud_roundtrip(tmp_path: Path) -> None:
assert code == HTTPStatus.NO_CONTENT assert code == HTTPStatus.NO_CONTENT
# Ensure gone # Ensure gone
with pytest.raises(urllib.error.HTTPError) as exc_info: with pytest.raises(_HTTPError) as exc_info:
_req(base + f"/api/articles/{art_id}") _req(base + f"/api/articles/{art_id}")
assert exc_info.value.code == HTTPStatus.NOT_FOUND assert exc_info.value.code == HTTPStatus.NOT_FOUND
exc_info.value.close()
finally: finally:
srv.terminate() srv.terminate()

View File

@ -20,12 +20,12 @@ class TestFindAlsDevice:
"glob", "glob",
return_value=[Path("/sys/bus/iio/devices/iio0/in_illuminance_raw")], return_value=[Path("/sys/bus/iio/devices/iio0/in_illuminance_raw")],
) )
def test_found(self, _mock_glob: MagicMock) -> None: def test_found(self, mock_glob: MagicMock) -> None:
result = auto_brightness_daemon._find_als_device() result = auto_brightness_daemon._find_als_device()
assert result == Path("/sys/bus/iio/devices/iio0") assert result == Path("/sys/bus/iio/devices/iio0")
@patch.object(Path, "glob", return_value=[]) @patch.object(Path, "glob", return_value=[])
def test_not_found(self, _mock_glob: MagicMock) -> None: def test_not_found(self, mock_glob: MagicMock) -> None:
assert auto_brightness_daemon._find_als_device() is None assert auto_brightness_daemon._find_als_device() is None

View File

@ -17,7 +17,7 @@ class TestMainNoAls:
"""Tests for main() when no ALS device is found.""" """Tests for main() when no ALS device is found."""
@patch(f"{MOD}._find_als_device", return_value=None) @patch(f"{MOD}._find_als_device", return_value=None)
def test_exits_when_no_als(self, _mock_find: MagicMock) -> None: def test_exits_when_no_als(self, mock_find: MagicMock) -> None:
with pytest.raises(SystemExit, match="1"): with pytest.raises(SystemExit, match="1"):
auto_brightness_daemon.main() auto_brightness_daemon.main()
@ -62,10 +62,10 @@ class TestMainDaemonLoop:
patch(f"{MOD}._lux_to_brightness", return_value=75), patch(f"{MOD}._lux_to_brightness", return_value=75),
patch(f"{MOD}._get_brightness", return_value=current_brightness), patch(f"{MOD}._get_brightness", return_value=current_brightness),
patch(f"{MOD}._set_brightness", mock_set_brightness), patch(f"{MOD}._set_brightness", mock_set_brightness),
contextlib.suppress(KeyboardInterrupt),
): ):
# Simulate SIGINT by raising KeyboardInterrupt in sleep # Simulate SIGINT by raising KeyboardInterrupt in sleep
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
return mock_set_brightness, mock_lux return mock_set_brightness, mock_lux
@ -96,9 +96,9 @@ class TestMainDaemonLoop:
patch(f"{MOD}._lux_to_brightness", return_value=74), patch(f"{MOD}._lux_to_brightness", return_value=74),
patch(f"{MOD}._get_brightness", return_value=74), patch(f"{MOD}._get_brightness", return_value=74),
patch(f"{MOD}._set_brightness") as mock_set, patch(f"{MOD}._set_brightness") as mock_set,
contextlib.suppress(KeyboardInterrupt),
): ):
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
mock_set.assert_not_called() mock_set.assert_not_called()
def test_skips_when_brightness_negative(self) -> None: def test_skips_when_brightness_negative(self) -> None:
@ -116,9 +116,9 @@ class TestMainDaemonLoop:
patch(f"{MOD}._lux_to_brightness", return_value=75), patch(f"{MOD}._lux_to_brightness", return_value=75),
patch(f"{MOD}._get_brightness", return_value=-1), patch(f"{MOD}._get_brightness", return_value=-1),
patch(f"{MOD}._set_brightness") as mock_set, patch(f"{MOD}._set_brightness") as mock_set,
contextlib.suppress(KeyboardInterrupt),
): ):
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
mock_set.assert_not_called() mock_set.assert_not_called()
def test_creates_control_file_when_missing(self) -> None: def test_creates_control_file_when_missing(self) -> None:
@ -133,9 +133,9 @@ class TestMainDaemonLoop:
patch(f"{MOD}.signal.signal"), patch(f"{MOD}.signal.signal"),
patch(f"{MOD}.time.sleep", side_effect=KeyboardInterrupt), patch(f"{MOD}.time.sleep", side_effect=KeyboardInterrupt),
patch(f"{MOD}._is_enabled", return_value=False), patch(f"{MOD}._is_enabled", return_value=False),
contextlib.suppress(KeyboardInterrupt),
): ):
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
mock_set_enabled.assert_called_once_with(enabled=True) mock_set_enabled.assert_called_once_with(enabled=True)
def test_does_not_create_file_when_exists(self) -> None: def test_does_not_create_file_when_exists(self) -> None:
@ -150,9 +150,9 @@ class TestMainDaemonLoop:
patch(f"{MOD}.signal.signal"), patch(f"{MOD}.signal.signal"),
patch(f"{MOD}.time.sleep", side_effect=KeyboardInterrupt), patch(f"{MOD}.time.sleep", side_effect=KeyboardInterrupt),
patch(f"{MOD}._is_enabled", return_value=False), patch(f"{MOD}._is_enabled", return_value=False),
contextlib.suppress(KeyboardInterrupt),
): ):
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
mock_set_enabled.assert_not_called() mock_set_enabled.assert_not_called()
def test_handles_exception_in_loop_gracefully(self) -> None: def test_handles_exception_in_loop_gracefully(self) -> None:
@ -166,9 +166,9 @@ class TestMainDaemonLoop:
patch(f"{MOD}.signal.signal"), patch(f"{MOD}.signal.signal"),
patch(f"{MOD}.time.sleep", side_effect=[None, KeyboardInterrupt]), patch(f"{MOD}.time.sleep", side_effect=[None, KeyboardInterrupt]),
patch(f"{MOD}._is_enabled", side_effect=OSError("disk fail")), patch(f"{MOD}._is_enabled", side_effect=OSError("disk fail")),
contextlib.suppress(KeyboardInterrupt),
): ):
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
# No crash = exception was handled # No crash = exception was handled
def test_signal_handler_stops_loop(self) -> None: def test_signal_handler_stops_loop(self) -> None:
@ -189,9 +189,9 @@ class TestMainDaemonLoop:
patch(f"{MOD}.signal.signal", side_effect=capture_signal), patch(f"{MOD}.signal.signal", side_effect=capture_signal),
patch(f"{MOD}.time.sleep", side_effect=KeyboardInterrupt), patch(f"{MOD}.time.sleep", side_effect=KeyboardInterrupt),
patch(f"{MOD}._is_enabled", return_value=False), patch(f"{MOD}._is_enabled", return_value=False),
contextlib.suppress(KeyboardInterrupt),
): ):
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
# Verify we captured a SIGTERM handler # Verify we captured a SIGTERM handler
assert signal.SIGTERM in captured_handler assert signal.SIGTERM in captured_handler
@ -217,9 +217,9 @@ class TestMainDaemonLoop:
patch(f"{MOD}._lux_to_brightness", return_value=10), patch(f"{MOD}._lux_to_brightness", return_value=10),
patch(f"{MOD}._get_brightness", return_value=90), patch(f"{MOD}._get_brightness", return_value=90),
patch(f"{MOD}._set_brightness") as mock_set, patch(f"{MOD}._set_brightness") as mock_set,
contextlib.suppress(KeyboardInterrupt),
): ):
with contextlib.suppress(KeyboardInterrupt): auto_brightness_daemon.main()
auto_brightness_daemon.main()
# delta=-80, step=-5, new_val=85 # delta=-80, step=-5, new_val=85
mock_set.assert_called_with(85) mock_set.assert_called_with(85)

View File

@ -20,12 +20,12 @@ class TestFindAlsDevice:
"glob", "glob",
return_value=[Path("/sys/bus/iio/devices/iio0/in_illuminance_raw")], return_value=[Path("/sys/bus/iio/devices/iio0/in_illuminance_raw")],
) )
def test_found(self, _mock_glob: MagicMock) -> None: def test_found(self, mock_glob: MagicMock) -> None:
result = brightness_controller._find_als_device() result = brightness_controller._find_als_device()
assert result == Path("/sys/bus/iio/devices/iio0") assert result == Path("/sys/bus/iio/devices/iio0")
@patch.object(Path, "glob", return_value=[]) @patch.object(Path, "glob", return_value=[])
def test_not_found(self, _mock_glob: MagicMock) -> None: def test_not_found(self, mock_glob: MagicMock) -> None:
assert brightness_controller._find_als_device() is None assert brightness_controller._find_als_device() is None
@ -327,7 +327,7 @@ class TestRefreshBrightness:
"python_pkg.brightness_controller.brightness_controller._get_brightness", "python_pkg.brightness_controller.brightness_controller._get_brightness",
return_value=75, return_value=75,
) )
def test_updates_ui(self, _mock_get: MagicMock) -> None: def test_updates_ui(self, mock_get: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
ctrl.slider_var = MagicMock() ctrl.slider_var = MagicMock()
@ -339,7 +339,7 @@ class TestRefreshBrightness:
"python_pkg.brightness_controller.brightness_controller._get_brightness", "python_pkg.brightness_controller.brightness_controller._get_brightness",
return_value=-1, return_value=-1,
) )
def test_error(self, _mock_get: MagicMock) -> None: def test_error(self, mock_get: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
ctrl._refresh_brightness() ctrl._refresh_brightness()
@ -378,7 +378,7 @@ class TestOnSliderMove:
ctrl.pct_var.set.assert_not_called() ctrl.pct_var.set.assert_not_called()
@patch("python_pkg.brightness_controller.brightness_controller._set_brightness") @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
def test_disables_auto_mode(self, _mock_set: MagicMock) -> None: def test_disables_auto_mode(self, mock_set: MagicMock) -> None:
ctrl = _make_controller(daemon_state=True) ctrl = _make_controller(daemon_state=True)
ctrl.auto_mode = True ctrl.auto_mode = True
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
@ -396,7 +396,7 @@ class TestSetPct:
"python_pkg.brightness_controller.brightness_controller._get_brightness", "python_pkg.brightness_controller.brightness_controller._get_brightness",
return_value=25, return_value=25,
) )
def test_sets_brightness(self, _mock_get: MagicMock, mock_set: MagicMock) -> None: def test_sets_brightness(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
ctrl.slider_var = MagicMock() ctrl.slider_var = MagicMock()
@ -417,7 +417,7 @@ class TestDecrease:
"python_pkg.brightness_controller.brightness_controller._get_brightness", "python_pkg.brightness_controller.brightness_controller._get_brightness",
return_value=50, return_value=50,
) )
def test_decrease(self, _mock_get: MagicMock, mock_set: MagicMock) -> None: def test_decrease(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
ctrl.slider_var = MagicMock() ctrl.slider_var = MagicMock()
@ -429,7 +429,7 @@ class TestDecrease:
"python_pkg.brightness_controller.brightness_controller._get_brightness", "python_pkg.brightness_controller.brightness_controller._get_brightness",
return_value=2, return_value=2,
) )
def test_clamps_to_zero(self, _mock_get: MagicMock, mock_set: MagicMock) -> None: def test_clamps_to_zero(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
ctrl.slider_var = MagicMock() ctrl.slider_var = MagicMock()
@ -449,7 +449,7 @@ class TestIncrease:
"python_pkg.brightness_controller.brightness_controller._get_brightness", "python_pkg.brightness_controller.brightness_controller._get_brightness",
return_value=50, return_value=50,
) )
def test_increase(self, _mock_get: MagicMock, mock_set: MagicMock) -> None: def test_increase(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
ctrl.slider_var = MagicMock() ctrl.slider_var = MagicMock()
@ -461,7 +461,7 @@ class TestIncrease:
"python_pkg.brightness_controller.brightness_controller._get_brightness", "python_pkg.brightness_controller.brightness_controller._get_brightness",
return_value=98, return_value=98,
) )
def test_clamps_to_100(self, _mock_get: MagicMock, mock_set: MagicMock) -> None: def test_clamps_to_100(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()
ctrl.slider_var = MagicMock() ctrl.slider_var = MagicMock()

View File

@ -91,7 +91,7 @@ class TestPollAls:
"""Tests for _poll_als.""" """Tests for _poll_als."""
@patch(f"{MOD}._read_lux", return_value=42.5) @patch(f"{MOD}._read_lux", return_value=42.5)
def test_updates_lux_display(self, _mock_lux: MagicMock) -> None: def test_updates_lux_display(self, mock_lux: MagicMock) -> None:
ctrl = _make_controller(als_path=Path("/fake")) ctrl = _make_controller(als_path=Path("/fake"))
ctrl.lux_var = MagicMock() ctrl.lux_var = MagicMock()
ctrl.root = MagicMock() ctrl.root = MagicMock()
@ -105,7 +105,7 @@ class TestPollAls:
ctrl.root.after.assert_called_once() ctrl.root.after.assert_called_once()
@patch(f"{MOD}._read_lux", side_effect=OSError("sensor fail")) @patch(f"{MOD}._read_lux", side_effect=OSError("sensor fail"))
def test_sensor_error(self, _mock_lux: MagicMock) -> None: def test_sensor_error(self, mock_lux: MagicMock) -> None:
ctrl = _make_controller(als_path=Path("/fake")) ctrl = _make_controller(als_path=Path("/fake"))
ctrl.lux_var = MagicMock() ctrl.lux_var = MagicMock()
ctrl.root = MagicMock() ctrl.root = MagicMock()
@ -118,7 +118,7 @@ class TestPollAls:
ctrl.lux_var.set.assert_called_with("sensor error") ctrl.lux_var.set.assert_called_with("sensor error")
@patch(f"{MOD}._read_lux", side_effect=ValueError("bad value")) @patch(f"{MOD}._read_lux", side_effect=ValueError("bad value"))
def test_sensor_value_error(self, _mock_lux: MagicMock) -> None: def test_sensor_value_error(self, mock_lux: MagicMock) -> None:
ctrl = _make_controller(als_path=Path("/fake")) ctrl = _make_controller(als_path=Path("/fake"))
ctrl.lux_var = MagicMock() ctrl.lux_var = MagicMock()
ctrl.root = MagicMock() ctrl.root = MagicMock()
@ -131,7 +131,7 @@ class TestPollAls:
ctrl.lux_var.set.assert_called_with("sensor error") ctrl.lux_var.set.assert_called_with("sensor error")
@patch(f"{MOD}._read_lux", return_value=10.0) @patch(f"{MOD}._read_lux", return_value=10.0)
def test_syncs_daemon_state_change(self, _mock_lux: MagicMock) -> None: def test_syncs_daemon_state_change(self, mock_lux: MagicMock) -> None:
"""When daemon state differs from auto_mode, syncs it.""" """When daemon state differs from auto_mode, syncs it."""
ctrl = _make_controller(als_path=Path("/fake")) ctrl = _make_controller(als_path=Path("/fake"))
ctrl.auto_mode = False ctrl.auto_mode = False
@ -148,7 +148,7 @@ class TestPollAls:
assert ctrl.auto_mode is True assert ctrl.auto_mode is True
@patch(f"{MOD}._read_lux", return_value=10.0) @patch(f"{MOD}._read_lux", return_value=10.0)
def test_no_sync_when_same(self, _mock_lux: MagicMock) -> None: def test_no_sync_when_same(self, mock_lux: MagicMock) -> None:
"""When daemon state matches auto_mode, no sync needed.""" """When daemon state matches auto_mode, no sync needed."""
ctrl = _make_controller(als_path=Path("/fake")) ctrl = _make_controller(als_path=Path("/fake"))
ctrl.auto_mode = False ctrl.auto_mode = False
@ -177,7 +177,7 @@ class TestPollBrightness:
"""Tests for _poll_brightness.""" """Tests for _poll_brightness."""
@patch(f"{MOD}._get_brightness", return_value=60) @patch(f"{MOD}._get_brightness", return_value=60)
def test_refreshes_when_not_auto(self, _mock_get: MagicMock) -> None: def test_refreshes_when_not_auto(self, mock_get: MagicMock) -> None:
ctrl = _make_controller() ctrl = _make_controller()
ctrl.auto_mode = False ctrl.auto_mode = False
ctrl.pct_var = MagicMock() ctrl.pct_var = MagicMock()

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
import subprocess import subprocess
from typing import Any from typing import TYPE_CHECKING
from unittest.mock import MagicMock, mock_open, patch from unittest.mock import MagicMock, mock_open, patch
import pytest import pytest
@ -24,6 +24,9 @@ from python_pkg.cinema_planner._cinema_parsing import (
parse_time, parse_time,
) )
if TYPE_CHECKING:
import contextlib
class TestParseTime: class TestParseTime:
"""Tests for parse_time.""" """Tests for parse_time."""
@ -208,13 +211,13 @@ class TestParseCinemaCityHtml:
f"{times_html}" f"{times_html}"
) )
def _patch_open(self, html: str) -> Any: def _patch_open(self, html: str) -> contextlib.AbstractContextManager[MagicMock]:
return patch.object(Path, "open", mock_open(read_data=html)) return patch.object(Path, "open", mock_open(read_data=html))
def test_parse_single_movie(self) -> None: def test_parse_single_movie(self) -> None:
html = "header" + self._make_html_section("Movie A", 120, ["10:00", "14:00"]) html = "header" + self._make_html_section("Movie A", 120, ["10:00", "14:00"])
with self._patch_open(html): with self._patch_open(html):
movies, date = parse_cinema_city_html("test.html") movies, _ = parse_cinema_city_html("test.html")
assert len(movies) == 1 assert len(movies) == 1
assert movies[0].name == "Movie A" assert movies[0].name == "Movie A"
assert movies[0].duration == 120 assert movies[0].duration == 120
@ -223,7 +226,7 @@ class TestParseCinemaCityHtml:
def test_parse_with_date(self) -> None: def test_parse_with_date(self) -> None:
html = "2025-01-25 stuff" + self._make_html_section("Movie A", 90, ["18:00"]) html = "2025-01-25 stuff" + self._make_html_section("Movie A", 90, ["18:00"])
with self._patch_open(html): with self._patch_open(html):
movies, date = parse_cinema_city_html("test.html") _, date = parse_cinema_city_html("test.html")
assert date == "2025-01-25" assert date == "2025-01-25"
def test_parse_with_genres(self) -> None: def test_parse_with_genres(self) -> None:
@ -231,7 +234,7 @@ class TestParseCinemaCityHtml:
"Horror Film", 100, ["20:00"], genre="Horror, Thriller" "Horror Film", 100, ["20:00"], genre="Horror, Thriller"
) )
with self._patch_open(html): with self._patch_open(html):
movies, date = parse_cinema_city_html("test.html") movies, _ = parse_cinema_city_html("test.html")
assert len(movies) == 1 assert len(movies) == 1
assert "Horror" in movies[0].genres assert "Horror" in movies[0].genres
assert "Thriller" in movies[0].genres assert "Thriller" in movies[0].genres
@ -239,7 +242,7 @@ class TestParseCinemaCityHtml:
def test_no_name_match(self) -> None: def test_no_name_match(self) -> None:
html = 'header class="row movie-row"> no name here' html = 'header class="row movie-row"> no name here'
with self._patch_open(html): with self._patch_open(html):
movies, date = parse_cinema_city_html("test.html") movies, _ = parse_cinema_city_html("test.html")
assert len(movies) == 0 assert len(movies) == 0
def test_no_duration_match(self) -> None: def test_no_duration_match(self) -> None:
@ -250,7 +253,7 @@ class TestParseCinemaCityHtml:
'<button class="btn btn-primary btn-lg">10:00</button>' '<button class="btn btn-primary btn-lg">10:00</button>'
) )
with self._patch_open(html): with self._patch_open(html):
movies, date = parse_cinema_city_html("test.html") movies, _ = parse_cinema_city_html("test.html")
assert len(movies) == 0 assert len(movies) == 0
def test_no_times_match(self) -> None: def test_no_times_match(self) -> None:
@ -260,7 +263,7 @@ class TestParseCinemaCityHtml:
"<span>100 min</span>" "<span>100 min</span>"
) )
with self._patch_open(html): with self._patch_open(html):
movies, date = parse_cinema_city_html("test.html") movies, _ = parse_cinema_city_html("test.html")
assert len(movies) == 0 assert len(movies) == 0
def test_alternate_time_pattern(self) -> None: def test_alternate_time_pattern(self) -> None:
@ -271,7 +274,7 @@ class TestParseCinemaCityHtml:
"> 10:00 (HTTPS://something" "> 10:00 (HTTPS://something"
) )
with self._patch_open(html): with self._patch_open(html):
movies, date = parse_cinema_city_html("test.html") movies, _ = parse_cinema_city_html("test.html")
assert len(movies) == 1 assert len(movies) == 1
def test_deduplicate_movies(self) -> None: def test_deduplicate_movies(self) -> None:
@ -467,7 +470,7 @@ class TestParseCinemaCityText:
text = "MOVIE TITLE\n110 min\n10:00\n" text = "MOVIE TITLE\n110 min\n10:00\n"
with patch( with patch(
"python_pkg.cinema_planner._cinema_parsing._try_parse_time", "python_pkg.cinema_planner._cinema_parsing._try_parse_time",
side_effect=lambda t: None, side_effect=lambda _t: None,
): ):
result = parse_cinema_city_text(text) result = parse_cinema_city_text(text)
assert len(result) == 0 assert len(result) == 0

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import argparse import argparse
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, mock_open, patch from unittest.mock import MagicMock, mock_open, patch
import pytest import pytest
@ -94,7 +93,7 @@ class TestLoadMoviesInteractive:
"""Tests for _load_movies_interactive.""" """Tests for _load_movies_interactive."""
@patch("builtins.input", side_effect=["Movie A, 10:00, 90min", ""]) @patch("builtins.input", side_effect=["Movie A, 10:00, 90min", ""])
def test_single_movie(self, _mock: MagicMock) -> None: def test_single_movie(self, mock: MagicMock) -> None:
result = _load_movies_interactive() result = _load_movies_interactive()
assert len(result) == 1 assert len(result) == 1
assert result[0].name == "Movie A" assert result[0].name == "Movie A"
@ -107,17 +106,17 @@ class TestLoadMoviesInteractive:
"", "",
], ],
) )
def test_multiple_movies(self, _mock: MagicMock) -> None: def test_multiple_movies(self, mock: MagicMock) -> None:
result = _load_movies_interactive() result = _load_movies_interactive()
assert len(result) == 2 assert len(result) == 2
@patch("builtins.input", side_effect=EOFError) @patch("builtins.input", side_effect=EOFError)
def test_eof(self, _mock: MagicMock) -> None: def test_eof(self, mock: MagicMock) -> None:
result = _load_movies_interactive() result = _load_movies_interactive()
assert result == [] assert result == []
@patch("builtins.input", side_effect=["bad line", ""]) @patch("builtins.input", side_effect=["bad line", ""])
def test_invalid_input(self, _mock: MagicMock) -> None: def test_invalid_input(self, mock: MagicMock) -> None:
result = _load_movies_interactive() result = _load_movies_interactive()
assert result == [] assert result == []
@ -125,7 +124,7 @@ class TestLoadMoviesInteractive:
"builtins.input", "builtins.input",
side_effect=["bad line", "Movie A, 10:00, 90min", ""], side_effect=["bad line", "Movie A, 10:00, 90min", ""],
) )
def test_mixed_valid_invalid(self, _mock: MagicMock) -> None: def test_mixed_valid_invalid(self, mock: MagicMock) -> None:
result = _load_movies_interactive() result = _load_movies_interactive()
assert len(result) == 1 assert len(result) == 1
@ -147,7 +146,7 @@ class TestLoadMoviesFromFile:
) )
def test_htm_file(self, mock_parse: MagicMock) -> None: def test_htm_file(self, mock_parse: MagicMock) -> None:
mock_parse.return_value = ([Movie("A", [600], 120)], None) mock_parse.return_value = ([Movie("A", [600], 120)], None)
movies, date = _load_movies_from_file(Path("test.htm")) _, _ = _load_movies_from_file(Path("test.htm"))
mock_parse.assert_called_once() mock_parse.assert_called_once()
@patch( @patch(
@ -161,17 +160,21 @@ class TestLoadMoviesFromFile:
def test_text_file(self) -> None: def test_text_file(self) -> None:
content = "Movie A, 10:00, 90min\n# comment\nMovie B, 14:00, 120min\n" content = "Movie A, 10:00, 90min\n# comment\nMovie B, 14:00, 120min\n"
with patch.object(Path, "open", mock_open(read_data=content)): with (
with patch.object(Path, "suffix", new=".txt"): patch.object(Path, "open", mock_open(read_data=content)),
movies, date = _load_movies_from_file(Path("test.txt")) patch.object(Path, "suffix", new=".txt"),
):
movies, date = _load_movies_from_file(Path("test.txt"))
assert len(movies) == 2 assert len(movies) == 2
assert date is None assert date is None
def test_text_file_with_bad_line(self) -> None: def test_text_file_with_bad_line(self) -> None:
content = "Movie A, 10:00, 90min\nbad line\n" content = "Movie A, 10:00, 90min\nbad line\n"
with patch.object(Path, "open", mock_open(read_data=content)): with (
with patch.object(Path, "suffix", new=".txt"): patch.object(Path, "open", mock_open(read_data=content)),
movies, date = _load_movies_from_file(Path("test.txt")) patch.object(Path, "suffix", new=".txt"),
):
movies, _ = _load_movies_from_file(Path("test.txt"))
assert len(movies) == 1 assert len(movies) == 1
@ -192,7 +195,7 @@ class TestLoadMoviesFromStdin:
class TestFilterMovies: class TestFilterMovies:
"""Tests for _filter_movies.""" """Tests for _filter_movies."""
def _make_args(self, **kwargs: Any) -> argparse.Namespace: def _make_args(self, **kwargs: str | bool | None) -> argparse.Namespace:
defaults = { defaults = {
"select": None, "select": None,
"exclude": None, "exclude": None,
@ -204,7 +207,7 @@ class TestFilterMovies:
def test_no_filters(self) -> None: def test_no_filters(self) -> None:
movies = [Movie("A", [600], 120)] movies = [Movie("A", [600], 120)]
result, excluded = _filter_movies(movies, self._make_args()) result, _ = _filter_movies(movies, self._make_args())
# Default horror exclusion but no genre matches # Default horror exclusion but no genre matches
assert len(result) == 1 assert len(result) == 1
@ -259,7 +262,7 @@ class TestFilterMovies:
Movie("Action Movie", [600], 120, ["Action"]), Movie("Action Movie", [600], 120, ["Action"]),
Movie("Drama Movie", [600], 120, ["Drama"]), Movie("Drama Movie", [600], 120, ["Drama"]),
] ]
result, excluded = _filter_movies( result, _ = _filter_movies(
movies, movies,
self._make_args(all_genres=True, exclude_genre="action"), self._make_args(all_genres=True, exclude_genre="action"),
) )
@ -268,7 +271,7 @@ class TestFilterMovies:
def test_no_genre_filtered(self) -> None: def test_no_genre_filtered(self) -> None:
movies = [Movie("Movie", [600], 120, ["Comedy"])] movies = [Movie("Movie", [600], 120, ["Comedy"])]
result, excluded = _filter_movies(movies, self._make_args()) result, _ = _filter_movies(movies, self._make_args())
assert len(result) == 1 assert len(result) == 1
@ -301,7 +304,7 @@ class TestApplyMustWatchFilter:
class TestOutputSchedules: class TestOutputSchedules:
"""Tests for _output_schedules.""" """Tests for _output_schedules."""
def _make_args(self, **kwargs: Any) -> argparse.Namespace: def _make_args(self, **kwargs: str | int | None) -> argparse.Namespace:
defaults = { defaults = {
"buffer": 0, "buffer": 0,
"max_schedules": 5, "max_schedules": 5,

View File

@ -297,7 +297,9 @@ class KeyboardCoopGame:
pygame.draw.rect(self.screen, TEXT_COLOR, rect, 2) pygame.draw.rect(self.screen, TEXT_COLOR, rect, 2)
# Draw letter # Draw letter
text = self.fonts.small.render(letter.upper(), True, TEXT_COLOR) text = self.fonts.small.render(
letter.upper(), antialias=True, color=TEXT_COLOR
)
text_rect = text.get_rect(center=rect.center) text_rect = text.get_rect(center=rect.center)
self.screen.blit(text, text_rect) self.screen.blit(text, text_rect)
@ -305,14 +307,14 @@ class KeyboardCoopGame:
self, text: str, pos: tuple[int, int], font: pygame.font.Font self, text: str, pos: tuple[int, int], font: pygame.font.Font
) -> None: ) -> None:
"""Draw a single line of text at the given position.""" """Draw a single line of text at the given position."""
rendered = font.render(text, True, TEXT_COLOR) rendered = font.render(text, antialias=True, color=TEXT_COLOR)
self.screen.blit(rendered, pos) self.screen.blit(rendered, pos)
def _draw_button(self, rect: pygame.Rect, label: str) -> None: def _draw_button(self, rect: pygame.Rect, label: str) -> None:
"""Draw a button with the given label.""" """Draw a button with the given label."""
pygame.draw.rect(self.screen, KEY_COLOR, rect) pygame.draw.rect(self.screen, KEY_COLOR, rect)
pygame.draw.rect(self.screen, TEXT_COLOR, rect, 2) pygame.draw.rect(self.screen, TEXT_COLOR, rect, 2)
text = self.fonts.small.render(label, True, TEXT_COLOR) text = self.fonts.small.render(label, antialias=True, color=TEXT_COLOR)
self.screen.blit(text, text.get_rect(center=rect.center)) self.screen.blit(text, text.get_rect(center=rect.center))
def _draw_ui(self) -> tuple[pygame.Rect, pygame.Rect]: def _draw_ui(self) -> tuple[pygame.Rect, pygame.Rect]:
@ -329,7 +331,9 @@ class KeyboardCoopGame:
# Current player with color # Current player with color
player_color = PLAYER_COLORS[self.state.current_player] player_color = PLAYER_COLORS[self.state.current_player]
player_text = self.fonts.normal.render( player_text = self.fonts.normal.render(
f"Current Player: {self.state.current_player + 1}", True, player_color f"Current Player: {self.state.current_player + 1}",
antialias=True,
color=player_color,
) )
self.screen.blit(player_text, (30, 100)) self.screen.blit(player_text, (30, 100))

View File

@ -194,7 +194,9 @@ def main() -> None:
def _build(tmpdir: str) -> None: def _build(tmpdir: str) -> None:
# ── Lazy imports of moved part builders ─────────────────────── # ── Lazy imports to avoid circular dependency ────────────────
# These submodules import constants from this module, so they
# cannot be imported at the top level.
from moviepy.audio.fx import MultiplyVolume from moviepy.audio.fx import MultiplyVolume
from python_pkg.moviepy_showcase._moviepy_audio_output import ( from python_pkg.moviepy_showcase._moviepy_audio_output import (
@ -208,7 +210,9 @@ def _build(tmpdir: str) -> None:
part1_clip_types, part1_clip_types,
part2_clip_methods, part2_clip_methods,
) )
from python_pkg.moviepy_showcase._moviepy_video_effects import part3_video_effects from python_pkg.moviepy_showcase._moviepy_video_effects import (
part3_video_effects,
)
# ── Render each part to its own temp file ───────────────────── # ── Render each part to its own temp file ─────────────────────
# Title card # Title card

View File

@ -16,7 +16,7 @@ import pytest
_H, _W = 1080, 1920 _H, _W = 1080, 1920
def create_mock_clip(**overrides: Any) -> MagicMock: def create_mock_clip(**overrides: float | tuple[int, int]) -> MagicMock:
"""Return a MagicMock that behaves enough like a moviepy clip.""" """Return a MagicMock that behaves enough like a moviepy clip."""
clip = MagicMock() clip = MagicMock()
clip.duration = overrides.get("duration", 2.0) clip.duration = overrides.get("duration", 2.0)
@ -71,12 +71,12 @@ _clip_classes = [
"CompositeAudioClip", "CompositeAudioClip",
] ]
for _cls in _clip_classes: for _cls in _clip_classes:
getattr(mock_moviepy, _cls).side_effect = lambda *a, **kw: create_mock_clip() getattr(mock_moviepy, _cls).side_effect = lambda *_a, **_kw: create_mock_clip()
mock_moviepy.concatenate_videoclips.side_effect = lambda *a, **kw: create_mock_clip() mock_moviepy.concatenate_videoclips.side_effect = lambda *_a, **_kw: create_mock_clip()
mock_moviepy.concatenate_audioclips.side_effect = lambda *a, **kw: create_mock_clip() mock_moviepy.concatenate_audioclips.side_effect = lambda *_a, **_kw: create_mock_clip()
mock_moviepy.video.compositing.CompositeVideoClip.clips_array.side_effect = ( mock_moviepy.video.compositing.CompositeVideoClip.clips_array.side_effect = (
lambda *a, **kw: create_mock_clip() lambda *_a, **_kw: create_mock_clip()
) )
# Drawing tools must return real numpy arrays (used in numpy ops) # Drawing tools must return real numpy arrays (used in numpy ops)

View File

@ -25,7 +25,7 @@ def test_make_sine_maker_scalar() -> None:
"""maker() with scalar t → t_arr.ndim == 0 → returns 1-D.""" """maker() with scalar t → t_arr.ndim == 0 → returns 1-D."""
import moviepy as mp import moviepy as mp
mp.AudioClip.side_effect = lambda *a, **kw: MagicMock() mp.AudioClip.side_effect = lambda *_a, **_kw: MagicMock()
_make_sine(440.0, 1.0) _make_sine(440.0, 1.0)
maker = mp.AudioClip.call_args[0][0] maker = mp.AudioClip.call_args[0][0]
@ -39,7 +39,7 @@ def test_make_sine_maker_array() -> None:
"""maker() with array t → t_arr.ndim > 0 → returns 2-D.""" """maker() with array t → t_arr.ndim > 0 → returns 2-D."""
import moviepy as mp import moviepy as mp
mp.AudioClip.side_effect = lambda *a, **kw: MagicMock() mp.AudioClip.side_effect = lambda *_a, **_kw: MagicMock()
_make_sine(440.0, 1.0) _make_sine(440.0, 1.0)
maker = mp.AudioClip.call_args[0][0] maker = mp.AudioClip.call_args[0][0]

View File

@ -25,7 +25,7 @@ def test_part1_data_to_frame() -> None:
"""Extract and test the inner data_to_frame function.""" """Extract and test the inner data_to_frame function."""
import moviepy as mp import moviepy as mp
mp.DataVideoClip.side_effect = lambda *a, **kw: create_mock_clip() mp.DataVideoClip.side_effect = lambda *_a, **_kw: create_mock_clip()
result = part1_clip_types() result = part1_clip_types()
assert len(result) > 0 assert len(result) > 0

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import contextlib import contextlib
from pathlib import Path from pathlib import Path
import tempfile
from typing import Any from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -102,17 +103,18 @@ def test_resize_to_canvas() -> None:
def test_render_part() -> None: def test_render_part() -> None:
s1 = create_mock_clip() s1 = create_mock_clip()
s2 = create_mock_clip() s2 = create_mock_clip()
_render_part([s1, s2], "/tmp/test_part.mp4", "test") _render_part([s1, s2], tempfile.gettempdir() + "/test_part.mp4", "test")
s1.close.assert_called_once() s1.close.assert_called_once()
s2.close.assert_called_once() s2.close.assert_called_once()
# ── main ───────────────────────────────────────────────────────── # ── main ─────────────────────────────────────────────────────────
def test_main_success() -> None: def test_main_success() -> None:
mock_dir = tempfile.gettempdir() + "/mock_dir"
with ( with (
patch( patch(
"python_pkg.moviepy_showcase.moviepy_showcase.tempfile.mkdtemp", "python_pkg.moviepy_showcase.moviepy_showcase.tempfile.mkdtemp",
return_value="/tmp/mock_dir", return_value=mock_dir,
), ),
patch( patch(
"python_pkg.moviepy_showcase.moviepy_showcase._build", "python_pkg.moviepy_showcase.moviepy_showcase._build",
@ -122,15 +124,16 @@ def test_main_success() -> None:
) as mock_rmtree, ) as mock_rmtree,
): ):
main() main()
mock_build.assert_called_once_with("/tmp/mock_dir") mock_build.assert_called_once_with(mock_dir)
mock_rmtree.assert_called_once_with("/tmp/mock_dir", ignore_errors=True) mock_rmtree.assert_called_once_with(mock_dir, ignore_errors=True)
def test_main_build_raises() -> None: def test_main_build_raises() -> None:
mock_dir = tempfile.gettempdir() + "/mock_dir"
with ( with (
patch( patch(
"python_pkg.moviepy_showcase.moviepy_showcase.tempfile.mkdtemp", "python_pkg.moviepy_showcase.moviepy_showcase.tempfile.mkdtemp",
return_value="/tmp/mock_dir", return_value=mock_dir,
), ),
patch( patch(
"python_pkg.moviepy_showcase.moviepy_showcase._build", "python_pkg.moviepy_showcase.moviepy_showcase._build",
@ -142,7 +145,7 @@ def test_main_build_raises() -> None:
): ):
with contextlib.suppress(RuntimeError): with contextlib.suppress(RuntimeError):
main() main()
mock_rmtree.assert_called_once_with("/tmp/mock_dir", ignore_errors=True) mock_rmtree.assert_called_once_with(mock_dir, ignore_errors=True)
# ── _build ─────────────────────────────────────────────────────── # ── _build ───────────────────────────────────────────────────────
@ -155,4 +158,4 @@ def test_build() -> None:
), ),
patch.object(Path, "stat", return_value=mock_stat), patch.object(Path, "stat", return_value=mock_stat),
): ):
_build("/tmp/test_build") _build(tempfile.gettempdir() + "/test_build")

View File

@ -14,6 +14,8 @@ Usage:
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import importlib.util
import logging
from pathlib import Path from pathlib import Path
import sys import sys
import warnings import warnings
@ -49,6 +51,8 @@ from python_pkg.music_gen._music_speech import (
warnings.filterwarnings("ignore", category=FutureWarning) warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=UserWarning)
logger = logging.getLogger(__name__)
# Re-export all public symbols for backwards compatibility # Re-export all public symbols for backwards compatibility
__all__ = [ __all__ = [
"BARK_MAX_CHARS", "BARK_MAX_CHARS",
@ -85,8 +89,6 @@ def check_dependencies(*, include_bark: bool = False) -> bool:
Args: Args:
include_bark: Whether to check for Bark dependencies as well. include_bark: Whether to check for Bark dependencies as well.
""" """
import importlib.util
missing = [] missing = []
if importlib.util.find_spec("torch") is None: if importlib.util.find_spec("torch") is None:
@ -102,87 +104,128 @@ def check_dependencies(*, include_bark: bool = False) -> bool:
missing.append("git+https://github.com/suno-ai/bark.git") missing.append("git+https://github.com/suno-ai/bark.git")
if missing: if missing:
print("Missing dependencies. Install with:") logger.error("Missing dependencies. Install with:")
print(f" pip install {' '.join(missing)}") logger.error(" pip install %s", " ".join(missing))
print("\nFor CUDA support:") logger.error("For CUDA support:")
print(" pip install torch --index-url https://download.pytorch.org/whl/cu121") logger.error(
print(" pip install transformers scipy") " pip install torch --index-url https://download.pytorch.org/whl/cu121",
)
logger.error(" pip install transformers scipy")
if include_bark: if include_bark:
print("\nFor Bark vocals:") logger.error("For Bark vocals:")
print(" pip install git+https://github.com/suno-ai/bark.git") logger.error(
" pip install git+https://github.com/suno-ai/bark.git",
)
return False return False
return True return True
EXAMPLE_PROMPTS = [
"upbeat electronic dance music with heavy bass",
"calm acoustic guitar melody with soft percussion",
"epic orchestral soundtrack with dramatic strings",
"lo-fi hip hop beats for studying",
"80s synthwave with retro vibes",
"jazz piano trio with upright bass",
"ambient electronic music for relaxation",
"rock guitar riff with drums",
"classical piano sonata in minor key",
"tropical house with steel drums",
]
def _show_help() -> None:
"""Display example prompts."""
logger.info("Example prompts:")
for i, ex in enumerate(EXAMPLE_PROMPTS, 1):
logger.info(" %d. %s", i, ex)
def _handle_duration(raw: str) -> int | None:
"""Parse and return a new duration, or None on failure."""
try:
value = int(raw.strip())
except ValueError:
logger.warning(
"Invalid duration. Use ':d <number>' e.g., ':d 15'",
)
return None
else:
clamped = max(1, min(30, value))
logger.info("Duration set to %ds", clamped)
return clamped
def _resolve_prompt(prompt: str) -> str | None:
"""Resolve a numeric prompt to an example, or return as-is.
Returns None if the number is out of range.
"""
if prompt.isdigit():
idx = int(prompt) - 1
if 0 <= idx < len(EXAMPLE_PROMPTS):
resolved = EXAMPLE_PROMPTS[idx]
logger.info("Using: %s", resolved)
return resolved
logger.warning(
"Invalid number. Enter 1-%d",
len(EXAMPLE_PROMPTS),
)
return None
return prompt
def interactive_mode(model: object, processor: object) -> None: def interactive_mode(model: object, processor: object) -> None:
"""Run interactive prompt mode.""" """Run interactive prompt mode."""
print("\n" + "=" * 60) banner = "=" * 60
print("INTERACTIVE MODE") logger.info("\n%s", banner)
print("=" * 60) logger.info("INTERACTIVE MODE")
print("Enter prompts to generate music. Commands:") logger.info("%s", banner)
print(" :q or :quit - Exit") logger.info("Enter prompts to generate music. Commands:")
print(" :d <seconds> - Set duration (e.g., ':d 15')") logger.info(" :q or :quit - Exit")
print(" :h or :help - Show example prompts") logger.info(" :d <seconds> - Set duration (e.g., ':d 15')")
print("=" * 60) logger.info(" :h or :help - Show example prompts")
logger.info("%s", banner)
duration = 10 duration = 10
example_prompts = [
"upbeat electronic dance music with heavy bass",
"calm acoustic guitar melody with soft percussion",
"epic orchestral soundtrack with dramatic strings",
"lo-fi hip hop beats for studying",
"80s synthwave with retro vibes",
"jazz piano trio with upright bass",
"ambient electronic music for relaxation",
"rock guitar riff with drums",
"classical piano sonata in minor key",
"tropical house with steel drums",
]
while True: while True:
try: try:
prompt = input(f"\n[{duration}s] Enter prompt: ").strip() prompt = input(f"\n[{duration}s] Enter prompt: ").strip()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
print("\nExiting...") logger.info("Exiting...")
break break
if not prompt: if not prompt:
continue continue
if prompt.lower() in (":q", ":quit", "quit", "exit"): if prompt.lower() in (":q", ":quit", "quit", "exit"):
print("Exiting...") logger.info("Exiting...")
break break
if prompt.lower() in (":h", ":help", "help"): if prompt.lower() in (":h", ":help", "help"):
print("\nExample prompts:") _show_help()
for i, ex in enumerate(example_prompts, 1):
print(f" {i}. {ex}")
continue continue
if prompt.startswith(":d "): if prompt.startswith(":d "):
try: new_dur = _handle_duration(prompt[3:])
duration = int(prompt[3:].strip()) if new_dur is not None:
duration = max(1, min(30, duration)) # Clamp to 1-30 duration = new_dur
print(f"Duration set to {duration}s")
except ValueError:
print("Invalid duration. Use ':d <number>' e.g., ':d 15'")
continue continue
# Check if user entered a number to use example prompt resolved = _resolve_prompt(prompt)
if prompt.isdigit(): if resolved is None:
idx = int(prompt) - 1 continue
if 0 <= idx < len(example_prompts):
prompt = example_prompts[idx]
print(f"Using: {prompt}")
else:
print(f"Invalid number. Enter 1-{len(example_prompts)}")
continue
try: try:
generate_music(prompt, model, processor, duration_seconds=duration) generate_music(
except (RuntimeError, ValueError, OSError) as e: resolved,
print(f"Error generating music: {e}") model,
processor,
duration_seconds=duration,
)
except (RuntimeError, ValueError, OSError):
logger.exception("Error generating music")
def main() -> None: def main() -> None:
@ -275,7 +318,9 @@ Bark tokens: [laughter] [laughs] [sighs] [music] [gasps] ♪ (singing)
if not args.prompt and not args.interactive: if not args.prompt and not args.interactive:
parser.print_help() parser.print_help()
print("\nError: Either provide a prompt or use --interactive mode") logger.error(
"Either provide a prompt or use --interactive mode",
)
sys.exit(1) sys.exit(1)
# Check dependencies # Check dependencies

View File

@ -57,9 +57,9 @@ class TestGetDevice:
patch.dict("sys.modules", {"torch": mock_torch}), patch.dict("sys.modules", {"torch": mock_torch}),
patch("shutil.which", return_value="/usr/bin/nvidia-smi"), patch("shutil.which", return_value="/usr/bin/nvidia-smi"),
patch("subprocess.run", return_value=mock_result), patch("subprocess.run", return_value=mock_result),
pytest.raises(RuntimeError, match="NVIDIA GPU detected"),
): ):
with pytest.raises(RuntimeError, match="NVIDIA GPU detected"): get_device()
get_device()
def test_nvidia_smi_not_found(self) -> None: def test_nvidia_smi_not_found(self) -> None:
mock_torch = MagicMock() mock_torch = MagicMock()

View File

@ -2,7 +2,8 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any import logging
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.music_gen.music_generator import ( from python_pkg.music_gen.music_generator import (
@ -21,56 +22,64 @@ class TestCheckDependencies:
with patch("importlib.util.find_spec", return_value=MagicMock()): with patch("importlib.util.find_spec", return_value=MagicMock()):
assert check_dependencies() is True assert check_dependencies() is True
def test_torch_missing(self, capsys: pytest.CaptureFixture[str]) -> None: def test_torch_missing(self, caplog: pytest.LogCaptureFixture) -> None:
def mock_find_spec(name: str) -> Any: def mock_find_spec(name: str) -> MagicMock | None:
if name == "torch": if name == "torch":
return None return None
return MagicMock() return MagicMock()
with patch("importlib.util.find_spec", side_effect=mock_find_spec): with (
caplog.at_level(logging.DEBUG),
patch("importlib.util.find_spec", side_effect=mock_find_spec),
):
assert check_dependencies() is False assert check_dependencies() is False
captured = capsys.readouterr() assert "torch" in caplog.text
assert "torch" in captured.out
def test_transformers_missing(self, capsys: pytest.CaptureFixture[str]) -> None: def test_transformers_missing(self, caplog: pytest.LogCaptureFixture) -> None:
def mock_find_spec(name: str) -> Any: def mock_find_spec(name: str) -> MagicMock | None:
if name == "transformers": if name == "transformers":
return None return None
return MagicMock() return MagicMock()
with patch("importlib.util.find_spec", side_effect=mock_find_spec): with (
caplog.at_level(logging.DEBUG),
patch("importlib.util.find_spec", side_effect=mock_find_spec),
):
assert check_dependencies() is False assert check_dependencies() is False
captured = capsys.readouterr() assert "transformers" in caplog.text
assert "transformers" in captured.out
def test_scipy_missing(self, capsys: pytest.CaptureFixture[str]) -> None: def test_scipy_missing(self, caplog: pytest.LogCaptureFixture) -> None:
def mock_find_spec(name: str) -> Any: def mock_find_spec(name: str) -> MagicMock | None:
if name == "scipy": if name == "scipy":
return None return None
return MagicMock() return MagicMock()
with patch("importlib.util.find_spec", side_effect=mock_find_spec): with (
caplog.at_level(logging.DEBUG),
patch("importlib.util.find_spec", side_effect=mock_find_spec),
):
assert check_dependencies() is False assert check_dependencies() is False
captured = capsys.readouterr() assert "scipy" in caplog.text
assert "scipy" in captured.out
def test_bark_missing_with_include_bark( def test_bark_missing_with_include_bark(
self, self,
capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
def mock_find_spec(name: str) -> Any: def mock_find_spec(name: str) -> MagicMock | None:
if name == "bark": if name == "bark":
return None return None
return MagicMock() return MagicMock()
with patch("importlib.util.find_spec", side_effect=mock_find_spec): with (
caplog.at_level(logging.DEBUG),
patch("importlib.util.find_spec", side_effect=mock_find_spec),
):
assert check_dependencies(include_bark=True) is False assert check_dependencies(include_bark=True) is False
captured = capsys.readouterr() assert "bark" in caplog.text.lower()
assert "bark" in captured.out.lower()
def test_bark_not_checked_without_flag(self) -> None: def test_bark_not_checked_without_flag(self) -> None:
with patch("importlib.util.find_spec", return_value=MagicMock()): with patch("importlib.util.find_spec", return_value=MagicMock()):
@ -84,64 +93,78 @@ class TestCheckDependencies:
class TestInteractiveMode: class TestInteractiveMode:
"""Tests for interactive_mode().""" """Tests for interactive_mode()."""
def test_quit_command(self, capsys: pytest.CaptureFixture[str]) -> None: def test_quit_command(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", return_value=":q"): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", return_value=":q"),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Exiting" in caplog.text
assert "Exiting" in captured.out
def test_quit_word(self, capsys: pytest.CaptureFixture[str]) -> None: def test_quit_word(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", return_value="quit"): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", return_value="quit"),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Exiting" in caplog.text
assert "Exiting" in captured.out
def test_exit_word(self, capsys: pytest.CaptureFixture[str]) -> None: def test_exit_word(self) -> None:
with patch("builtins.input", return_value="exit"): with patch("builtins.input", return_value="exit"):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
def test_help_command(self, capsys: pytest.CaptureFixture[str]) -> None: def test_help_command(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", side_effect=[":h", ":q"]): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=[":h", ":q"]),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Example prompts" in caplog.text
assert "Example prompts" in captured.out
def test_help_word(self, capsys: pytest.CaptureFixture[str]) -> None: def test_help_word(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", side_effect=["help", ":q"]): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=["help", ":q"]),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Example prompts" in caplog.text
assert "Example prompts" in captured.out
def test_set_duration(self, capsys: pytest.CaptureFixture[str]) -> None: def test_set_duration(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", side_effect=[":d 15", ":q"]): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=[":d 15", ":q"]),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Duration set to 15s" in caplog.text
assert "Duration set to 15s" in captured.out
def test_set_duration_clamped(self, capsys: pytest.CaptureFixture[str]) -> None: def test_set_duration_clamped(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", side_effect=[":d 100", ":q"]): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=[":d 100", ":q"]),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Duration set to 30s" in caplog.text
assert "Duration set to 30s" in captured.out
def test_set_duration_invalid(self, capsys: pytest.CaptureFixture[str]) -> None: def test_set_duration_invalid(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", side_effect=[":d abc", ":q"]): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=[":d abc", ":q"]),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Invalid duration" in caplog.text
assert "Invalid duration" in captured.out
def test_empty_prompt(self) -> None: def test_empty_prompt(self) -> None:
with patch("builtins.input", side_effect=["", ":q"]): with patch("builtins.input", side_effect=["", ":q"]):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
def test_number_prompt_valid(self, capsys: pytest.CaptureFixture[str]) -> None: def test_number_prompt_valid(self) -> None:
with ( with (
patch("builtins.input", side_effect=["1", ":q"]), patch("builtins.input", side_effect=["1", ":q"]),
patch( patch(
@ -152,12 +175,14 @@ class TestInteractiveMode:
mock_gen.assert_called_once() mock_gen.assert_called_once()
def test_number_prompt_invalid(self, capsys: pytest.CaptureFixture[str]) -> None: def test_number_prompt_invalid(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", side_effect=["99", ":q"]): with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=["99", ":q"]),
):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Invalid number" in caplog.text
assert "Invalid number" in captured.out
def test_normal_prompt(self) -> None: def test_normal_prompt(self) -> None:
with ( with (
@ -170,8 +195,9 @@ class TestInteractiveMode:
mock_gen.assert_called_once() mock_gen.assert_called_once()
def test_generation_error(self, capsys: pytest.CaptureFixture[str]) -> None: def test_generation_error(self, caplog: pytest.LogCaptureFixture) -> None:
with ( with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=["jazz music", ":q"]), patch("builtins.input", side_effect=["jazz music", ":q"]),
patch( patch(
"python_pkg.music_gen.music_generator.generate_music", "python_pkg.music_gen.music_generator.generate_music",
@ -180,46 +206,56 @@ class TestInteractiveMode:
): ):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Error generating music" in caplog.text
assert "Error generating music" in captured.out
def test_eof_error(self, capsys: pytest.CaptureFixture[str]) -> None: def test_eof_error(self, caplog: pytest.LogCaptureFixture) -> None:
with patch("builtins.input", side_effect=EOFError):
interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr()
assert "Exiting" in captured.out
def test_keyboard_interrupt(self, capsys: pytest.CaptureFixture[str]) -> None:
with patch("builtins.input", side_effect=KeyboardInterrupt):
interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr()
assert "Exiting" in captured.out
def test_quit_long(self, capsys: pytest.CaptureFixture[str]) -> None:
with patch("builtins.input", return_value=":quit"):
interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr()
assert "Exiting" in captured.out
def test_help_long(self, capsys: pytest.CaptureFixture[str]) -> None:
with patch("builtins.input", side_effect=[":help", ":q"]):
interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr()
assert "Example prompts" in captured.out
def test_duration_clamp_minimum(self, capsys: pytest.CaptureFixture[str]) -> None:
with patch("builtins.input", side_effect=[":d 0", ":q"]):
interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr()
assert "Duration set to 1s" in captured.out
def test_generation_value_error(self, capsys: pytest.CaptureFixture[str]) -> None:
with ( with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=EOFError),
):
interactive_mode(MagicMock(), MagicMock())
assert "Exiting" in caplog.text
def test_keyboard_interrupt(self, caplog: pytest.LogCaptureFixture) -> None:
with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=KeyboardInterrupt),
):
interactive_mode(MagicMock(), MagicMock())
assert "Exiting" in caplog.text
def test_quit_long(self, caplog: pytest.LogCaptureFixture) -> None:
with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", return_value=":quit"),
):
interactive_mode(MagicMock(), MagicMock())
assert "Exiting" in caplog.text
def test_help_long(self, caplog: pytest.LogCaptureFixture) -> None:
with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=[":help", ":q"]),
):
interactive_mode(MagicMock(), MagicMock())
assert "Example prompts" in caplog.text
def test_duration_clamp_minimum(self, caplog: pytest.LogCaptureFixture) -> None:
with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=[":d 0", ":q"]),
):
interactive_mode(MagicMock(), MagicMock())
assert "Duration set to 1s" in caplog.text
def test_generation_value_error(self, caplog: pytest.LogCaptureFixture) -> None:
with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=["jazz", ":q"]), patch("builtins.input", side_effect=["jazz", ":q"]),
patch( patch(
"python_pkg.music_gen.music_generator.generate_music", "python_pkg.music_gen.music_generator.generate_music",
@ -228,11 +264,11 @@ class TestInteractiveMode:
): ):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Error generating music" in caplog.text
assert "Error generating music" in captured.out
def test_generation_os_error(self, capsys: pytest.CaptureFixture[str]) -> None: def test_generation_os_error(self, caplog: pytest.LogCaptureFixture) -> None:
with ( with (
caplog.at_level(logging.DEBUG),
patch("builtins.input", side_effect=["jazz", ":q"]), patch("builtins.input", side_effect=["jazz", ":q"]),
patch( patch(
"python_pkg.music_gen.music_generator.generate_music", "python_pkg.music_gen.music_generator.generate_music",
@ -241,5 +277,4 @@ class TestInteractiveMode:
): ):
interactive_mode(MagicMock(), MagicMock()) interactive_mode(MagicMock(), MagicMock())
captured = capsys.readouterr() assert "Error generating music" in caplog.text
assert "Error generating music" in captured.out

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import tempfile
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
@ -227,7 +228,7 @@ class TestMain:
with ( with (
patch( patch(
"sys.argv", "sys.argv",
["music_generator", "--output", "/tmp/out", "test"], ["music_generator", "--output", tempfile.gettempdir() + "/out", "test"],
), ),
patch( patch(
"python_pkg.music_gen.music_generator.check_dependencies", "python_pkg.music_gen.music_generator.check_dependencies",

View File

@ -426,7 +426,7 @@ class TestGenerateVocalsForSong:
return_value=["Hello."], return_value=["Hello."],
), ),
): ):
vocals, sr = _generate_vocals_for_song("Hello.", "v2/en_speaker_6") _, sr = _generate_vocals_for_song("Hello.", "v2/en_speaker_6")
assert sr == 24000 assert sr == 24000
# The original_load should have been called via patched_load # The original_load should have been called via patched_load
@ -487,6 +487,6 @@ class TestGenerateInstrumentalForSong:
return_value=audio, return_value=audio,
), ),
): ):
instrumental, sr = _generate_instrumental_for_song("test", 60) _, sr = _generate_instrumental_for_song("test", 60)
assert sr == 100 assert sr == 100

View File

@ -31,7 +31,7 @@ class PokerGuiMixin:
self.root.title("🃏 Texas Hold'em Modifier") self.root.title("🃏 Texas Hold'em Modifier")
self.root.geometry("650x750") self.root.geometry("650x750")
self.root.configure(bg="#0f4c3a") self.root.configure(bg="#0f4c3a")
self.root.resizable(True, True) self.root.resizable(width=True, height=True)
style = ttk.Style() style = ttk.Style()
style.theme_use("clam") style.theme_use("clam")
@ -188,7 +188,7 @@ class PokerGuiMixin:
parent, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150 parent, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150
) )
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20), padx=10) self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20), padx=10)
self.result_frame.pack_propagate(False) self.result_frame.pack_propagate(flag=False)
self.result_label = tk.Label( self.result_label = tk.Label(
self.result_frame, self.result_frame,

View File

@ -3,9 +3,12 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import Any from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
if TYPE_CHECKING:
from python_pkg.poker_modifier_app._poker_gui import PokerGuiMixin
def _install_tk_mocks() -> dict[str, MagicMock]: def _install_tk_mocks() -> dict[str, MagicMock]:
"""Install mock tkinter modules and return them.""" """Install mock tkinter modules and return them."""
@ -26,19 +29,19 @@ def _install_tk_mocks() -> dict[str, MagicMock]:
# Make constructors return fresh mocks each time # Make constructors return fresh mocks each time
mock_tk.Tk.return_value = MagicMock(name="root") mock_tk.Tk.return_value = MagicMock(name="root")
mock_tk.Frame.side_effect = lambda *a, **kw: MagicMock(name="Frame") mock_tk.Frame.side_effect = lambda *_a, **_kw: MagicMock(name="Frame")
mock_tk.Label.side_effect = lambda *a, **kw: MagicMock(name="Label") mock_tk.Label.side_effect = lambda *_a, **_kw: MagicMock(name="Label")
mock_tk.LabelFrame.side_effect = lambda *a, **kw: MagicMock(name="LabelFrame") mock_tk.LabelFrame.side_effect = lambda *_a, **_kw: MagicMock(name="LabelFrame")
mock_tk.Scale.side_effect = lambda *a, **kw: MagicMock(name="Scale") mock_tk.Scale.side_effect = lambda *_a, **_kw: MagicMock(name="Scale")
mock_tk.IntVar.side_effect = lambda *a, **kw: MagicMock(name="IntVar") mock_tk.IntVar.side_effect = lambda *_a, **_kw: MagicMock(name="IntVar")
mock_tk.BooleanVar.side_effect = lambda *a, **kw: MagicMock(name="BooleanVar") mock_tk.BooleanVar.side_effect = lambda *_a, **_kw: MagicMock(name="BooleanVar")
mock_tk.Checkbutton.side_effect = lambda *a, **kw: MagicMock(name="Checkbutton") mock_tk.Checkbutton.side_effect = lambda *_a, **_kw: MagicMock(name="Checkbutton")
mock_tk.Button.side_effect = lambda *a, **kw: MagicMock(name="Button") mock_tk.Button.side_effect = lambda *_a, **_kw: MagicMock(name="Button")
return {"tk": mock_tk, "ttk": mock_ttk} return {"tk": mock_tk, "ttk": mock_ttk}
def _make_mixin() -> Any: def _make_mixin() -> tuple[PokerGuiMixin, MagicMock, MagicMock]:
"""Create a PokerGuiMixin instance with mocked tkinter.""" """Create a PokerGuiMixin instance with mocked tkinter."""
tk_mocks = _install_tk_mocks() tk_mocks = _install_tk_mocks()
@ -99,7 +102,7 @@ class TestSetupMainWindow:
root.title.assert_called_once_with("🃏 Texas Hold'em Modifier") root.title.assert_called_once_with("🃏 Texas Hold'em Modifier")
root.geometry.assert_called_once_with("650x750") root.geometry.assert_called_once_with("650x750")
root.configure.assert_called_once_with(bg="#0f4c3a") root.configure.assert_called_once_with(bg="#0f4c3a")
root.resizable.assert_called_once_with(True, True) root.resizable.assert_called_once_with(width=True, height=True)
mock_ttk.Style.assert_called_once() mock_ttk.Style.assert_called_once()
mock_ttk.Style.return_value.theme_use.assert_called_once_with("clam") mock_ttk.Style.return_value.theme_use.assert_called_once_with("clam")
@ -239,7 +242,7 @@ class TestCreateResultDisplay:
frame_calls = mock_tk.Frame.call_args_list frame_calls = mock_tk.Frame.call_args_list
assert any(c[1].get("height") == 150 for c in frame_calls) assert any(c[1].get("height") == 150 for c in frame_calls)
assert hasattr(mixin, "result_frame") assert hasattr(mixin, "result_frame")
mixin.result_frame.pack_propagate.assert_called_once_with(False) mixin.result_frame.pack_propagate.assert_called_once_with(flag=False)
# Result label # Result label
label_calls = mock_tk.Label.call_args_list label_calls = mock_tk.Label.call_args_list

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.poker_modifier_app._poker_modifiers import ( from python_pkg.poker_modifier_app._poker_modifiers import (
@ -11,8 +11,11 @@ from python_pkg.poker_modifier_app._poker_modifiers import (
Modifier, Modifier,
) )
if TYPE_CHECKING:
from python_pkg.poker_modifier_app.poker_modifier_app import PokerModifierApp
def _make_app() -> Any:
def _make_app() -> PokerModifierApp:
"""Create a PokerModifierApp with setup_gui mocked out.""" """Create a PokerModifierApp with setup_gui mocked out."""
with patch( with patch(
"python_pkg.poker_modifier_app.poker_modifier_app.PokerGuiMixin.setup_gui" "python_pkg.poker_modifier_app.poker_modifier_app.PokerGuiMixin.setup_gui"
@ -422,7 +425,7 @@ class TestMainBlock:
"""Test the if __name__ == '__main__' block.""" """Test the if __name__ == '__main__' block."""
@patch("python_pkg.poker_modifier_app.poker_modifier_app.PokerGuiMixin.setup_gui") @patch("python_pkg.poker_modifier_app.poker_modifier_app.PokerGuiMixin.setup_gui")
def test_main_block(self, _mock_setup: MagicMock) -> None: def test_main_block(self, mock_setup: MagicMock) -> None:
with patch( with patch(
"python_pkg.poker_modifier_app.poker_modifier_app.PokerModifierApp.run" "python_pkg.poker_modifier_app.poker_modifier_app.PokerModifierApp.run"
): ):

View File

@ -221,14 +221,16 @@ def _transformer_seg_demo() -> list[CompositeVideoClip]:
(100, 480), (100, 480),
), ),
( (
"Z\u0142o\u017cono\u015b\u0107: O(n\u00b2) pami\u0119ci \u2014 n = liczba pikseli/token\u00f3w", "Z\u0142o\u017cono\u015b\u0107: O(n\u00b2) pami\u0119ci \u2014 n = liczba "
"pikseli/token\u00f3w",
16, 16,
"#EF9A9A", "#EF9A9A",
FONT_R, FONT_R,
(100, 535), (100, 535),
), ),
( (
"Dlatego SegFormer u\u017cywa efficient attention (liniowa z\u0142o\u017cono\u015b\u0107)", "Dlatego SegFormer u\u017cywa efficient attention (liniowa "
"z\u0142o\u017cono\u015b\u0107)",
15, 15,
"#78909C", "#78909C",
FONT_R, FONT_R,
@ -269,14 +271,16 @@ def _transformer_seg_demo() -> list[CompositeVideoClip]:
(80, 90), (80, 90),
), ),
( (
"Encoder: obraz \u2192 cechy (zmniejsza rozdzielczo\u015b\u0107, wyci\u0105ga CO)", "Encoder: obraz \u2192 cechy (zmniejsza rozdzielczo\u015b\u0107, "
"wyci\u0105ga CO)",
16, 16,
"#64B5F6", "#64B5F6",
FONT_R, FONT_R,
(100, 140), (100, 140),
), ),
( (
"Decoder: cechy \u2192 mapa (zwi\u0119ksza rozdzielczo\u015b\u0107, odtwarza GDZIE)", "Decoder: cechy \u2192 mapa (zwi\u0119ksza rozdzielczo\u015b\u0107, "
"odtwarza GDZIE)",
16, 16,
"#A5D6A7", "#A5D6A7",
FONT_R, FONT_R,
@ -334,7 +338,8 @@ def _transformer_seg_demo() -> list[CompositeVideoClip]:
(80, 465), (80, 465),
), ),
( (
" CNN lokal. \u2192 dilated (szersze RF) \u2192 transformer (global) \u2192 masked att.", " CNN lokal. \u2192 dilated (szersze RF) \u2192 transformer (global) "
"\u2192 masked att.",
16, 16,
"#B0BEC5", "#B0BEC5",
FONT_R, FONT_R,

View File

@ -2,7 +2,9 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import importlib import importlib
import importlib.util as _ilu
from pathlib import Path from pathlib import Path
import sys import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -12,6 +14,7 @@ import numpy as np
import pytest import pytest
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType from types import ModuleType
# Add the source directory to sys.path so bare imports like # Add the source directory to sys.path so bare imports like
@ -35,7 +38,11 @@ def _make_moviepy_mocks() -> dict[str, ModuleType | MagicMock]:
moviepy_mod = MagicMock() moviepy_mod = MagicMock()
# VideoClip: needs to accept make_frame callable -> return mock with methods # VideoClip: needs to accept make_frame callable -> return mock with methods
def _video_clip_factory(make_frame=None, duration=None, **kw): def _video_clip_factory(
make_frame: Callable[[float], np.ndarray] | None = None,
duration: float | None = None,
**_kw: object,
) -> MagicMock:
clip = MagicMock() clip = MagicMock()
clip.make_frame = make_frame clip.make_frame = make_frame
clip.duration = duration clip.duration = duration
@ -57,14 +64,18 @@ def _make_moviepy_mocks() -> dict[str, ModuleType | MagicMock]:
moviepy_mod.VideoClip = _video_clip_factory moviepy_mod.VideoClip = _video_clip_factory
def _color_clip_factory(size=None, color=None, **kw): def _color_clip_factory(
_size: tuple[int, int] | None = None,
_color: tuple[int, ...] | None = None,
**_kw: object,
) -> MagicMock:
clip = MagicMock() clip = MagicMock()
clip.with_duration.return_value = clip clip.with_duration.return_value = clip
return clip return clip
moviepy_mod.ColorClip = _color_clip_factory moviepy_mod.ColorClip = _color_clip_factory
def _text_clip_factory(**kw): def _text_clip_factory(**_kw: object) -> MagicMock:
clip = MagicMock() clip = MagicMock()
clip.with_duration.return_value = clip clip.with_duration.return_value = clip
clip.with_position.return_value = clip clip.with_position.return_value = clip
@ -72,7 +83,11 @@ def _make_moviepy_mocks() -> dict[str, ModuleType | MagicMock]:
moviepy_mod.TextClip = _text_clip_factory moviepy_mod.TextClip = _text_clip_factory
def _composite_factory(clips=None, size=None, **kw): def _composite_factory(
_clips: list[MagicMock] | None = None,
_size: tuple[int, int] | None = None,
**_kw: object,
) -> MagicMock:
clip = MagicMock() clip = MagicMock()
clip.with_effects.return_value = clip clip.with_effects.return_value = clip
clip.with_duration.return_value = clip clip.with_duration.return_value = clip
@ -81,7 +96,11 @@ def _make_moviepy_mocks() -> dict[str, ModuleType | MagicMock]:
moviepy_mod.CompositeVideoClip = _composite_factory moviepy_mod.CompositeVideoClip = _composite_factory
def _concat_factory(clips=None, method=None, **kw): def _concat_factory(
_clips: list[MagicMock] | None = None,
_method: str | None = None,
**_kw: object,
) -> MagicMock:
clip = MagicMock() clip = MagicMock()
clip.write_videofile = MagicMock() clip.write_videofile = MagicMock()
return clip return clip
@ -117,7 +136,6 @@ for _name, _mock in _MOVIEPY_MOCKS.items():
# modules (``_q24_classical.py``, etc.) find ``BG_COLOR`` etc. # modules (``_q24_classical.py``, etc.) find ``BG_COLOR`` etc.
# 3. Register both under their full package paths for coverage. # 3. Register both under their full package paths for coverage.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
import importlib.util as _ilu
# Load generate_images _q24_common first. # Load generate_images _q24_common first.
_gen_q24_spec = _ilu.spec_from_file_location( _gen_q24_spec = _ilu.spec_from_file_location(
@ -127,9 +145,9 @@ _gen_q24_spec = _ilu.spec_from_file_location(
assert _gen_q24_spec is not None assert _gen_q24_spec is not None
assert _gen_q24_spec.loader is not None assert _gen_q24_spec.loader is not None
_q24_common_gen = _ilu.module_from_spec(_gen_q24_spec) _q24_common_gen = _ilu.module_from_spec(_gen_q24_spec)
_gen_q24_spec.loader.exec_module(_q24_common_gen) # Register BEFORE exec so @dataclass can resolve __module__ in Python 3.14+.
# Cache as bare name so generate_images imports work during _BARE_MODULES.
sys.modules["_q24_common"] = _q24_common_gen sys.modules["_q24_common"] = _q24_common_gen
_gen_q24_spec.loader.exec_module(_q24_common_gen)
# Load top-level _q24_common. # Load top-level _q24_common.
_top_q24_spec = _ilu.spec_from_file_location( _top_q24_spec = _ilu.spec_from_file_location(
@ -139,6 +157,8 @@ _top_q24_spec = _ilu.spec_from_file_location(
assert _top_q24_spec is not None assert _top_q24_spec is not None
assert _top_q24_spec.loader is not None assert _top_q24_spec.loader is not None
_q24_common_top = _ilu.module_from_spec(_top_q24_spec) _q24_common_top = _ilu.module_from_spec(_top_q24_spec)
# Register BEFORE exec so @dataclass can resolve __module__ in Python 3.14+.
sys.modules["_q24_common_top"] = _q24_common_top
_top_q24_spec.loader.exec_module(_q24_common_top) _top_q24_spec.loader.exec_module(_q24_common_top)
@ -205,11 +225,9 @@ _BARE_MODULES = [
"generate_scheduling_diagrams", "generate_scheduling_diagrams",
] ]
for _bare in _BARE_MODULES: for _bare in _BARE_MODULES:
try: with contextlib.suppress(ImportError):
_mod = importlib.import_module(_bare) _mod = importlib.import_module(_bare)
sys.modules.setdefault(f"{_GEN_PKG}.{_bare}", _mod) sys.modules.setdefault(f"{_GEN_PKG}.{_bare}", _mod)
except ImportError:
pass
# Now swap _q24_common to the top-level version so that top-level source # Now swap _q24_common to the top-level version so that top-level source
# modules (``_q24_classical.py`` etc.) find BG_COLOR, W, H, etc. # modules (``_q24_classical.py`` etc.) find BG_COLOR, W, H, etc.
@ -242,6 +260,7 @@ def _no_savefig(monkeypatch: pytest.MonkeyPatch) -> None:
def _compat_auto_set_font_size( def _compat_auto_set_font_size(
self: matplotlib.table.Table, self: matplotlib.table.Table,
*,
value: bool = True, value: bool = True,
**_kw: object, **_kw: object,
) -> None: ) -> None:

View File

@ -117,7 +117,7 @@ def test_get_file_metadata_no_match(tmp_path: Path) -> None:
p = tmp_path / "readme.txt" p = tmp_path / "readme.txt"
p.write_text("No Przedmiot here", encoding="utf-8") p.write_text("No Przedmiot here", encoding="utf-8")
num, subject, content = get_file_metadata(str(p)) num, subject, _ = get_file_metadata(str(p))
assert num == "00" assert num == "00"
assert subject == "Ogólne" assert subject == "Ogólne"
@ -386,7 +386,7 @@ def test_generate_anki_basic(tmp_path: Path) -> None:
out_dir.mkdir() out_dir.mkdir()
with ( with (
patch(f"{_PKG}.Path.__truediv__", side_effect=lambda self, x: tmp_path / x), patch(f"{_PKG}.Path.__truediv__", side_effect=lambda _self, x: tmp_path / x),
patch( patch(
f"{_PKG}.generate_anki.__defaults__", f"{_PKG}.generate_anki.__defaults__",
(False, False, False), (False, False, False),

View File

@ -14,6 +14,27 @@ mpl.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from python_pkg.praca_magisterska_video.generate_images._agent_cognitive import (
draw_bdi_model,
draw_behavior_tree,
)
from python_pkg.praca_magisterska_video.generate_images._agent_reactive import (
draw_3t_architecture,
draw_see_think_act,
)
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
BG,
DPI,
GRAY5,
OUTPUT_DIR,
ArrowCfg,
BoxStyle,
DashedArrowCfg,
draw_arrow,
draw_box,
draw_dashed_arrow,
)
pytestmark = pytest.mark.usefixtures("_no_savefig") pytestmark = pytest.mark.usefixtures("_no_savefig")
_MOD = "python_pkg.praca_magisterska_video.generate_images" _MOD = "python_pkg.praca_magisterska_video.generate_images"
@ -26,79 +47,41 @@ class TestAgentHelpers:
"""Test draw_box, draw_arrow, draw_dashed_arrow and dataclasses.""" """Test draw_box, draw_arrow, draw_dashed_arrow and dataclasses."""
def test_draw_box_rounded(self) -> None: def test_draw_box_rounded(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
BoxStyle,
draw_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_box(ax, (0, 0), (1, 1), "hi", BoxStyle(rounded=True)) draw_box(ax, (0, 0), (1, 1), "hi", BoxStyle(rounded=True))
plt.close(fig) plt.close(fig)
def test_draw_box_not_rounded(self) -> None: def test_draw_box_not_rounded(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
BoxStyle,
draw_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_box(ax, (0, 0), (1, 1), "hi", BoxStyle(rounded=False)) draw_box(ax, (0, 0), (1, 1), "hi", BoxStyle(rounded=False))
plt.close(fig) plt.close(fig)
def test_draw_box_no_style(self) -> None: def test_draw_box_no_style(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
draw_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_box(ax, (0, 0), (1, 1), "hi") draw_box(ax, (0, 0), (1, 1), "hi")
plt.close(fig) plt.close(fig)
def test_draw_arrow_with_label(self) -> None: def test_draw_arrow_with_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
ArrowCfg,
draw_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_arrow(ax, (0, 0), (1, 1), ArrowCfg(label="lbl")) draw_arrow(ax, (0, 0), (1, 1), ArrowCfg(label="lbl"))
plt.close(fig) plt.close(fig)
def test_draw_arrow_no_label(self) -> None: def test_draw_arrow_no_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
draw_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_arrow(ax, (0, 0), (1, 1)) draw_arrow(ax, (0, 0), (1, 1))
plt.close(fig) plt.close(fig)
def test_draw_dashed_arrow_with_label(self) -> None: def test_draw_dashed_arrow_with_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
DashedArrowCfg,
draw_dashed_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_dashed_arrow(ax, (0, 0), (1, 1), DashedArrowCfg(label="lbl")) draw_dashed_arrow(ax, (0, 0), (1, 1), DashedArrowCfg(label="lbl"))
plt.close(fig) plt.close(fig)
def test_draw_dashed_arrow_no_label(self) -> None: def test_draw_dashed_arrow_no_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
draw_dashed_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_dashed_arrow(ax, (0, 0), (1, 1)) draw_dashed_arrow(ax, (0, 0), (1, 1))
plt.close(fig) plt.close(fig)
def test_dataclass_defaults(self) -> None: def test_dataclass_defaults(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
ArrowCfg,
BoxStyle,
DashedArrowCfg,
)
bs = BoxStyle() bs = BoxStyle()
assert bs.rounded is True assert bs.rounded is True
assert bs.fill == "white" assert bs.fill == "white"
@ -108,13 +91,6 @@ class TestAgentHelpers:
assert dc.label == "" assert dc.label == ""
def test_module_constants(self) -> None: def test_module_constants(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_agent_diagrams import (
BG,
DPI,
GRAY5,
OUTPUT_DIR,
)
assert DPI == 300 assert DPI == 300
assert BG == "white" assert BG == "white"
assert isinstance(GRAY5, str) assert isinstance(GRAY5, str)
@ -128,17 +104,9 @@ class TestAgentReactive:
"""Test draw_see_think_act and draw_3t_architecture.""" """Test draw_see_think_act and draw_3t_architecture."""
def test_draw_see_think_act(self) -> None: def test_draw_see_think_act(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._agent_reactive import (
draw_see_think_act,
)
draw_see_think_act() draw_see_think_act()
def test_draw_3t_architecture(self) -> None: def test_draw_3t_architecture(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._agent_reactive import (
draw_3t_architecture,
)
draw_3t_architecture() draw_3t_architecture()
@ -149,15 +117,7 @@ class TestAgentCognitive:
"""Test draw_behavior_tree (covers all node types) and draw_bdi_model.""" """Test draw_behavior_tree (covers all node types) and draw_bdi_model."""
def test_draw_behavior_tree(self) -> None: def test_draw_behavior_tree(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._agent_cognitive import (
draw_behavior_tree,
)
draw_behavior_tree() draw_behavior_tree()
def test_draw_bdi_model(self) -> None: def test_draw_bdi_model(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._agent_cognitive import (
draw_bdi_model,
)
draw_bdi_model() draw_bdi_model()

View File

@ -122,7 +122,7 @@ class TestAnkiApproach1:
patch.object( patch.object(
Path, Path,
"open", "open",
side_effect=lambda *a, **kw: StringIO(fake_md), side_effect=lambda *_a, **_kw: StringIO(fake_md),
), ),
): ):
main() main()
@ -187,7 +187,7 @@ class TestAnkiApproach1:
patch.object( patch.object(
Path, Path,
"open", "open",
side_effect=lambda *a, **kw: StringIO(fake_md), side_effect=lambda *_a, **_kw: StringIO(fake_md),
), ),
): ):
main() main()
@ -324,7 +324,7 @@ class TestAnkiApproach2:
patch.object( patch.object(
Path, Path,
"open", "open",
side_effect=lambda *a, **kw: StringIO(fake_md), side_effect=lambda *_a, **_kw: StringIO(fake_md),
), ),
): ):
main() main()
@ -387,7 +387,7 @@ class TestAnkiApproach2:
patch.object( patch.object(
Path, Path,
"open", "open",
side_effect=lambda *a, **kw: StringIO(fake_md), side_effect=lambda *_a, **_kw: StringIO(fake_md),
), ),
): ):
main() main()

View File

@ -17,6 +17,47 @@ mpl.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from python_pkg.praca_magisterska_video.generate_images import (
generate_automata_diagrams as _auto_diags,
)
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
BG,
DPI,
FS,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
INNER_RATIO,
LIGHT_BLUE,
LIGHT_GREEN,
LIGHT_RED,
LIGHT_YELLOW,
LN,
OUTPUT_DIR,
ArrowStyle,
LoopStyle,
StateStyle,
draw_curved_arrow,
draw_self_loop,
draw_state_circle,
)
from python_pkg.praca_magisterska_video.generate_images._automata_fa import (
draw_fa_recognition,
)
from python_pkg.praca_magisterska_video.generate_images._automata_lba import (
draw_lba_recognition,
)
from python_pkg.praca_magisterska_video.generate_images._automata_pda import (
draw_pda_recognition,
)
from python_pkg.praca_magisterska_video.generate_images._automata_tm import (
draw_tm_recognition,
)
pytestmark = pytest.mark.usefixtures("_no_savefig") pytestmark = pytest.mark.usefixtures("_no_savefig")
@ -27,10 +68,6 @@ class TestAutomataCommon:
"""Test draw_state_circle, draw_curved_arrow, draw_self_loop.""" """Test draw_state_circle, draw_curved_arrow, draw_self_loop."""
def test_state_circle_basic(self) -> None: def test_state_circle_basic(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
draw_state_circle,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -38,11 +75,6 @@ class TestAutomataCommon:
plt.close(fig) plt.close(fig)
def test_state_circle_accepting(self) -> None: def test_state_circle_accepting(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
StateStyle,
draw_state_circle,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -50,11 +82,6 @@ class TestAutomataCommon:
plt.close(fig) plt.close(fig)
def test_state_circle_initial(self) -> None: def test_state_circle_initial(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
StateStyle,
draw_state_circle,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -62,11 +89,6 @@ class TestAutomataCommon:
plt.close(fig) plt.close(fig)
def test_state_circle_both(self) -> None: def test_state_circle_both(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
StateStyle,
draw_state_circle,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -76,20 +98,11 @@ class TestAutomataCommon:
plt.close(fig) plt.close(fig)
def test_curved_arrow(self) -> None: def test_curved_arrow(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
draw_curved_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_curved_arrow(ax, (0, 0), (1, 1), "a") draw_curved_arrow(ax, (0, 0), (1, 1), "a")
plt.close(fig) plt.close(fig)
def test_self_loop_top(self) -> None: def test_self_loop_top(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
LoopStyle,
draw_self_loop,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -97,11 +110,6 @@ class TestAutomataCommon:
plt.close(fig) plt.close(fig)
def test_self_loop_bottom(self) -> None: def test_self_loop_bottom(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
LoopStyle,
draw_self_loop,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -109,10 +117,6 @@ class TestAutomataCommon:
plt.close(fig) plt.close(fig)
def test_self_loop_default(self) -> None: def test_self_loop_default(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
draw_self_loop,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -121,11 +125,6 @@ class TestAutomataCommon:
def test_self_loop_unknown_direction(self) -> None: def test_self_loop_unknown_direction(self) -> None:
"""Cover implicit else when direction is not top/bottom.""" """Cover implicit else when direction is not top/bottom."""
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
LoopStyle,
draw_self_loop,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-2, 2) ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2) ax.set_ylim(-2, 2)
@ -133,12 +132,6 @@ class TestAutomataCommon:
plt.close(fig) plt.close(fig)
def test_dataclass_defaults(self) -> None: def test_dataclass_defaults(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
ArrowStyle,
LoopStyle,
StateStyle,
)
ss = StateStyle() ss = StateStyle()
assert ss.accepting is False assert ss.accepting is False
assert ss.initial is False assert ss.initial is False
@ -148,26 +141,6 @@ class TestAutomataCommon:
assert ls.direction == "top" assert ls.direction == "top"
def test_module_constants(self) -> None: def test_module_constants(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_common import (
BG,
DPI,
FS,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
INNER_RATIO,
LIGHT_BLUE,
LIGHT_GREEN,
LIGHT_RED,
LIGHT_YELLOW,
LN,
OUTPUT_DIR,
)
assert DPI == 300 assert DPI == 300
assert BG == "white" assert BG == "white"
assert isinstance(FS, int | float) assert isinstance(FS, int | float)
@ -194,31 +167,15 @@ class TestAutomataDiagrams:
"""Test all recognition diagram functions.""" """Test all recognition diagram functions."""
def test_fa_recognition(self) -> None: def test_fa_recognition(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_fa import (
draw_fa_recognition,
)
draw_fa_recognition() draw_fa_recognition()
def test_pda_recognition(self) -> None: def test_pda_recognition(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_pda import (
draw_pda_recognition,
)
draw_pda_recognition() draw_pda_recognition()
def test_lba_recognition(self) -> None: def test_lba_recognition(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_lba import (
draw_lba_recognition,
)
draw_lba_recognition() draw_lba_recognition()
def test_tm_recognition(self) -> None: def test_tm_recognition(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._automata_tm import (
draw_tm_recognition,
)
draw_tm_recognition() draw_tm_recognition()
@ -229,15 +186,9 @@ class TestAutomataEntry:
"""Verify generate_automata_diagrams exports are accessible.""" """Verify generate_automata_diagrams exports are accessible."""
def test_all_exports(self) -> None: def test_all_exports(self) -> None:
import python_pkg.praca_magisterska_video.generate_images.generate_automata_diagrams as mod assert hasattr(_auto_diags, "__all__")
for name in _auto_diags.__all__:
assert hasattr(mod, "__all__") assert hasattr(_auto_diags, name)
for name in mod.__all__:
assert hasattr(mod, name)
def test_output_dir(self) -> None: def test_output_dir(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_automata_diagrams import ( assert isinstance(_auto_diags.OUTPUT_DIR, str)
OUTPUT_DIR,
)
assert isinstance(OUTPUT_DIR, str)

View File

@ -13,6 +13,15 @@ mpl.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from python_pkg.praca_magisterska_video.generate_images import (
generate_bf_negative_diagram as _bf_neg,
)
from python_pkg.praca_magisterska_video.generate_images._bf_negative_diagrams import (
_add_annotation_box,
generate_bf_negative_cycle,
generate_bf_negative_weights,
)
pytestmark = pytest.mark.usefixtures("_no_savefig") pytestmark = pytest.mark.usefixtures("_no_savefig")
_MOD = "python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram" _MOD = "python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram"
@ -25,157 +34,100 @@ class TestBFHelpers:
"""Test draw_node, _choose_edge_style, draw_edge, draw_neg_graph.""" """Test draw_node, _choose_edge_style, draw_edge, draw_neg_graph."""
def test_draw_node_default(self) -> None: def test_draw_node_default(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_node,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_node(ax, "S", (1, 1)) _bf_neg.draw_node(ax, "S", (1, 1))
plt.close(fig) plt.close(fig)
def test_draw_node_current(self) -> None: def test_draw_node_current(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_node,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_node(ax, "A", (1, 1), current=True, dist_label="2") _bf_neg.draw_node(ax, "A", (1, 1), current=True, dist_label="2")
plt.close(fig) plt.close(fig)
def test_draw_node_visited(self) -> None: def test_draw_node_visited(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_node,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_node(ax, "B", (1, 1), visited=True, dist_label="5") _bf_neg.draw_node(ax, "B", (1, 1), visited=True, dist_label="5")
plt.close(fig) plt.close(fig)
def test_draw_node_error(self) -> None: def test_draw_node_error(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_node,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_node(ax, "C", (1, 1), error=True, dist_label="?") _bf_neg.draw_node(ax, "C", (1, 1), error=True, dist_label="?")
plt.close(fig) plt.close(fig)
def test_draw_node_no_dist_label(self) -> None: def test_draw_node_no_dist_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_node,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_node(ax, "X", (1, 1), visited=True) _bf_neg.draw_node(ax, "X", (1, 1), visited=True)
plt.close(fig) plt.close(fig)
def test_choose_edge_style_cycle(self) -> None: def test_choose_edge_style_cycle(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import ( _, lw, ls = _bf_neg._choose_edge_style(
_choose_edge_style,
)
color, lw, ls = _choose_edge_style(
negative=False, relaxed=False, highlighted=False, cycle_edge=True negative=False, relaxed=False, highlighted=False, cycle_edge=True
) )
assert ls == "--" assert ls == "--"
assert lw == 2.5 assert lw == 2.5
def test_choose_edge_style_negative(self) -> None: def test_choose_edge_style_negative(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import ( _, lw, ls = _bf_neg._choose_edge_style(
_choose_edge_style,
)
color, lw, ls = _choose_edge_style(
negative=True, relaxed=False, highlighted=False, cycle_edge=False negative=True, relaxed=False, highlighted=False, cycle_edge=False
) )
assert lw == 2.5 assert lw == 2.5
assert ls == "-" assert ls == "-"
def test_choose_edge_style_relaxed(self) -> None: def test_choose_edge_style_relaxed(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import ( _, lw, _ = _bf_neg._choose_edge_style(
_choose_edge_style,
)
color, lw, ls = _choose_edge_style(
negative=False, relaxed=True, highlighted=False, cycle_edge=False negative=False, relaxed=True, highlighted=False, cycle_edge=False
) )
assert lw == 2.5 assert lw == 2.5
def test_choose_edge_style_highlighted(self) -> None: def test_choose_edge_style_highlighted(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import ( color, _, ls = _bf_neg._choose_edge_style(
_choose_edge_style,
)
color, lw, ls = _choose_edge_style(
negative=False, relaxed=False, highlighted=True, cycle_edge=False negative=False, relaxed=False, highlighted=True, cycle_edge=False
) )
assert ls == "-" assert ls == "-"
assert color == "#1565C0" assert color == "#1565C0"
def test_choose_edge_style_default(self) -> None: def test_choose_edge_style_default(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import ( color, lw, _ = _bf_neg._choose_edge_style(
GRAY3,
_choose_edge_style,
)
color, lw, ls = _choose_edge_style(
negative=False, relaxed=False, highlighted=False, cycle_edge=False negative=False, relaxed=False, highlighted=False, cycle_edge=False
) )
assert color == GRAY3 assert color == _bf_neg.GRAY3
assert lw == 1.5 assert lw == 1.5
def test_draw_edge_no_offset(self) -> None: def test_draw_edge_no_offset(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_edge,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_edge(ax, (0, 0), (2, 2), 3) _bf_neg.draw_edge(ax, (0, 0), (2, 2), 3)
plt.close(fig) plt.close(fig)
def test_draw_edge_with_offset(self) -> None: def test_draw_edge_with_offset(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_edge,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_edge(ax, (0, 0), (2, 2), -3, negative=True, offset=0.3) _bf_neg.draw_edge(ax, (0, 0), (2, 2), -3, negative=True, offset=0.3)
plt.close(fig) plt.close(fig)
def test_draw_edge_highlighted(self) -> None: def test_draw_edge_highlighted(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_edge,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_edge(ax, (0, 0), (2, 2), 5, highlighted=True) _bf_neg.draw_edge(ax, (0, 0), (2, 2), 5, highlighted=True)
plt.close(fig) plt.close(fig)
def test_draw_edge_cycle(self) -> None: def test_draw_edge_cycle(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_edge,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(-1, 5) ax.set_xlim(-1, 5)
ax.set_ylim(-1, 5) ax.set_ylim(-1, 5)
draw_edge(ax, (0, 0), (2, 2), -2, cycle_edge=True) _bf_neg.draw_edge(ax, (0, 0), (2, 2), -2, cycle_edge=True)
plt.close(fig) plt.close(fig)
@ -184,36 +136,20 @@ class TestDrawNegGraph:
def test_minimal(self) -> None: def test_minimal(self) -> None:
"""All-defaults: visited, relaxed, dist, error_nodes all None.""" """All-defaults: visited, relaxed, dist, error_nodes all None."""
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
NEG_EDGES,
draw_neg_graph,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_neg_graph(ax, NEG_EDGES) _bf_neg.draw_neg_graph(ax, _bf_neg.NEG_EDGES)
plt.close(fig) plt.close(fig)
def test_with_title(self) -> None: def test_with_title(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
NEG_EDGES,
draw_neg_graph,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_neg_graph(ax, NEG_EDGES, title="Test") _bf_neg.draw_neg_graph(ax, _bf_neg.NEG_EDGES, title="Test")
plt.close(fig) plt.close(fig)
def test_with_all_options(self) -> None: def test_with_all_options(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
NEG_EDGES,
NEG_POS,
draw_neg_graph,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_neg_graph( _bf_neg.draw_neg_graph(
ax, ax,
NEG_EDGES, _bf_neg.NEG_EDGES,
title="Full", title="Full",
dist={"S": "0", "A": "1", "B": "5", "C": "4"}, dist={"S": "0", "A": "1", "B": "5", "C": "4"},
current="S", current="S",
@ -221,19 +157,15 @@ class TestDrawNegGraph:
relaxed_edges={("S", "A")}, relaxed_edges={("S", "A")},
error_nodes={"C"}, error_nodes={"C"},
extra_edges=[("C", "B", -3)], extra_edges=[("C", "B", -3)],
node_positions=NEG_POS, node_positions=_bf_neg.NEG_POS,
) )
plt.close(fig) plt.close(fig)
def test_explicit_node_positions(self) -> None: def test_explicit_node_positions(self) -> None:
"""Cover node_positions is not None branch.""" """Cover node_positions is not None branch."""
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import (
draw_neg_graph,
)
pos = {"X": (1.0, 1.0), "Y": (3.0, 1.0)} pos = {"X": (1.0, 1.0), "Y": (3.0, 1.0)}
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_neg_graph( _bf_neg.draw_neg_graph(
ax, ax,
[("X", "Y", 2)], [("X", "Y", 2)],
node_positions=pos, node_positions=pos,
@ -250,24 +182,12 @@ class TestBFDiagramFunctions:
"""Test the main diagram generation functions.""" """Test the main diagram generation functions."""
def test_generate_bf_negative_weights(self) -> None: def test_generate_bf_negative_weights(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._bf_negative_diagrams import (
generate_bf_negative_weights,
)
generate_bf_negative_weights() generate_bf_negative_weights()
def test_generate_bf_negative_cycle(self) -> None: def test_generate_bf_negative_cycle(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._bf_negative_diagrams import (
generate_bf_negative_cycle,
)
generate_bf_negative_cycle() generate_bf_negative_cycle()
def test_add_annotation_box(self) -> None: def test_add_annotation_box(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._bf_negative_diagrams import (
_add_annotation_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
_add_annotation_box(ax, 1, 1, "test", color="red", bg_color="white") _add_annotation_box(ax, 1, 1, "test", color="red", bg_color="white")
plt.close(fig) plt.close(fig)
@ -277,40 +197,20 @@ class TestBFModuleConstants:
"""Verify module-level constants.""" """Verify module-level constants."""
def test_constants(self) -> None: def test_constants(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_bf_negative_diagram import ( assert _bf_neg.DPI == 300
BG, assert _bf_neg.BG == "white"
DPI, assert isinstance(_bf_neg.FS, int | float)
FS, assert isinstance(_bf_neg.FS_EDGE, int | float)
FS_EDGE, assert isinstance(_bf_neg.FS_SMALL, int | float)
FS_SMALL, assert isinstance(_bf_neg.FS_TITLE, int | float)
FS_TITLE, assert isinstance(_bf_neg.GRAY1, str)
GRAY1, assert isinstance(_bf_neg.GRAY2, str)
GRAY2, assert isinstance(_bf_neg.GRAY3, str)
GRAY3, assert isinstance(_bf_neg.GRAY4, str)
GRAY4, assert isinstance(_bf_neg.LIGHT_GREEN, str)
LIGHT_GREEN, assert isinstance(_bf_neg.LIGHT_RED, str)
LIGHT_RED, assert isinstance(_bf_neg.LIGHT_YELLOW, str)
LIGHT_YELLOW, assert isinstance(_bf_neg.LN, str)
LN, assert isinstance(_bf_neg.OUTPUT_DIR, str)
NEG_EDGES, assert len(_bf_neg.NEG_EDGES) > 0
NEG_POS, assert len(_bf_neg.NEG_POS) > 0
OUTPUT_DIR,
)
assert DPI == 300
assert BG == "white"
assert isinstance(FS, int | float)
assert isinstance(FS_EDGE, int | float)
assert isinstance(FS_SMALL, int | float)
assert isinstance(FS_TITLE, int | float)
assert isinstance(GRAY1, str)
assert isinstance(GRAY2, str)
assert isinstance(GRAY3, str)
assert isinstance(GRAY4, str)
assert isinstance(LIGHT_GREEN, str)
assert isinstance(LIGHT_RED, str)
assert isinstance(LIGHT_YELLOW, str)
assert isinstance(LN, str)
assert isinstance(OUTPUT_DIR, str)
assert len(NEG_EDGES) > 0
assert len(NEG_POS) > 0

View File

@ -17,6 +17,24 @@ mpl.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from python_pkg.praca_magisterska_video.generate_images import (
generate_normalization_diagrams as _norm_mod,
)
from python_pkg.praca_magisterska_video.generate_images._norm_advanced import (
draw_3nf,
draw_4nf,
draw_bcnf,
)
from python_pkg.praca_magisterska_video.generate_images._norm_basic import (
draw_0nf,
draw_1nf,
draw_2nf,
)
from python_pkg.praca_magisterska_video.generate_images._norm_higher import (
draw_5nf,
draw_summary_flow,
)
pytestmark = pytest.mark.usefixtures("_no_savefig") pytestmark = pytest.mark.usefixtures("_no_savefig")
_GEN = ( _GEN = (
@ -34,51 +52,28 @@ class TestNormHelpers:
"""Test _compute_col_widths, draw_table, create_figure, add_arrow, add_label.""" """Test _compute_col_widths, draw_table, create_figure, add_arrow, add_label."""
def test_compute_col_widths_normal(self) -> None: def test_compute_col_widths_normal(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( result = _norm_mod._compute_col_widths(["Name", "Age"], [["Alice", "30"]])
_compute_col_widths,
)
result = _compute_col_widths(["Name", "Age"], [["Alice", "30"]])
assert len(result) == 2 assert len(result) == 2
assert all(w >= 0.5 for w in result) assert all(w >= 0.5 for w in result)
def test_compute_col_widths_jagged(self) -> None: def test_compute_col_widths_jagged(self) -> None:
"""Row shorter than headers → c < len(r) False branch.""" """Row shorter than headers → c < len(r) False branch."""
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( result = _norm_mod._compute_col_widths(["A", "B", "C"], [["x"]])
_compute_col_widths,
)
result = _compute_col_widths(["A", "B", "C"], [["x"]])
assert len(result) == 3 assert len(result) == 3
def test_draw_table_auto_widths(self) -> None: def test_draw_table_auto_widths(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
create_figure, _norm_mod.draw_table(ax, 0, 5, "T", ["A", "B"], [["1", "2"]])
draw_table,
)
fig, ax = create_figure()
draw_table(ax, 0, 5, "T", ["A", "B"], [["1", "2"]])
plt.close(fig) plt.close(fig)
def test_draw_table_explicit_widths(self) -> None: def test_draw_table_explicit_widths(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
create_figure, _norm_mod.draw_table(ax, 0, 5, "T", ["A"], [["x"]], col_widths=[1.0])
draw_table,
)
fig, ax = create_figure()
draw_table(ax, 0, 5, "T", ["A"], [["x"]], col_widths=[1.0])
plt.close(fig) plt.close(fig)
def test_draw_table_highlight_cols(self) -> None: def test_draw_table_highlight_cols(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
create_figure, _norm_mod.draw_table(
draw_table,
)
fig, ax = create_figure()
draw_table(
ax, ax,
0, 0,
5, 5,
@ -90,13 +85,8 @@ class TestNormHelpers:
plt.close(fig) plt.close(fig)
def test_draw_table_highlight_rows(self) -> None: def test_draw_table_highlight_rows(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
create_figure, _norm_mod.draw_table(
draw_table,
)
fig, ax = create_figure()
draw_table(
ax, ax,
0, 0,
5, 5,
@ -108,13 +98,8 @@ class TestNormHelpers:
plt.close(fig) plt.close(fig)
def test_draw_table_highlight_cells(self) -> None: def test_draw_table_highlight_cells(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
create_figure, _norm_mod.draw_table(
draw_table,
)
fig, ax = create_figure()
draw_table(
ax, ax,
0, 0,
5, 5,
@ -126,13 +111,8 @@ class TestNormHelpers:
plt.close(fig) plt.close(fig)
def test_draw_table_strikethrough(self) -> None: def test_draw_table_strikethrough(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
create_figure, _norm_mod.draw_table(
draw_table,
)
fig, ax = create_figure()
draw_table(
ax, ax,
0, 0,
5, 5,
@ -145,13 +125,8 @@ class TestNormHelpers:
def test_draw_table_all_options(self) -> None: def test_draw_table_all_options(self) -> None:
"""All highlight/strikethrough at once, with matching+non-matching cells.""" """All highlight/strikethrough at once, with matching+non-matching cells."""
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
create_figure, w, h = _norm_mod.draw_table(
draw_table,
)
fig, ax = create_figure()
w, h = draw_table(
ax, ax,
0, 0,
5, 5,
@ -169,65 +144,35 @@ class TestNormHelpers:
plt.close(fig) plt.close(fig)
def test_create_figure(self) -> None: def test_create_figure(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure(10, 8)
create_figure,
)
fig, ax = create_figure(10, 8)
assert fig is not None assert fig is not None
assert ax is not None assert ax is not None
plt.close(fig) plt.close(fig)
def test_add_arrow_with_label(self) -> None: def test_add_arrow_with_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
add_arrow, _norm_mod.add_arrow(ax, 0, 5, 3, 5, "lbl", color="black")
create_figure,
)
fig, ax = create_figure()
add_arrow(ax, 0, 5, 3, 5, "lbl", color="black")
plt.close(fig) plt.close(fig)
def test_add_arrow_no_label(self) -> None: def test_add_arrow_no_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
add_arrow, _norm_mod.add_arrow(ax, 0, 5, 3, 5)
create_figure,
)
fig, ax = create_figure()
add_arrow(ax, 0, 5, 3, 5)
plt.close(fig) plt.close(fig)
def test_add_label(self) -> None: def test_add_label(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( fig, ax = _norm_mod.create_figure()
add_label, _norm_mod.add_label(ax, 0, 5, "note", fontsize=10, color="red")
create_figure,
)
fig, ax = create_figure()
add_label(ax, 0, 5, "note", fontsize=10, color="red")
plt.close(fig) plt.close(fig)
def test_module_constants(self) -> None: def test_module_constants(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_normalization_diagrams import ( assert _norm_mod.DPI == 300
CELL_COLOR, assert isinstance(_norm_mod.OUTPUT_DIR, str)
DPI, assert isinstance(_norm_mod.HEADER_COLOR, str)
FD_ARROW_COLOR, assert isinstance(_norm_mod.CELL_COLOR, str)
FIXED_COLOR, assert isinstance(_norm_mod.HIGHLIGHT_COLOR, str)
FONT_SIZE, assert isinstance(_norm_mod.FIXED_COLOR, str)
HEADER_COLOR, assert isinstance(_norm_mod.FD_ARROW_COLOR, str)
HIGHLIGHT_COLOR, assert isinstance(_norm_mod.FONT_SIZE, int | float)
OUTPUT_DIR,
)
assert DPI == 300
assert isinstance(OUTPUT_DIR, str)
assert isinstance(HEADER_COLOR, str)
assert isinstance(CELL_COLOR, str)
assert isinstance(HIGHLIGHT_COLOR, str)
assert isinstance(FIXED_COLOR, str)
assert isinstance(FD_ARROW_COLOR, str)
assert isinstance(FONT_SIZE, int | float)
# ── _norm_basic (draw_table has positional-arg signature mismatch) ───── # ── _norm_basic (draw_table has positional-arg signature mismatch) ─────
@ -243,29 +188,17 @@ class TestNormBasic:
@patch(f"{_BASIC}.add_arrow") @patch(f"{_BASIC}.add_arrow")
@patch(f"{_BASIC}.draw_table") @patch(f"{_BASIC}.draw_table")
def test_draw_0nf(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_0nf(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_basic import (
draw_0nf,
)
draw_0nf() draw_0nf()
@patch(f"{_BASIC}.add_arrow") @patch(f"{_BASIC}.add_arrow")
@patch(f"{_BASIC}.draw_table") @patch(f"{_BASIC}.draw_table")
def test_draw_1nf(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_1nf(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_basic import (
draw_1nf,
)
draw_1nf() draw_1nf()
@patch(f"{_BASIC}.add_arrow") @patch(f"{_BASIC}.add_arrow")
@patch(f"{_BASIC}.draw_table") @patch(f"{_BASIC}.draw_table")
def test_draw_2nf(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_2nf(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_basic import (
draw_2nf,
)
draw_2nf() draw_2nf()
@ -277,29 +210,17 @@ class TestNormAdvanced:
@patch(f"{_ADV}.add_arrow") @patch(f"{_ADV}.add_arrow")
@patch(f"{_ADV}.draw_table") @patch(f"{_ADV}.draw_table")
def test_draw_3nf(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_3nf(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_advanced import (
draw_3nf,
)
draw_3nf() draw_3nf()
@patch(f"{_ADV}.add_arrow") @patch(f"{_ADV}.add_arrow")
@patch(f"{_ADV}.draw_table") @patch(f"{_ADV}.draw_table")
def test_draw_bcnf(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_bcnf(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_advanced import (
draw_bcnf,
)
draw_bcnf() draw_bcnf()
@patch(f"{_ADV}.add_arrow") @patch(f"{_ADV}.add_arrow")
@patch(f"{_ADV}.draw_table") @patch(f"{_ADV}.draw_table")
def test_draw_4nf(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_4nf(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_advanced import (
draw_4nf,
)
draw_4nf() draw_4nf()
@ -311,18 +232,10 @@ class TestNormHigher:
@patch(f"{_HIGH}.add_arrow") @patch(f"{_HIGH}.add_arrow")
@patch(f"{_HIGH}.draw_table") @patch(f"{_HIGH}.draw_table")
def test_draw_5nf(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_5nf(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_higher import (
draw_5nf,
)
draw_5nf() draw_5nf()
@patch(f"{_HIGH}.add_arrow") @patch(f"{_HIGH}.add_arrow")
@patch(f"{_HIGH}.draw_table") @patch(f"{_HIGH}.draw_table")
def test_draw_summary_flow(self, _mock_dt: MagicMock, _mock_aa: MagicMock) -> None: def test_draw_summary_flow(self, mock_dt: MagicMock, mock_aa: MagicMock) -> None:
from python_pkg.praca_magisterska_video.generate_images._norm_higher import (
draw_summary_flow,
)
draw_summary_flow() draw_summary_flow()

View File

@ -16,6 +16,19 @@ mpl.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from python_pkg.praca_magisterska_video.generate_images import (
_pattern_pillars_observer as _pat_pillars,
)
from python_pkg.praca_magisterska_video.generate_images import (
_pattern_template_catalog as _pat_tmpl,
)
from python_pkg.praca_magisterska_video.generate_images import (
generate_pattern_diagrams as _pat_diags,
)
from python_pkg.praca_magisterska_video.generate_images._pattern_navigation import (
generate_pattern_language_navigation,
)
pytestmark = pytest.mark.usefixtures("_no_savefig") pytestmark = pytest.mark.usefixtures("_no_savefig")
_GEN = "python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams" _GEN = "python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams"
@ -31,74 +44,47 @@ class TestPatternConstants:
"""Constants and module-level values.""" """Constants and module-level values."""
def test_dpi(self) -> None: def test_dpi(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import ( assert _pat_diags.DPI == 300
DPI,
)
assert DPI == 300
def test_bg(self) -> None: def test_bg(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import ( assert _pat_diags.BG == "white"
BG,
)
assert BG == "white"
def test_gray_constants(self) -> None: def test_gray_constants(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import ( assert all(
GRAY1, isinstance(g, str)
GRAY2, for g in [
GRAY3, _pat_diags.GRAY1,
GRAY4, _pat_diags.GRAY2,
GRAY5, _pat_diags.GRAY3,
_pat_diags.GRAY4,
_pat_diags.GRAY5,
]
) )
assert all(isinstance(g, str) for g in [GRAY1, GRAY2, GRAY3, GRAY4, GRAY5])
def test_band_heights(self) -> None: def test_band_heights(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import ( assert len(_pat_diags._BAND_HEIGHTS) == 5
_BAND_HEIGHTS, assert all(isinstance(h, float) for h in _pat_diags._BAND_HEIGHTS)
)
assert len(_BAND_HEIGHTS) == 5
assert all(isinstance(h, float) for h in _BAND_HEIGHTS)
def test_output_dir_is_str(self) -> None: def test_output_dir_is_str(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import ( assert isinstance(_pat_diags.OUTPUT_DIR, str)
OUTPUT_DIR,
)
assert isinstance(OUTPUT_DIR, str)
class TestDrawBox: class TestDrawBox:
"""Test draw_box helper.""" """Test draw_box helper."""
def test_rounded(self) -> None: def test_rounded(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import (
draw_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_box(ax, 0, 0, 1, 1, "test", rounded=True) _pat_diags.draw_box(ax, 0, 0, 1, 1, "test", rounded=True)
plt.close(fig) plt.close(fig)
def test_not_rounded(self) -> None: def test_not_rounded(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import (
draw_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_box(ax, 0, 0, 1, 1, "test", rounded=False) _pat_diags.draw_box(ax, 0, 0, 1, 1, "test", rounded=False)
plt.close(fig) plt.close(fig)
def test_custom_style(self) -> None: def test_custom_style(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import (
draw_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_box( _pat_diags.draw_box(
ax, ax,
0, 0,
0, 0,
@ -120,21 +106,13 @@ class TestDrawArrow:
"""Test draw_arrow helper.""" """Test draw_arrow helper."""
def test_default(self) -> None: def test_default(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import (
draw_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_arrow(ax, 0, 0, 1, 1) _pat_diags.draw_arrow(ax, 0, 0, 1, 1)
plt.close(fig) plt.close(fig)
def test_custom(self) -> None: def test_custom(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_pattern_diagrams import (
draw_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_arrow(ax, 0, 0, 1, 1, lw=2.5, style="<->", color="red") _pat_diags.draw_arrow(ax, 0, 0, 1, 1, lw=2.5, style="<->", color="red")
plt.close(fig) plt.close(fig)
@ -145,22 +123,14 @@ class TestPatternTemplate:
"""Test generate_pattern_template.""" """Test generate_pattern_template."""
def test_runs(self) -> None: def test_runs(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._pattern_template_catalog import ( _pat_tmpl.generate_pattern_template()
generate_pattern_template,
)
generate_pattern_template()
class TestCatalogMap: class TestCatalogMap:
"""Test generate_catalog_map.""" """Test generate_catalog_map."""
def test_runs(self) -> None: def test_runs(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._pattern_template_catalog import ( _pat_tmpl.generate_catalog_map()
generate_catalog_map,
)
generate_catalog_map()
# ── _pattern_pillars_observer ────────────────────────────────────────── # ── _pattern_pillars_observer ──────────────────────────────────────────
@ -170,34 +140,22 @@ class TestThreePillars:
"""Test generate_three_pillars.""" """Test generate_three_pillars."""
def test_runs(self) -> None: def test_runs(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._pattern_pillars_observer import ( _pat_pillars.generate_three_pillars()
generate_three_pillars,
)
generate_three_pillars()
class TestObserverCard: class TestObserverCard:
"""Test generate_observer_card_filled.""" """Test generate_observer_card_filled."""
def test_runs(self) -> None: def test_runs(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._pattern_pillars_observer import ( _pat_pillars.generate_observer_card_filled()
generate_observer_card_filled,
)
generate_observer_card_filled()
class TestGetObserverBandHeight: class TestGetObserverBandHeight:
"""Test _get_observer_band_height.""" """Test _get_observer_band_height."""
def test_all_indices(self) -> None: def test_all_indices(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._pattern_pillars_observer import (
_get_observer_band_height,
)
for i in range(5): for i in range(5):
h = _get_observer_band_height(i) h = _pat_pillars._get_observer_band_height(i)
assert isinstance(h, float) assert isinstance(h, float)
assert h > 0 assert h > 0
@ -209,8 +167,4 @@ class TestPatternLanguageNavigation:
"""Test generate_pattern_language_navigation.""" """Test generate_pattern_language_navigation."""
def test_runs(self) -> None: def test_runs(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._pattern_navigation import (
generate_pattern_language_navigation,
)
generate_pattern_language_navigation() generate_pattern_language_navigation()

View File

@ -16,6 +16,36 @@ mpl.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pytest import pytest
from python_pkg.praca_magisterska_video.generate_images import (
generate_process_diagrams as _proc_diags,
)
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
_draw_bpmn_elements,
_draw_bpmn_legend,
_draw_bpmn_pool_and_lanes,
_draw_uml_elements,
_draw_uml_legend,
generate_bpmn,
generate_uml_activity,
)
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
_draw_epc_branches,
_draw_epc_connector,
_draw_epc_event,
_draw_epc_flow,
_draw_epc_function,
_draw_epc_legend,
generate_epc,
)
from python_pkg.praca_magisterska_video.generate_images._process_fc import (
_draw_fc_elements,
_draw_fc_io_shape,
_draw_fc_legend,
_draw_fc_process_box,
_draw_fc_terminal,
generate_flowchart,
)
pytestmark = pytest.mark.usefixtures("_no_savefig") pytestmark = pytest.mark.usefixtures("_no_savefig")
_GEN = "python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams" _GEN = "python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams"
@ -31,37 +61,21 @@ class TestProcessConstants:
"""Constants and module-level values.""" """Constants and module-level values."""
def test_dpi(self) -> None: def test_dpi(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import ( assert _proc_diags.DPI == 300
DPI,
)
assert DPI == 300
def test_bg_color(self) -> None: def test_bg_color(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import ( assert _proc_diags.BG_COLOR == "white"
BG_COLOR,
)
assert BG_COLOR == "white"
def test_output_dir(self) -> None: def test_output_dir(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import ( assert isinstance(_proc_diags.OUTPUT_DIR, str)
OUTPUT_DIR,
)
assert isinstance(OUTPUT_DIR, str)
class TestProcessDrawArrow: class TestProcessDrawArrow:
"""Test draw_arrow helper.""" """Test draw_arrow helper."""
def test_default(self) -> None: def test_default(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import (
draw_arrow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_arrow(ax, 0, 0, 1, 1) _proc_diags.draw_arrow(ax, 0, 0, 1, 1)
plt.close(fig) plt.close(fig)
@ -69,12 +83,8 @@ class TestProcessDrawLine:
"""Test draw_line helper.""" """Test draw_line helper."""
def test_default(self) -> None: def test_default(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import (
draw_line,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_line(ax, 0, 0, 5, 5) _proc_diags.draw_line(ax, 0, 0, 5, 5)
plt.close(fig) plt.close(fig)
@ -82,21 +92,15 @@ class TestProcessDrawRoundedRect:
"""Test draw_rounded_rect helper.""" """Test draw_rounded_rect helper."""
def test_default(self) -> None: def test_default(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import (
draw_rounded_rect,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_rounded_rect(ax, 5, 5, 10, 4, "Hello") _proc_diags.draw_rounded_rect(ax, 5, 5, 10, 4, "Hello")
plt.close(fig) plt.close(fig)
def test_custom_params(self) -> None: def test_custom_params(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import (
draw_rounded_rect,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_rounded_rect(ax, 0, 0, 8, 3, "styled", fill="#CCC", lw=3, fontsize=12) _proc_diags.draw_rounded_rect(
ax, 0, 0, 8, 3, "styled", fill="#CCC", lw=3, fontsize=12
)
plt.close(fig) plt.close(fig)
@ -104,30 +108,18 @@ class TestProcessDrawDiamond:
"""Test draw_diamond helper.""" """Test draw_diamond helper."""
def test_with_text(self) -> None: def test_with_text(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import (
draw_diamond,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_diamond(ax, 5, 5, 3, "XOR") _proc_diags.draw_diamond(ax, 5, 5, 3, "XOR")
plt.close(fig) plt.close(fig)
def test_without_text(self) -> None: def test_without_text(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import (
draw_diamond,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_diamond(ax, 5, 5, 3) _proc_diags.draw_diamond(ax, 5, 5, 3)
plt.close(fig) plt.close(fig)
def test_custom_fill(self) -> None: def test_custom_fill(self) -> None:
from python_pkg.praca_magisterska_video.generate_images.generate_process_diagrams import (
draw_diamond,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
draw_diamond(ax, 5, 5, 3, "Y", fill="#EEE", fontsize=12) _proc_diags.draw_diamond(ax, 5, 5, 3, "Y", fill="#EEE", fontsize=12)
plt.close(fig) plt.close(fig)
@ -138,17 +130,9 @@ class TestBPMN:
"""Test generate_bpmn and its sub-helpers.""" """Test generate_bpmn and its sub-helpers."""
def test_generate_bpmn(self) -> None: def test_generate_bpmn(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
generate_bpmn,
)
generate_bpmn() generate_bpmn()
def test_draw_bpmn_pool_and_lanes(self) -> None: def test_draw_bpmn_pool_and_lanes(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
_draw_bpmn_pool_and_lanes,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 110) ax.set_xlim(0, 110)
ax.set_ylim(0, 75) ax.set_ylim(0, 75)
@ -157,10 +141,6 @@ class TestBPMN:
plt.close(fig) plt.close(fig)
def test_draw_bpmn_elements(self) -> None: def test_draw_bpmn_elements(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
_draw_bpmn_elements,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 110) ax.set_xlim(0, 110)
ax.set_ylim(0, 75) ax.set_ylim(0, 75)
@ -168,10 +148,6 @@ class TestBPMN:
plt.close(fig) plt.close(fig)
def test_draw_bpmn_legend(self) -> None: def test_draw_bpmn_legend(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
_draw_bpmn_legend,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 110) ax.set_xlim(0, 110)
ax.set_ylim(0, 75) ax.set_ylim(0, 75)
@ -183,17 +159,9 @@ class TestUMLActivity:
"""Test generate_uml_activity and its sub-helpers.""" """Test generate_uml_activity and its sub-helpers."""
def test_generate_uml_activity(self) -> None: def test_generate_uml_activity(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
generate_uml_activity,
)
generate_uml_activity() generate_uml_activity()
def test_draw_uml_elements(self) -> None: def test_draw_uml_elements(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
_draw_uml_elements,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 100) ax.set_xlim(0, 100)
ax.set_ylim(0, 100) ax.set_ylim(0, 100)
@ -201,10 +169,6 @@ class TestUMLActivity:
plt.close(fig) plt.close(fig)
def test_draw_uml_legend(self) -> None: def test_draw_uml_legend(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_bpmn_uml import (
_draw_uml_legend,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 100) ax.set_xlim(0, 100)
ax.set_ylim(0, 100) ax.set_ylim(0, 100)
@ -219,44 +183,24 @@ class TestEPC:
"""Test generate_epc and its sub-helpers.""" """Test generate_epc and its sub-helpers."""
def test_generate_epc(self) -> None: def test_generate_epc(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
generate_epc,
)
generate_epc() generate_epc()
def test_draw_epc_event(self) -> None: def test_draw_epc_event(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
_draw_epc_event,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
_draw_epc_event(ax, 50, 50, "test event") _draw_epc_event(ax, 50, 50, "test event")
plt.close(fig) plt.close(fig)
def test_draw_epc_function(self) -> None: def test_draw_epc_function(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
_draw_epc_function,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
_draw_epc_function(ax, 50, 50, "test function") _draw_epc_function(ax, 50, 50, "test function")
plt.close(fig) plt.close(fig)
def test_draw_epc_connector(self) -> None: def test_draw_epc_connector(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
_draw_epc_connector,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
_draw_epc_connector(ax, 50, 50, "XOR") _draw_epc_connector(ax, 50, 50, "XOR")
plt.close(fig) plt.close(fig)
def test_draw_epc_flow(self) -> None: def test_draw_epc_flow(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
_draw_epc_flow,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 100) ax.set_xlim(0, 100)
ax.set_ylim(0, 120) ax.set_ylim(0, 120)
@ -267,10 +211,6 @@ class TestEPC:
plt.close(fig) plt.close(fig)
def test_draw_epc_branches(self) -> None: def test_draw_epc_branches(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
_draw_epc_branches,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 100) ax.set_xlim(0, 100)
ax.set_ylim(0, 120) ax.set_ylim(0, 120)
@ -278,10 +218,6 @@ class TestEPC:
plt.close(fig) plt.close(fig)
def test_draw_epc_legend(self) -> None: def test_draw_epc_legend(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_epc_fc import (
_draw_epc_legend,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 100) ax.set_xlim(0, 100)
ax.set_ylim(0, 120) ax.set_ylim(0, 120)
@ -296,44 +232,24 @@ class TestFlowchart:
"""Test generate_flowchart and its sub-helpers.""" """Test generate_flowchart and its sub-helpers."""
def test_generate_flowchart(self) -> None: def test_generate_flowchart(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_fc import (
generate_flowchart,
)
generate_flowchart() generate_flowchart()
def test_draw_fc_terminal(self) -> None: def test_draw_fc_terminal(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_fc import (
_draw_fc_terminal,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
_draw_fc_terminal(ax, 50, 50, "START") _draw_fc_terminal(ax, 50, 50, "START")
plt.close(fig) plt.close(fig)
def test_draw_fc_process_box(self) -> None: def test_draw_fc_process_box(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_fc import (
_draw_fc_process_box,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
_draw_fc_process_box(ax, 50, 50, "Process") _draw_fc_process_box(ax, 50, 50, "Process")
plt.close(fig) plt.close(fig)
def test_draw_fc_io_shape(self) -> None: def test_draw_fc_io_shape(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_fc import (
_draw_fc_io_shape,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
_draw_fc_io_shape(ax, 50, 50, "I/O") _draw_fc_io_shape(ax, 50, 50, "I/O")
plt.close(fig) plt.close(fig)
def test_draw_fc_elements(self) -> None: def test_draw_fc_elements(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_fc import (
_draw_fc_elements,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 100) ax.set_xlim(0, 100)
ax.set_ylim(0, 110) ax.set_ylim(0, 110)
@ -341,10 +257,6 @@ class TestFlowchart:
plt.close(fig) plt.close(fig)
def test_draw_fc_legend(self) -> None: def test_draw_fc_legend(self) -> None:
from python_pkg.praca_magisterska_video.generate_images._process_fc import (
_draw_fc_legend,
)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.set_xlim(0, 100) ax.set_xlim(0, 100)
ax.set_ylim(0, 110) ax.set_ylim(0, 110)

View File

@ -48,7 +48,7 @@ class TestSplitQuestions:
source_file = FakeFile(source_content) source_file = FakeFile(source_content)
written_files: dict[str, FakeFile] = {} written_files: dict[str, FakeFile] = {}
def fake_open(self_path: Path, *args: object, **kwargs: object) -> FakeFile: def fake_open(self_path: Path, *_args: object, **_kwargs: object) -> FakeFile:
path_str = str(self_path) path_str = str(self_path)
if "OBRONA_MAGISTERSKA_ODPOWIEDZI" in path_str: if "OBRONA_MAGISTERSKA_ODPOWIEDZI" in path_str:
return source_file return source_file
@ -59,7 +59,7 @@ class TestSplitQuestions:
with ( with (
patch.object(Path, "open", fake_open), patch.object(Path, "open", fake_open),
patch.object(Path, "mkdir", lambda *a, **kw: None), patch.object(Path, "mkdir", lambda *_a, **_kw: None),
): ):
importlib.import_module(mod_name) importlib.import_module(mod_name)

View File

@ -144,7 +144,7 @@ def test_get_file_metadata(sample_file: Path) -> None:
_get_file_metadata, _get_file_metadata,
) )
num, subject, content = _get_file_metadata(str(sample_file)) num, subject, _ = _get_file_metadata(str(sample_file))
assert num == "01" assert num == "01"
assert subject == "Informatyka" assert subject == "Informatyka"
@ -157,7 +157,7 @@ def test_get_file_metadata_no_match(tmp_path: Path) -> None:
p = tmp_path / "readme.txt" p = tmp_path / "readme.txt"
p.write_text("No Przedmiot", encoding="utf-8") p.write_text("No Przedmiot", encoding="utf-8")
num, subject, content = _get_file_metadata(str(p)) num, subject, _ = _get_file_metadata(str(p))
assert num == "00" assert num == "00"
assert subject == "Ogólne" assert subject == "Ogólne"

View File

@ -207,7 +207,7 @@ def test_body_parts_long_para_truncation() -> None:
# --- _extract_subsection_cards: empty parts / multiple parts --- # --- _extract_subsection_cards: empty parts / multiple parts ---
def test_subsection_empty_answer_parts(tmp_path: Path) -> None: def test_subsection_empty_answer_parts() -> None:
"""Subsection where _extract_body_parts returns [] (182->173).""" """Subsection where _extract_body_parts returns [] (182->173)."""
from python_pkg.praca_magisterska_video.generate_images.generate_anki_final import ( from python_pkg.praca_magisterska_video.generate_images.generate_anki_final import (
_extract_subsection_cards, _extract_subsection_cards,
@ -403,7 +403,7 @@ def test_main_function(tmp_path: Path) -> None:
call_count = 0 call_count = 0
def fake_extract(filepath: object) -> list[dict[str, str]]: def fake_extract(_filepath: object) -> list[dict[str, str]]:
nonlocal call_count nonlocal call_count
call_count += 1 call_count += 1
if call_count == 1: if call_count == 1:

View File

@ -79,7 +79,7 @@ def test_get_metadata(sample_file: Path) -> None:
_get_metadata, _get_metadata,
) )
num, topic, title, main_q, content = _get_metadata(str(sample_file)) num, topic, _, main_q, content = _get_metadata(str(sample_file))
assert num == "01" assert num == "01"
assert "test" in topic assert "test" in topic
assert "main concept" in main_q assert "main concept" in main_q
@ -92,7 +92,7 @@ def test_get_metadata_no_match(minimal_file: Path) -> None:
_get_metadata, _get_metadata,
) )
num, topic, title, main_q, content = _get_metadata(str(minimal_file)) num, topic, _, _, _ = _get_metadata(str(minimal_file))
assert num == "00" assert num == "00"
assert topic == "unknown" assert topic == "unknown"
@ -104,7 +104,7 @@ def test_extract_main_card(sample_file: Path) -> None:
_get_metadata, _get_metadata,
) )
num, topic, title, main_q, content = _get_metadata(str(sample_file)) num, topic, _, main_q, content = _get_metadata(str(sample_file))
cards = _extract_main_card(content, main_q, "Informatyka", num, topic) cards = _extract_main_card(content, main_q, "Informatyka", num, topic)
assert len(cards) == 1 assert len(cards) == 1
assert "main concept" in cards[0]["question"] assert "main concept" in cards[0]["question"]

View File

@ -115,7 +115,7 @@ def test_main_card_def_outside_length(def_length_file: Path) -> None:
_get_metadata, _get_metadata,
) )
num, topic, title, main_q, content = _get_metadata(str(def_length_file)) num, topic, _, main_q, content = _get_metadata(str(def_length_file))
cards = _extract_main_card(content, main_q, "Informatyka", num, topic) cards = _extract_main_card(content, main_q, "Informatyka", num, topic)
assert isinstance(cards, list) assert isinstance(cards, list)
@ -135,7 +135,7 @@ def test_sub_cards_short_body(subsection_file: Path) -> None:
assert isinstance(cards, list) assert isinstance(cards, list)
def test_sub_cards_no_answer_text(tmp_path: Path) -> None: def test_sub_cards_no_answer_text() -> None:
"""Subsection where _extract_subsection_answer returns None (line 145).""" """Subsection where _extract_subsection_answer returns None (line 145)."""
from python_pkg.praca_magisterska_video.generate_images.generate_anki import ( from python_pkg.praca_magisterska_video.generate_images.generate_anki import (
_extract_sub_cards, _extract_sub_cards,
@ -221,7 +221,7 @@ def test_main_function(tmp_path: Path) -> None:
call_count = 0 call_count = 0
def fake_extract(filepath: object) -> list[dict[str, str]]: def fake_extract(_filepath: object) -> list[dict[str, str]]:
nonlocal call_count nonlocal call_count
call_count += 1 call_count += 1
if call_count == 1: if call_count == 1:

View File

@ -255,7 +255,7 @@ def test_main_error_branch(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N
return real_path(out_file) return real_path(out_file)
return real_path(s) return real_path(s)
def failing_process(filepath: str) -> list[dict[str, str]]: def failing_process(_filepath: str) -> list[dict[str, str]]:
msg = "test error" msg = "test error"
raise ValueError(msg) raise ValueError(msg)

View File

@ -113,9 +113,11 @@ Some introductory text that is ignored completely.
- **Gamma** - **Gamma**
""" """
_PLAIN_BODY = """\ _PLAIN_BODY = (
This is a plain first paragraph without any structured content and it is long enough to be captured by regex. "This is a plain first paragraph without any"
""" " structured content and it is long enough"
" to be captured by regex.\n"
)
_PARA_ONLY_MD = """\ _PARA_ONLY_MD = """\
# Pytanie 03: Para Only # Pytanie 03: Para Only
@ -251,7 +253,7 @@ def test_read_file_metadata_matching(sample_file: Path) -> None:
_read_file_metadata, _read_file_metadata,
) )
content, base_tags, main_question = _read_file_metadata(sample_file) _, base_tags, main_question = _read_file_metadata(sample_file)
assert "pyt01" in base_tags assert "pyt01" in base_tags
assert "Informatyka" in base_tags assert "Informatyka" in base_tags
assert main_question is not None assert main_question is not None
@ -266,7 +268,7 @@ def test_read_file_metadata_no_match(tmp_path: Path) -> None:
p = tmp_path / "readme.txt" p = tmp_path / "readme.txt"
p.write_text(_MINIMAL_MD, encoding="utf-8") p.write_text(_MINIMAL_MD, encoding="utf-8")
content, base_tags, main_question = _read_file_metadata(p) _, base_tags, main_question = _read_file_metadata(p)
assert "pyt00" in base_tags assert "pyt00" in base_tags
assert "Og\u00f3lne" in base_tags assert "Og\u00f3lne" in base_tags
assert main_question is None assert main_question is None

View File

@ -224,7 +224,7 @@ def test_main_error_branch(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N
return real_path(out_file) return real_path(out_file)
return real_path(s) return real_path(s)
def failing_extract(filepath: object) -> list[dict[str, str]]: def failing_extract(_filepath: object) -> list[dict[str, str]]:
msg = "test error" msg = "test error"
raise ValueError(msg) raise ValueError(msg)

View File

@ -2,16 +2,24 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import numpy as np import numpy as np
if TYPE_CHECKING:
from collections.abc import Callable
def _spy_vc() -> tuple[object, list[tuple[object, float]]]: def _spy_vc() -> tuple[object, list[tuple[object, float]]]:
"""VideoClip spy capturing make_frame closures.""" """VideoClip spy capturing make_frame closures."""
captured: list[tuple[object, float]] = [] captured: list[tuple[object, float]] = []
def spy(make_frame=None, duration=None, **_kw: object) -> MagicMock: def spy(
make_frame: Callable[[float], np.ndarray] | None = None,
duration: float | None = None,
**_kw: object,
) -> MagicMock:
if callable(make_frame): if callable(make_frame):
captured.append((make_frame, duration or 1.0)) captured.append((make_frame, duration or 1.0))
clip = MagicMock() clip = MagicMock()

View File

@ -2,16 +2,24 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import numpy as np import numpy as np
if TYPE_CHECKING:
from collections.abc import Callable
def _spy_vc() -> tuple[object, list[tuple[object, float]]]: def _spy_vc() -> tuple[object, list[tuple[object, float]]]:
"""VideoClip spy capturing make_frame closures.""" """VideoClip spy capturing make_frame closures."""
captured: list[tuple[object, float]] = [] captured: list[tuple[object, float]] = []
def spy(make_frame=None, duration=None, **_kw: object) -> MagicMock: def spy(
make_frame: Callable[[float], np.ndarray] | None = None,
duration: float | None = None,
**_kw: object,
) -> MagicMock:
if callable(make_frame): if callable(make_frame):
captured.append((make_frame, duration or 1.0)) captured.append((make_frame, duration or 1.0))
clip = MagicMock() clip = MagicMock()

View File

@ -2,16 +2,24 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import numpy as np import numpy as np
if TYPE_CHECKING:
from collections.abc import Callable
def _spy_vc() -> tuple[object, list[tuple[object, float]]]: def _spy_vc() -> tuple[object, list[tuple[object, float]]]:
"""VideoClip spy capturing make_frame closures.""" """VideoClip spy capturing make_frame closures."""
captured: list[tuple[object, float]] = [] captured: list[tuple[object, float]] = []
def spy(make_frame=None, duration=None, **_kw: object) -> MagicMock: def spy(
make_frame: Callable[[float], np.ndarray] | None = None,
duration: float | None = None,
**_kw: object,
) -> MagicMock:
if callable(make_frame): if callable(make_frame):
captured.append((make_frame, duration or 1.0)) captured.append((make_frame, duration or 1.0))
clip = MagicMock() clip = MagicMock()

View File

@ -2,16 +2,24 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import numpy as np import numpy as np
if TYPE_CHECKING:
from collections.abc import Callable
def _spy_vc() -> tuple[object, list[tuple[object, float]]]: def _spy_vc() -> tuple[object, list[tuple[object, float]]]:
"""VideoClip spy capturing make_frame closures.""" """VideoClip spy capturing make_frame closures."""
captured: list[tuple[object, float]] = [] captured: list[tuple[object, float]] = []
def spy(make_frame=None, duration=None, **_kw: object) -> MagicMock: def spy(
make_frame: Callable[[float], np.ndarray] | None = None,
duration: float | None = None,
**_kw: object,
) -> MagicMock:
if callable(make_frame): if callable(make_frame):
captured.append((make_frame, duration or 1.0)) captured.append((make_frame, duration or 1.0))
clip = MagicMock() clip = MagicMock()

View File

@ -2,16 +2,24 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import numpy as np import numpy as np
if TYPE_CHECKING:
from collections.abc import Callable
def _spy_vc() -> tuple[object, list[tuple[object, float]]]: def _spy_vc() -> tuple[object, list[tuple[object, float]]]:
"""VideoClip spy capturing make_frame closures.""" """VideoClip spy capturing make_frame closures."""
captured: list[tuple[object, float]] = [] captured: list[tuple[object, float]] = []
def spy(make_frame=None, duration=None, **_kw: object) -> MagicMock: def spy(
make_frame: Callable[[float], np.ndarray] | None = None,
duration: float | None = None,
**_kw: object,
) -> MagicMock:
if callable(make_frame): if callable(make_frame):
captured.append((make_frame, duration or 1.0)) captured.append((make_frame, duration or 1.0))
clip = MagicMock() clip = MagicMock()

View File

@ -2,16 +2,24 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import numpy as np import numpy as np
if TYPE_CHECKING:
from collections.abc import Callable
def _spy_vc() -> tuple[object, list[tuple[object, float]]]: def _spy_vc() -> tuple[object, list[tuple[object, float]]]:
"""VideoClip spy capturing make_frame closures.""" """VideoClip spy capturing make_frame closures."""
captured: list[tuple[object, float]] = [] captured: list[tuple[object, float]] = []
def spy(make_frame=None, duration=None, **_kw: object) -> MagicMock: def spy(
make_frame: Callable[[float], np.ndarray] | None = None,
duration: float | None = None,
**_kw: object,
) -> MagicMock:
if callable(make_frame): if callable(make_frame):
captured.append((make_frame, duration or 1.0)) captured.append((make_frame, duration or 1.0))
clip = MagicMock() clip = MagicMock()

View File

@ -60,7 +60,7 @@ def test_make_frame_closure_returns_ndarray() -> None:
captured: list[object] = [] captured: list[object] = []
def capturing_video_clip(make_frame: object = None, **kw: object) -> MagicMock: def capturing_video_clip(make_frame: object = None, **_kw: object) -> MagicMock:
captured.append(make_frame) captured.append(make_frame)
clip = MagicMock() clip = MagicMock()
clip.with_fps.return_value = clip clip.with_fps.return_value = clip

View File

@ -18,6 +18,7 @@ Usage
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from collections import Counter
import json import json
from pathlib import Path from pathlib import Path
import sys import sys
@ -70,8 +71,6 @@ def cmd_debug(args: argparse.Namespace) -> None:
data = parse_image(args.image, threshold=args.threshold) data = parse_image(args.image, threshold=args.threshold)
out = args.output or args.image.rsplit(".", 1)[0] + "_debug.png" out = args.output or args.image.rsplit(".", 1)[0] + "_debug.png"
draw_debug(args.image, data, out) draw_debug(args.image, data, out)
from collections import Counter
counts = Counter(sq["type"] for sq in data["squares"]) counts = Counter(sq["type"] for sq in data["squares"])
for _t, _n in counts.most_common(): for _t, _n in counts.most_common():
pass pass

View File

@ -0,0 +1,9 @@
"""Mock cv2/numpy if not installed before puzzle_solver tests."""
from __future__ import annotations
import sys
from unittest.mock import MagicMock
sys.modules.setdefault("cv2", MagicMock())
sys.modules.setdefault("numpy", MagicMock())

View File

@ -3,16 +3,11 @@
from __future__ import annotations from __future__ import annotations
import json import json
import sys
from typing import Any from typing import Any
from unittest.mock import MagicMock, mock_open, patch from unittest.mock import MagicMock, mock_open, patch
import pytest import pytest
# Ensure cv2 and numpy are available as mocks before importing main
sys.modules.setdefault("cv2", MagicMock())
sys.modules.setdefault("numpy", MagicMock())
from python_pkg.puzzle_solver.main import ( from python_pkg.puzzle_solver.main import (
cmd_debug, cmd_debug,
cmd_parse, cmd_parse,

View File

@ -2,17 +2,10 @@
from __future__ import annotations from __future__ import annotations
import sys
from unittest.mock import MagicMock, mock_open, patch from unittest.mock import MagicMock, mock_open, patch
import pytest import pytest
# Install mock modules before any parse_image imports
_cv2_mock = MagicMock()
_np_mock = MagicMock()
sys.modules.setdefault("cv2", _cv2_mock)
sys.modules.setdefault("numpy", _np_mock)
from python_pkg.puzzle_solver.parse_image import ( from python_pkg.puzzle_solver.parse_image import (
_classify_by_fill, _classify_by_fill,
_classify_interior_feature, _classify_interior_feature,

View File

@ -2,16 +2,11 @@
from __future__ import annotations from __future__ import annotations
import sys
from typing import Any from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import numpy as np import numpy as np
# Install mock modules before any parse_image imports
sys.modules.setdefault("cv2", MagicMock())
sys.modules.setdefault("numpy", MagicMock())
from python_pkg.puzzle_solver.parse_image import ( from python_pkg.puzzle_solver.parse_image import (
_assign_teleporter_and_kl_groups, _assign_teleporter_and_kl_groups,
_build_output, _build_output,

View File

@ -2,14 +2,9 @@
from __future__ import annotations from __future__ import annotations
import sys
from typing import Any from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
# Install mock modules before any parse_image imports
sys.modules.setdefault("cv2", MagicMock())
sys.modules.setdefault("numpy", MagicMock())
from python_pkg.puzzle_solver.parse_image import ( from python_pkg.puzzle_solver.parse_image import (
_assign_teleporter_and_kl_groups, _assign_teleporter_and_kl_groups,
draw_debug, draw_debug,

View File

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.repo_explorer._discovery import ( from python_pkg.repo_explorer._discovery import (
@ -173,7 +172,7 @@ class TestGetDescription:
readme.exists.return_value = True readme.exists.return_value = True
readme.read_text.return_value = "# My Project\nDetails here" readme.read_text.return_value = "# My Project\nDetails here"
def truediv(_self: Any, name: str) -> MagicMock: def truediv(_self: object, name: str) -> MagicMock:
if name == "README.md": if name == "README.md":
return readme return readme
m = MagicMock(spec=Path) m = MagicMock(spec=Path)
@ -187,7 +186,7 @@ class TestGetDescription:
def test_readme_txt(self) -> None: def test_readme_txt(self) -> None:
mock_path = MagicMock(spec=Path) mock_path = MagicMock(spec=Path)
def truediv(_self: Any, name: str) -> MagicMock: def truediv(_self: object, name: str) -> MagicMock:
m = MagicMock(spec=Path) m = MagicMock(spec=Path)
if name == "README.txt": if name == "README.txt":
m.exists.return_value = True m.exists.return_value = True
@ -203,7 +202,7 @@ class TestGetDescription:
def test_readme_lower(self) -> None: def test_readme_lower(self) -> None:
mock_path = MagicMock(spec=Path) mock_path = MagicMock(spec=Path)
def truediv(_self: Any, name: str) -> MagicMock: def truediv(_self: object, name: str) -> MagicMock:
m = MagicMock(spec=Path) m = MagicMock(spec=Path)
if name == "readme.md": if name == "readme.md":
m.exists.return_value = True m.exists.return_value = True
@ -220,7 +219,7 @@ class TestGetDescription:
"""README exists but all lines strip to empty.""" """README exists but all lines strip to empty."""
mock_path = MagicMock(spec=Path) mock_path = MagicMock(spec=Path)
def truediv(_self: Any, name: str) -> MagicMock: def truediv(_self: object, name: str) -> MagicMock:
m = MagicMock(spec=Path) m = MagicMock(spec=Path)
if name == "README.md": if name == "README.md":
m.exists.return_value = True m.exists.return_value = True
@ -243,7 +242,7 @@ class TestGetDescription:
run_sh = MagicMock(spec=Path) run_sh = MagicMock(spec=Path)
run_sh.exists.return_value = True run_sh.exists.return_value = True
def truediv(_self: Any, name: str) -> MagicMock: def truediv(_self: object, name: str) -> MagicMock:
if name == "run.sh": if name == "run.sh":
return run_sh return run_sh
m = MagicMock(spec=Path) m = MagicMock(spec=Path)
@ -261,7 +260,7 @@ class TestGetDescription:
run_sh = MagicMock(spec=Path) run_sh = MagicMock(spec=Path)
run_sh.exists.return_value = True run_sh.exists.return_value = True
def truediv(_self: Any, name: str) -> MagicMock: def truediv(_self: object, name: str) -> MagicMock:
if name == "run.sh": if name == "run.sh":
return run_sh return run_sh
m = MagicMock(spec=Path) m = MagicMock(spec=Path)
@ -275,7 +274,7 @@ class TestGetDescription:
def test_no_readme_no_run_sh(self) -> None: def test_no_readme_no_run_sh(self) -> None:
mock_path = MagicMock(spec=Path) mock_path = MagicMock(spec=Path)
def truediv(_self: Any, _name: str) -> MagicMock: def truediv(_self: object, _name: str) -> MagicMock:
m = MagicMock(spec=Path) m = MagicMock(spec=Path)
m.exists.return_value = False m.exists.return_value = False
return m return m

View File

@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
from python_pkg.repo_explorer._execution import ExecutionMixin from python_pkg.repo_explorer._execution import ExecutionMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path
import subprocess import subprocess
# ── Protocol stub coverage ─────────────────────────────────────────── # ── Protocol stub coverage ───────────────────────────────────────────
@ -45,7 +46,7 @@ class StubExecution(ExecutionMixin):
self._path: Any = None self._path: Any = None
self._after_calls: list[tuple[Any, ...]] = [] self._after_calls: list[tuple[Any, ...]] = []
def _selected_path(self) -> Any: def _selected_path(self) -> Path | None:
return self._path return self._path
def after(self, ms: int, *args: object) -> str: def after(self, ms: int, *args: object) -> str:
@ -338,7 +339,11 @@ class TestReadPty:
read_calls = [0] read_calls = [0]
def fake_select(rlist: list[int], *_a: Any, **_kw: Any) -> Any: def fake_select(
_rlist: list[int],
*_a: object,
**_kw: object,
) -> tuple[list[int], list[object], list[object]]:
read_calls[0] += 1 read_calls[0] += 1
if read_calls[0] == 1: if read_calls[0] == 1:
# First call: return data (no newline → stays in buf) # First call: return data (no newline → stays in buf)
@ -393,7 +398,11 @@ class TestReadPty:
call_count = [0] call_count = [0]
def fake_select(rlist: list[int], *_a: Any, **_kw: Any) -> Any: def fake_select(
_rlist: list[int],
*_a: object,
**_kw: object,
) -> tuple[list[int], list[object], list[object]]:
call_count[0] += 1 call_count[0] += 1
if call_count[0] == 1: if call_count[0] == 1:
return ([10], [], []) return ([10], [], [])

View File

@ -10,6 +10,7 @@ from unittest.mock import MagicMock
from python_pkg.repo_explorer._execution import ExecutionMixin from python_pkg.repo_explorer._execution import ExecutionMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path
import subprocess import subprocess
@ -31,7 +32,7 @@ class StubExecution(ExecutionMixin):
self._path: Any = None self._path: Any = None
self._after_calls: list[tuple[Any, ...]] = [] self._after_calls: list[tuple[Any, ...]] = []
def _selected_path(self) -> Any: def _selected_path(self) -> Path | None:
return self._path return self._path
def after(self, ms: int, *args: object) -> str: def after(self, ms: int, *args: object) -> str:

View File

@ -4,13 +4,12 @@ from __future__ import annotations
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
import tkinter as tk import tkinter as tk
from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
# ── Helper to create a RepoExplorer without a real display ─────────── # ── Helper to create a RepoExplorer without a real display ───────────
def _make_explorer(**overrides: Any) -> Any: def _make_explorer(**overrides: object) -> object:
"""Build a RepoExplorer instance without a real Tk display. """Build a RepoExplorer instance without a real Tk display.
Mocks tk.Tk.__init__ and all GUI construction so no X server is needed. Mocks tk.Tk.__init__ and all GUI construction so no X server is needed.
@ -108,83 +107,73 @@ class TestBuildStyle:
class TestBuildUI: class TestBuildUI:
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Scrollbar") def test_build_ui_with_terminal(self) -> None:
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Treeview") with (
@patch("python_pkg.repo_explorer.repo_explorer.font.Font") patch(
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Button") "python_pkg.repo_explorer.repo_explorer.tk.StringVar"
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Entry") ) as mock_stringvar,
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Separator") patch("python_pkg.repo_explorer.repo_explorer.tk.Text") as mock_text,
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Label") patch(
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Frame") "python_pkg.repo_explorer.repo_explorer.ttk.PanedWindow"
@patch("python_pkg.repo_explorer.repo_explorer.ttk.PanedWindow") ) as mock_paned,
@patch("python_pkg.repo_explorer.repo_explorer.tk.Text") patch("python_pkg.repo_explorer.repo_explorer.ttk.Frame"),
@patch("python_pkg.repo_explorer.repo_explorer.tk.StringVar") patch("python_pkg.repo_explorer.repo_explorer.ttk.Label"),
def test_build_ui_with_terminal( patch("python_pkg.repo_explorer.repo_explorer.ttk.Separator"),
self, patch("python_pkg.repo_explorer.repo_explorer.ttk.Entry"),
mock_stringvar: MagicMock, patch("python_pkg.repo_explorer.repo_explorer.ttk.Button"),
mock_text: MagicMock, patch("python_pkg.repo_explorer.repo_explorer.font.Font"),
mock_paned: MagicMock, patch(
mock_frame: MagicMock, "python_pkg.repo_explorer.repo_explorer.ttk.Treeview"
mock_label: MagicMock, ) as mock_treeview,
mock_sep: MagicMock, patch("python_pkg.repo_explorer.repo_explorer.ttk.Scrollbar"),
mock_entry: MagicMock, ):
mock_button: MagicMock, app = _make_explorer()
mock_font: MagicMock, mock_sv = MagicMock()
mock_treeview: MagicMock, mock_stringvar.return_value = mock_sv
mock_scrollbar: MagicMock, paned = MagicMock()
) -> None: mock_paned.return_value = paned
app = _make_explorer()
mock_sv = MagicMock()
mock_stringvar.return_value = mock_sv
paned = MagicMock()
mock_paned.return_value = paned
tree = MagicMock() tree = MagicMock()
mock_treeview.return_value = tree mock_treeview.return_value = tree
text = MagicMock() text = MagicMock()
mock_text.return_value = text mock_text.return_value = text
app.pack = MagicMock() app.pack = MagicMock()
app._build_ui() app._build_ui()
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Scrollbar") def test_build_ui_no_terminal(self) -> None:
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Treeview") with (
@patch("python_pkg.repo_explorer.repo_explorer.font.Font") patch(
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Button") "python_pkg.repo_explorer.repo_explorer.tk.StringVar"
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Entry") ) as mock_stringvar,
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Separator") patch("python_pkg.repo_explorer.repo_explorer.tk.Text") as mock_text,
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Label") patch(
@patch("python_pkg.repo_explorer.repo_explorer.ttk.Frame") "python_pkg.repo_explorer.repo_explorer.ttk.PanedWindow"
@patch("python_pkg.repo_explorer.repo_explorer.ttk.PanedWindow") ) as mock_paned,
@patch("python_pkg.repo_explorer.repo_explorer.tk.Text") patch("python_pkg.repo_explorer.repo_explorer.ttk.Frame"),
@patch("python_pkg.repo_explorer.repo_explorer.tk.StringVar") patch("python_pkg.repo_explorer.repo_explorer.ttk.Label"),
def test_build_ui_no_terminal( patch("python_pkg.repo_explorer.repo_explorer.ttk.Separator"),
self, patch("python_pkg.repo_explorer.repo_explorer.ttk.Entry"),
mock_stringvar: MagicMock, patch("python_pkg.repo_explorer.repo_explorer.ttk.Button"),
mock_text: MagicMock, patch("python_pkg.repo_explorer.repo_explorer.font.Font"),
mock_paned: MagicMock, patch(
mock_frame: MagicMock, "python_pkg.repo_explorer.repo_explorer.ttk.Treeview"
mock_label: MagicMock, ) as mock_treeview,
mock_sep: MagicMock, patch("python_pkg.repo_explorer.repo_explorer.ttk.Scrollbar"),
mock_entry: MagicMock, ):
mock_button: MagicMock, app = _make_explorer(terminal_args=[])
mock_font: MagicMock, mock_sv = MagicMock()
mock_treeview: MagicMock, mock_stringvar.return_value = mock_sv
mock_scrollbar: MagicMock, paned = MagicMock()
) -> None: mock_paned.return_value = paned
app = _make_explorer(terminal_args=[])
mock_sv = MagicMock()
mock_stringvar.return_value = mock_sv
paned = MagicMock()
mock_paned.return_value = paned
tree = MagicMock() tree = MagicMock()
mock_treeview.return_value = tree mock_treeview.return_value = tree
text = MagicMock() text = MagicMock()
mock_text.return_value = text mock_text.return_value = text
app.pack = MagicMock() app.pack = MagicMock()
app._build_ui() app._build_ui()
# ── _load_projects ─────────────────────────────────────────────────── # ── _load_projects ───────────────────────────────────────────────────

View File

@ -196,7 +196,7 @@ def main(argv: Sequence[str] | None = None) -> int:
parser = _build_parser() parser = _build_parser()
args = parser.parse_args(argv) args = parser.parse_args(argv)
if not _trans._check_argos(): if not _trans.check_argos():
sys.stderr.write( sys.stderr.write(
"Error: argostranslate is not installed.\n" "Error: argostranslate is not installed.\n"
"Install it with: pip install argostranslate\n", "Install it with: pip install argostranslate\n",

View File

@ -67,7 +67,7 @@ class ArgosAvailableMock:
translator, "_ensure_language_pair", lambda _f, _t: None translator, "_ensure_language_pair", lambda _f, _t: None
) )
self._check_argos_patcher = patch.object( self._check_argos_patcher = patch.object(
translator, "_check_argos", return_value=True translator, "check_argos", return_value=True
) )
self._sys_modules_patcher.start() self._sys_modules_patcher.start()

View File

@ -15,9 +15,9 @@ from python_pkg.word_frequency import translator
@pytest.fixture @pytest.fixture
def _mock_argos_unavailable() -> Generator[None, None, None]: def mock_argos_unavailable() -> Generator[None, None, None]:
"""Mock argostranslate being unavailable (for legacy tests).""" """Mock argostranslate being unavailable (for legacy tests)."""
with patch.object(translator, "_check_argos", return_value=False): with patch.object(translator, "check_argos", return_value=False):
yield yield

View File

@ -28,9 +28,11 @@ class TestGetCacheDir:
"""Tests for get_cache_dir.""" """Tests for get_cache_dir."""
def test_returns_default(self, tmp_path: Path) -> None: def test_returns_default(self, tmp_path: Path) -> None:
with patch("python_pkg.word_frequency.cache.DEFAULT_CACHE_DIR", tmp_path): with (
with patch.dict("os.environ", {}, clear=False): patch("python_pkg.word_frequency.cache.DEFAULT_CACHE_DIR", tmp_path),
d = get_cache_dir() patch.dict("os.environ", {}, clear=False),
):
d = get_cache_dir()
assert d == tmp_path assert d == tmp_path
def test_respects_env_var(self, tmp_path: Path) -> None: def test_respects_env_var(self, tmp_path: Path) -> None:

View File

@ -142,14 +142,14 @@ class TestAnkiDeckCache:
cache = AnkiDeckCache(cache_dir=tmp_path) cache = AnkiDeckCache(cache_dir=tmp_path)
fp = tmp_path / "text.txt" fp = tmp_path / "text.txt"
fp.write_text("hello", encoding="utf-8") fp.write_text("hello", encoding="utf-8")
dk = AnkiDeckKey(fp, 10, "es", False, True) dk = AnkiDeckKey(fp, 10, "es", include_context=False, all_vocab=True)
assert cache.get(dk) is None assert cache.get(dk) is None
def test_get_hash_mismatch(self, tmp_path: Path) -> None: def test_get_hash_mismatch(self, tmp_path: Path) -> None:
cache = AnkiDeckCache(cache_dir=tmp_path) cache = AnkiDeckCache(cache_dir=tmp_path)
fp = tmp_path / "text.txt" fp = tmp_path / "text.txt"
fp.write_text("hello", encoding="utf-8") fp.write_text("hello", encoding="utf-8")
dk = AnkiDeckKey(fp, 10, "es", False, True) dk = AnkiDeckKey(fp, 10, "es", include_context=False, all_vocab=True)
cache.set(dk, "content", "hello", 1, 1) cache.set(dk, "content", "hello", 1, 1)
# Modify file to change hash # Modify file to change hash
fp.write_text("changed content", encoding="utf-8") fp.write_text("changed content", encoding="utf-8")
@ -160,7 +160,7 @@ class TestAnkiDeckCache:
cache = AnkiDeckCache(cache_dir=tmp_path) cache = AnkiDeckCache(cache_dir=tmp_path)
fp = tmp_path / "text.txt" fp = tmp_path / "text.txt"
fp.write_text("hello", encoding="utf-8") fp.write_text("hello", encoding="utf-8")
dk = AnkiDeckKey(fp, 10, "es", False, True) dk = AnkiDeckKey(fp, 10, "es", include_context=False, all_vocab=True)
cache.set(dk, "content", "hello", 1, 1) cache.set(dk, "content", "hello", 1, 1)
# Tamper with stored hash in metadata # Tamper with stored hash in metadata
m = cache._load_metadata() m = cache._load_metadata()
@ -174,7 +174,7 @@ class TestAnkiDeckCache:
cache = AnkiDeckCache(cache_dir=tmp_path) cache = AnkiDeckCache(cache_dir=tmp_path)
fp = tmp_path / "text.txt" fp = tmp_path / "text.txt"
fp.write_text("hello", encoding="utf-8") fp.write_text("hello", encoding="utf-8")
dk = AnkiDeckKey(fp, 10, "es", False, True) dk = AnkiDeckKey(fp, 10, "es", include_context=False, all_vocab=True)
cache.set(dk, "content", "hello", 1, 1) cache.set(dk, "content", "hello", 1, 1)
# Remove all .txt files in cache dir # Remove all .txt files in cache dir
for f in cache.cache_dir.glob("*.txt"): for f in cache.cache_dir.glob("*.txt"):
@ -185,7 +185,7 @@ class TestAnkiDeckCache:
cache = AnkiDeckCache(cache_dir=tmp_path) cache = AnkiDeckCache(cache_dir=tmp_path)
fp = tmp_path / "text.txt" fp = tmp_path / "text.txt"
fp.write_text("hello", encoding="utf-8") fp.write_text("hello", encoding="utf-8")
dk = AnkiDeckKey(fp, 10, "es", False, True) dk = AnkiDeckKey(fp, 10, "es", include_context=False, all_vocab=True)
cache.set(dk, "content", "hello", 1, 1) cache.set(dk, "content", "hello", 1, 1)
# Mock read_text to raise OSError # Mock read_text to raise OSError
with patch("pathlib.Path.read_text", side_effect=OSError("read error")): with patch("pathlib.Path.read_text", side_effect=OSError("read error")):
@ -212,7 +212,7 @@ class TestAnkiDeckCache:
cache = AnkiDeckCache(cache_dir=tmp_path) cache = AnkiDeckCache(cache_dir=tmp_path)
fp = tmp_path / "text.txt" fp = tmp_path / "text.txt"
fp.write_text("hello", encoding="utf-8") fp.write_text("hello", encoding="utf-8")
dk = AnkiDeckKey(fp, 10, "es", False, True) dk = AnkiDeckKey(fp, 10, "es", include_context=False, all_vocab=True)
cache.set(dk, "content", "hello", 1, 1) cache.set(dk, "content", "hello", 1, 1)
cache.clear() cache.clear()
assert cache.get(dk) is None assert cache.get(dk) is None
@ -226,7 +226,7 @@ class TestAnkiDeckCache:
cache = AnkiDeckCache(cache_dir=tmp_path) cache = AnkiDeckCache(cache_dir=tmp_path)
fp = tmp_path / "text.txt" fp = tmp_path / "text.txt"
fp.write_text("hello", encoding="utf-8") fp.write_text("hello", encoding="utf-8")
dk = AnkiDeckKey(fp, 10, "es", False, True) dk = AnkiDeckKey(fp, 10, "es", include_context=False, all_vocab=True)
cache.set(dk, "content", "hello", 1, 1) cache.set(dk, "content", "hello", 1, 1)
stats = cache.stats() stats = cache.stats()
assert stats["total_entries"] == 1 assert stats["total_entries"] == 1

View File

@ -118,19 +118,19 @@ class TestCaching:
mock.return_value.set.assert_called_once() mock.return_value.set.assert_called_once()
def test_get_cached_deck_force(self) -> None: def test_get_cached_deck_force(self) -> None:
key = AnkiDeckKey(Path("x"), 10, "es", False, True) key = AnkiDeckKey(Path("x"), 10, "es", include_context=False, all_vocab=True)
result = get_cached_deck(key, force=True) result = get_cached_deck(key, force=True)
assert result is None assert result is None
def test_get_cached_deck_delegates(self) -> None: def test_get_cached_deck_delegates(self) -> None:
key = AnkiDeckKey(Path("x"), 10, "es", False, True) key = AnkiDeckKey(Path("x"), 10, "es", include_context=False, all_vocab=True)
with patch("python_pkg.word_frequency._generation.get_anki_deck_cache") as mock: with patch("python_pkg.word_frequency._generation.get_anki_deck_cache") as mock:
mock.return_value.get.return_value = ("c", "e", 2, 5) mock.return_value.get.return_value = ("c", "e", 2, 5)
result = get_cached_deck(key) result = get_cached_deck(key)
assert result == ("c", "e", 2, 5) assert result == ("c", "e", 2, 5)
def test_cache_deck_delegates(self) -> None: def test_cache_deck_delegates(self) -> None:
key = AnkiDeckKey(Path("x"), 10, "es", False, True) key = AnkiDeckKey(Path("x"), 10, "es", include_context=False, all_vocab=True)
with patch("python_pkg.word_frequency._generation.get_anki_deck_cache") as mock: with patch("python_pkg.word_frequency._generation.get_anki_deck_cache") as mock:
cache_deck(key, "content", "excerpt", 2, 5) cache_deck(key, "content", "excerpt", 2, 5)
mock.return_value.set.assert_called_once() mock.return_value.set.assert_called_once()
@ -215,7 +215,7 @@ VOCAB_DUMP_END
"python_pkg.word_frequency._generation.get_anki_deck_cache" "python_pkg.word_frequency._generation.get_anki_deck_cache"
) as mock_cache, ) as mock_cache,
): ):
content, excerpt, num_words, max_rank = generate_flashcards( content, excerpt, _, _ = generate_flashcards(
fp, fp,
5, 5,
FlashcardOptions(source_lang="en"), FlashcardOptions(source_lang="en"),
@ -331,7 +331,7 @@ VOCAB_DUMP_END
), ),
patch("python_pkg.word_frequency._generation.get_anki_deck_cache"), patch("python_pkg.word_frequency._generation.get_anki_deck_cache"),
): ):
content, excerpt, num_words, max_rank = generate_flashcards( content, _, _, _ = generate_flashcards(
fp, 5, FlashcardOptions(source_lang=None, no_translate=True) fp, 5, FlashcardOptions(source_lang=None, no_translate=True)
) )
assert content == "deck" assert content == "deck"

View File

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from collections import Counter from collections import Counter
from typing import Any
from unittest.mock import patch from unittest.mock import patch
from python_pkg.word_frequency._learning_batch import ( from python_pkg.word_frequency._learning_batch import (
@ -106,8 +105,8 @@ class TestGenerateBatchSectionWithTranslation:
def fake_batch( def fake_batch(
words: list[str], words: list[str],
from_lang: Any, _from_lang: str | None,
to_lang: Any, _to_lang: str | None,
) -> list[TranslationResult]: ) -> list[TranslationResult]:
return [ return [
TranslationResult( TranslationResult(

View File

@ -29,7 +29,7 @@ if TYPE_CHECKING:
@pytest.fixture @pytest.fixture
def _mock_translation() -> Generator[MagicMock, None, None]: def mock_translation() -> Generator[MagicMock, None, None]:
"""Mock translation to avoid requiring argostranslate.""" """Mock translation to avoid requiring argostranslate."""
def fake_batch_translate( def fake_batch_translate(
@ -262,7 +262,7 @@ class TestMain:
"""Tests for main CLI function.""" """Tests for main CLI function."""
def test_basic_text_input( def test_basic_text_input(
self, caplog: pytest.LogCaptureFixture, _mock_translation: None self, caplog: pytest.LogCaptureFixture, mock_translation: None
) -> None: ) -> None:
"""Test with text input.""" """Test with text input."""
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):
@ -280,7 +280,7 @@ class TestMain:
assert "LANGUAGE LEARNING LESSON" in caplog.text assert "LANGUAGE LEARNING LESSON" in caplog.text
def test_file_input( def test_file_input(
self, tmp_path: Path, caplog: pytest.LogCaptureFixture, _mock_translation: None self, tmp_path: Path, caplog: pytest.LogCaptureFixture, mock_translation: None
) -> None: ) -> None:
"""Test with file input.""" """Test with file input."""
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
@ -300,7 +300,7 @@ class TestMain:
assert exit_code == 0 assert exit_code == 0
assert "hello" in caplog.text.lower() assert "hello" in caplog.text.lower()
def test_output_to_file(self, tmp_path: Path, _mock_translation: None) -> None: def test_output_to_file(self, tmp_path: Path, mock_translation: None) -> None:
"""Test outputting to file.""" """Test outputting to file."""
output_file = tmp_path / "lesson.txt" output_file = tmp_path / "lesson.txt"
@ -319,7 +319,7 @@ class TestMain:
content = output_file.read_text(encoding="utf-8") content = output_file.read_text(encoding="utf-8")
assert "LANGUAGE LEARNING LESSON" in content assert "LANGUAGE LEARNING LESSON" in content
def test_custom_stopwords(self, tmp_path: Path, _mock_translation: None) -> None: def test_custom_stopwords(self, tmp_path: Path, mock_translation: None) -> None:
"""Test with custom stopwords file.""" """Test with custom stopwords file."""
stopwords_file = tmp_path / "stop.txt" stopwords_file = tmp_path / "stop.txt"
stopwords_file.write_text("hello\n", encoding="utf-8") stopwords_file.write_text("hello\n", encoding="utf-8")
@ -340,7 +340,7 @@ class TestMain:
# "hello" should be filtered by custom stopwords # "hello" should be filtered by custom stopwords
def test_multiple_batches_option( def test_multiple_batches_option(
self, caplog: pytest.LogCaptureFixture, _mock_translation: None self, caplog: pytest.LogCaptureFixture, mock_translation: None
) -> None: ) -> None:
"""Test --batches option.""" """Test --batches option."""
text = " ".join(f"word{i}" * (50 - i) for i in range(30)) text = " ".join(f"word{i}" * (50 - i) for i in range(30))
@ -385,7 +385,7 @@ class TestMain:
assert exit_code == 1 assert exit_code == 1
def test_output_to_file_branch( def test_output_to_file_branch(
self, tmp_path: Path, _mock_translation: None self, tmp_path: Path, mock_translation: None
) -> None: ) -> None:
"""Test --output to verify the file writing path.""" """Test --output to verify the file writing path."""
out = tmp_path / "out.txt" out = tmp_path / "out.txt"

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from unittest.mock import patch from unittest.mock import patch
from python_pkg.word_frequency._learning_constants import LessonConfig from python_pkg.word_frequency._learning_constants import LessonConfig
@ -19,8 +18,8 @@ class TestDoTranslateBranch:
def fake_batch( def fake_batch(
words: list[str], words: list[str],
from_lang: Any, _from_lang: str | None,
to_lang: Any, _to_lang: str | None,
) -> list[TranslationResult]: ) -> list[TranslationResult]:
return [ return [
TranslationResult( TranslationResult(
@ -56,8 +55,8 @@ class TestDoTranslateBranch:
def fake_batch( def fake_batch(
words: list[str], words: list[str],
from_lang: Any, from_lang: str | None,
to_lang: Any, to_lang: str | None,
) -> list[TranslationResult]: ) -> list[TranslationResult]:
return [ return [
TranslationResult( TranslationResult(

View File

@ -109,7 +109,7 @@ VOCAB_DUMP_END
Excerpt: Excerpt:
"hello world foo" "hello world foo"
""" """
excerpt, length, max_rank, vocab = parse_inverse_mode_output(output) _, length, max_rank, _ = parse_inverse_mode_output(output)
assert length == 3 assert length == 3
assert max_rank == 0 assert max_rank == 0
@ -122,17 +122,17 @@ Excerpt:
def test_short_longest_excerpt_line(self) -> None: def test_short_longest_excerpt_line(self) -> None:
output = "LONGEST EXCERPT: 0" output = "LONGEST EXCERPT: 0"
excerpt, length, max_rank, vocab = parse_inverse_mode_output(output) _, length, _, _ = parse_inverse_mode_output(output)
assert length == 0 assert length == 0
def test_too_few_parts_in_longest_excerpt(self) -> None: def test_too_few_parts_in_longest_excerpt(self) -> None:
output = "LONGEST EXCERPT:" output = "LONGEST EXCERPT:"
excerpt, length, max_rank, vocab = parse_inverse_mode_output(output) _, length, _, _ = parse_inverse_mode_output(output)
assert length == 0 assert length == 0
def test_rarest_word_without_hash_number(self) -> None: def test_rarest_word_without_hash_number(self) -> None:
output = "Rarest word used: unknown" output = "Rarest word used: unknown"
excerpt, length, max_rank, vocab = parse_inverse_mode_output(output) _, _, max_rank, _ = parse_inverse_mode_output(output)
assert max_rank == 0 assert max_rank == 0
@ -165,7 +165,7 @@ class TestParseTargetLengthBlock:
"[Length 3] Vocab needed: 2", "[Length 3] Vocab needed: 2",
" Words: hello(#1)", " Words: hello(#1)",
] ]
excerpt, words = _parse_target_length_block(lines, 3) excerpt, _ = _parse_target_length_block(lines, 3)
assert excerpt == "" assert excerpt == ""
def test_no_words_line(self) -> None: def test_no_words_line(self) -> None:

View File

@ -195,7 +195,7 @@ class TestTranslateWordsBatch:
mock_parent.package = mock_package_module mock_parent.package = mock_package_module
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",

View File

@ -158,7 +158,7 @@ class TestHandleTranslation:
_cli._trans, _cli._trans,
"translate_words_batch", "translate_words_batch",
return_value=[ return_value=[
TranslationResult("hello", "hola", "en", "es", True), TranslationResult("hello", "hola", "en", "es", success=True),
], ],
), ),
patch.object( patch.object(
@ -195,7 +195,7 @@ class TestHandleTranslation:
_cli._trans, _cli._trans,
"translate_words_batch", "translate_words_batch",
return_value=[ return_value=[
TranslationResult("hello", "hola", "en", "es", True), TranslationResult("hello", "hola", "en", "es", success=True),
], ],
), ),
patch.object( patch.object(
@ -219,8 +219,15 @@ class TestHandleTranslation:
_cli._trans, _cli._trans,
"translate_words_batch", "translate_words_batch",
return_value=[ return_value=[
TranslationResult("hello", "hola", "en", "es", True), TranslationResult("hello", "hola", "en", "es", success=True),
TranslationResult("xyz", "", "en", "es", False, "error"), TranslationResult(
"xyz",
"",
"en",
"es",
success=False,
error="error",
),
], ],
), ),
patch.object( patch.object(
@ -237,7 +244,7 @@ class TestMain:
"""Tests for main entry point.""" """Tests for main entry point."""
def test_argos_not_available(self, capsys: pytest.CaptureFixture[str]) -> None: def test_argos_not_available(self, capsys: pytest.CaptureFixture[str]) -> None:
with patch.object(_cli._trans, "_check_argos", return_value=False): with patch.object(_cli._trans, "check_argos", return_value=False):
result = main(["--text", "hello", "--from", "en", "--to", "es"]) result = main(["--text", "hello", "--from", "en", "--to", "es"])
assert result == 1 assert result == 1
captured = capsys.readouterr() captured = capsys.readouterr()
@ -245,7 +252,7 @@ class TestMain:
def test_list_languages(self) -> None: def test_list_languages(self) -> None:
with ( with (
patch.object(_cli._trans, "_check_argos", return_value=True), patch.object(_cli._trans, "check_argos", return_value=True),
patch.object( patch.object(
_cli._trans, _cli._trans,
"get_installed_languages", "get_installed_languages",
@ -257,7 +264,7 @@ class TestMain:
def test_list_available(self) -> None: def test_list_available(self) -> None:
with ( with (
patch.object(_cli._trans, "_check_argos", return_value=True), patch.object(_cli._trans, "check_argos", return_value=True),
patch.object(_cli._trans, "get_available_packages", return_value=[]), patch.object(_cli._trans, "get_available_packages", return_value=[]),
): ):
result = main(["--list-available"]) result = main(["--list-available"])
@ -265,7 +272,7 @@ class TestMain:
def test_download(self, capsys: pytest.CaptureFixture[str]) -> None: def test_download(self, capsys: pytest.CaptureFixture[str]) -> None:
with ( with (
patch.object(_cli._trans, "_check_argos", return_value=True), patch.object(_cli._trans, "check_argos", return_value=True),
patch.object( patch.object(
_cli._trans, _cli._trans,
"download_languages", "download_languages",
@ -276,7 +283,7 @@ class TestMain:
assert result == 0 assert result == 0
def test_no_input_shows_help(self) -> None: def test_no_input_shows_help(self) -> None:
with patch.object(_cli._trans, "_check_argos", return_value=True): with patch.object(_cli._trans, "check_argos", return_value=True):
result = main([]) result = main([])
assert result == 1 assert result == 1
@ -284,7 +291,7 @@ class TestMain:
self, capsys: pytest.CaptureFixture[str] self, capsys: pytest.CaptureFixture[str]
) -> None: ) -> None:
with ( with (
patch.object(_cli._trans, "_check_argos", return_value=True), patch.object(_cli._trans, "check_argos", return_value=True),
patch.object( patch.object(
_cli._trans, _cli._trans,
"read_file", "read_file",

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import importlib import importlib
import tempfile
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -235,7 +236,7 @@ class TestEnsureLanguagePair:
mock_pkg = MagicMock() mock_pkg = MagicMock()
mock_pkg.from_code = "en" mock_pkg.from_code = "en"
mock_pkg.to_code = "es" mock_pkg.to_code = "es"
mock_pkg.download.return_value = "/tmp/pkg.argosmodel" mock_pkg.download.return_value = tempfile.gettempdir() + "/pkg.argosmodel"
mock_argos = MagicMock() mock_argos = MagicMock()
mock_argos.translate.get_installed_languages.return_value = [ mock_argos.translate.get_installed_languages.return_value = [
mock_from, mock_from,
@ -262,7 +263,7 @@ class TestEnsureLanguagePair:
mock_pkg = MagicMock() mock_pkg = MagicMock()
mock_pkg.from_code = "en" mock_pkg.from_code = "en"
mock_pkg.to_code = "es" mock_pkg.to_code = "es"
mock_pkg.download.return_value = "/tmp/pkg" mock_pkg.download.return_value = tempfile.gettempdir() + "/pkg"
mock_argos = MagicMock() mock_argos = MagicMock()
mock_argos.translate.get_installed_languages.return_value = [mock_to] mock_argos.translate.get_installed_languages.return_value = [mock_to]
mock_argos.package.get_available_packages.return_value = [mock_pkg] mock_argos.package.get_available_packages.return_value = [mock_pkg]
@ -275,14 +276,14 @@ class TestFormatTranslations:
def test_failed_with_no_error(self) -> None: def test_failed_with_no_error(self) -> None:
results = [ results = [
TranslationResult("xyz", "", "en", "es", False), TranslationResult("xyz", "", "en", "es", success=False),
] ]
output = format_translations(results) output = format_translations(results)
assert "[Failed]" in output assert "[Failed]" in output
def test_all_failed_max_trans(self) -> None: def test_all_failed_max_trans(self) -> None:
results = [ results = [
TranslationResult("xyz", "", "en", "es", False, "err"), TranslationResult("xyz", "", "en", "es", success=False, error="err"),
] ]
output = format_translations(results) output = format_translations(results)
assert "Translation" in output assert "Translation" in output

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import tempfile
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -30,7 +31,7 @@ if TYPE_CHECKING:
class TestGetInstalledLanguages: class TestGetInstalledLanguages:
"""Tests for get_installed_languages function.""" """Tests for get_installed_languages function."""
def test_argos_unavailable(self, _mock_argos_unavailable: None) -> None: def test_argos_unavailable(self, mock_argos_unavailable: None) -> None:
"""Test when argos is unavailable.""" """Test when argos is unavailable."""
result = get_installed_languages() result = get_installed_languages()
assert result == [] assert result == []
@ -56,7 +57,7 @@ class TestGetInstalledLanguages:
mock_parent.package = mock_package_module mock_parent.package = mock_package_module
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",
@ -79,7 +80,7 @@ class TestGetInstalledLanguages:
class TestGetAvailablePackages: class TestGetAvailablePackages:
"""Tests for get_available_packages function.""" """Tests for get_available_packages function."""
def test_argos_unavailable(self, _mock_argos_unavailable: None) -> None: def test_argos_unavailable(self, mock_argos_unavailable: None) -> None:
"""Test when argos is unavailable.""" """Test when argos is unavailable."""
result = get_available_packages() result = get_available_packages()
assert result == [] assert result == []
@ -91,7 +92,7 @@ class TestGetAvailablePackages:
class TestDownloadLanguages: class TestDownloadLanguages:
"""Tests for download_languages function.""" """Tests for download_languages function."""
def test_argos_unavailable(self, _mock_argos_unavailable: None) -> None: def test_argos_unavailable(self, mock_argos_unavailable: None) -> None:
"""Test when argos is unavailable.""" """Test when argos is unavailable."""
result = download_languages(["en", "es"]) result = download_languages(["en", "es"])
assert result == {} assert result == {}
@ -124,7 +125,7 @@ class TestReadFile:
class TestMain: class TestMain:
"""Tests for main CLI function.""" """Tests for main CLI function."""
def test_argos_unavailable_error(self, _mock_argos_unavailable: None) -> None: def test_argos_unavailable_error(self, mock_argos_unavailable: None) -> None:
"""Test error when argos not installed.""" """Test error when argos not installed."""
result = main(["--text", "hello", "--from", "en", "--to", "es"]) result = main(["--text", "hello", "--from", "en", "--to", "es"])
assert result == 1 assert result == 1
@ -139,7 +140,7 @@ class TestMain:
mock_parent.package = mock_package_module mock_parent.package = mock_package_module
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",
@ -172,7 +173,7 @@ class TestMain:
mock_parent.package = mock_package_module mock_parent.package = mock_package_module
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",
@ -326,7 +327,7 @@ class TestGetAvailablePackagesWithArgos:
mock_parent.translate = mock_translate mock_parent.translate = mock_translate
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",
@ -348,7 +349,7 @@ class TestDownloadLanguagesFull:
pkg = MagicMock() pkg = MagicMock()
pkg.from_code = "en" pkg.from_code = "en"
pkg.to_code = "es" pkg.to_code = "es"
pkg.download.return_value = "/tmp/fake.argosmodel" pkg.download.return_value = tempfile.gettempdir() + "/fake.argosmodel"
mock_package = MagicMock() mock_package = MagicMock()
mock_package.update_package_index.return_value = None mock_package.update_package_index.return_value = None
@ -359,7 +360,7 @@ class TestDownloadLanguagesFull:
mock_parent.translate = mock_translate mock_parent.translate = mock_translate
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",
@ -384,7 +385,7 @@ class TestDownloadLanguagesFull:
mock_parent.translate = mock_translate mock_parent.translate = mock_translate
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",
@ -414,7 +415,7 @@ class TestDownloadLanguagesFull:
mock_parent.translate = mock_translate mock_parent.translate = mock_translate
with ( with (
patch.object(translator, "_check_argos", return_value=True), patch.object(translator, "check_argos", return_value=True),
patch.object(translator, "argostranslate", mock_parent, create=True), patch.object(translator, "argostranslate", mock_parent, create=True),
patch.dict( patch.dict(
"sys.modules", "sys.modules",

View File

@ -65,7 +65,7 @@ logger = logging.getLogger(__name__)
_BATCH_SIZE = 100 _BATCH_SIZE = 100
def _check_argos() -> bool: def check_argos() -> bool:
"""Check if argostranslate is available.""" """Check if argostranslate is available."""
return argostranslate is not None return argostranslate is not None
@ -76,7 +76,7 @@ def get_installed_languages() -> list[tuple[str, str]]:
Returns: Returns:
List of (code, name) tuples for installed languages. List of (code, name) tuples for installed languages.
""" """
if not _check_argos(): if not check_argos():
return [] return []
languages = argostranslate.translate.get_installed_languages() languages = argostranslate.translate.get_installed_languages()
@ -89,7 +89,7 @@ def get_available_packages() -> list[tuple[str, str, str, str]]:
Returns: Returns:
List of (from_code, from_name, to_code, to_name) tuples. List of (from_code, from_name, to_code, to_name) tuples.
""" """
if not _check_argos(): if not check_argos():
return [] return []
argostranslate.package.update_package_index() argostranslate.package.update_package_index()
@ -111,7 +111,7 @@ def download_languages(lang_codes: Sequence[str]) -> dict[str, bool]:
Returns: Returns:
Dict mapping "from->to" to success boolean. Dict mapping "from->to" to success boolean.
""" """
if not _check_argos(): if not check_argos():
return {} return {}
results: dict[str, bool] = {} results: dict[str, bool] = {}