mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
refact: enable ruff preview rules
This commit is contained in:
@@ -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
|
||||
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" }
|
||||
lint = {shell = "ruff check && mypy && basedpyright" }
|
||||
fix = {shell = "ruff check --fix" }
|
||||
lint = {shell = "ruff check --preview && mypy && basedpyright" }
|
||||
fix = {shell = "ruff check --preview --fix" }
|
||||
test = "python -m pytest --capture=tee-sys -v"
|
||||
utest = "python -m pytest --capture=tee-sys -v -m 'not itest'"
|
||||
itest = "python -m pytest --capture=tee-sys -v -m 'itest'"
|
||||
@@ -128,10 +128,10 @@ select = [
|
||||
#"BLE", # flake8-blind-except
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
#"COM", # flake8-commas
|
||||
#"CPY", # flake8-copyright
|
||||
"COM", # flake8-commas
|
||||
"CPY", # flake8-copyright
|
||||
"DTZ", # flake8-datetimez
|
||||
#"EM", # TODO flake8-errmsg
|
||||
#"EM", # flake8-errmsg
|
||||
#"ERA", # eradicate commented-out code
|
||||
"EXE", # flake8-executable
|
||||
"FA", # flake8-future-annotations
|
||||
@@ -177,6 +177,7 @@ ignore = [
|
||||
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
|
||||
"ASYNC210", # TODO Async functions should not call blocking HTTP methods
|
||||
"ASYNC230", # TODO Async functions should not open files with blocking methods like `open`
|
||||
"COM812", # Trailing comma missing
|
||||
"D1", # Missing docstring in ...
|
||||
"D200", # One-line docstring should fit on one line
|
||||
"D202", # No blank lines allowed after function docstring (found 1)
|
||||
@@ -195,12 +196,12 @@ ignore = [
|
||||
"D417", # Missing argument description in the docstring for
|
||||
#"E124", # 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
|
||||
#"E251", # Don't remove whitespace around parameter '=' sign.
|
||||
"E231", # Don't add whitespace after colon (:) on type declaration
|
||||
"E251", # Don't remove whitespace around parameter '=' sign.
|
||||
"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
|
||||
"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`
|
||||
"SIM102", # Use a single `if` statement instead of nested `if` statements
|
||||
"SIM105", # Use `contextlib.suppress(TimeoutError)` instead of `try`-`except`-`pass`
|
||||
@@ -219,20 +220,25 @@ ignore = [
|
||||
"INP",
|
||||
"SLF",
|
||||
"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]
|
||||
# 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
|
||||
#max-args = 6 # max. number of args for function / method (R0913)
|
||||
#max-attributes = 15 # max. number of instance attrs for a class (R0902)
|
||||
max-args = 5 # max. number of args for function / method (R0913)
|
||||
# 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-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-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-positional-arguments = 6 # max. number of positional args for function / method (R0917)
|
||||
max-public-methods = 20 # max. number of public methods for a class (R0904)
|
||||
# max-positional-arguments = 5 # max. number of positional args for function / method (R0917)
|
||||
|
||||
|
||||
#####################
|
||||
|
||||
@@ -224,7 +224,7 @@ class PreferDoubleQuotesRule(FormatterRule):
|
||||
raw = new_lines[start_line]
|
||||
# apply shift so we match against current edited line
|
||||
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
|
||||
|
||||
# match literal at that column
|
||||
|
||||
@@ -348,7 +348,7 @@ class KleinanzeigenBot(WebScrapingMixin):
|
||||
for ad_file, ad_file_relative in sorted(ad_files.items()):
|
||||
ad_cfg_orig = dicts.load_dict(ad_file, "ad")
|
||||
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)
|
||||
|
||||
if ignore_inactive and not ad_cfg["active"]:
|
||||
|
||||
@@ -42,7 +42,7 @@ class AdExtractor(WebScrapingMixin):
|
||||
os.mkdir(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):
|
||||
LOG.info("Deleting current folder of ad %s...", ad_id)
|
||||
shutil.rmtree(new_base_dir)
|
||||
@@ -51,7 +51,7 @@ class AdExtractor(WebScrapingMixin):
|
||||
|
||||
# call extraction function
|
||||
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)
|
||||
|
||||
async def _download_images_from_ad_page(self, directory:str, ad_id:int) -> list[str]:
|
||||
@@ -96,7 +96,7 @@ class AdExtractor(WebScrapingMixin):
|
||||
try:
|
||||
# click next button, wait, and re-establish reference
|
||||
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)
|
||||
except TimeoutError:
|
||||
LOG.error("NEXT button in image gallery somehow missing, aborting image fetching.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
__, 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)
|
||||
|
||||
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)
|
||||
|
||||
__, 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')
|
||||
|
||||
content = get_resource_as_string(module, filename) # pylint: disable=deprecated-method
|
||||
|
||||
@@ -13,7 +13,12 @@ from . import i18n
|
||||
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 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:
|
||||
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:
|
||||
|
||||
@@ -33,7 +33,7 @@ __all__ = [
|
||||
LOG:Final[loggers.Logger] = loggers.get_logger(__name__)
|
||||
|
||||
# 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):
|
||||
@@ -535,13 +535,13 @@ class WebScrapingMixin:
|
||||
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
|
||||
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)
|
||||
|
||||
if scroll_back_top: # scroll back to top in same style
|
||||
while current_y_pos > 0:
|
||||
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
|
||||
|
||||
async def web_select(self, selector_type:By, selector_value:str, selected_value:Any, timeout:int | float = 5) -> Element:
|
||||
|
||||
@@ -380,7 +380,7 @@ class TestAdExtractorContent:
|
||||
):
|
||||
try:
|
||||
info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
|
||||
assert info["description"] == ""
|
||||
assert not info["description"]
|
||||
except TimeoutError:
|
||||
# This is also acceptable - depends on how we want to handle timeouts
|
||||
pass
|
||||
|
||||
@@ -257,7 +257,7 @@ def _find_translation(translations:TranslationDict,
|
||||
return bool(translations.get(module, {}).get(function, {}).get(message))
|
||||
|
||||
# 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
|
||||
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
|
||||
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
|
||||
module_msgs = code_messages.get(module_path)
|
||||
|
||||
Reference in New Issue
Block a user