refact: enable ruff preview rules

This commit is contained in:
sebthom
2025-04-28 00:33:58 +02:00
parent f98251ade3
commit bda0acf943
9 changed files with 70 additions and 59 deletions

View File

@@ -83,8 +83,8 @@ compile.cmd = "python -O -m PyInstaller pyinstaller.spec --clean"
compile.env = {PYTHONHASHSEED = "1", SOURCE_DATE_EPOCH = "0"} # https://pyinstaller.org/en/stable/advanced-topics.html#creating-a-reproducible-build compile.env = {PYTHONHASHSEED = "1", SOURCE_DATE_EPOCH = "0"} # https://pyinstaller.org/en/stable/advanced-topics.html#creating-a-reproducible-build
debug = "python -m pdb -m kleinanzeigen_bot" debug = "python -m pdb -m kleinanzeigen_bot"
format = {shell = "autopep8 --recursive --in-place scripts src tests --verbose && python scripts/post_autopep8.py scripts src tests" } format = {shell = "autopep8 --recursive --in-place scripts src tests --verbose && python scripts/post_autopep8.py scripts src tests" }
lint = {shell = "ruff check && mypy && basedpyright" } lint = {shell = "ruff check --preview && mypy && basedpyright" }
fix = {shell = "ruff check --fix" } fix = {shell = "ruff check --preview --fix" }
test = "python -m pytest --capture=tee-sys -v" test = "python -m pytest --capture=tee-sys -v"
utest = "python -m pytest --capture=tee-sys -v -m 'not itest'" utest = "python -m pytest --capture=tee-sys -v -m 'not itest'"
itest = "python -m pytest --capture=tee-sys -v -m 'itest'" itest = "python -m pytest --capture=tee-sys -v -m 'itest'"
@@ -128,10 +128,10 @@ select = [
#"BLE", # flake8-blind-except #"BLE", # flake8-blind-except
"B", # flake8-bugbear "B", # flake8-bugbear
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
#"COM", # flake8-commas "COM", # flake8-commas
#"CPY", # flake8-copyright "CPY", # flake8-copyright
"DTZ", # flake8-datetimez "DTZ", # flake8-datetimez
#"EM", # TODO flake8-errmsg #"EM", # flake8-errmsg
#"ERA", # eradicate commented-out code #"ERA", # eradicate commented-out code
"EXE", # flake8-executable "EXE", # flake8-executable
"FA", # flake8-future-annotations "FA", # flake8-future-annotations
@@ -177,6 +177,7 @@ ignore = [
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed "ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"ASYNC210", # TODO Async functions should not call blocking HTTP methods "ASYNC210", # TODO Async functions should not call blocking HTTP methods
"ASYNC230", # TODO Async functions should not open files with blocking methods like `open` "ASYNC230", # TODO Async functions should not open files with blocking methods like `open`
"COM812", # Trailing comma missing
"D1", # Missing docstring in ... "D1", # Missing docstring in ...
"D200", # One-line docstring should fit on one line "D200", # One-line docstring should fit on one line
"D202", # No blank lines allowed after function docstring (found 1) "D202", # No blank lines allowed after function docstring (found 1)
@@ -195,12 +196,12 @@ ignore = [
"D417", # Missing argument description in the docstring for "D417", # Missing argument description in the docstring for
#"E124", # Don't change indention of multi-line statements #"E124", # Don't change indention of multi-line statements
#"E128", # Don't change indention of multi-line statements #"E128", # Don't change indention of multi-line statements
#"E231", # Don't add whitespace after colon (:) on type declaration "E231", # Don't add whitespace after colon (:) on type declaration
#"E251", # Don't remove whitespace around parameter '=' sign. "E251", # Don't remove whitespace around parameter '=' sign.
"E401", # Don't put imports on separate lines "E401", # Don't put imports on separate lines
"Q000", # TODO Single quotes found but double quotes preferred
"PERF203", # `try`-`except` within a loop incurs performance overhead "PERF203", # `try`-`except` within a loop incurs performance overhead
"RET504", # Unnecessary assignment to `...` before `return` statement "RET504", # Unnecessary assignment to `...` before `return` statement
"PLR6301", # Method `...` could be a function, class method, or static method
"PYI041", # Use `float` instead of `int | float` "PYI041", # Use `float` instead of `int | float`
"SIM102", # Use a single `if` statement instead of nested `if` statements "SIM102", # Use a single `if` statement instead of nested `if` statements
"SIM105", # Use `contextlib.suppress(TimeoutError)` instead of `try`-`except`-`pass` "SIM105", # Use `contextlib.suppress(TimeoutError)` instead of `try`-`except`-`pass`
@@ -219,20 +220,25 @@ ignore = [
"INP", "INP",
"SLF", "SLF",
"S101", # Use of `assert` detected "S101", # Use of `assert` detected
"PLR2004" # Magic value used in comparison "PLR0904", # Too many public methods (12 > 10)
"PLR2004", # Magic value used in comparison
] ]
[tool.ruff.lint.flake8-copyright]
notice-rgx = "SPDX-FileCopyrightText: .*"
min-file-size = 256
[tool.ruff.lint.pylint] [tool.ruff.lint.pylint]
# https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#design-checker # https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#design-checker
# https://pylint.pycqa.org/en/latest/user_guide/checkers/features.html#design-checker-messages # https://pylint.pycqa.org/en/latest/user_guide/checkers/features.html#design-checker-messages
#max-args = 6 # max. number of args for function / method (R0913) max-args = 5 # max. number of args for function / method (R0913)
#max-attributes = 15 # max. number of instance attrs for a class (R0902) # max-attributes = 15 # max. number of instance attrs for a class (R0902)
max-branches = 40 # max. number of branch for function / method body (R0912) max-branches = 40 # max. number of branch for function / method body (R0912)
#max-locals = 30 # max. number of local vars for function / method body (R0914) max-locals = 30 # max. number of local vars for function / method body (R0914)
max-returns = 15 # max. number of return / yield for function / method body (R0911) max-returns = 15 # max. number of return / yield for function / method body (R0911)
max-statements = 150 # max. number of statements in function / method body (R0915) max-statements = 150 # max. number of statements in function / method body (R0915)
#max-public-methods = 30 # max. number of public methods for a class (R0904) max-public-methods = 20 # max. number of public methods for a class (R0904)
#max-positional-arguments = 6 # max. number of positional args for function / method (R0917) # max-positional-arguments = 5 # max. number of positional args for function / method (R0917)
##################### #####################

View File

@@ -224,7 +224,7 @@ class PreferDoubleQuotesRule(FormatterRule):
raw = new_lines[start_line] raw = new_lines[start_line]
# apply shift so we match against current edited line # apply shift so we match against current edited line
idx = starting_col_offset + shift idx = starting_col_offset + shift
if idx >= len(raw) or raw[idx] not in ("'", "r", "u", "b", "f", "R", "U", "B", "F"): if idx >= len(raw) or raw[idx] not in {"'", "r", "u", "b", "f", "R", "U", "B", "F"}:
continue continue
# match literal at that column # match literal at that column

View File

@@ -348,7 +348,7 @@ class KleinanzeigenBot(WebScrapingMixin):
for ad_file, ad_file_relative in sorted(ad_files.items()): for ad_file, ad_file_relative in sorted(ad_files.items()):
ad_cfg_orig = dicts.load_dict(ad_file, "ad") ad_cfg_orig = dicts.load_dict(ad_file, "ad")
ad_cfg = copy.deepcopy(ad_cfg_orig) ad_cfg = copy.deepcopy(ad_cfg_orig)
dicts.apply_defaults(ad_cfg, self.config["ad_defaults"], ignore = lambda k, _: k == "description", override = lambda _, v: v == "") dicts.apply_defaults(ad_cfg, self.config["ad_defaults"], ignore = lambda k, _: k == "description", override = lambda _, v: not v)
dicts.apply_defaults(ad_cfg, ad_fields) dicts.apply_defaults(ad_cfg, ad_fields)
if ignore_inactive and not ad_cfg["active"]: if ignore_inactive and not ad_cfg["active"]:

View File

@@ -42,7 +42,7 @@ class AdExtractor(WebScrapingMixin):
os.mkdir(relative_directory) os.mkdir(relative_directory)
LOG.info("Created ads directory at ./%s.", relative_directory) LOG.info("Created ads directory at ./%s.", relative_directory)
new_base_dir = os.path.join(relative_directory, f'ad_{ad_id}') new_base_dir = os.path.join(relative_directory, f"ad_{ad_id}")
if os.path.exists(new_base_dir): if os.path.exists(new_base_dir):
LOG.info("Deleting current folder of ad %s...", ad_id) LOG.info("Deleting current folder of ad %s...", ad_id)
shutil.rmtree(new_base_dir) shutil.rmtree(new_base_dir)
@@ -51,7 +51,7 @@ class AdExtractor(WebScrapingMixin):
# call extraction function # call extraction function
info = await self._extract_ad_page_info(new_base_dir, ad_id) info = await self._extract_ad_page_info(new_base_dir, ad_id)
ad_file_path = new_base_dir + "/" + f'ad_{ad_id}.yaml' ad_file_path = new_base_dir + "/" + f"ad_{ad_id}.yaml"
dicts.save_dict(ad_file_path, info) dicts.save_dict(ad_file_path, info)
async def _download_images_from_ad_page(self, directory:str, ad_id:int) -> list[str]: async def _download_images_from_ad_page(self, directory:str, ad_id:int) -> list[str]:
@@ -96,7 +96,7 @@ class AdExtractor(WebScrapingMixin):
try: try:
# click next button, wait, and re-establish reference # click next button, wait, and re-establish reference
await (await self.web_find(By.CLASS_NAME, "galleryimage--navigation--next")).click() await (await self.web_find(By.CLASS_NAME, "galleryimage--navigation--next")).click()
new_div = await self.web_find(By.CSS_SELECTOR, f'div.galleryimage-element:nth-child({img_nr + 1})') new_div = await self.web_find(By.CSS_SELECTOR, f"div.galleryimage-element:nth-child({img_nr + 1})")
img_element = await self.web_find(By.TAG_NAME, "img", parent = new_div) img_element = await self.web_find(By.TAG_NAME, "img", parent = new_div)
except TimeoutError: except TimeoutError:
LOG.error("NEXT button in image gallery somehow missing, aborting image fetching.") LOG.error("NEXT button in image gallery somehow missing, aborting image fetching.")

View File

@@ -61,7 +61,7 @@ def load_dict_if_exists(filepath:str, content_label:str = "") -> dict[str, Any]
LOG.info("Loading %s[%s]...", content_label and content_label + _(" from ") or "", abs_filepath) LOG.info("Loading %s[%s]...", content_label and content_label + _(" from ") or "", abs_filepath)
__, file_ext = os.path.splitext(filepath) __, file_ext = os.path.splitext(filepath)
if file_ext not in (".json", ".yaml", ".yml"): if file_ext not in {".json", ".yaml", ".yml"}:
raise ValueError(_('Unsupported file type. The filename "%s" must end with *.json, *.yaml, or *.yml') % filepath) raise ValueError(_('Unsupported file type. The filename "%s" must end with *.json, *.yaml, or *.yml') % filepath)
if not os.path.exists(filepath): if not os.path.exists(filepath):
@@ -78,7 +78,7 @@ def load_dict_from_module(module:ModuleType, filename:str, content_label:str = "
LOG.debug("Loading %s[%s.%s]...", content_label and content_label + " from " or "", module.__name__, filename) LOG.debug("Loading %s[%s.%s]...", content_label and content_label + " from " or "", module.__name__, filename)
__, file_ext = os.path.splitext(filename) __, file_ext = os.path.splitext(filename)
if file_ext not in (".json", ".yaml", ".yml"): if file_ext not in {".json", ".yaml", ".yml"}:
raise ValueError(f'Unsupported file type. The filename "{filename}" must end with *.json, *.yaml, or *.yml') raise ValueError(f'Unsupported file type. The filename "{filename}" must end with *.json, *.yaml, or *.yml')
content = get_resource_as_string(module, filename) # pylint: disable=deprecated-method content = get_resource_as_string(module, filename) # pylint: disable=deprecated-method

View File

@@ -13,7 +13,12 @@ from . import i18n
T = TypeVar("T") T = TypeVar("T")
def ensure(condition:Any | bool | Callable[[], bool], error_message:str, timeout:float = 5, poll_requency:float = 0.5) -> None: def ensure(
condition:Any | bool | Callable[[], bool], # noqa: FBT001 Boolean-typed positional argument in function definition
error_message:str,
timeout:float = 5,
poll_requency:float = 0.5
) -> None:
""" """
:param timeout: timespan in seconds until when the condition must become `True`, default is 5 seconds :param timeout: timespan in seconds until when the condition must become `True`, default is 5 seconds
:param poll_requency: sleep interval between calls in seconds, default is 0.5 seconds :param poll_requency: sleep interval between calls in seconds, default is 0.5 seconds
@@ -50,7 +55,7 @@ def is_frozen() -> bool:
async def ainput(prompt:str) -> str: async def ainput(prompt:str) -> str:
return await asyncio.to_thread(input, f'{prompt} ') return await asyncio.to_thread(input, f"{prompt} ")
def parse_decimal(number:float | int | str) -> decimal.Decimal: def parse_decimal(number:float | int | str) -> decimal.Decimal:

View File

@@ -33,7 +33,7 @@ __all__ = [
LOG:Final[loggers.Logger] = loggers.get_logger(__name__) LOG:Final[loggers.Logger] = loggers.get_logger(__name__)
# see https://api.jquery.com/category/selectors/ # see https://api.jquery.com/category/selectors/
METACHAR_ESCAPER:Final[dict[int, str]] = str.maketrans({ch: f'\\{ch}' for ch in '!"#$%&\'()*+,./:;<=>?@[\\]^`{|}~'}) METACHAR_ESCAPER:Final[dict[int, str]] = str.maketrans({ch: f"\\{ch}" for ch in '!"#$%&\'()*+,./:;<=>?@[\\]^`{|}~'})
class By(enum.Enum): class By(enum.Enum):
@@ -535,13 +535,13 @@ class WebScrapingMixin:
bottom_y_pos:int = await self.web_execute("document.body.scrollHeight") # get bottom position bottom_y_pos:int = await self.web_execute("document.body.scrollHeight") # get bottom position
while current_y_pos < bottom_y_pos: # scroll in steps until bottom reached while current_y_pos < bottom_y_pos: # scroll in steps until bottom reached
current_y_pos += scroll_length current_y_pos += scroll_length
await self.web_execute(f'window.scrollTo(0, {current_y_pos})') # scroll one step await self.web_execute(f"window.scrollTo(0, {current_y_pos})") # scroll one step
await asyncio.sleep(scroll_length / scroll_speed) await asyncio.sleep(scroll_length / scroll_speed)
if scroll_back_top: # scroll back to top in same style if scroll_back_top: # scroll back to top in same style
while current_y_pos > 0: while current_y_pos > 0:
current_y_pos -= scroll_length current_y_pos -= scroll_length
await self.web_execute(f'window.scrollTo(0, {current_y_pos})') await self.web_execute(f"window.scrollTo(0, {current_y_pos})")
await asyncio.sleep(scroll_length / scroll_speed / 2) # double speed await asyncio.sleep(scroll_length / scroll_speed / 2) # double speed
async def web_select(self, selector_type:By, selector_value:str, selected_value:Any, timeout:int | float = 5) -> Element: async def web_select(self, selector_type:By, selector_value:str, selected_value:Any, timeout:int | float = 5) -> Element:

View File

@@ -380,7 +380,7 @@ class TestAdExtractorContent:
): ):
try: try:
info = await test_extractor._extract_ad_page_info("/some/dir", 12345) info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
assert info["description"] == "" assert not info["description"]
except TimeoutError: except TimeoutError:
# This is also acceptable - depends on how we want to handle timeouts # This is also acceptable - depends on how we want to handle timeouts
pass pass

View File

@@ -257,7 +257,7 @@ def _find_translation(translations:TranslationDict,
return bool(translations.get(module, {}).get(function, {}).get(message)) return bool(translations.get(module, {}).get(function, {}).get(message))
# Add kleinanzeigen_bot/ prefix if not present # Add kleinanzeigen_bot/ prefix if not present
module_path = f'kleinanzeigen_bot/{module}' if not module.startswith("kleinanzeigen_bot/") else module module_path = f"kleinanzeigen_bot/{module}" if not module.startswith("kleinanzeigen_bot/") else module
# Check if module exists in translations # Check if module exists in translations
module_trans = translations.get(module_path, {}) module_trans = translations.get(module_path, {})
@@ -301,7 +301,7 @@ def _message_exists_in_code(code_messages:dict[str, MessageDict],
# Remove kleinanzeigen_bot/ prefix if present for code message lookup # Remove kleinanzeigen_bot/ prefix if present for code message lookup
module_path = module[len("kleinanzeigen_bot/"):] if module.startswith("kleinanzeigen_bot/") else module module_path = module[len("kleinanzeigen_bot/"):] if module.startswith("kleinanzeigen_bot/") else module
module_path = f'kleinanzeigen_bot/{module_path}' module_path = f"kleinanzeigen_bot/{module_path}"
# Check if module exists in code messages # Check if module exists in code messages
module_msgs = code_messages.get(module_path) module_msgs = code_messages.get(module_path)