Files
kleinanzeigen-bot/src/kleinanzeigen_bot/utils/files.py
Jens 220c01f257 fix: eliminate async safety violations and migrate to pathlib (#697)
## ℹ️ Description
Eliminate all blocking I/O operations in async contexts and modernize
file path handling by migrating from os.path to pathlib.Path.

- Link to the related issue(s): #692 
- Get rid of the TODO in pyproject.toml
- The added debug logging will ease the troubleshooting for path related
issues.

## 📋 Changes Summary

- Enable ASYNC210, ASYNC230, ASYNC240, ASYNC250 Ruff rules
- Wrap blocking urllib.request.urlopen() in run_in_executor
- Wrap blocking file operations (open, write) in run_in_executor
- Replace blocking os.path calls with async helpers using
run_in_executor
- Replace blocking input() with await ainput()
- Migrate extract.py from os.path to pathlib.Path
- Use Path() constructor and / operator for path joining
- Use Path.mkdir(), Path.rename() in executor instead of os functions
- Create mockable _path_exists() and _path_is_dir() helpers
- Add debug logging for all file system operations

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [X] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ]  New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


##  Checklist
Before requesting a review, confirm the following:
- [X] I have reviewed my changes to ensure they meet the project's
standards.
- [X] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [X] I have formatted the code (`pdm run format`).
- [X] I have verified that linting passes (`pdm run lint`).
- [X] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
  * Made user prompt non‑blocking to improve responsiveness.
* Converted filesystem/path handling and prefs I/O to async‑friendly
operations; moved blocking network and file work to background tasks.
* Added async file/path helpers and async port‑check before browser
connections.

* **Tests**
* Expanded unit tests for path helpers, image download success/failure,
prefs writing, and directory creation/renaming workflows.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 20:53:40 +01:00

48 lines
1.5 KiB
Python

# 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 asyncio, os # isort: skip
from pathlib import Path
def abspath(relative_path:str, relative_to:str | None = None) -> str:
"""
Return a normalized absolute path based on *relative_to*.
If 'relative_path' is already absolute, it is normalized and returned.
Otherwise, the function joins 'relative_path' with 'relative_to' (or the current working directory if not provided),
normalizes the result, and returns the absolute path.
"""
if not relative_to:
return os.path.abspath(relative_path)
if os.path.isabs(relative_path):
return os.path.normpath(relative_path)
base = os.path.abspath(relative_to)
if os.path.isfile(base):
base = os.path.dirname(base)
return os.path.normpath(os.path.join(base, relative_path))
async def exists(path:str | Path) -> bool:
"""
Asynchronously check if a file or directory exists.
:param path: Path to check
:return: True if path exists, False otherwise
"""
return await asyncio.get_running_loop().run_in_executor(None, Path(path).exists)
async def is_dir(path:str | Path) -> bool:
"""
Asynchronously check if a path is a directory.
:param path: Path to check
:return: True if path is a directory, False otherwise
"""
return await asyncio.get_running_loop().run_in_executor(None, Path(path).is_dir)