diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b58db13 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + pip install pytest # For running tests + + - name: Run tests + run: | + pytest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1cc0a42 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Use an official Python runtime as a parent image +FROM python:3.10-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the project files into the container +COPY . . + +# Install the project and its dependencies +RUN pip install --upgrade pip && \ + pip install typing_extensions && \ + pip install . + +# Set the entrypoint for the container +ENTRYPOINT ["ddg-cli"] + +# Default command to show help (optional) +CMD ["--help"] \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 82f49cf..0000000 --- a/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -# Makefile for DuckDuckGo CLI - -# Variables -PYTHON = python3 -PIP = pip3 -PROJECT_DIR = $(shell pwd) - -# Default target -.PHONY: help -help: - @echo "DuckDuckGo CLI Makefile" - @echo "======================" - @echo "Available targets:" - @echo " install - Install the CLI tool system-wide" - @echo " setup - Set up the development environment" - @echo " test - Run the test suite" - @echo " clean - Clean up temporary files" - @echo " help - Show this help message" - -# Install the CLI tool system-wide -.PHONY: install -install: - @echo "Installing DuckDuckGo CLI system-wide..." - $(PROJECT_DIR)/install.sh - -# Set up the development environment -.PHONY: setup -setup: - @echo "Setting up development environment..." - $(PROJECT_DIR)/setup.sh - -# Run the test suite -.PHONY: test -test: - @echo "Running test suite..." - $(PYTHON) $(PROJECT_DIR)/tests/test_ddgs.py - -# Clean up temporary files -.PHONY: clean -clean: - @echo "Cleaning up temporary files..." - rm -f *.tmp *.log - rm -rf __pycache__ */__pycache__ - rm -rf src/__pycache__ - -# Run a basic search test (will only work if network allows) -.PHONY: search-test -search-test: - @echo "Running basic search test..." - $(PROJECT_DIR)/ddgs search "Python programming" --results 3 \ No newline at end of file diff --git a/README.md b/README.md index b443e66..87df224 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,17 @@ A feature-rich command-line interface for DuckDuckGo search that brings the powe ## Quick Start ```bash -# Install dependencies -pip install -r requirements.txt - -# Make the script executable -chmod +x ddgs +# Install the package +pip install . # Search for something -PYTHONPATH=./src python3 ddgs search "python web scraping" +ddg-cli search "python web scraping" # Get more results -PYTHONPATH=./src python3 ddgs search "machine learning tutorials" --results 20 +ddg-cli search "machine learning tutorials" --results 20 # Export to JSON for processing -PYTHONPATH=./src python3 ddgs search "api documentation" --format json --output results.json +ddg-cli search "api documentation" --format json --output results.json ``` ## Installation @@ -42,7 +39,7 @@ PYTHONPATH=./src python3 ddgs search "api documentation" --format json --output - Python 3.8 or higher - pip package manager -### Step-by-Step Installation +### Method 1: Install with pip (Recommended) 1. **Clone the repository** ```bash @@ -50,18 +47,27 @@ PYTHONPATH=./src python3 ddgs search "api documentation" --format json --output cd duckduckgo-cli ``` -2. **Install dependencies** +2. **Install the package** ```bash - pip install -r requirements.txt + pip install . ``` -3. **Set up the tool (optional)** +3. **Start using the CLI** ```bash - chmod +x ddgs - ./setup.sh # Creates system-wide symlink + ddg-cli search "your search query" ``` -After setup, you can use `ddg-cli` from anywhere in your terminal. +### Method 2: Docker + +1. **Build the Docker image** + ```bash + docker build -t ddg-cli . + ``` + +2. **Run searches in Docker** + ```bash + docker run ddg-cli search "hello world" + ``` ## Core Features @@ -228,8 +234,8 @@ This project is well-organized for easy maintenance and extension: ``` duckduckgo-cli/ -├── ddgs # Main executable -├── src/ # Core modules +├── duckduckgo_cli/ # Main package +│ ├── main.py # Main CLI entry point │ ├── search.py # DuckDuckGo search integration │ ├── display.py # Result formatting │ ├── history.py # Search history management @@ -239,7 +245,9 @@ duckduckgo-cli/ │ ├── filter.py # Result filtering │ └── utils.py # Browser and download utilities ├── tests/ # Test suite -└── docs/ # Additional documentation +├── pyproject.toml # Modern Python packaging +├── Dockerfile # Container configuration +└── .github/workflows/ # CI/CD configuration ``` ## Testing @@ -247,11 +255,11 @@ duckduckgo-cli/ Run the comprehensive test suite to ensure everything works correctly: ```bash -# Run all tests -PYTHONPATH=./src python3 tests/test_ddgs.py +# Run all tests with pytest +pytest -# Or use make -make test +# Or run tests with verbose output +pytest -v ``` ## Troubleshooting @@ -260,8 +268,8 @@ make test **"Module not found" errors** ```bash -# Ensure PYTHONPATH is set correctly -export PYTHONPATH="./src:$PYTHONPATH" +# Make sure the package is installed +pip install . ``` **Network connectivity issues** @@ -271,8 +279,8 @@ export PYTHONPATH="./src:$PYTHONPATH" **Permission denied on installation** ```bash -# Make scripts executable -chmod +x ddgs setup.sh install.sh +# Install in user mode if needed +pip install --user . ``` ### Getting Help @@ -325,8 +333,8 @@ done While primarily a CLI tool, the modular architecture makes it easy to import individual components: ```python -from src.search import search_duckduckgo -from src.filter import filter_results +from duckduckgo_cli.search import search_duckduckgo +from duckduckgo_cli.filter import filter_results results = search_duckduckgo("python programming", 10) filtered = filter_results(results, include="github.com") diff --git a/duckduckgo_cli/__init__.py b/duckduckgo_cli/__init__.py new file mode 100644 index 0000000..ebd919d --- /dev/null +++ b/duckduckgo_cli/__init__.py @@ -0,0 +1,26 @@ +""" +DuckDuckGo CLI Package +""" + +from .search import search_duckduckgo +from .display import display_results, format_results +from .history import HistoryManager +from .bookmarks import BookmarkManager +from .config import ConfigManager +from .export import export_results +from .filter import filter_results +from .utils import open_urls, download_content + +__version__ = "1.1.0" +__all__ = [ + "search_duckduckgo", + "display_results", + "format_results", + "HistoryManager", + "BookmarkManager", + "ConfigManager", + "export_results", + "filter_results", + "open_urls", + "download_content" +] \ No newline at end of file diff --git a/duckduckgo_cli/bookmarks.py b/duckduckgo_cli/bookmarks.py new file mode 100644 index 0000000..498e20f --- /dev/null +++ b/duckduckgo_cli/bookmarks.py @@ -0,0 +1,55 @@ +""" +Bookmarks module for the enhanced ddgs tool +""" + +import json +import os +from pathlib import Path + +class BookmarkManager: + def __init__(self, bookmarks_file=None): + if bookmarks_file is None: + self.bookmarks_file = Path.home() / '.ddgs_bookmarks.json' + else: + self.bookmarks_file = Path(bookmarks_file) + + # Create bookmarks file if it doesn't exist + if not self.bookmarks_file.exists(): + self.bookmarks_file.write_text('[]') + + def add_bookmark(self, query): + """Add a query to bookmarks.""" + bookmarks = self.load_bookmarks() + if query not in bookmarks: + bookmarks.append(query) + self.save_bookmarks(bookmarks) + print(f"Bookmark added: {query}") + else: + print(f"Query already bookmarked: {query}") + + def load_bookmarks(self): + """Load bookmarks from file.""" + try: + return json.loads(self.bookmarks_file.read_text()) + except (json.JSONDecodeError, FileNotFoundError): + return [] + + def save_bookmarks(self, bookmarks): + """Save bookmarks to file.""" + self.bookmarks_file.write_text(json.dumps(bookmarks, indent=2)) + + def list_bookmarks(self): + """List bookmarks.""" + bookmarks = self.load_bookmarks() + if not bookmarks: + print("No bookmarks found.") + return + + print("Bookmarks:") + for i, query in enumerate(bookmarks, 1): + print(f"{i}. {query}") + + def clear_bookmarks(self): + """Clear bookmarks.""" + self.bookmarks_file.write_text('[]') + print("Bookmarks cleared.") \ No newline at end of file diff --git a/duckduckgo_cli/config.py b/duckduckgo_cli/config.py new file mode 100644 index 0000000..1810bde --- /dev/null +++ b/duckduckgo_cli/config.py @@ -0,0 +1,59 @@ +""" +Configuration module for the enhanced ddgs tool +""" + +import configparser +import os +from pathlib import Path + +class ConfigManager: + def __init__(self, config_file=None): + if config_file is None: + # Use XDG standard configuration path + self.config_file = Path.home() / ".config" / "duckduckgo-cli" / "config.ini" + # Create directory if it doesn't exist + self.config_file.parent.mkdir(parents=True, exist_ok=True) + else: + self.config_file = Path(config_file) + + # Default configuration + self.default_config = { + 'general': { + 'default_results': '10', + 'default_format': 'text', + 'safe_search': '0' + }, + 'display': { + 'color_output': '1', + 'max_snippet_length': '200' + }, + 'network': { + 'timeout': '10', + 'retries': '3' + } + } + + def load_config(self): + """Load configuration from file.""" + config = configparser.ConfigParser() + + # Set defaults + for section, options in self.default_config.items(): + config.add_section(section) + for key, value in options.items(): + config.set(section, key, value) + + # Load user config if it exists + if self.config_file.exists(): + config.read(self.config_file) + + return config + + def save_config(self, config): + """Save configuration to file.""" + with open(self.config_file, 'w') as f: + config.write(f) + + def get_config_path(self): + """Get the configuration file path.""" + return self.config_file \ No newline at end of file diff --git a/duckduckgo_cli/display.py b/duckduckgo_cli/display.py new file mode 100644 index 0000000..cf8c086 --- /dev/null +++ b/duckduckgo_cli/display.py @@ -0,0 +1,31 @@ +""" +Display module for the enhanced ddgs tool +""" + +import json + +def display_results(results): + """Display search results in a formatted way.""" + if not results: + print("No results found.") + return + + for i, result in enumerate(results, 1): + print(f"{i}. {result['title']}") + print(f" URL: {result['href']}") + print(f" Snippet: {result['body']}") + print() + +def format_results(results, format_type): + """Format results in the specified format.""" + if format_type == "json": + return json.dumps(results, indent=2) + else: + # Default to text format + output = [] + for i, result in enumerate(results, 1): + output.append(f"{i}. {result['title']}") + output.append(f" URL: {result['href']}") + output.append(f" Snippet: {result['body']}") + output.append("") + return "\n".join(output) diff --git a/duckduckgo_cli/export.py b/duckduckgo_cli/export.py new file mode 100644 index 0000000..4a640db --- /dev/null +++ b/duckduckgo_cli/export.py @@ -0,0 +1,44 @@ +""" +Export module for the enhanced ddgs tool +""" + +import json +import csv +from xml.etree.ElementTree import Element, SubElement, tostring +from xml.dom import minidom + +def export_results(results, format_type, output_file): + """Export results to a file in the specified format.""" + if format_type == "json": + with open(output_file, "w") as f: + json.dump(results, f, indent=2) + elif format_type == "csv": + if not results: + return + with open(output_file, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(results[0].keys()) + for result in results: + writer.writerow(result.values()) + elif format_type == "xml": + root = Element("results") + for result in results: + item = SubElement(root, "result") + for key, value in result.items(): + child = SubElement(item, key) + child.text = str(value) + rough_string = tostring(root, "utf-8") + reparsed = minidom.parseString(rough_string) + with open(output_file, "w") as f: + f.write(reparsed.toprettyxml(indent=" ")) + elif format_type == "markdown": + if not results: + return + with open(output_file, "w") as f: + f.write("| Title | URL | Snippet |\n") + f.write("|-------|-----|---------|\n") + for result in results: + title = result["title"].replace("|", "\\|") + href = result["href"].replace("|", "\\|") + body = result["body"].replace("|", "\\|") + f.write(f"| {title} | {href} | {body} |\n") diff --git a/duckduckgo_cli/filter.py b/duckduckgo_cli/filter.py new file mode 100644 index 0000000..db1e8b2 --- /dev/null +++ b/duckduckgo_cli/filter.py @@ -0,0 +1,19 @@ +""" +Filter module for the enhanced ddgs tool +""" + +def filter_results(results, include=None, exclude=None): + """Filter results based on include/exclude criteria.""" + filtered = results + + if include: + filtered = [r for r in filtered if include.lower() in r['title'].lower() or + include.lower() in r['href'].lower() or + include.lower() in r['body'].lower()] + + if exclude: + filtered = [r for r in filtered if exclude.lower() not in r['title'].lower() and + exclude.lower() not in r['href'].lower() and + exclude.lower() not in r['body'].lower()] + + return filtered \ No newline at end of file diff --git a/duckduckgo_cli/history.py b/duckduckgo_cli/history.py new file mode 100644 index 0000000..a4c9e60 --- /dev/null +++ b/duckduckgo_cli/history.py @@ -0,0 +1,56 @@ +""" +History module for the enhanced ddgs tool +""" + +import json +import os +from datetime import datetime +from pathlib import Path + +class HistoryManager: + def __init__(self, history_file=None): + if history_file is None: + self.history_file = Path.home() / '.ddgs_history.json' + else: + self.history_file = Path(history_file) + + # Create history file if it doesn't exist + if not self.history_file.exists(): + self.history_file.write_text('[]') + + def add_to_history(self, query, result_count): + """Add a search query to history.""" + history = self.load_history() + history.append({ + 'query': query, + 'timestamp': datetime.now().isoformat(), + 'result_count': result_count + }) + self.save_history(history) + + def load_history(self): + """Load search history from file.""" + try: + return json.loads(self.history_file.read_text()) + except (json.JSONDecodeError, FileNotFoundError): + return [] + + def save_history(self, history): + """Save search history to file.""" + self.history_file.write_text(json.dumps(history, indent=2)) + + def list_history(self): + """List search history.""" + history = self.load_history() + if not history: + print("No search history found.") + return + + print("Search History:") + for i, entry in enumerate(history, 1): + print(f"{i}. {entry['query']} (Results: {entry['result_count']}) - {entry['timestamp']}") + + def clear_history(self): + """Clear search history.""" + self.history_file.write_text('[]') + print("Search history cleared.") \ No newline at end of file diff --git a/ddgs b/duckduckgo_cli/main.py similarity index 84% rename from ddgs rename to duckduckgo_cli/main.py index 7befa37..a76c737 100755 --- a/ddgs +++ b/duckduckgo_cli/main.py @@ -13,21 +13,30 @@ from datetime import datetime from pathlib import Path -# Get the directory where this script is located -SCRIPT_DIR = Path(__file__).parent.absolute() - -# Add the project src directory to the Python path -sys.path.insert(0, str(SCRIPT_DIR / "src")) - -# Import our enhanced modules -from search import search_duckduckgo -from display import display_results, format_results -from history import HistoryManager -from bookmarks import BookmarkManager -from config import ConfigManager -from export import export_results -from filter import filter_results -from utils import open_urls, download_content +# Try to import from installed package first, fallback to local development +try: + # Import from package structure + from .search import search_duckduckgo + from .display import display_results, format_results + from .history import HistoryManager + from .bookmarks import BookmarkManager + from .config import ConfigManager + from .export import export_results + from .filter import filter_results + from .utils import open_urls, download_content +except ImportError: + # Fallback for development - add src to path + SCRIPT_DIR = Path(__file__).parent.parent.absolute() + sys.path.insert(0, str(SCRIPT_DIR / "src")) + + from search import search_duckduckgo + from display import display_results, format_results + from history import HistoryManager + from bookmarks import BookmarkManager + from config import ConfigManager + from export import export_results + from filter import filter_results + from utils import open_urls, download_content def main(): """Main function to handle command-line arguments and execute commands.""" diff --git a/duckduckgo_cli/search.py b/duckduckgo_cli/search.py new file mode 100644 index 0000000..6d079dc --- /dev/null +++ b/duckduckgo_cli/search.py @@ -0,0 +1,35 @@ +""" +Search module for the enhanced ddgs tool +""" + +try: + from ddgs import DDGS +except ImportError: + try: + from duckduckgo_search import DDGS + except ImportError: + print("Error: Neither 'ddgs' nor 'duckduckgo-search' package is installed.") + print("Please install one of them using pip:") + print(" pip install ddgs") + print("or") + print(" pip install duckduckgo-search") + exit(1) + +def search_duckduckgo(query, max_results=10): + """Perform a search on DuckDuckGo and return results.""" + results = [] + try: + with DDGS() as ddgs: + # Perform the search + search_results = ddgs.text(query, max_results=max_results) + for result in search_results: + results.append({ + 'title': result['title'], + 'href': result['href'], + 'body': result['body'] + }) + except Exception as e: + print(f"An error occurred during the search: {e}") + return [] + + return results \ No newline at end of file diff --git a/duckduckgo_cli/utils.py b/duckduckgo_cli/utils.py new file mode 100644 index 0000000..cf29bf6 --- /dev/null +++ b/duckduckgo_cli/utils.py @@ -0,0 +1,38 @@ +""" +Utility module for the enhanced ddgs tool +""" + +import webbrowser +import requests +import os +from pathlib import Path + +def open_urls(urls): + """Open URLs in the default browser.""" + for url in urls: + webbrowser.open(url) + +def download_content(urls, download_dir=None): + """Download content from URLs.""" + if download_dir is None: + download_dir = Path.home() / 'ddgs_downloads' + + # Create download directory if it doesn't exist + download_dir.mkdir(parents=True, exist_ok=True) + + for url in urls: + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + + # Extract filename from URL or use a default name + filename = url.split('/')[-1] or 'downloaded_file' + if '.' not in filename: + filename += '.html' + + filepath = download_dir / filename + with open(filepath, 'wb') as f: + f.write(response.content) + print(f"Downloaded: {filepath}") + except Exception as e: + print(f"Failed to download {url}: {e}") \ No newline at end of file diff --git a/install.sh b/install.sh deleted file mode 100755 index 813f0fc..0000000 --- a/install.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Installation script for DuckDuckGo CLI - -# Get the current directory (where the script is located) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -echo "Installing DuckDuckGo CLI system-wide..." - -# Create a symlink in ~/bin if it doesn't exist -mkdir -p ~/bin - -# Create a wrapper script instead of direct symlink to handle PYTHONPATH -cat > ~/bin/ddg-cli << EOF -#!/bin/bash -# DuckDuckGo CLI wrapper script -export PYTHONPATH="$SCRIPT_DIR/src:\$PYTHONPATH" -python3 "$SCRIPT_DIR/ddgs" "\$@" -EOF - -chmod +x ~/bin/ddg-cli - -# Add to PATH if not already there -if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then - echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc - echo 'export PATH="$HOME/bin:$PATH"' >> ~/.profile - echo "Added ~/bin to PATH. Please restart your terminal or run 'source ~/.bashrc' to apply changes." -fi - -echo "✅ DuckDuckGo CLI tool is now available as 'ddg-cli'" -echo "Try: ddg-cli search 'hello world'" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8d511f7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "duckduckgo-cli" +version = "1.1.0" +authors = [ + { name="GrecAndrei", email="your-email@example.com" }, +] +description = "A feature-rich command-line interface for DuckDuckGo search." +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ + "ddgs>=9.5.5", + "requests>=2.32.5", +] + +[project.urls] +"Homepage" = "https://github.com/GrecAndrei/duckduckgo-cli" +"Bug Tracker" = "https://github.com/GrecAndrei/duckduckgo-cli/issues" + +[project.scripts] +ddg-cli = "duckduckgo_cli.main:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["duckduckgo_cli*"] \ No newline at end of file diff --git a/setup.sh b/setup.sh deleted file mode 100755 index e907235..0000000 --- a/setup.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# Setup script for DuckDuckGo CLI - -# Get the current directory (where the script is located) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -echo "Setting up DuckDuckGo CLI..." - -# Create directories if they don't exist -mkdir -p ~/bin -mkdir -p ~/.config/duckduckgo-cli - -# Install Python dependencies -echo "Installing Python dependencies..." -pip3 install -r "$SCRIPT_DIR/requirements.txt" - -# Make the main script executable -chmod +x "$SCRIPT_DIR/ddgs" - -# Create a wrapper script in ~/bin -cat > ~/bin/ddg-cli << EOF -#!/bin/bash -# DuckDuckGo CLI wrapper script -export PYTHONPATH="$SCRIPT_DIR/src:\$PYTHONPATH" -python3 "$SCRIPT_DIR/ddgs" "\$@" -EOF - -chmod +x ~/bin/ddg-cli - -# Add ~/bin to PATH if not already there -if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then - echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc - echo 'export PATH="$HOME/bin:$PATH"' >> ~/.profile -fi - -echo "" -echo "✅ DuckDuckGo CLI setup complete!" -echo "" -echo "You can now use the tool with the command: ddg-cli" -echo "Examples:" -echo " ddg-cli search 'python programming'" -echo " ddg-cli search 'machine learning' --results 20" -echo " ddg-cli history list" -echo "" -echo "Note: You may need to restart your terminal or run 'source ~/.bashrc' for the PATH changes to take effect." \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..ebd919d --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,26 @@ +""" +DuckDuckGo CLI Package +""" + +from .search import search_duckduckgo +from .display import display_results, format_results +from .history import HistoryManager +from .bookmarks import BookmarkManager +from .config import ConfigManager +from .export import export_results +from .filter import filter_results +from .utils import open_urls, download_content + +__version__ = "1.1.0" +__all__ = [ + "search_duckduckgo", + "display_results", + "format_results", + "HistoryManager", + "BookmarkManager", + "ConfigManager", + "export_results", + "filter_results", + "open_urls", + "download_content" +] \ No newline at end of file diff --git a/src/config.py b/src/config.py index ebbcd6c..1810bde 100644 --- a/src/config.py +++ b/src/config.py @@ -9,7 +9,10 @@ class ConfigManager: def __init__(self, config_file=None): if config_file is None: - self.config_file = Path.home() / '.ddgs_config.ini' + # Use XDG standard configuration path + self.config_file = Path.home() / ".config" / "duckduckgo-cli" / "config.ini" + # Create directory if it doesn't exist + self.config_file.parent.mkdir(parents=True, exist_ok=True) else: self.config_file = Path(config_file) diff --git a/src/display.py b/src/display.py index 0558552..cf8c086 100644 --- a/src/display.py +++ b/src/display.py @@ -11,9 +11,9 @@ def display_results(results): return for i, result in enumerate(results, 1): - print(f"{i}. {result["title"]}") - print(f" URL: {result["href"]}") - print(f" Snippet: {result["body"]}") + print(f"{i}. {result['title']}") + print(f" URL: {result['href']}") + print(f" Snippet: {result['body']}") print() def format_results(results, format_type): @@ -24,8 +24,8 @@ def format_results(results, format_type): # Default to text format output = [] for i, result in enumerate(results, 1): - output.append(f"{i}. {result["title"]}") - output.append(f" URL: {result["href"]}") - output.append(f" Snippet: {result["body"]}") + output.append(f"{i}. {result['title']}") + output.append(f" URL: {result['href']}") + output.append(f" Snippet: {result['body']}") output.append("") return "\n".join(output) diff --git a/tests/test_ddgs.py b/tests/test_ddgs.py index c2cea33..dfe922e 100755 --- a/tests/test_ddgs.py +++ b/tests/test_ddgs.py @@ -6,6 +6,7 @@ import sys import os +import pytest from pathlib import Path # Get the directory where this script is located and add src to path @@ -13,53 +14,103 @@ sys.path.insert(0, str(SCRIPT_DIR / "src")) # Test imports -try: - from search import search_duckduckgo - from display import display_results, format_results - from history import HistoryManager - from bookmarks import BookmarkManager - from config import ConfigManager - from export import export_results - from filter import filter_results - from utils import open_urls, download_content - - print("All modules imported successfully!") - - # Test search functionality - print("\nTesting search functionality...") +from search import search_duckduckgo +from display import display_results, format_results +from history import HistoryManager +from bookmarks import BookmarkManager +from config import ConfigManager +from export import export_results +from filter import filter_results +from utils import open_urls, download_content + + +def test_imports(): + """Test that all modules can be imported successfully.""" + # If we reach this point, imports were successful + assert True + + +def test_search_functionality(): + """Test search functionality.""" results = search_duckduckgo("Python programming", 5) - print(f"Found {len(results)} results") + assert len(results) > 0 + assert len(results) <= 5 - # Test display functionality - print("\nTesting display functionality...") - display_results(results[:2]) # Display first 2 results + # Check that results have expected structure + for result in results: + assert 'title' in result + assert 'href' in result + assert 'body' in result # The actual field name is 'body', not 'snippet' + + +def test_display_functionality(): + """Test display functionality.""" + # Create test results + test_results = [ + { + 'title': 'Test Title', + 'href': 'https://example.com', + 'body': 'Test body content' # Use 'body' instead of 'snippet' + } + ] - # Test formatting - print("\nTesting JSON formatting...") - json_output = format_results(results[:2], 'json') - print(json_output[:100] + "..." if len(json_output) > 100 else json_output) + # Test that display_results doesn't crash + try: + display_results(test_results) + assert True + except Exception: + assert False, "display_results should not raise exceptions" + + +def test_formatting(): + """Test result formatting.""" + test_results = [ + { + 'title': 'Test Title', + 'href': 'https://example.com', + 'body': 'Test body content' # Use 'body' instead of 'snippet' + } + ] - # Test history manager - print("\nTesting history manager...") + json_output = format_results(test_results, 'json') + assert isinstance(json_output, str) + assert 'Test Title' in json_output + + +def test_history_manager(): + """Test history manager functionality.""" history_manager = HistoryManager() - history_manager.add_to_history("Python programming", len(results)) - print("Added search to history") - # Test bookmark manager - print("\nTesting bookmark manager...") + # Test adding to history + try: + history_manager.add_to_history("Python programming", 5) + assert True + except Exception: + assert False, "add_to_history should not raise exceptions" + + +def test_bookmark_manager(): + """Test bookmark manager functionality.""" bookmark_manager = BookmarkManager() - bookmark_manager.add_bookmark("Python programming") - print("Added bookmark") - # Test configuration manager - print("\nTesting configuration manager...") + # Test adding bookmark + try: + bookmark_manager.add_bookmark("Python programming") + assert True + except Exception: + assert False, "add_bookmark should not raise exceptions" + + +def test_configuration_manager(): + """Test configuration manager.""" config_manager = ConfigManager() config = config_manager.load_config() - print(f"Default results: {config.get('general', 'default_results')}") - - print("\nAll tests passed!") -except Exception as e: - print(f"Error during testing: {e}") - import traceback - traceback.print_exc() \ No newline at end of file + # Check that config has expected sections + assert config.has_section('general') + assert config.get('general', 'default_results') == '10' + + +if __name__ == "__main__": + # If run directly, run tests with output + pytest.main([__file__, "-v"]) \ No newline at end of file