refact: use ruff instead of autopep8,bandit,pylint for linting

This commit is contained in:
sebthom
2025-04-28 12:51:51 +02:00
parent f0b84ab335
commit 376ec76226
27 changed files with 437 additions and 605 deletions

View File

@@ -10,8 +10,8 @@ from unittest.mock import MagicMock
import pytest
from kleinanzeigen_bot import KleinanzeigenBot
from kleinanzeigen_bot.utils import loggers
from kleinanzeigen_bot.extract import AdExtractor
from kleinanzeigen_bot.utils import loggers
from kleinanzeigen_bot.utils.web_scraping_mixin import Browser
loggers.configure_console_logging()

View File

@@ -1,12 +1,12 @@
"""
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
import os, platform
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import os
import platform
from typing import cast
import nodriver, pytest
import nodriver
import pytest
from kleinanzeigen_bot.utils import loggers
from kleinanzeigen_bot.utils.misc import ensure

View File

@@ -1,8 +1,6 @@
"""
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
from typing import Any
import pytest
@@ -33,7 +31,7 @@ def test_calculate_content_hash_with_none_values() -> None:
assert len(hash_value) == 64 # SHA-256 hash is 64 characters long
@pytest.mark.parametrize("config,prefix,expected", [
@pytest.mark.parametrize(("config", "prefix", "expected"), [
# Test new flattened format - prefix
(
{"ad_defaults": {"description_prefix": "Hello"}},
@@ -129,11 +127,11 @@ def test_get_description_affixes(
expected: str
) -> None:
"""Test get_description_affixes function with various inputs."""
result = ads.get_description_affixes(config, prefix)
result = ads.get_description_affixes(config, prefix = prefix)
assert result == expected
@pytest.mark.parametrize("config,prefix,expected", [
@pytest.mark.parametrize(("config", "prefix", "expected"), [
# Add test for malformed config
(
{}, # Empty config
@@ -161,16 +159,16 @@ def test_get_description_affixes(
])
def test_get_description_affixes_edge_cases(config: dict[str, Any], prefix: bool, expected: str) -> None:
"""Test edge cases for description affix handling."""
assert ads.get_description_affixes(config, prefix) == expected
assert ads.get_description_affixes(config, prefix = prefix) == expected
@pytest.mark.parametrize("config,expected", [
(None, ""), # Test with None
([], ""), # Test with an empty list
@pytest.mark.parametrize(("config", "expected"), [
(None, ""), # Test with None
([], ""), # Test with an empty list
("string", ""), # Test with a string
(123, ""), # Test with an integer
(3.14, ""), # Test with a float
(set(), ""), # Test with an empty set
(123, ""), # Test with an integer
(3.14, ""), # Test with a float
(set(), ""), # Test with an empty set
])
def test_get_description_affixes_edge_cases_non_dict(config: Any, expected: str) -> None:
"""Test get_description_affixes function with non-dict inputs."""

View File

@@ -1,9 +1,8 @@
"""
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
import gc, pytest
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import gc, pytest # isort: skip
from kleinanzeigen_bot import KleinanzeigenBot

View File

@@ -1,9 +1,7 @@
"""
SPDX-FileCopyrightText: © Jens Bergmann and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
import json, os
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import json, os # isort: skip
from typing import Any, TypedDict
from unittest.mock import AsyncMock, MagicMock, call, patch
@@ -30,7 +28,7 @@ class _SpecialAttributesDict(TypedDict, total = False):
condition_s: str
class _TestCaseDict(TypedDict):
class _TestCaseDict(TypedDict): # noqa: PYI049 Private TypedDict `...` is never used
belen_conf: _BelenConfDict
expected: _SpecialAttributesDict
@@ -44,15 +42,12 @@ class TestAdExtractorBasics:
assert extractor.browser == browser_mock
assert extractor.config == sample_config
@pytest.mark.parametrize(
"url,expected_id",
[
("https://www.kleinanzeigen.de/s-anzeige/test-title/12345678", 12345678),
("https://www.kleinanzeigen.de/s-anzeige/another-test/98765432", 98765432),
("https://www.kleinanzeigen.de/s-anzeige/invalid-id/abc", -1),
("https://www.kleinanzeigen.de/invalid-url", -1),
],
)
@pytest.mark.parametrize(("url", "expected_id"), [
("https://www.kleinanzeigen.de/s-anzeige/test-title/12345678", 12345678),
("https://www.kleinanzeigen.de/s-anzeige/another-test/98765432", 98765432),
("https://www.kleinanzeigen.de/s-anzeige/invalid-id/abc", -1),
("https://www.kleinanzeigen.de/invalid-url", -1),
])
def test_extract_ad_id_from_ad_url(self, test_extractor: AdExtractor, url: str, expected_id: int) -> None:
"""Test extraction of ad ID from different URL formats."""
assert test_extractor.extract_ad_id_from_ad_url(url) == expected_id
@@ -61,16 +56,13 @@ class TestAdExtractorBasics:
class TestAdExtractorPricing:
"""Tests for pricing related functionality."""
@pytest.mark.parametrize(
"price_text,expected_price,expected_type",
[
("50 €", 50, "FIXED"),
("1.234 €", 1234, "FIXED"),
("50 € VB", 50, "NEGOTIABLE"),
("VB", None, "NEGOTIABLE"),
("Zu verschenken", None, "GIVE_AWAY"),
],
)
@pytest.mark.parametrize(("price_text", "expected_price", "expected_type"), [
("50 €", 50, "FIXED"),
("1.234 €", 1234, "FIXED"),
("50 € VB", 50, "NEGOTIABLE"),
("VB", None, "NEGOTIABLE"),
("Zu verschenken", None, "GIVE_AWAY"),
])
@pytest.mark.asyncio
# pylint: disable=protected-access
async def test_extract_pricing_info(
@@ -95,14 +87,11 @@ class TestAdExtractorPricing:
class TestAdExtractorShipping:
"""Tests for shipping related functionality."""
@pytest.mark.parametrize(
"shipping_text,expected_type,expected_cost",
[
("+ Versand ab 2,99 €", "SHIPPING", 2.99),
("Nur Abholung", "PICKUP", None),
("Versand möglich", "SHIPPING", None),
],
)
@pytest.mark.parametrize(("shipping_text", "expected_type", "expected_cost"), [
("+ Versand ab 2,99 €", "SHIPPING", 2.99),
("Nur Abholung", "PICKUP", None),
("Versand möglich", "SHIPPING", None),
])
@pytest.mark.asyncio
# pylint: disable=protected-access
async def test_extract_shipping_info(
@@ -272,9 +261,9 @@ class TestAdExtractorNavigation:
# Mocks needed for the actual execution flow
ad_list_container_mock = MagicMock()
pagination_section_mock = MagicMock()
cardbox_mock = MagicMock() # Represents the <li> element
link_mock = MagicMock() # Represents the <a> element
link_mock.attrs = {'href': '/s-anzeige/test/12345'} # Configure the desired output
cardbox_mock = MagicMock() # Represents the <li> element
link_mock = MagicMock() # Represents the <a> element
link_mock.attrs = {'href': '/s-anzeige/test/12345'} # Configure the desired output
# Mocks for elements potentially checked but maybe not strictly needed for output
# (depending on how robust the mocking is)
@@ -287,18 +276,18 @@ class TestAdExtractorNavigation:
# 3. Find for ad list container (inside loop)
# 4. Find for the link (inside list comprehension)
mock_web_find.side_effect = [
ad_list_container_mock, # Call 1: find #my-manageitems-adlist (before loop)
pagination_section_mock, # Call 2: find .Pagination
ad_list_container_mock, # Call 3: find #my-manageitems-adlist (inside loop)
link_mock # Call 4: find 'div.manageitems-item-ad h3 a.text-onSurface'
ad_list_container_mock, # Call 1: find #my-manageitems-adlist (before loop)
pagination_section_mock, # Call 2: find .Pagination
ad_list_container_mock, # Call 3: find #my-manageitems-adlist (inside loop)
link_mock # Call 4: find 'div.manageitems-item-ad h3 a.text-onSurface'
# Add more mocks here if the pagination navigation logic calls web_find again
]
# 1. Find all 'Nächste' buttons (pagination check) - Return empty list for single page test case
# 2. Find all '.cardbox' elements (inside loop)
mock_web_find_all.side_effect = [
[], # Call 1: find 'button[aria-label="Nächste"]' -> No next button = single page
[cardbox_mock] # Call 2: find .cardbox -> One ad item
[], # Call 1: find 'button[aria-label="Nächste"]' -> No next button = single page
[cardbox_mock] # Call 2: find .cardbox -> One ad item
# Add more mocks here if pagination navigation calls web_find_all
]
@@ -550,9 +539,9 @@ class TestAdExtractorContact:
"""Test contact info extraction when elements are not found."""
with patch.object(extractor, 'page', MagicMock()), \
patch.object(extractor, 'web_text', new_callable = AsyncMock, side_effect = TimeoutError()), \
patch.object(extractor, 'web_find', new_callable = AsyncMock, side_effect = TimeoutError()):
patch.object(extractor, 'web_find', new_callable = AsyncMock, side_effect = TimeoutError()), \
pytest.raises(TimeoutError):
with pytest.raises(TimeoutError):
await extractor._extract_contact_from_ad_page()
@pytest.mark.asyncio

View File

@@ -1,14 +1,13 @@
"""
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import pytest
from _pytest.monkeypatch import MonkeyPatch # pylint: disable=import-private-name
from kleinanzeigen_bot.utils import i18n
@pytest.mark.parametrize("lang, expected", [
@pytest.mark.parametrize(("lang", "expected"), [
(None, ("en", "US", "UTF-8")), # Test with no LANG variable (should default to ("en", "US", "UTF-8"))
("fr", ("fr", None, "UTF-8")), # Test with just a language code
("fr_CA", ("fr", "CA", "UTF-8")), # Test with language + region, no encoding
@@ -29,7 +28,7 @@ def test_detect_locale(monkeypatch: MonkeyPatch, lang: str | None, expected: i18
assert result == expected, f"For LANG={lang}, expected {expected} but got {result}"
@pytest.mark.parametrize("lang, noun, count, prefix_with_count, expected", [
@pytest.mark.parametrize(("lang", "noun", "count", "prefix_with_count", "expected"), [
("en", "field", 1, True, "1 field"),
("en", "field", 2, True, "2 fields"),
("en", "field", 2, False, "fields"),
@@ -54,5 +53,5 @@ def test_pluralize(
) -> None:
i18n.set_current_locale(i18n.Locale(lang, "US", "UTF_8"))
result = i18n.pluralize(noun, count, prefix_with_count)
result = i18n.pluralize(noun, count, prefix_with_count = prefix_with_count)
assert result == expected, f"For LANG={lang}, expected {expected} but got {result}"

View File

@@ -1,11 +1,9 @@
"""
SPDX-FileCopyrightText: © Jens Bergmann and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
import copy, os, tempfile
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import copy, os, tempfile # isort: skip
from collections.abc import Generator
from datetime import datetime, timedelta
from datetime import timedelta
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
@@ -13,7 +11,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from ruamel.yaml import YAML
from kleinanzeigen_bot import LOG, KleinanzeigenBot
from kleinanzeigen_bot import LOG, KleinanzeigenBot, misc
from kleinanzeigen_bot._version import __version__
from kleinanzeigen_bot.ads import calculate_content_hash
from kleinanzeigen_bot.utils import loggers
@@ -191,7 +189,7 @@ class TestKleinanzeigenBotLogging:
class TestKleinanzeigenBotCommandLine:
"""Tests for command line argument parsing."""
@pytest.mark.parametrize("args,expected_command,expected_selector,expected_keep_old", [
@pytest.mark.parametrize(("args", "expected_command", "expected_selector", "expected_keep_old"), [
(["publish", "--ads=all"], "publish", "all", False),
(["verify"], "verify", "due", False),
(["download", "--ads=12345"], "download", "12345", False),
@@ -833,7 +831,7 @@ class TestKleinanzeigenBotAdDeletion:
patch.object(test_bot, 'web_click', new_callable = AsyncMock), \
patch.object(test_bot, 'web_check', new_callable = AsyncMock, return_value = True):
mock_find.return_value.attrs = {"content": "some-token"}
result = await test_bot.delete_ad(ad_cfg, True, published_ads)
result = await test_bot.delete_ad(ad_cfg, published_ads, delete_old_ads_by_title = True)
assert result is True
@pytest.mark.asyncio
@@ -859,7 +857,7 @@ class TestKleinanzeigenBotAdDeletion:
patch.object(test_bot, 'web_click', new_callable = AsyncMock), \
patch.object(test_bot, 'web_check', new_callable = AsyncMock, return_value = True):
mock_find.return_value.attrs = {"content": "some-token"}
result = await test_bot.delete_ad(ad_cfg, False, published_ads)
result = await test_bot.delete_ad(ad_cfg, published_ads, delete_old_ads_by_title = False)
assert result is True
@@ -910,7 +908,7 @@ class TestKleinanzeigenBotAdRepublication:
def test_check_ad_republication_no_changes(self, test_bot: KleinanzeigenBot, base_ad_config: dict[str, Any]) -> None:
"""Test that unchanged ads within interval are not marked for republication."""
current_time = datetime.utcnow()
current_time = misc.now()
three_days_ago = (current_time - timedelta(days = 3)).isoformat()
# Create ad config with timestamps for republication check
@@ -1235,7 +1233,7 @@ class TestKleinanzeigenBotChangedAds:
# Mock the loading of the ad configuration
with patch('kleinanzeigen_bot.utils.dicts.load_dict', side_effect=[
changed_ad, # First call returns the changed ad
{} # Second call for ad_fields.yaml
{} # Second call for ad_fields.yaml
]):
ads_to_publish = test_bot.load_ads()
@@ -1255,7 +1253,7 @@ class TestKleinanzeigenBotChangedAds:
}
# Create a changed ad that is also due for republication
current_time = datetime.utcnow()
current_time = misc.now()
old_date = (current_time - timedelta(days=10)).isoformat() # Past republication interval
changed_ad = create_ad_config(
@@ -1287,7 +1285,7 @@ class TestKleinanzeigenBotChangedAds:
# Mock the loading of the ad configuration
with patch('kleinanzeigen_bot.utils.dicts.load_dict', side_effect=[
changed_ad, # First call returns the changed ad
{} # Second call for ad_fields.yaml
{} # Second call for ad_fields.yaml
]):
ads_to_publish = test_bot.load_ads()

View File

@@ -1,9 +1,9 @@
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
This module contains tests for verifying the completeness and correctness of translations in the project.
It ensures that:
1. All log messages in the code have corresponding translations
2. All translations in the YAML files are actually used in the code
@@ -15,7 +15,7 @@ The tests work by:
3. Comparing the extracted messages with translations
4. Verifying no unused translations exist
"""
import ast, os
import ast, os # isort: skip
from collections import defaultdict
from dataclasses import dataclass
from importlib.resources import files
@@ -105,7 +105,7 @@ def _extract_log_messages(file_path: str, exclude_debug:bool = False) -> Message
messages: MessageDict = defaultdict(lambda: defaultdict(set))
def add_message(function: str, msg: str) -> None:
"""Helper to add a message to the messages dictionary."""
"""Add a message to the messages dictionary."""
if function not in messages:
messages[function] = defaultdict(set)
if msg not in messages[function]:
@@ -128,7 +128,7 @@ def _extract_log_messages(file_path: str, exclude_debug:bool = False) -> Message
if (isinstance(node.func, ast.Attribute) and
isinstance(node.func.value, ast.Name) and
node.func.value.id in {'LOG', 'logger', 'logging'} and
node.func.attr in {None if exclude_debug else 'debug', 'info', 'warning', 'error', 'critical'}):
node.func.attr in {None if exclude_debug else 'debug', 'info', 'warning', 'error', 'exception', 'critical'}):
if node.args:
msg = extract_string_value(node.args[0])
if msg:
@@ -390,7 +390,7 @@ def test_no_obsolete_translations(lang: str) -> None:
if not isinstance(function_trans, dict):
continue
for original_message in function_trans.keys():
for original_message in function_trans:
# Check if this message exists in the code
message_exists = _message_exists_in_code(messages_by_file, module, function, original_message)

View File

@@ -1,9 +1,8 @@
"""
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
"""
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
import pytest
from kleinanzeigen_bot.utils import misc