From ba9b14b71bdc8058535ef824ef9e7069bd2a89fc Mon Sep 17 00:00:00 2001 From: Jens <1742418+1cu@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:17:51 +0100 Subject: [PATCH] fix: address codeql notes and warnings (#740) --- scripts/post_autopep8.py | 2 +- src/kleinanzeigen_bot/__init__.py | 10 ++ src/kleinanzeigen_bot/extract.py | 11 +- src/kleinanzeigen_bot/utils/loggers.py | 19 ++- .../utils/web_scraping_mixin.py | 2 + tests/unit/test_extract.py | 157 ++++++++++-------- tests/unit/test_init.py | 26 ++- tests/unit/test_price_reduction.py | 2 +- tests/unit/test_web_scraping_mixin.py | 2 +- 9 files changed, 128 insertions(+), 103 deletions(-) diff --git a/scripts/post_autopep8.py b/scripts/post_autopep8.py index d40c7d8..ff650ca 100644 --- a/scripts/post_autopep8.py +++ b/scripts/post_autopep8.py @@ -18,7 +18,7 @@ class FormatterRule(Protocol): """ def apply(self, tree:ast.AST, lines:List[str], path:Path) -> List[str]: - ... + raise NotImplementedError class NoSpaceAfterColonInTypeAnnotationRule(FormatterRule): diff --git a/src/kleinanzeigen_bot/__init__.py b/src/kleinanzeigen_bot/__init__.py index 5b71ba0..4f60429 100644 --- a/src/kleinanzeigen_bot/__init__.py +++ b/src/kleinanzeigen_bot/__init__.py @@ -746,6 +746,7 @@ class KleinanzeigenBot(WebScrapingMixin): await ainput(_("Press a key to continue...")) except TimeoutError: + # No captcha detected within timeout. pass async def login(self) -> None: @@ -796,6 +797,7 @@ class KleinanzeigenBot(WebScrapingMixin): LOG.warning("############################################") await ainput("Press ENTER when done...") except TimeoutError: + # No SMS verification prompt detected. pass try: @@ -807,6 +809,7 @@ class KleinanzeigenBot(WebScrapingMixin): "//div[@id='ConsentManagementPage']//*//button//*[contains(., 'Alle ablehnen und fortfahren')]", timeout = gdpr_timeout) except TimeoutError: + # GDPR banner not shown within timeout. pass async def is_logged_in(self) -> bool: @@ -994,6 +997,7 @@ class KleinanzeigenBot(WebScrapingMixin): try: await self.web_select(By.CSS_SELECTOR, "select#price-type-react, select#micro-frontend-price-type, select#priceType", price_type) except TimeoutError: + # Price type selector not present on this page variant. pass if ad_cfg.price: if mode == AdUpdateStrategy.MODIFY: @@ -1112,6 +1116,7 @@ class KleinanzeigenBot(WebScrapingMixin): if not ad_cfg.images and await self.web_check(By.XPATH, image_hint_xpath, Is.DISPLAYED): await self.web_click(By.XPATH, image_hint_xpath) except TimeoutError: + # Image hint not shown; continue publish flow. pass # nosec ############################# @@ -1127,6 +1132,7 @@ class KleinanzeigenBot(WebScrapingMixin): await self.web_scroll_page_down() await ainput(_("Press a key to continue...")) except TimeoutError: + # Payment form not present. pass confirmation_timeout = self._timeout("publishing_confirmation") @@ -1234,6 +1240,7 @@ class KleinanzeigenBot(WebScrapingMixin): if await self.web_text(By.ID, "postad-category-path"): is_category_auto_selected = True except TimeoutError: + # Category auto-selection indicator not available within timeout. pass if category: @@ -1267,6 +1274,7 @@ class KleinanzeigenBot(WebScrapingMixin): if not await self.web_check(By.XPATH, select_container_xpath, Is.DISPLAYED): await (await self.web_find(By.XPATH, select_container_xpath)).apply("elem => elem.singleNodeValue.style.display = 'block'") except TimeoutError: + # Skip visibility adjustment when container cannot be located in time. pass # nosec try: @@ -1341,6 +1349,7 @@ class KleinanzeigenBot(WebScrapingMixin): await self.web_click(By.XPATH, '//dialog//button[contains(., "Andere Versandmethoden")]') except TimeoutError: + # Dialog option not present; already on the individual shipping page. pass try: @@ -1350,6 +1359,7 @@ class KleinanzeigenBot(WebScrapingMixin): '//input[contains(@placeholder, "Versandkosten (optional)")]', timeout = short_timeout) except TimeoutError: + # Input not visible yet; click the individual shipping option. await self.web_click(By.XPATH, '//*[contains(@id, "INDIVIDUAL") and contains(@data-testid, "Individueller Versand")]') if ad_cfg.shipping_costs is not None: diff --git a/src/kleinanzeigen_bot/extract.py b/src/kleinanzeigen_bot/extract.py index cc9b5cf..a56c0c1 100644 --- a/src/kleinanzeigen_bot/extract.py +++ b/src/kleinanzeigen_bot/extract.py @@ -59,7 +59,10 @@ class AdExtractor(WebScrapingMixin): # Save the ad configuration file (offload to executor to avoid blocking the event loop) ad_file_path = str(Path(final_dir) / f"ad_{ad_id}.yaml") - header_string = "# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/refs/heads/main/schemas/ad.schema.json" + header_string = ( + "# yaml-language-server: $schema=" + "https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/refs/heads/main/schemas/ad.schema.json" + ) await asyncio.get_running_loop().run_in_executor( None, lambda: dicts.save_dict(ad_file_path, ad_cfg.model_dump(), header = header_string) @@ -141,9 +144,6 @@ class AdExtractor(WebScrapingMixin): :return: the ad ID, a (ten-digit) integer number """ - num_part = url.rsplit("/", maxsplit = 1)[-1] # suffix - id_part = num_part.split("-", maxsplit = 1)[0] - try: path = url.split("?", maxsplit = 1)[0] # Remove query string if present last_segment = path.rstrip("/").rsplit("/", maxsplit = 1)[-1] # Get last path component @@ -165,7 +165,7 @@ class AdExtractor(WebScrapingMixin): # Try to find the main ad list container first try: - ad_list_container = await self.web_find(By.ID, "my-manageitems-adlist") + _ = await self.web_find(By.ID, "my-manageitems-adlist") except TimeoutError: LOG.warning("Ad list container #my-manageitems-adlist not found. Maybe no ads present?") return [] @@ -290,6 +290,7 @@ class AdExtractor(WebScrapingMixin): await self.web_click(By.CLASS_NAME, "mfp-close") await self.web_sleep() except TimeoutError: + # Popup did not appear within timeout. pass return True diff --git a/src/kleinanzeigen_bot/utils/loggers.py b/src/kleinanzeigen_bot/utils/loggers.py index 09ffdbf..fa1269c 100644 --- a/src/kleinanzeigen_bot/utils/loggers.py +++ b/src/kleinanzeigen_bot/utils/loggers.py @@ -3,14 +3,11 @@ # SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ import copy, logging, os, re, sys # isort: skip from gettext import gettext as _ -from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, Logger from logging.handlers import RotatingFileHandler from typing import Any, Final # @UnusedImport import colorama -from . import i18n, reflect - __all__ = [ "Logger", "LogFileHandle", @@ -26,7 +23,14 @@ __all__ = [ "is_debug" ] -LOG_ROOT:Final[logging.Logger] = logging.getLogger() +CRITICAL = logging.CRITICAL +DEBUG = logging.DEBUG +ERROR = logging.ERROR +INFO = logging.INFO +WARNING = logging.WARNING +Logger = logging.Logger + +LOG_ROOT:Final[Logger] = logging.getLogger() class _MaxLevelFilter(logging.Filter): @@ -141,7 +145,7 @@ def configure_console_logging() -> None: class LogFileHandle: """Encapsulates a log file handler with close and status methods.""" - def __init__(self, file_path:str, handler:RotatingFileHandler, logger:logging.Logger) -> None: + def __init__(self, file_path:str, handler:RotatingFileHandler, logger:Logger) -> None: self.file_path = file_path self._handler:RotatingFileHandler | None = handler self._logger = logger @@ -183,15 +187,16 @@ def flush_all_handlers() -> None: handler.flush() -def get_logger(name:str | None = None) -> logging.Logger: +def get_logger(name:str | None = None) -> Logger: """ Returns a localized logger """ - class TranslatingLogger(logging.Logger): + class TranslatingLogger(Logger): def _log(self, level:int, msg:object, *args:Any, **kwargs:Any) -> None: if level != DEBUG: # debug messages should not be translated + from . import i18n, reflect # noqa: PLC0415 # avoid cyclic import at module load msg = i18n.translate(msg, reflect.get_caller(2)) super()._log(level, msg, *args, **kwargs) diff --git a/src/kleinanzeigen_bot/utils/web_scraping_mixin.py b/src/kleinanzeigen_bot/utils/web_scraping_mixin.py index 56eb528..7a591fa 100644 --- a/src/kleinanzeigen_bot/utils/web_scraping_mixin.py +++ b/src/kleinanzeigen_bot/utils/web_scraping_mixin.py @@ -474,6 +474,7 @@ class WebScrapingMixin: if is_relevant_browser: browser_processes.append(proc.info) except (psutil.NoSuchProcess, psutil.AccessDenied): + # Process ended or is not accessible; skip it. pass except (psutil.Error, PermissionError) as exc: LOG.warning("(warn) Unable to inspect browser processes: %s", exc) @@ -518,6 +519,7 @@ class WebScrapingMixin: self.page = None # pyright: ignore[reportAttributeAccessIssue] def get_compatible_browser(self) -> str: + browser_paths:list[str | None] = [] match platform.system(): case "Linux": browser_paths = [ diff --git a/tests/unit/test_extract.py b/tests/unit/test_extract.py index 4468e2c..63db581 100644 --- a/tests/unit/test_extract.py +++ b/tests/unit/test_extract.py @@ -1076,29 +1076,31 @@ class TestAdExtractorDownload: page_mock.url = "https://www.kleinanzeigen.de/s-anzeige/test/12345" extractor.page = page_mock - with patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ + with ( + patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ "Test Title", # Title extraction "Test Title", # Second title call for full extraction "Description text", # Description "03.02.2025" # Creation date - ]), \ - patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { - "universalAnalyticsOpts": { - "dimensions": { - "dimension92": "", - "dimension108": "" - } + ]), + patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { + "universalAnalyticsOpts": { + "dimensions": { + "dimension92": "", + "dimension108": "" } - }), \ - patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), \ - patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), \ - patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), \ - patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), \ - patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), \ - patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), \ - patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( - name = "Test", zipcode = "12345", location = "Berlin" - )): + } + }), + patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), + patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), + patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), + patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), + patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), + patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), + patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( + name = "Test", zipcode = "12345", location = "Berlin" + )), + ): ad_cfg, result_dir = await extractor._extract_ad_page_info_with_directory_handling( base_dir, 12345 @@ -1133,29 +1135,31 @@ class TestAdExtractorDownload: page_mock.url = "https://www.kleinanzeigen.de/s-anzeige/test/12345" extractor.page = page_mock - with patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ + with ( + patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ "Test Title", # Title extraction "Test Title", # Second title call for full extraction "Description text", # Description "03.02.2025" # Creation date - ]), \ - patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { - "universalAnalyticsOpts": { - "dimensions": { - "dimension92": "", - "dimension108": "" - } + ]), + patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { + "universalAnalyticsOpts": { + "dimensions": { + "dimension92": "", + "dimension108": "" } - }), \ - patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), \ - patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), \ - patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), \ - patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), \ - patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), \ - patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), \ - patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( - name = "Test", zipcode = "12345", location = "Berlin" - )): + } + }), + patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), + patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), + patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), + patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), + patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), + patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), + patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( + name = "Test", zipcode = "12345", location = "Berlin" + )), + ): ad_cfg, result_dir = await extractor._extract_ad_page_info_with_directory_handling( base_dir, 12345 @@ -1192,29 +1196,31 @@ class TestAdExtractorDownload: page_mock.url = "https://www.kleinanzeigen.de/s-anzeige/test/12345" extractor.page = page_mock - with patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ + with ( + patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ "Test Title", # Title extraction "Test Title", # Second title call for full extraction "Description text", # Description "03.02.2025" # Creation date - ]), \ - patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { - "universalAnalyticsOpts": { - "dimensions": { - "dimension92": "", - "dimension108": "" - } + ]), + patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { + "universalAnalyticsOpts": { + "dimensions": { + "dimension92": "", + "dimension108": "" } - }), \ - patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), \ - patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), \ - patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), \ - patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), \ - patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), \ - patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), \ - patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( - name = "Test", zipcode = "12345", location = "Berlin" - )): + } + }), + patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), + patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), + patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), + patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), + patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), + patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), + patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( + name = "Test", zipcode = "12345", location = "Berlin" + )), + ): ad_cfg, result_dir = await extractor._extract_ad_page_info_with_directory_handling( base_dir, 12345 @@ -1246,29 +1252,31 @@ class TestAdExtractorDownload: base_dir = tmp_path / "downloaded-ads" base_dir.mkdir() - with patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ + with ( + patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = [ title_with_umlauts, # Title extraction title_with_umlauts, # Second title call for full extraction "Description text", # Description "03.02.2025" # Creation date - ]), \ - patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { - "universalAnalyticsOpts": { - "dimensions": { - "dimension92": "", - "dimension108": "" - } + ]), + patch.object(extractor, "web_execute", new_callable = AsyncMock, return_value = { + "universalAnalyticsOpts": { + "dimensions": { + "dimension92": "", + "dimension108": "" } - }), \ - patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), \ - patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), \ - patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), \ - patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), \ - patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), \ - patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), \ - patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( - name = "Test", zipcode = "12345", location = "Berlin" - )): + } + }), + patch.object(extractor, "_extract_category_from_ad_page", new_callable = AsyncMock, return_value = "160"), + patch.object(extractor, "_extract_special_attributes_from_ad_page", new_callable = AsyncMock, return_value = {}), + patch.object(extractor, "_extract_pricing_info_from_ad_page", new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), + patch.object(extractor, "_extract_shipping_info_from_ad_page", new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), + patch.object(extractor, "_extract_sell_directly_from_ad_page", new_callable = AsyncMock, return_value = False), + patch.object(extractor, "_download_images_from_ad_page", new_callable = AsyncMock, return_value = []), + patch.object(extractor, "_extract_contact_from_ad_page", new_callable = AsyncMock, return_value = ContactPartial( + name = "Test", zipcode = "12345", location = "Berlin" + )), + ): ad_cfg, result_dir = await extractor._extract_ad_page_info_with_directory_handling( base_dir, 12345 @@ -1285,7 +1293,10 @@ class TestAdExtractorDownload: from kleinanzeigen_bot.utils import dicts # noqa: PLC0415 - header_string = "# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/refs/heads/main/schemas/ad.schema.json" + header_string = ( + "# yaml-language-server: $schema=" + "https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/refs/heads/main/schemas/ad.schema.json" + ) # save_dict normalizes path to NFC, matching the NFC directory name dicts.save_dict(str(ad_file_path), ad_cfg.model_dump(), header = header_string) diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index 2485f01..d8214e3 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -1582,6 +1582,14 @@ def test_file_logger_writes_message(tmp_path:Path, caplog:pytest.LogCaptureFixtu assert "Logger test log message" in contents +def _apply_price_reduction_persistence(count:int | None) -> dict[str, Any]: + """Return a dict with price_reduction_count only when count is positive (count -> dict[str, Any]).""" + ad_cfg_orig:dict[str, Any] = {} + if count is not None and count > 0: + ad_cfg_orig["price_reduction_count"] = count + return ad_cfg_orig + + class TestPriceReductionPersistence: """Tests for price_reduction_count persistence logic.""" @@ -1589,12 +1597,8 @@ class TestPriceReductionPersistence: def test_persistence_logic_saves_when_count_positive(self) -> None: """Test the conditional logic that decides whether to persist price_reduction_count.""" # Simulate the logic from publish_ad lines 1076-1079 - ad_cfg_orig:dict[str, Any] = {} - # Test case 1: price_reduction_count = 3 (should persist) - price_reduction_count = 3 - if price_reduction_count is not None and price_reduction_count > 0: - ad_cfg_orig["price_reduction_count"] = price_reduction_count + ad_cfg_orig = _apply_price_reduction_persistence(3) assert "price_reduction_count" in ad_cfg_orig assert ad_cfg_orig["price_reduction_count"] == 3 @@ -1602,23 +1606,15 @@ class TestPriceReductionPersistence: @pytest.mark.unit def test_persistence_logic_skips_when_count_zero(self) -> None: """Test that price_reduction_count == 0 does not get persisted.""" - ad_cfg_orig:dict[str, Any] = {} - # Test case 2: price_reduction_count = 0 (should NOT persist) - price_reduction_count = 0 - if price_reduction_count is not None and price_reduction_count > 0: - ad_cfg_orig["price_reduction_count"] = price_reduction_count + ad_cfg_orig = _apply_price_reduction_persistence(0) assert "price_reduction_count" not in ad_cfg_orig @pytest.mark.unit def test_persistence_logic_skips_when_count_none(self) -> None: """Test that price_reduction_count == None does not get persisted.""" - ad_cfg_orig:dict[str, Any] = {} - # Test case 3: price_reduction_count = None (should NOT persist) - price_reduction_count = None - if price_reduction_count is not None and price_reduction_count > 0: - ad_cfg_orig["price_reduction_count"] = price_reduction_count + ad_cfg_orig = _apply_price_reduction_persistence(None) assert "price_reduction_count" not in ad_cfg_orig diff --git a/tests/unit/test_price_reduction.py b/tests/unit/test_price_reduction.py index 64cf7b7..dd5e8c4 100644 --- a/tests/unit/test_price_reduction.py +++ b/tests/unit/test_price_reduction.py @@ -18,7 +18,7 @@ from kleinanzeigen_bot.utils.pydantics import ContextualValidationError @runtime_checkable class _ApplyAutoPriceReduction(Protocol): def __call__(self, ad_cfg:SimpleNamespace, ad_cfg_orig:dict[str, Any], ad_file_relative:str) -> None: - ... + pass @pytest.fixture diff --git a/tests/unit/test_web_scraping_mixin.py b/tests/unit/test_web_scraping_mixin.py index c7f1f14..9fab531 100644 --- a/tests/unit/test_web_scraping_mixin.py +++ b/tests/unit/test_web_scraping_mixin.py @@ -36,7 +36,7 @@ class ConfigProtocol(Protocol): user_data_dir:str | None def add_extension(self, ext:str) -> None: - ... + pass def _nodriver_start_mock() -> Mock: