From ca62bc66e7ce96c504e174c0e75a6191a28c492e Mon Sep 17 00:00:00 2001 From: PaoloLeonard Date: Mon, 30 Jun 2025 11:39:19 +0200 Subject: [PATCH 1/3] revert back to writer and improve tests --- .pre-commit-config.yaml | 25 +- pyproject.toml | 4 - src/docbinder_oss/cli/search.py | 69 +--- src/docbinder_oss/helpers/config.py | 5 +- src/docbinder_oss/helpers/rich_helpers.py | 19 - .../helpers/writers/multiformat_writer.py | 9 +- .../helpers/writers/writer_console.py | 17 +- .../helpers/writers/writer_csv.py | 46 +-- .../helpers/writers/writer_json.py | 30 +- .../google_drive/google_drive_files.py | 2 +- tests/commands/test_search_command.py | 372 ++++++++++-------- tests/conftest.py | 135 +++++++ tests/helpers/test_writer.py | 18 +- tests/providers/google_drive/conftest.py | 46 --- 14 files changed, 430 insertions(+), 367 deletions(-) delete mode 100644 src/docbinder_oss/helpers/rich_helpers.py create mode 100644 tests/conftest.py delete mode 100644 tests/providers/google_drive/conftest.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9168817..9591f18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,24 @@ repos: - - repo: https://github.com/psf/black - rev: 24.3.0 + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.7.16 hooks: - - id: black + - id: uv-export + - id: uv-lock + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.4 + # Ruff version. + rev: v0.12.1 hooks: - - id: ruff + # Run the linter. + - id: ruff-check + types_or: [ python, pyi ] + args: [ --select, I, --fix ] + # Run the formatter. + - id: ruff-format + types_or: [ python, pyi ] diff --git a/pyproject.toml b/pyproject.toml index ced6ecc..ed3fec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,10 +48,6 @@ testpaths = [ "tests", ] -[tool.black] -line-length = 125 -skip-string-normalization = false - [tool.ruff] line-length = 125 diff --git a/src/docbinder_oss/cli/search.py b/src/docbinder_oss/cli/search.py index 6cfd24a..34feb90 100644 --- a/src/docbinder_oss/cli/search.py +++ b/src/docbinder_oss/cli/search.py @@ -1,7 +1,8 @@ from datetime import datetime +import logging import re import typer -from typing import Optional +from typing import Dict, List, Optional import csv from docbinder_oss.core.schemas import File @@ -64,7 +65,7 @@ def search( def __filter_files( - files, + files: Dict[str, List[File]], name=None, owner=None, updated_after=None, @@ -73,7 +74,7 @@ def __filter_files( created_before=None, min_size=None, max_size=None, -): +) -> Dict[str, List[File]]: """ Filters a collection of files based on various criteria such as name, owner, modification/creation dates, and file size. @@ -103,14 +104,14 @@ def file_matches(file: File): if owner and (not file.owners or not any(owner in u.email_address for u in file.owners)): return False if updated_after: - file_mod_time = __parse_dt(file.modified_time) + file_modified_time = __parse_dt(file.modified_time) updated_after_dt = __parse_dt(updated_after) - if file_mod_time is None or updated_after_dt is None or file_mod_time < updated_after_dt: + if file_modified_time is None or updated_after_dt is None or file_modified_time < updated_after_dt: return False if updated_before: - file_mod_time = __parse_dt(file.modified_time) + file_modified_time = __parse_dt(file.modified_time) updated_before_dt = __parse_dt(updated_before) - if file_mod_time is None or updated_before_dt is None or file_mod_time > updated_before_dt: + if file_modified_time is None or updated_before_dt is None or file_modified_time > updated_before_dt: return False if created_after: file_created_time = __parse_dt(file.created_time) @@ -120,11 +121,12 @@ def file_matches(file: File): if created_before: file_created_time = __parse_dt(file.created_time) created_before_dt = __parse_dt(created_before) + logging.debug(f"File created time: {file_created_time}, Created before: {created_before_dt}, Type: {type(file_created_time)}, Type: {type(created_before_dt)}") if file_created_time is not None and created_before_dt is not None and file_created_time > created_before_dt: return False - if min_size and file.size < min_size * 1024: + if min_size and file.size < min_size: return False - if max_size and file.size > max_size * 1024: + if max_size and file.size > max_size: return False return True @@ -139,49 +141,6 @@ def __parse_dt(val): return val try: return datetime.fromisoformat(val) - except Exception: - return val - - -def __write_csv(files_by_provider, filename): - # Collect all possible fieldnames from all files - all_fieldnames = set(["provider"]) - for files in files_by_provider.values(): - for file in files: - file_dict = file.model_dump() if hasattr(file, "model_dump") else file.__dict__.copy() - all_fieldnames.update(file_dict.keys()) - # Move provider to the front, rest sorted - fieldnames = ["provider"] + sorted(f for f in all_fieldnames if f != "provider") - with open(filename, "w", newline="") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - writer.writeheader() - for provider, files in files_by_provider.items(): - for file in files: - file_dict = file.model_dump() if hasattr(file, "model_dump") else file.__dict__.copy() - file_dict["provider"] = provider - # Flatten owners for CSV (only email addresses) - owners = file_dict.get("owners") - if isinstance(owners, list): - emails = [] - for u in owners: - if hasattr(u, "email_address") and u.email_address: - emails.append(u.email_address) - elif isinstance(u, dict) and u.get("email_address"): - emails.append(u["email_address"]) - elif isinstance(u, str): - emails.append(u) - file_dict["owners"] = ";".join(emails) - # Flatten last_modifying_user for CSV (only email address) - last_mod = file_dict.get("last_modifying_user") - if last_mod is not None: - if hasattr(last_mod, "email_address"): - file_dict["last_modifying_user"] = last_mod.email_address - elif isinstance(last_mod, dict) and "email_address" in last_mod: - file_dict["last_modifying_user"] = last_mod["email_address"] - else: - file_dict["last_modifying_user"] = str(last_mod) - # Flatten parents for CSV - parents = file_dict.get("parents") - if isinstance(parents, list): - file_dict["parents"] = ";".join(str(p) for p in parents) - writer.writerow({fn: file_dict.get(fn, "") for fn in fieldnames}) + except Exception as e: + typer.echo(f"Failed to parse datetime from value: {val} with error: {e}", err=True) + raise ValueError(f"Invalid datetime format: {val}") from e diff --git a/src/docbinder_oss/helpers/config.py b/src/docbinder_oss/helpers/config.py index 8a49070..088d95d 100644 --- a/src/docbinder_oss/helpers/config.py +++ b/src/docbinder_oss/helpers/config.py @@ -1,11 +1,12 @@ import logging import os +from typing import List import typer import yaml from pydantic import BaseModel, ValidationError -from docbinder_oss.providers import get_provider_registry +from docbinder_oss.providers import ServiceUnion, get_provider_registry logger = logging.getLogger(__name__) @@ -15,7 +16,7 @@ class Config(BaseModel): """Main configuration model that holds a list of all provider configs.""" - providers: list + providers: List[ServiceUnion] # type: ignore def load_config() -> Config: diff --git a/src/docbinder_oss/helpers/rich_helpers.py b/src/docbinder_oss/helpers/rich_helpers.py deleted file mode 100644 index 6faefe5..0000000 --- a/src/docbinder_oss/helpers/rich_helpers.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List -from rich.table import Table - - -def create_rich_table(headers: List[str], rows: List[List[str]]) -> Table: - """ - Create a Rich table with the given headers and rows. - - Args: - headers (List[str]): The headers for the table. - rows (List[List[str]]): The data rows for the table. - - Returns: - Table: A Rich Table object. - """ - table = Table(*headers, show_header=True, header_style="bold magenta") - for row in rows: - table.add_row(*row) - return table diff --git a/src/docbinder_oss/helpers/writers/multiformat_writer.py b/src/docbinder_oss/helpers/writers/multiformat_writer.py index 4cae081..c6b688e 100644 --- a/src/docbinder_oss/helpers/writers/multiformat_writer.py +++ b/src/docbinder_oss/helpers/writers/multiformat_writer.py @@ -1,6 +1,8 @@ from pathlib import Path -from typing import Any +from typing import Any, Dict, List +from docbinder_oss.core.schemas import File +from docbinder_oss.helpers.writers.base import Writer from docbinder_oss.helpers.writers.writer_console import ConsoleWriter from docbinder_oss.helpers.writers.writer_csv import CSVWriter from docbinder_oss.helpers.writers.writer_json import JSONWriter @@ -20,7 +22,7 @@ class MultiFormatWriter: } @classmethod - def write(cls, data: Any, file_path: str | None = None) -> None: + def write(cls, data: Dict[str, List[File]], file_path: str | None = None) -> None: if not file_path: ConsoleWriter().write(data) return @@ -30,5 +32,6 @@ def write(cls, data: Any, file_path: str | None = None) -> None: if writer_key not in cls._writers: raise ValueError(f"Unsupported format: {file_path}") writer_class = cls._writers[writer_key] - writer = writer_class() + writer: Writer = writer_class() writer.write(data, file_path) + \ No newline at end of file diff --git a/src/docbinder_oss/helpers/writers/writer_console.py b/src/docbinder_oss/helpers/writers/writer_console.py index 0fae481..ff17bff 100644 --- a/src/docbinder_oss/helpers/writers/writer_console.py +++ b/src/docbinder_oss/helpers/writers/writer_console.py @@ -1,5 +1,7 @@ from pathlib import Path from typing import Any +from rich.table import Table +from rich import print from docbinder_oss.helpers.writers.base import Writer @@ -7,23 +9,12 @@ class ConsoleWriter(Writer): """Writer for pretty-printing data to the console using rich tables.""" def write(self, data: Any, file_path: str | Path | None = None) -> None: - from rich.table import Table - table = Table(title="Files and Folders") table.add_column("Provider", justify="right", style="cyan", no_wrap=True) table.add_column("Id", style="magenta") table.add_column("Name", style="magenta") table.add_column("Kind", style="magenta") - for provider, items in data.items() if isinstance(data, dict) else [("?", data)]: + for provider, items in data.items(): for item in items: - if hasattr(item, "model_dump"): - item = item.model_dump() - elif hasattr(item, "__dict__"): - item = dict(item.__dict__) - table.add_row( - str(provider), - str(item.get("id", "")), - str(item.get("name", "")), - str(item.get("kind", "")), - ) + table.add_row(provider, item.id, item.name, item.kind) print(table) diff --git a/src/docbinder_oss/helpers/writers/writer_csv.py b/src/docbinder_oss/helpers/writers/writer_csv.py index 3d6eb64..0d9c281 100644 --- a/src/docbinder_oss/helpers/writers/writer_csv.py +++ b/src/docbinder_oss/helpers/writers/writer_csv.py @@ -1,41 +1,29 @@ import csv import logging from pathlib import Path -from typing import Any +from typing import List, Dict, Union +from pydantic import BaseModel from docbinder_oss.helpers.writers.base import Writer -from docbinder_oss.helpers.writers.helper_functions import flatten_file + +logger = logging.getLogger(__name__) class CSVWriter(Writer): """Writer for exporting data to CSV files.""" + def get_fieldnames(self, data: Dict[str, List[BaseModel]]) -> List[str]: + fieldnames = next(iter(data.values()))[0].model_fields_set + return ["provider", *fieldnames] - def get_fieldnames(self, rows: list) -> list: - fieldnames = set() - for row in rows: - fieldnames.update(row.keys()) - # Provider first, then the rest sorted - return ["provider"] + sorted(f for f in fieldnames if f != "provider") + def write(self, data: List[Dict], file_path: Union[str, Path]) -> None: + if not data: + logger.warning("No data to write to CSV.") + return - def write(self, data: Any, file_path: str | Path | None = None) -> None: - """ - Always flattens grouped dicts to a flat list for CSV export. - """ - rows = [] - if isinstance(data, dict): + with open(file_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=self.get_fieldnames(data)) + writer.writeheader() for provider, items in data.items(): for item in items: - rows.append(flatten_file(item, provider)) - elif isinstance(data, list): - for item in data: - provider = item.get("provider") if isinstance(item, dict) else getattr(item, "provider", None) - rows.append(flatten_file(item, provider)) - else: - return - if not rows or not file_path: - logging.warning("No data to write to CSV.") - return - with open(file_path, "w", newline="", encoding="utf-8") as f: - writer = csv.DictWriter(f, fieldnames=self.get_fieldnames(rows)) - writer.writeheader() - for row in rows: - writer.writerow(row) + item_dict = item.model_dump() if isinstance(item, BaseModel) else item + item_dict['provider'] = provider + writer.writerow(item_dict) diff --git a/src/docbinder_oss/helpers/writers/writer_json.py b/src/docbinder_oss/helpers/writers/writer_json.py index 977ce3f..d928814 100644 --- a/src/docbinder_oss/helpers/writers/writer_json.py +++ b/src/docbinder_oss/helpers/writers/writer_json.py @@ -1,29 +1,17 @@ import json from pathlib import Path -from typing import Any +from typing import Dict, List, Union +from docbinder_oss.core.schemas import File from docbinder_oss.helpers.writers.base import Writer -from docbinder_oss.helpers.writers.helper_functions import flatten_file class JSONWriter(Writer): """Writer for exporting data to JSON files.""" - def write(self, data: Any, file_path: str | Path | None = None) -> None: - """ - Always flattens grouped dicts to a flat list for JSON export. - """ - flat = [] - if isinstance(data, dict): - for provider, items in data.items(): - for item in items: - flat.append(flatten_file(item, provider)) - elif isinstance(data, list): - for item in data: - provider = item.get("provider") if isinstance(item, dict) else getattr(item, "provider", None) - flat.append(flatten_file(item, provider)) - else: - return - if not file_path: - return - with open(file_path, "w", encoding="utf-8") as f: - json.dump(flat, f, indent=2, ensure_ascii=False, default=str) + def write(self, data: Dict[str, List[File]], file_path: Union[str, Path]) -> None: + data = { + provider: [item.model_dump() for item in items] + for provider, items in data.items() + } + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False, default=str) diff --git a/src/docbinder_oss/providers/google_drive/google_drive_files.py b/src/docbinder_oss/providers/google_drive/google_drive_files.py index 76512d3..18fbb58 100644 --- a/src/docbinder_oss/providers/google_drive/google_drive_files.py +++ b/src/docbinder_oss/providers/google_drive/google_drive_files.py @@ -27,7 +27,7 @@ def list_files_in_folder(self, bucket_id: str | None = None) -> list[File]: if bucket_id: args["q"] = f"'{bucket_id}' in parents and trashed=false" else: - args["q"] = None + args["q"] = "trashed=false" resp = self.service.files().list(**args).execute() files = resp.get("files", []) diff --git a/tests/commands/test_search_command.py b/tests/commands/test_search_command.py index 8608fac..1ccc378 100644 --- a/tests/commands/test_search_command.py +++ b/tests/commands/test_search_command.py @@ -1,220 +1,272 @@ -import os import csv import json +from typing import Dict import pytest +from pathlib import Path from typer.testing import CliRunner +from docbinder_oss.core.schemas import User from docbinder_oss.main import app +from conftest import DummyModel -class DummyFile: - def __init__(self, **kwargs): - self.id = kwargs.get("id", "fileid1") - self.name = kwargs.get("name", "Test File") - self.size = kwargs.get("size", 12345) - self.mime_type = kwargs.get("mime_type", "application/pdf") - self.created_time = kwargs.get("created_time", "2024-01-01T00:00:00") - self.modified_time = kwargs.get("modified_time", "2024-01-02T00:00:00") - self.owners = kwargs.get("owners", [type("User", (), {"email_address": "owner@example.com"})()]) - self.last_modifying_user = kwargs.get( - "last_modifying_user", type("User", (), {"email_address": "mod@example.com"})() - ) - self.web_view_link = kwargs.get("web_view_link", "http://example.com/view") - self.web_content_link = kwargs.get("web_content_link", "http://example.com/content") - self.shared = kwargs.get("shared", True) - self.trashed = kwargs.get("trashed", False) - - def model_dump(self): - # Simulate pydantic's model_dump for test compatibility - return { - "id": self.id, - "name": self.name, - "size": self.size, - "mime_type": self.mime_type, - "created_time": self.created_time, - "modified_time": self.modified_time, - "owners": [u.email_address for u in self.owners], - "last_modifying_user": getattr(self.last_modifying_user, "email_address", None), - "web_view_link": self.web_view_link, - "web_content_link": self.web_content_link, - "shared": self.shared, - "trashed": self.trashed, - } - - -@pytest.fixture(autouse=True) -def patch_provider(monkeypatch, tmp_path): - # Patch config loader to return two dummy provider configs - class DummyProviderConfig: - def __init__(self, name): - self.name = name - self.type = name # Simulate type for registry - - class DummyConfig: - providers = [DummyProviderConfig("dummy1"), DummyProviderConfig("dummy2")] - - # Patch load_config in the CLI's namespace - monkeypatch.setattr("docbinder_oss.cli.search.load_config", lambda: DummyConfig()) - - # Patch create_provider_instance in the CLI's namespace - def create_provider_instance(cfg): - if cfg.name == "dummy1": - return type( - "DummyClient", - (), - { - "list_all_files": lambda self: [ - DummyFile( - id="f1", - name="Alpha Report", - size=2048, - owners=[type("User", (), {"email_address": "alpha@a.com"})()], - created_time="2024-01-01T10:00:00", - modified_time="2024-01-02T10:00:00", - ) - ] - }, - )() - else: - return type( - "DummyClient", - (), - { - "list_all_files": lambda self: [ - DummyFile( - id="f2", - name="Beta Notes", - size=4096, - owners=[type("User", (), {"email_address": "beta@b.com"})()], - created_time="2024-02-01T10:00:00", - modified_time="2024-02-02T10:00:00", - ) - ] - }, - )() - - monkeypatch.setattr("docbinder_oss.cli.search.create_provider_instance", create_provider_instance) - - # Change working directory to a temp dir for file output - orig_cwd = os.getcwd() - os.chdir(tmp_path) - yield - os.chdir(orig_cwd) - - -def test_search_export_csv(): - runner = CliRunner() +runner = CliRunner() + +@pytest.mark.parametrize('load_config_mock', [("dummy", 2)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file"), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file"), + ])], indirect=True) +def test_search_export_csv(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test happy path for search command with CSV export.""" result = runner.invoke(app, ["search", "--export-file", "search_results.csv"]) assert result.exit_code == 0 - assert os.path.exists("search_results.csv") + assert Path("search_results.csv").exists() with open("search_results.csv") as f: reader = csv.DictReader(f) rows = list(reader) - assert len(rows) == 2 - names = set(r["name"] for r in rows) - assert names == {"Alpha Report", "Beta Notes"} - # Check owners field is a string and contains the expected email - for r in rows: - owners = r["owners"] - if r["name"] == "Alpha Report": - assert "alpha@a.com" in owners - if r["name"] == "Beta Notes": - assert "beta@b.com" in owners - - -def test_search_export_json(): - runner = CliRunner() + assert len(rows) == 4 + assert set(r["provider"] for r in rows) == {"dummy1", "dummy2"} + +@pytest.mark.parametrize('load_config_mock', [("dummy", 2)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file"), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file"), + ])], indirect=True) +def test_search_export_json(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test happy path for search command with CSV export.""" result = runner.invoke(app, ["search", "--export-file", "search_results.json"]) assert result.exit_code == 0 - assert os.path.exists("search_results.json") + assert Path("search_results.json").exists() with open("search_results.json") as f: - data = json.load(f) - assert isinstance(data, list) - assert len(data) == 2 - names = set(d["name"] for d in data) - assert names == {"Alpha Report", "Beta Notes"} - # Check owners field is a string or list - for d in data: - if d["name"] == "Alpha Report": - assert "alpha@a.com" in d["owners"] - if d["name"] == "Beta Notes": - assert "beta@b.com" in d["owners"] - + data: Dict = json.load(f) + assert len(data.keys()) == 2 + assert len(data["dummy1"]) == 2 + assert len(data["dummy2"]) == 2 + assert all(key in data for key in ("dummy1", "dummy2")) -def test_search_name_filter(): - runner = CliRunner() +@pytest.mark.parametrize('load_config_mock', [("dummy", 2)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file"), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file"), + ])], indirect=True) +def test_search_name_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """ + Test search command with name filter that returns no results. + """ result = runner.invoke(app, ["search", "--name", "Alpha", "--export-file", "search_results.json"]) assert result.exit_code == 0 with open("search_results.json") as f: data = json.load(f) - assert len(data) == 1 - assert data[0]["name"] == "Alpha Report" + assert len(data["dummy1"]) == 0 + assert len(data["dummy2"]) == 0 + +@pytest.mark.parametrize('load_config_mock', [("dummy", 2)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file"), + DummyModel(id="dummy_file2", name="File 2", kind="file"), + ])], indirect=True) +def test_search_name_filter_not_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """ + Test search command with name filter that returns some results. + """ + result = runner.invoke(app, ["search", "--name", "dummy", "--export-file", "search_results.json"]) + assert result.exit_code == 0 + with open("search_results.json") as f: + data = json.load(f) + assert len(data) == 2 + assert data["dummy1"][0]["name"] == "dummy File 1" + assert data["dummy2"][0]["name"] == "dummy File 1" +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", owners=[User(display_name="test", email_address="beta@a.com", photo_link="https://test.com", kind="")]), + DummyModel(id="dummy_file2", name="File 2", kind="file", owners=[]), + ])], indirect=True) +def test_search_owner_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with owner filter that returns no results.""" + result = runner.invoke(app, ["search", "--owner", "beta@b.com", "--export-file", "search_results.json"]) + assert result.exit_code == 0 + with open("search_results.json") as f: + data = json.load(f) + assert len(data["dummy1"]) == 0 -def test_search_owner_filter(): - runner = CliRunner() +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", owners=[User(display_name="test", email_address="beta@b.com", photo_link="https://test.com", kind="")]), + DummyModel(id="dummy_file2", name="File 2", kind="file", owners=[]), + ])], indirect=True) +def test_search_owner_filter_not_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with owner filter that returns some results.""" result = runner.invoke(app, ["search", "--owner", "beta@b.com", "--export-file", "search_results.json"]) assert result.exit_code == 0 with open("search_results.json") as f: data = json.load(f) assert len(data) == 1 - assert data[0]["name"] == "Beta Notes" - + assert data["dummy1"][0]["owners"][0]["email_address"] == "beta@b.com" -def test_search_updated_after_filter(): - runner = CliRunner() +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", modified_time="2023-02-02T00:00:00"), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", modified_time="2024-01-31T00:00:00"), + ])], indirect=True) +def test_search_updated_after_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with updated_after filter that returns no results.""" result = runner.invoke(app, ["search", "--updated-after", "2024-02-01T00:00:00", "--export-file", "search_results.json"]) assert result.exit_code == 0 with open("search_results.json") as f: data = json.load(f) - assert len(data) == 1 - assert data[0]["name"] == "Beta Notes" + assert len(data["dummy1"]) == 0 + +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", modified_time="2024-02-02T00:00:00"), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", modified_time="2024-01-31T00:00:00"), + ])], indirect=True) +def test_search_updated_after_filter_not_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with updated_after filter that returns some results.""" + result = runner.invoke(app, ["search", "--updated-after", "2024-02-01T00:00:00", "--export-file", "search_results.json"]) + assert result.exit_code == 0 + with open("search_results.json") as f: + data = json.load(f) + assert len(data["dummy1"]) == 1 + assert data["dummy1"][0]["name"] == "dummy File 1" +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", created_time="2024-04-02T00:00:00"), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", created_time="2024-04-30T00:00:00"), + ])], indirect=True) +def test_search_created_before_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with created_before filter that returns no results.""" + result = runner.invoke( + app, ["search", "--created-before", "2024-02-01T00:00:00", "--export-file", "search_results.json"] + ) + assert result.exit_code == 0 + with open("search_results.json") as f: + data = json.load(f) + assert len(data["dummy1"]) == 0 -def test_search_created_before_filter(): - runner = CliRunner() +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", created_time="2024-02-02T00:00:00"), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", created_time="2024-01-31T00:00:00"), + ])], indirect=True) +def test_search_created_before_filter_not_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with created_before filter that returns some results.""" result = runner.invoke( app, ["search", "--created-before", "2024-02-01T00:00:00", "--export-file", "search_results.json"] ) assert result.exit_code == 0 with open("search_results.json") as f: data = json.load(f) - assert len(data) == 1 - assert data[0]["name"] == "Alpha Report" + assert len(data["dummy1"]) == 1 + assert data["dummy1"][0]["name"] == "dummy File 2" +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", size=1), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=2), + ])], indirect=True) +def test_search_min_size_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with min_size filter that returns no results.""" + result = runner.invoke(app, ["search", "--min-size", 3, "--export-file", "search_results.json"]) + assert result.exit_code == 0 + with open("search_results.json") as f: + data = json.load(f) + assert len(data["dummy1"]) == 0 -def test_search_min_size_filter(): +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", size=5), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=2), + ])], indirect=True) +def test_search_min_size_filter_not_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): runner = CliRunner() - result = runner.invoke(app, ["search", "--min-size", "3", "--export-file", "search_results.json"]) + result = runner.invoke(app, ["search", "--min-size", 3, "--export-file", "search_results.json"]) assert result.exit_code == 0 with open("search_results.json") as f: data = json.load(f) - assert len(data) == 1 - assert data[0]["name"] == "Beta Notes" + assert len(data["dummy1"]) == 1 + assert data["dummy1"][0]["name"] == "dummy File 1" +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", size=5), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=3), + ])], indirect=True) +def test_search_max_size_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with max_size filter that returns no results.""" + result = runner.invoke(app, ["search", "--max-size", "3", "--export-file", "search_results.json"]) + assert result.exit_code == 0 + with open("search_results.json") as f: + data = json.load(f) + assert len(data["dummy1"]) == 1 -def test_search_max_size_filter(): - runner = CliRunner() +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", size=5), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=2), + ])], indirect=True) +def test_search_max_size_filter_not_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with max_size filter that returns some results.""" result = runner.invoke(app, ["search", "--max-size", "3", "--export-file", "search_results.json"]) assert result.exit_code == 0 with open("search_results.json") as f: data = json.load(f) - assert len(data) == 1 - assert data[0]["name"] == "Alpha Report" + assert len(data["dummy1"]) == 1 + assert data["dummy1"][0]["name"] == "dummy File 2" +@pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", size=5), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=2), + ])], indirect=True) +def test_search_provider_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with provider filter that returns no results.""" + result = runner.invoke(app, ["search", "--provider", "dummy2", "--export-file", "search_results.json"]) + assert result.exit_code == 0 + with open("search_results.json") as f: + data = json.load(f) + assert len(data) == 0 -def test_search_provider_filter(): - runner = CliRunner() +@pytest.mark.parametrize('load_config_mock', [("dummy", 2)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="dummy File 1", kind="file", size=5), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=2), + ])], indirect=True) +def test_search_provider_filter(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with provider filter that returns some results.""" result = runner.invoke(app, ["search", "--provider", "dummy2", "--export-file", "search_results.json"]) assert result.exit_code == 0 with open("search_results.json") as f: data = json.load(f) assert len(data) == 1 - assert data[0]["provider"] == "dummy2" - assert data[0]["name"] == "Beta Notes" - + assert "dummy2" in data -def test_search_combined_filters(): - runner = CliRunner() +@pytest.mark.parametrize('load_config_mock', [("dummy", 2)], indirect=True) +@pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) +@pytest.mark.parametrize('list_all_files_mock', [([ + DummyModel(id="dummy_file1", name="Beta File 1", kind="file", size=5, owners=[User(display_name="test", email_address="beta@b.com", photo_link="https://test.com", kind="")]), + DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=2), + ])], indirect=True) +def test_search_combined_filters(load_config_mock, create_provider_instance_mock, list_all_files_mock): + """Test search command with combined filters.""" result = runner.invoke( app, [ @@ -235,6 +287,6 @@ def test_search_combined_filters(): with open("search_results.json") as f: data = json.load(f) assert len(data) == 1 - assert data[0]["name"] == "Beta Notes" - assert data[0]["provider"] == "dummy2" - assert "beta@b.com" in data[0]["owners"] + assert "dummy2" in data + assert data["dummy2"][0]["name"] == "Beta File 1" + assert data["dummy2"][0]["owners"][0]["email_address"] == "beta@b.com" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e3f37ee --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,135 @@ +from typing import List +from unittest.mock import MagicMock, patch + +from pydantic import BaseModel, ConfigDict +import pytest + +from docbinder_oss.providers.base_class import BaseProvider +from docbinder_oss.providers.google_drive.google_drive_client import ( + GoogleDriveClient, +) +from docbinder_oss.providers.google_drive.google_drive_service_config import ( + GoogleDriveServiceConfig, +) + + +class DummyModel(BaseModel): + id: str + name: str + kind: str + + model_config = ConfigDict(extra="allow") + + +class DummyProvider(BaseProvider): + def __init__(self, name, type=None): + self.name = name + self.type = type if type else f"{name}_type" + + def list_all_files(self): + raise NotImplementedError("Please use the pytest parametrize settings to add your test data.") + def test_connection(self): + raise NotImplementedError("This provider does not implement connection testing") + def list_buckets(self): + raise NotImplementedError("This provider does not implement buckets") + def get_permissions(self): + raise NotImplementedError("This provider does not implement permissions") + def list_files_in_folder(self): + raise NotImplementedError("This provider does not implement folder listing") + def get_file_metadata(self, item_id): + raise NotImplementedError("This provider does not implement file metadata retrieval") + +class DummyConfig: + providers: List[DummyProvider] = [] + +@pytest.fixture +def sample_data(): + return { + "provider1": [ + DummyModel(id="1", name="FileA", kind="file"), + DummyModel(id="2", name="FolderB", kind="folder"), + ], + "provider2": [ + DummyModel(id="3", name="FileC", kind="file"), + ], + } + +@pytest.fixture +def mock_gdrive_provider(): + """ + This is the core of our testing strategy. We use 'patch' to replace + the `build` function from the googleapiclient library. + + Whenever `GoogleDriveClient` calls `build('drive', 'v3', ...)`, it will + receive our mock object instead of making a real network call. + """ + with patch("docbinder_oss.providers.google_drive.google_drive_client.build") as mock_build: + # Create a mock for the provider object that `build` would return + mock_provider = MagicMock() + # Configure the `build` function to return our mock provider + mock_build.return_value = mock_provider + yield mock_provider + + +@pytest.fixture +def gdrive_client(mock_gdrive_provider): + """ + Creates an instance of our GoogleDriveClient. + It will be initialized with a fake config and will use + the mock_gdrive_provider fixture internally. + """ + # Patch _get_credentials to avoid real auth + with patch( + "docbinder_oss.providers.google_drive.google_drive_client.GoogleDriveClient._get_credentials", + return_value=MagicMock(), + ): + config = GoogleDriveServiceConfig( + name="test_gdrive", + gcp_credentials_json="fake_creds.json", + ) + return GoogleDriveClient(config=config) + +@pytest.fixture(scope='session') +def load_config_mock(request, create_config_mock): + """ + This fixture mocks the `load_config` function to return + a dummy configuration with a specified number of providers. + """ + name, number_of_providers = request.param + with patch("docbinder_oss.cli.search.load_config", return_value=create_config_mock(name, number_of_providers)) as _fixture: + yield _fixture + +@pytest.fixture(scope='session') +def create_provider_instance_mock(request, create_provider_mock): + """ + This fixture mocks the `create_provider_instance` function to return + a dummy provider instance based on the provider name. + """ + with patch("docbinder_oss.cli.search.create_provider_instance", return_value=create_provider_mock(request.param)) as _fixture: + yield _fixture + +@pytest.fixture(scope="session") +def list_all_files_mock(request): + """ + + Yields: + _type_: _description_ + """ + data = request.param + with patch("conftest.DummyProvider.list_all_files", return_value=data) as _fixture: + yield _fixture + +@pytest.fixture(scope='session') +def create_provider_mock(): + def create_dummy_provider(name): + return DummyProvider(name=name, type=f"{name}_type") + yield create_dummy_provider + +@pytest.fixture(scope='session') +def create_config_mock(create_provider_mock): + """This fixture creates a dummy configuration with a specified number of providers.""" + def create_dummy_config(name, number_of_providers=2): + dummy_config = DummyConfig() + dummy_config.providers = [create_provider_mock(f"{name}{i+1}") for i in range(number_of_providers)] + return dummy_config + yield create_dummy_config \ No newline at end of file diff --git a/tests/helpers/test_writer.py b/tests/helpers/test_writer.py index 651bf87..abe0920 100644 --- a/tests/helpers/test_writer.py +++ b/tests/helpers/test_writer.py @@ -50,12 +50,12 @@ def test_json_writer(tmp_path, sample_data): assert file_path.exists() with open(file_path, encoding="utf-8") as f: data = json.load(f) - assert isinstance(data, list) - assert len(data) == 3 - providers = {d["provider"] for d in data} - assert "provider1" in providers - assert "provider2" in providers - assert any(d["id"] == "1" and d["provider"] == "provider1" for d in data) + assert isinstance(data, dict) + assert len(data) == 2 + assert "provider1" in data + assert "provider2" in data + assert data["provider1"][0]["id"] == "1" + assert data["provider2"][0]["id"] == "3" def test_multiformat_writer_csv(tmp_path, sample_data): @@ -74,9 +74,9 @@ def test_multiformat_writer_json(tmp_path, sample_data): assert file_path.exists() with open(file_path, encoding="utf-8") as f: data = json.load(f) - assert isinstance(data, list) - providers = {d["provider"] for d in data} - assert "provider2" in providers + assert isinstance(data, dict) + assert "provider1" in data + assert "provider2" in data def test_multiformat_writer_unsupported(tmp_path, sample_data): diff --git a/tests/providers/google_drive/conftest.py b/tests/providers/google_drive/conftest.py deleted file mode 100644 index b248aac..0000000 --- a/tests/providers/google_drive/conftest.py +++ /dev/null @@ -1,46 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest - -from docbinder_oss.providers.google_drive.google_drive_client import ( - GoogleDriveClient, -) -from docbinder_oss.providers.google_drive.google_drive_service_config import ( - GoogleDriveServiceConfig, -) - - -@pytest.fixture -def mock_gdrive_provider(): - """ - This is the core of our testing strategy. We use 'patch' to replace - the `build` function from the googleapiclient library. - - Whenever `GoogleDriveClient` calls `build('drive', 'v3', ...)`, it will - receive our mock object instead of making a real network call. - """ - with patch("docbinder_oss.providers.google_drive.google_drive_client.build") as mock_build: - # Create a mock for the provider object that `build` would return - mock_provider = MagicMock() - # Configure the `build` function to return our mock provider - mock_build.return_value = mock_provider - yield mock_provider - - -@pytest.fixture -def gdrive_client(mock_gdrive_provider): - """ - Creates an instance of our GoogleDriveClient. - It will be initialized with a fake config and will use - the mock_gdrive_provider fixture internally. - """ - # Patch _get_credentials to avoid real auth - with patch( - "docbinder_oss.providers.google_drive.google_drive_client.GoogleDriveClient._get_credentials", - return_value=MagicMock(), - ): - config = GoogleDriveServiceConfig( - name="test_gdrive", - gcp_credentials_json="fake_creds.json", - ) - return GoogleDriveClient(config=config) From 090ee2983b229435e489d746d0d737541f77a45c Mon Sep 17 00:00:00 2001 From: PaoloLeonard Date: Mon, 30 Jun 2025 11:41:00 +0200 Subject: [PATCH 2/3] remove logger --- src/docbinder_oss/cli/search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/docbinder_oss/cli/search.py b/src/docbinder_oss/cli/search.py index 34feb90..0a58f1f 100644 --- a/src/docbinder_oss/cli/search.py +++ b/src/docbinder_oss/cli/search.py @@ -121,7 +121,6 @@ def file_matches(file: File): if created_before: file_created_time = __parse_dt(file.created_time) created_before_dt = __parse_dt(created_before) - logging.debug(f"File created time: {file_created_time}, Created before: {created_before_dt}, Type: {type(file_created_time)}, Type: {type(created_before_dt)}") if file_created_time is not None and created_before_dt is not None and file_created_time > created_before_dt: return False if min_size and file.size < min_size: From a803975aa68151aa963b7f89186a8ead3cd17a9c Mon Sep 17 00:00:00 2001 From: PaoloLeonard Date: Mon, 30 Jun 2025 11:55:29 +0200 Subject: [PATCH 3/3] fix linting --- .pre-commit-config.yaml | 2 +- src/docbinder_oss/cli/search.py | 2 - .../helpers/writers/multiformat_writer.py | 2 +- tests/commands/test_search_command.py | 43 +++++++++++++++++-- tests/conftest.py | 10 ++++- 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9591f18..e885ddb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # Run the linter. - id: ruff-check types_or: [ python, pyi ] - args: [ --select, I, --fix ] + args: [ --select, I, --fix, --select=E501 ] # Run the formatter. - id: ruff-format types_or: [ python, pyi ] diff --git a/src/docbinder_oss/cli/search.py b/src/docbinder_oss/cli/search.py index 0a58f1f..d4b63e3 100644 --- a/src/docbinder_oss/cli/search.py +++ b/src/docbinder_oss/cli/search.py @@ -1,9 +1,7 @@ from datetime import datetime -import logging import re import typer from typing import Dict, List, Optional -import csv from docbinder_oss.core.schemas import File from docbinder_oss.helpers.config import load_config diff --git a/src/docbinder_oss/helpers/writers/multiformat_writer.py b/src/docbinder_oss/helpers/writers/multiformat_writer.py index c6b688e..ba282fa 100644 --- a/src/docbinder_oss/helpers/writers/multiformat_writer.py +++ b/src/docbinder_oss/helpers/writers/multiformat_writer.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Dict, List +from typing import Dict, List from docbinder_oss.core.schemas import File from docbinder_oss.helpers.writers.base import Writer diff --git a/tests/commands/test_search_command.py b/tests/commands/test_search_command.py index 1ccc378..eb37e4d 100644 --- a/tests/commands/test_search_command.py +++ b/tests/commands/test_search_command.py @@ -84,7 +84,19 @@ def test_search_name_filter_not_empty(load_config_mock, create_provider_instance @pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) @pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) @pytest.mark.parametrize('list_all_files_mock', [([ - DummyModel(id="dummy_file1", name="dummy File 1", kind="file", owners=[User(display_name="test", email_address="beta@a.com", photo_link="https://test.com", kind="")]), + DummyModel( + id="dummy_file1", + name="dummy File 1", + kind="file", + owners=[ + User( + display_name="test", + email_address="beta@a.com", + photo_link="https://test.com", + kind="" + ) + ] + ), DummyModel(id="dummy_file2", name="File 2", kind="file", owners=[]), ])], indirect=True) def test_search_owner_filter_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): @@ -98,7 +110,19 @@ def test_search_owner_filter_empty(load_config_mock, create_provider_instance_mo @pytest.mark.parametrize('load_config_mock', [("dummy", 1)], indirect=True) @pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) @pytest.mark.parametrize('list_all_files_mock', [([ - DummyModel(id="dummy_file1", name="dummy File 1", kind="file", owners=[User(display_name="test", email_address="beta@b.com", photo_link="https://test.com", kind="")]), + DummyModel( + id="dummy_file1", + name="dummy File 1", + kind="file", + owners=[ + User( + display_name="test", + email_address="beta@b.com", + photo_link="https://test.com", + kind="" + ) + ] + ), DummyModel(id="dummy_file2", name="File 2", kind="file", owners=[]), ])], indirect=True) def test_search_owner_filter_not_empty(load_config_mock, create_provider_instance_mock, list_all_files_mock): @@ -262,7 +286,20 @@ def test_search_provider_filter(load_config_mock, create_provider_instance_mock, @pytest.mark.parametrize('load_config_mock', [("dummy", 2)], indirect=True) @pytest.mark.parametrize('create_provider_instance_mock', [("dummy")], indirect=True) @pytest.mark.parametrize('list_all_files_mock', [([ - DummyModel(id="dummy_file1", name="Beta File 1", kind="file", size=5, owners=[User(display_name="test", email_address="beta@b.com", photo_link="https://test.com", kind="")]), + DummyModel( + id="dummy_file1", + name="Beta File 1", + kind="file", + size=5, + owners=[ + User( + display_name="test", + email_address="beta@b.com", + photo_link="https://test.com", + kind="" + ) + ] + ), DummyModel(id="dummy_file2", name="dummy File 2", kind="file", size=2), ])], indirect=True) def test_search_combined_filters(load_config_mock, create_provider_instance_mock, list_all_files_mock): diff --git a/tests/conftest.py b/tests/conftest.py index e3f37ee..062bc2a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -96,7 +96,10 @@ def load_config_mock(request, create_config_mock): a dummy configuration with a specified number of providers. """ name, number_of_providers = request.param - with patch("docbinder_oss.cli.search.load_config", return_value=create_config_mock(name, number_of_providers)) as _fixture: + with patch( + "docbinder_oss.cli.search.load_config", + return_value=create_config_mock(name, number_of_providers) + ) as _fixture: yield _fixture @pytest.fixture(scope='session') @@ -105,7 +108,10 @@ def create_provider_instance_mock(request, create_provider_mock): This fixture mocks the `create_provider_instance` function to return a dummy provider instance based on the provider name. """ - with patch("docbinder_oss.cli.search.create_provider_instance", return_value=create_provider_mock(request.param)) as _fixture: + with patch( + "docbinder_oss.cli.search.create_provider_instance", + return_value=create_provider_mock(request.param) + ) as _fixture: yield _fixture @pytest.fixture(scope="session")