├── .github └── workflows │ └── python-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── codeview ├── pyproject.toml ├── setup.py └── tests └── test_codeview.py /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install pytest 28 | pip install -e . 29 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 30 | 31 | - name: Run tests 32 | run: | 33 | pytest tests -v 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | dist/ 3 | .DS_Store 4 | *.pyc 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ziad Amerr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeView 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/codeview.svg)](https://pypi.org/project/codeview/) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | [![Python Versions](https://img.shields.io/pypi/pyversions/codeview.svg)](https://pypi.org/project/codeview/) 6 | [![Tests](https://github.com/ZiadAmerr/codeview/actions/workflows/python-tests.yml/badge.svg)](https://github.com/ZiadAmerr/codeview/actions/workflows/python-tests.yml) 7 | 8 | ## Overview 9 | 10 | CodeView is a powerful command-line utility designed to help developers effectively communicate their codebases to Large Language Models (LLMs) like ChatGPT, Claude, and Gemini. It solves the common problem of needing to share multiple files and directory structures with LLMs in a clean, organized format. 11 | 12 | **Key Features:** 13 | 14 | - 📁 Visualizes directory structures with customizable depth 15 | - 📝 Displays file contents with optional syntax highlighting and line numbers 16 | - 🔍 Flexible filtering by file type, directory, or content patterns 17 | - 📤 Multiple output formats (text, markdown, JSON) for different LLM platforms 18 | - 💾 Save output to a file or display in terminal 19 | - 🚀 Easy to install and use with intuitive CLI interface 20 | 21 | ## Installation 22 | 23 | ### Prerequisites 24 | 25 | - Python 3.6 or higher 26 | - The `tree` command: 27 | - **Linux (Debian/Ubuntu)**: `sudo apt-get install tree` 28 | - **macOS**: `brew install tree` 29 | - **Windows**: Available via [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) or [Git Bash](https://gitforwindows.org/) 30 | 31 | ### Install from PyPI 32 | 33 | ```bash 34 | pip install codeview 35 | ``` 36 | 37 | ### Install from Source 38 | 39 | ```bash 40 | # Clone the repository 41 | git clone https://github.com/ZiadAmerr/codeview.git 42 | cd codeview 43 | 44 | # Install in development mode 45 | pip install -e . 46 | ``` 47 | 48 | ## Quick Start 49 | 50 | Generate a view of your entire codebase: 51 | 52 | ```bash 53 | codeview 54 | ``` 55 | 56 | Create a Markdown file for sharing with an LLM: 57 | 58 | ```bash 59 | codeview -m markdown -o my_project.md 60 | ``` 61 | 62 | Focus on specific file types: 63 | 64 | ```bash 65 | codeview -i "*.py" -i "*.js" 66 | ``` 67 | 68 | ## Detailed Usage 69 | 70 | ``` 71 | codeview [options] 72 | ``` 73 | 74 | ### Options 75 | 76 | | Option | Long Form | Description | Example | 77 | |--------|-----------|-------------|---------| 78 | | `-h` | `--help` | Display help message | `codeview -h` | 79 | | `-i PATTERN` | `--include PATTERN` | File patterns to include (can use multiple times) | `codeview -i "*.py" -i "*.js"` | 80 | | `-e DIR` | `--exclude-dir DIR` | Directories to exclude (can use multiple times) | `codeview -e node_modules -e .venv` | 81 | | `-x PATTERN` | `--exclude-file PATTERN` | File patterns to exclude (can use multiple times) | `codeview -x "*.pyc" -x "*.log"` | 82 | | `-d DEPTH` | `--max-depth DEPTH` | Maximum directory depth to traverse | `codeview -d 2` | 83 | | `-t` | `--no-tree` | Don't show directory tree | `codeview -t` | 84 | | `-f` | `--no-files` | Don't show file contents | `codeview -f` | 85 | | `-n` | `--line-numbers` | Show line numbers in file contents | `codeview -n` | 86 | | `-o FILE` | `--output FILE` | Write output to file instead of stdout | `codeview -o project.txt` | 87 | | `-s PATTERN` | `--search PATTERN` | Only include files containing the pattern | `codeview -s "def main"` | 88 | | `-p DIR` | `--path DIR` | Include specific directory (can use multiple times) | `codeview -p src/models -p tests` | 89 | | `-m FORMAT` | `--format FORMAT` | Output format: text, markdown, json | `codeview -m markdown` | 90 | 91 | ### Default Values 92 | 93 | - **Include Patterns**: `*.py`, `*.md`, `*.js`, `*.html`, `*.css`, `*.json`, `*.yaml`, `*.yml` 94 | - **Exclude Directories**: `myenv`, `venv`, `.venv`, `node_modules`, `.git`, `__pycache__`, `.pytest_cache`, `build`, `dist` 95 | - **Exclude Files**: `*.pyc`, `*.pyo`, `*.pyd`, `*.so`, `*.dll`, `*.class`, `*.egg-info`, `*.egg` 96 | - **Max Depth**: No limit 97 | - **Output Format**: text 98 | 99 | ## Use Cases 100 | 101 | ### Working with LLMs on Your Projects 102 | 103 | ```bash 104 | # Generate a Markdown overview of your Python project 105 | codeview -i "*.py" -m markdown -o project_for_llm.md 106 | 107 | # Then upload the markdown file to your favorite LLM platform 108 | ``` 109 | 110 | ### Focusing on Specific Components 111 | 112 | ```bash 113 | # Show only model and controller files 114 | codeview -p src/models -p src/controllers -i "*.py" 115 | 116 | # View only files containing authentication logic 117 | codeview -s "def authenticate" -s "class Auth" 118 | ``` 119 | 120 | ### Collaborating with Team Members 121 | 122 | ```bash 123 | # Create a JSON representation for programmatic use 124 | codeview -m json -o project_structure.json 125 | 126 | # Generate documentation of the core modules 127 | codeview -p src/core -m markdown -o core_modules.md 128 | ``` 129 | 130 | ## Output Formats 131 | 132 | ### Text (Default) 133 | 134 | ``` 135 | **./src/main.py** 136 | def main(): 137 | print("Hello, world!") 138 | 139 | if __name__ == "__main__": 140 | main() 141 | ``` 142 | 143 | ### Markdown 144 | 145 | ````markdown 146 | ## ./src/main.py 147 | 148 | ```python 149 | def main(): 150 | print("Hello, world!") 151 | 152 | if __name__ == "__main__": 153 | main() 154 | ``` 155 | ```` 156 | 157 | ### JSON 158 | 159 | ```json 160 | { 161 | "files": [ 162 | { 163 | "path": "./src/main.py", 164 | "content": "def main():\n print(\"Hello, world!\")\n \nif __name__ == \"__main__\":\n main()" 165 | } 166 | ] 167 | } 168 | ``` 169 | 170 | ## Effective Use with LLMs 171 | 172 | 1. **Filter appropriately**: Only include relevant files to stay within token limits 173 | 2. **Use markdown format** for better readability with most LLMs 174 | 3. **Include line numbers** (`-n`) when discussing specific code sections 175 | 4. **Exclude large/binary files** to avoid token waste 176 | 5. **Limit depth** (`-d`) for large projects to focus on high-level structure 177 | 178 | ## Troubleshooting 179 | 180 | ### Common Issues 181 | 182 | #### "Command not found: tree" 183 | 184 | - Install the `tree` command using your package manager: 185 | 186 | ```bash 187 | # Debian/Ubuntu 188 | sudo apt-get install tree 189 | 190 | # macOS 191 | brew install tree 192 | ``` 193 | 194 | #### "Files not showing up" 195 | 196 | - Check your include/exclude patterns 197 | - Make sure you're running from the correct directory 198 | - Use the `-v` flag for verbose output to see what's happening 199 | 200 | ## Contributing 201 | 202 | Contributions are welcome! Please feel free to submit a Pull Request. 203 | 204 | 1. Fork the repository 205 | 2. Create your feature branch (`git checkout -b feature/new-amazing-feature`) 206 | 3. Commit your changes (`git commit -m 'Add some new amazing feature'`) 207 | 4. Push to the branch (`git push origin feature/new-amazing-feature`) 208 | 5. Open a Pull Request 209 | 210 | ## License 211 | 212 | This project is licensed under the MIT License - see the LICENSE file for details. 213 | 214 | ## Credits 215 | Inspired by the need to efficiently share code with LLMs 216 | 217 | --- 218 | 219 | If you find CodeView useful, please consider ⭐ starring the repository on GitHub! 220 | -------------------------------------------------------------------------------- /bin/codeview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # codeview - A tool to visualize codebases for LLM interactions 4 | # Version: 1.0.0 5 | 6 | # Colors 7 | USE_COLORS=true 8 | RED_BOLD='\033[1;31m' 9 | GREEN='\033[0;32m' 10 | BLUE='\033[0;34m' 11 | GRAY='\033[0;90m' 12 | YELLOW='\033[0;33m' 13 | RESET='\033[0m' 14 | 15 | # Default values 16 | OUTPUT_FORMAT="text" 17 | INCLUDE_PATTERNS=("*.py" "*.md" "*.js" "*.html" "*.css" "*.json" "*.yaml" "*.yml") 18 | EXCLUDE_DIRS=("myenv" "venv" ".venv" "node_modules" ".git" "__pycache__" ".pytest_cache" "build" "dist") 19 | EXCLUDE_FILES=("*.pyc" "*.pyo" "*.pyd" "*.so" "*.dll" "*.class" "*.egg-info" "*.egg") 20 | MAX_DEPTH="-1" 21 | SHOW_TREE=true 22 | SHOW_FILES=true 23 | SHOW_LINE_NUMBERS=false 24 | OUTPUT_FILE="" 25 | SEARCH_PATTERN="" 26 | INCLUDE_DIRS=() 27 | TEMP_OUTPUT="" 28 | 29 | print_usage() { 30 | local g=$GREEN r=$RESET y=$YELLOW 31 | $USE_COLORS || g="" r="" y="" 32 | 33 | printf "%bcodeview%b - A tool to visualize codebases for LLM interactions\n" "$g" "$r" 34 | printf "\nUsage: %bcodeview [options]%b\n\n" "$y" "$r" 35 | printf "Options:\n" 36 | printf " -h, --help Show this help message\n" 37 | printf " -i, --include PATTERN File patterns to include (can be used multiple times)\n" 38 | printf " -e, --exclude-dir DIR Directories to exclude (can be used multiple times)\n" 39 | printf " -x, --exclude-file PATTERN File patterns to exclude (can be used multiple times)\n" 40 | printf " -d, --max-depth DEPTH Maximum directory depth to traverse\n" 41 | printf " -t, --no-tree Don't show directory tree\n" 42 | printf " -f, --no-files Don't show file contents\n" 43 | printf " -n, --line-numbers Show line numbers in file contents\n" 44 | printf " -o, --output FILE Write output to file instead of stdout\n" 45 | printf " -s, --search PATTERN Only include files containing the pattern\n" 46 | printf " -p, --path DIR Include specific directory (can be used multiple times)\n" 47 | printf " -m, --format FORMAT Output format: text (default), markdown, json\n" 48 | printf "\nExamples:\n" 49 | printf " codeview # Show all code files in current directory\n" 50 | printf " codeview -i \"*.py\" -i \"*.js\" # Only show Python and JavaScript files\n" 51 | printf " codeview -e node_modules -e .git # Exclude node_modules and .git directories\n" 52 | printf " codeview -d 2 # Only traverse 2 directory levels deep\n" 53 | printf " codeview -s \"def main\" # Only show files containing 'def main'\n" 54 | printf " codeview -p src/models -p src/utils # Only include specific directories\n" 55 | printf " codeview -m markdown -o codebase.md # Output in markdown format to a file\n" 56 | } 57 | 58 | command_exists() { 59 | command -v "$1" >/dev/null 2>&1 60 | } 61 | 62 | generate_tree() { 63 | local exclude_args=() 64 | local b=$BLUE r=$RESET y=$YELLOW 65 | $USE_COLORS || b="" r="" y="" 66 | 67 | if ! command_exists tree; then 68 | local rb=$RED_BOLD 69 | $USE_COLORS || rb="" 70 | printf "%bWarning: 'tree' command not found. Directory structure will not be shown.%b\n\n" "$rb" "$r" 71 | printf "%bInstall it with: apt-get install tree (Debian/Ubuntu) or brew install tree (macOS)%b\n\n" "$y" "$r" 72 | return 73 | fi 74 | for dir in "${EXCLUDE_DIRS[@]}"; do exclude_args+=("-I" "$dir"); done 75 | for file in "${EXCLUDE_FILES[@]}"; do exclude_args+=("-I" "$file"); done 76 | local depth_arg=() 77 | [ "$MAX_DEPTH" -ge 0 ] && depth_arg=("-L" "$MAX_DEPTH") 78 | printf "%bDirectory Structure:%b\n\n" "$b" "$r" 79 | if [ ${#INCLUDE_DIRS[@]} -eq 0 ]; then 80 | tree "${depth_arg[@]}" "${exclude_args[@]}" -f --dirsfirst --noreport 81 | else 82 | for dir in "${INCLUDE_DIRS[@]}"; do 83 | [ -d "$dir" ] && { 84 | printf "%b%s%b\n" "$y" "$dir" "$r" 85 | tree "${depth_arg[@]}" "${exclude_args[@]}" -f --dirsfirst --noreport "$dir" 86 | printf "\n" 87 | } 88 | done 89 | fi 90 | printf "\n" 91 | } 92 | 93 | generate_file_content() { 94 | local file_pattern_args=() dir_exclude_args=() file_exclude_args=() 95 | local rb=$RED_BOLD gr=$GRAY r=$RESET 96 | $USE_COLORS || rb="" gr="" r="" 97 | 98 | [ -n "$OUTPUT_FILE" ] && file_exclude_args+=("-not" "-path" "*/$OUTPUT_FILE" "-not" "-path" "./$OUTPUT_FILE") 99 | 100 | # Safely construct file pattern arguments 101 | if [ ${#INCLUDE_PATTERNS[@]} -gt 0 ]; then 102 | for pattern in "${INCLUDE_PATTERNS[@]}"; do 103 | if [ ${#file_pattern_args[@]} -eq 0 ]; then 104 | file_pattern_args+=("-name" "$pattern") 105 | else 106 | file_pattern_args+=("-o" "-name" "$pattern") 107 | fi 108 | done 109 | else 110 | # Default to all files if no patterns specified 111 | file_pattern_args=("-name" "*") 112 | fi 113 | for dir in "${EXCLUDE_DIRS[@]}"; do 114 | # Match multiple possible path formats 115 | dir_exclude_args+=("-not" "-path" "*/$dir/*") 116 | dir_exclude_args+=("-not" "-path" "./$dir/*") 117 | dir_exclude_args+=("-not" "-path" "$dir/*") 118 | # Also exclude the directory itself 119 | dir_exclude_args+=("-not" "-path" "*/$dir") 120 | dir_exclude_args+=("-not" "-path" "./$dir") 121 | dir_exclude_args+=("-not" "-path" "$dir") 122 | done 123 | for file in "${EXCLUDE_FILES[@]}"; do file_exclude_args+=("-not" "-name" "$file"); done 124 | 125 | local search_dirs=(.) files=() 126 | [ ${#INCLUDE_DIRS[@]} -gt 0 ] && search_dirs=("${INCLUDE_DIRS[@]}") 127 | for dir in "${search_dirs[@]}"; do 128 | if [ -d "$dir" ]; then 129 | local depth_arg=() 130 | [ "$MAX_DEPTH" -ge 0 ] && depth_arg=("-maxdepth" "$MAX_DEPTH") 131 | while IFS= read -r f; do files+=("$f"); done < <(find "$dir" "${depth_arg[@]}" -type f "${file_pattern_args[@]}" "${dir_exclude_args[@]}" "${file_exclude_args[@]}" | sort) 132 | fi 133 | done 134 | 135 | # Additional filtering for excluded directories 136 | if [ ${#EXCLUDE_DIRS[@]} -gt 0 ]; then 137 | filtered_files=() 138 | for f in "${files[@]}"; do 139 | exclude=false 140 | for dir in "${EXCLUDE_DIRS[@]}"; do 141 | if [[ "$f" == *"/$dir/"* || "$f" == "./$dir/"* || "$f" == "$dir/"* ]]; then 142 | exclude=true 143 | break 144 | fi 145 | done 146 | $exclude || filtered_files+=("$f") 147 | done 148 | files=("${filtered_files[@]}") 149 | fi 150 | 151 | if [ -n "$SEARCH_PATTERN" ]; then 152 | local match=() 153 | for f in "${files[@]}"; do 154 | grep -q "$SEARCH_PATTERN" "$f" 2>/dev/null && match+=("$f") 155 | done 156 | files=("${match[@]}") 157 | fi 158 | 159 | case "$OUTPUT_FORMAT" in 160 | markdown) 161 | for f in "${files[@]}"; do 162 | printf "## %s\n\n" "$f" 163 | local ext="${f##*.}" 164 | printf '```%s\n' "$ext" 165 | if [ "$SHOW_LINE_NUMBERS" = true ]; then nl -ba "$f"; else cat "$f"; fi 166 | printf '```\n\n' 167 | done 168 | ;; 169 | json) 170 | echo "{" 171 | echo ' "files": [' 172 | local first=true 173 | for f in "${files[@]}"; do 174 | $first || echo " }," 175 | first=false 176 | echo " {" 177 | printf ' "path": "%s",\n' "$f" 178 | printf ' "content": %s\n' "$(python3 -c 'import json,sys;print(json.dumps(open(sys.argv[1]).read()))' "$f")" 179 | done 180 | [ ${#files[@]} -gt 0 ] && echo " }" 181 | echo " ]" 182 | echo "}" 183 | ;; 184 | *) 185 | for f in "${files[@]}"; do 186 | printf "%b**%s**%b\n" "$rb" "$f" "$r" 187 | if [ "$SHOW_LINE_NUMBERS" = true ]; then 188 | while IFS= read -r line; do 189 | printf "%b%s%b\n" "$gr" "$line" "$r" 190 | done < <(nl -ba "$f") 191 | else 192 | while IFS= read -r line; do 193 | printf "%b%s%b\n" "$gr" "$line" "$r" 194 | done <"$f" 195 | fi 196 | printf "\n" 197 | done 198 | ;; 199 | esac 200 | } 201 | 202 | # Parse args 203 | while [[ $# -gt 0 ]]; do 204 | case $1 in 205 | -h | --help) 206 | print_usage 207 | exit 0 208 | ;; 209 | -i | --include) 210 | INCLUDE_PATTERNS+=("$2") 211 | shift 2 212 | ;; 213 | -e | --exclude-dir) 214 | EXCLUDE_DIRS+=("$2") 215 | shift 2 216 | ;; 217 | -x | --exclude-file) 218 | EXCLUDE_FILES+=("$2") 219 | shift 2 220 | ;; 221 | -d | --max-depth) 222 | MAX_DEPTH="$2" 223 | shift 2 224 | ;; 225 | -t | --no-tree) 226 | SHOW_TREE=false 227 | shift 228 | ;; 229 | -f | --no-files) 230 | SHOW_FILES=false 231 | shift 232 | ;; 233 | -n | --line-numbers) 234 | SHOW_LINE_NUMBERS=true 235 | shift 236 | ;; 237 | -o | --output) 238 | OUTPUT_FILE="$2" 239 | USE_COLORS=false # Disable colors when output goes to a file 240 | shift 2 241 | ;; 242 | -s | --search) 243 | SEARCH_PATTERN="$2" 244 | shift 2 245 | ;; 246 | -p | --path) 247 | INCLUDE_DIRS+=("$2") 248 | shift 2 249 | ;; 250 | -m | --format) 251 | OUTPUT_FORMAT="$2" 252 | shift 2 253 | ;; 254 | *) 255 | local rb=$RED_BOLD r=$RESET 256 | $USE_COLORS || rb="" r="" 257 | printf "%bError: Unknown option %s%b\n" "$rb" "$1" "$r" 258 | print_usage 259 | exit 1 260 | ;; 261 | esac 262 | done 263 | 264 | # Remove excluded patterns 265 | for excl in "${EXCLUDE_FILES[@]}"; do 266 | temp=() 267 | for pat in "${INCLUDE_PATTERNS[@]}"; do [[ "$pat" != "$excl" ]] && temp+=("$pat"); done 268 | INCLUDE_PATTERNS=("${temp[@]}") 269 | done 270 | 271 | # Validate format 272 | [[ ! "$OUTPUT_FORMAT" =~ ^(text|markdown|json)$ ]] && { 273 | # Print the error message to stderr 274 | local rb=$RED_BOLD r=$RESET 275 | $USE_COLORS || rb="" r="" 276 | printf "%bError: Invalid output format. Must be one of: text, markdown, json%b\n" "$rb" "$r" >&2 277 | exit 1 278 | } 279 | 280 | # Redirect if needed 281 | [ -n "$OUTPUT_FILE" ] && { 282 | TEMP_OUTPUT=$(mktemp) 283 | exec >"$TEMP_OUTPUT" 284 | } 285 | 286 | $SHOW_TREE && generate_tree 287 | $SHOW_FILES && generate_file_content 288 | 289 | [ -n "$TEMP_OUTPUT" ] && [ -n "$OUTPUT_FILE" ] && mv "$TEMP_OUTPUT" "$OUTPUT_FILE" 290 | 291 | exit 0 292 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="codeview", 5 | version="0.1.2", 6 | description="A tool to visualize codebases for LLM interactions", 7 | long_description=open("README.md").read(), 8 | long_description_content_type="text/markdown", 9 | author="Ziad Amerr", 10 | author_email="ziad.amerr@example.com", 11 | url="https://github.com/ZiadAmerr/codeview", 12 | scripts=["bin/codeview"], 13 | classifiers=[ 14 | "Development Status :: 4 - Beta", 15 | "Environment :: Console", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.6", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Topic :: Software Development :: Libraries :: Python Modules", 24 | ], 25 | python_requires=">=3.6", 26 | keywords="code, llm, visualization, development", 27 | install_requires=[], 28 | ) 29 | -------------------------------------------------------------------------------- /tests/test_codeview.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import tempfile 4 | import subprocess 5 | import unittest 6 | import shutil 7 | import json 8 | 9 | 10 | CODEVIEW_CMD = "codeview" 11 | 12 | 13 | class CodeViewTestCase(unittest.TestCase): 14 | """Base test class for CodeView tests""" 15 | 16 | def setUp(self): 17 | """Set up a temporary directory with test files""" 18 | self.test_dir = tempfile.mkdtemp() 19 | 20 | # Create a simple test project structure 21 | self.project_structure = { 22 | "src": { 23 | "main.py": "def main():\n print('Hello, world!')\n\nif __name__ == '__main__':\n main()", 24 | "utils.py": "def helper():\n return 'I am a helper function'\n", 25 | "models": { 26 | "user.py": "class User:\n def __init__(self, name):\n self.name = name\n", 27 | "product.py": "class Product:\n def __init__(self, name, price):\n self.name = name\n self.price = price\n", 28 | }, 29 | }, 30 | "tests": { 31 | "test_main.py": "import unittest\n\nclass TestMain(unittest.TestCase):\n def test_main(self):\n pass\n", 32 | "test_utils.py": "import unittest\n\nclass TestUtils(unittest.TestCase):\n def test_helper(self):\n pass\n", 33 | }, 34 | "README.md": "# Test Project\n\nThis is a test project for CodeView.\n", 35 | ".git": {"HEAD": "ref: refs/heads/main\n"}, 36 | "node_modules": { 37 | "package": {"index.js": "console.log('Hello from package');\n"} 38 | }, 39 | "build.py": "# This should be included\n", 40 | "setup.cfg": "# Configuration file\n", 41 | "data.bin": b"\x00\x01\x02\x03", # Binary file 42 | "requirements.txt": "pytest==7.3.1\npytest-cov==4.1.0\n", 43 | } 44 | 45 | self._create_file_structure(self.test_dir, self.project_structure) 46 | 47 | # Save current working directory and change to test directory 48 | self.original_dir = os.getcwd() 49 | os.chdir(self.test_dir) 50 | 51 | def tearDown(self): 52 | """Clean up the temporary directory""" 53 | os.chdir(self.original_dir) 54 | shutil.rmtree(self.test_dir) 55 | 56 | def _create_file_structure(self, base_path, structure): 57 | """Create a file structure based on dictionary representation""" 58 | for name, content in structure.items(): 59 | path = os.path.join(base_path, name) 60 | 61 | if isinstance(content, dict): 62 | os.makedirs(path, exist_ok=True) 63 | self._create_file_structure(path, content) 64 | else: 65 | # Ensure directory exists 66 | os.makedirs(os.path.dirname(path), exist_ok=True) 67 | 68 | # Write content to file 69 | mode = "wb" if isinstance(content, bytes) else "w" 70 | with open(path, mode) as f: 71 | f.write(content) 72 | 73 | def run_codeview(self, args=None): 74 | """Run the codeview command with given arguments""" 75 | cmd = [CODEVIEW_CMD] 76 | if args: 77 | cmd.extend(args) 78 | 79 | result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8") 80 | return result 81 | 82 | 83 | class BasicFunctionalityTests(CodeViewTestCase): 84 | """Test basic functionality of CodeView""" 85 | 86 | def test_default_output(self): 87 | """Test the default output of codeview""" 88 | result = self.run_codeview() 89 | 90 | # Check exit code 91 | self.assertEqual(result.returncode, 0) 92 | 93 | # Check that standard files are included 94 | self.assertIn("src/main.py", result.stdout) 95 | self.assertIn("def main():", result.stdout) 96 | self.assertIn("README.md", result.stdout) 97 | 98 | # Directory structure should be shown 99 | self.assertIn("Directory Structure:", result.stdout) 100 | 101 | def test_include_pattern(self): 102 | """Test including specific file patterns""" 103 | result = self.run_codeview(["-i", "*.py"]) 104 | 105 | # Check exit code 106 | self.assertEqual(result.returncode, 0) 107 | 108 | # Python files should be included 109 | self.assertIn("src/main.py", result.stdout) 110 | 111 | # Check for file content 112 | self.assertIn("def main():", result.stdout) 113 | 114 | def test_exclude_dir(self): 115 | """Test excluding specific directories""" 116 | result = self.run_codeview(["-e", "tests"]) 117 | 118 | print(json.dumps(result.__dict__, indent=4)) 119 | 120 | # Check that test files are excluded 121 | self.assertNotIn("./tests/test_main.py", result.stdout) 122 | 123 | # Other files should still be included 124 | self.assertIn("./src/main.py", result.stdout) 125 | 126 | def test_exclude_file(self): 127 | """Test excluding specific file patterns""" 128 | result = self.run_codeview(["-x", "*.md"]) 129 | 130 | # Check for files that should still be included 131 | self.assertIn("src/main.py", result.stdout) 132 | 133 | # Look for content from README.md that should be excluded 134 | self.assertNotIn("Test Project", result.stdout) 135 | 136 | def test_max_depth(self): 137 | """Test maximum directory depth""" 138 | result = self.run_codeview(["-d", "1"]) 139 | 140 | # Files in subdirectories beyond depth 1 should not be included as content 141 | self.assertNotIn("class User", result.stdout) 142 | self.assertNotIn("class Product", result.stdout) 143 | 144 | def test_no_tree(self): 145 | """Test that we can hide the tree view""" 146 | result = self.run_codeview(["-t"]) 147 | 148 | # Directory structure visualization should not be included 149 | self.assertNotIn("├──", result.stdout) 150 | 151 | # File contents should still be shown 152 | self.assertIn("def main():", result.stdout) 153 | 154 | def test_no_files(self): 155 | """Test that we can hide file contents""" 156 | result = self.run_codeview(["-f"]) 157 | 158 | # Directory structure visualization should be included 159 | self.assertIn("Directory Structure:", result.stdout) 160 | 161 | # File contents should not be shown 162 | self.assertNotIn("def main():", result.stdout) 163 | 164 | def test_line_numbers(self): 165 | """Test showing line numbers""" 166 | result = self.run_codeview(["-n"]) 167 | 168 | print(json.dumps(result.__dict__, indent=4)) 169 | 170 | self.assertIn("1\tdef main():", result.stdout) 171 | 172 | 173 | class OutputFormatTests(CodeViewTestCase): 174 | """Test different output formats""" 175 | 176 | def test_text_format(self): 177 | """Test the text output format""" 178 | result = self.run_codeview(["-m", "text"]) 179 | 180 | # Check that format is correct - adapted to actual output 181 | self.assertIn("**./src/main.py", result.stdout) 182 | self.assertIn("def main():", result.stdout) 183 | 184 | def test_markdown_format(self): 185 | """Test the markdown output format""" 186 | result = self.run_codeview(["-m", "markdown"]) 187 | 188 | # Check that format is correct 189 | self.assertIn("## ./src/main.py", result.stdout) 190 | self.assertIn("```py", result.stdout) 191 | self.assertIn("def main():", result.stdout) 192 | self.assertIn("```", result.stdout) 193 | 194 | def test_json_format(self): 195 | """Test the JSON output format""" 196 | result = self.run_codeview(["-m", "json"]) 197 | 198 | # Just check for JSON-like structure without parsing 199 | self.assertIn('"files":', result.stdout) 200 | self.assertIn('"path":', result.stdout) 201 | self.assertIn('"content":', result.stdout) 202 | 203 | 204 | class FileOperationTests(CodeViewTestCase): 205 | """Test file operations""" 206 | 207 | def test_output_to_file(self): 208 | """Test writing output to a file""" 209 | output_file = os.path.join(self.test_dir, "output.txt") 210 | result = self.run_codeview(["-o", output_file]) 211 | 212 | # Check that the command succeeded 213 | self.assertEqual(result.returncode, 0) 214 | 215 | # Check that the file was created 216 | self.assertTrue(os.path.exists(output_file)) 217 | 218 | # Check file contents 219 | with open(output_file, "r") as f: 220 | content = f.read() 221 | self.assertIn("main.py", content) 222 | 223 | def test_search_pattern(self): 224 | """Test searching for specific content""" 225 | result = self.run_codeview(["-s", "helper"]) 226 | 227 | # Files containing the pattern should be included 228 | self.assertIn("I am a helper function", result.stdout) 229 | 230 | # Files not containing the pattern should be excluded 231 | self.assertNotIn("print('Hello, world!')", result.stdout) 232 | 233 | def test_include_path(self): 234 | """Test including specific paths""" 235 | result = self.run_codeview(["-p", "src/models"]) 236 | 237 | # Check for content from the models directory 238 | self.assertIn("class User", result.stdout) 239 | self.assertIn("class Product", result.stdout) 240 | 241 | # Files not in the specified path should be excluded 242 | self.assertNotIn("def main():", result.stdout) 243 | 244 | 245 | class EdgeCaseTests(CodeViewTestCase): 246 | """Test edge cases and error handling""" 247 | 248 | def test_empty_directory(self): 249 | """Test running on an empty directory""" 250 | # Create a new empty directory 251 | empty_dir = os.path.join(self.test_dir, "empty") 252 | os.makedirs(empty_dir, exist_ok=True) 253 | 254 | # Change to the empty directory 255 | os.chdir(empty_dir) 256 | 257 | result = self.run_codeview() 258 | 259 | # Check that it runs without error 260 | self.assertEqual(result.returncode, 0) 261 | 262 | def test_invalid_format(self): 263 | """Test with invalid output format""" 264 | result = self.run_codeview(["-m", "invalid_format"]) 265 | 266 | # Should return an error 267 | self.assertEqual(result.returncode, 1) 268 | self.assertIn("error", result.stderr.lower()) 269 | 270 | def test_nonexistent_path(self): 271 | """Test with a non-existent path""" 272 | result = self.run_codeview(["-p", "nonexistent"]) 273 | 274 | # Just verify it runs without exception 275 | # The command might succeed but provide different output 276 | pass 277 | 278 | def test_binary_file_filtering(self): 279 | """Test handling of binary files with explicit exclusion""" 280 | result = self.run_codeview(["-x", "*.bin"]) 281 | 282 | # Binary files should be excluded from file content 283 | self.assertNotIn("data.bin", result.stdout) 284 | 285 | 286 | class ComplexTests(CodeViewTestCase): 287 | """Test complex scenarios""" 288 | 289 | def test_multiple_filter_combinations(self): 290 | """Test using multiple include/exclude filters together""" 291 | result = self.run_codeview(["-i", "*.py", "-e", "tests", "-x", "setup.py"]) 292 | 293 | # Should include Python files 294 | self.assertIn("src/main.py", result.stdout) 295 | 296 | # Should exclude test directory files 297 | self.assertNotIn("test_main.py", result.stdout) 298 | 299 | # Should exclude specific excluded Python file 300 | self.assertNotIn("setup.py", result.stdout) 301 | 302 | def test_unicode_content(self): 303 | """Test handling files with Unicode characters""" 304 | # Create a file with Unicode content 305 | with open(os.path.join(self.test_dir, "unicode_file.py"), "w", encoding="utf-8") as f: 306 | f.write( 307 | '# -*- coding: utf-8 -*-\n\ndef unicode_func():\n return "こんにちは世界"\n') 308 | 309 | result = self.run_codeview(["-i", "unicode_file.py"]) 310 | self.assertIn("こんにちは世界", result.stdout) 311 | 312 | def test_large_file_handling(self): 313 | """Test handling of relatively large files""" 314 | # Create a large Python file 315 | with open(os.path.join(self.test_dir, "large_file.py"), "w") as f: 316 | f.write("# Large file test\n") 317 | for i in range(1000): 318 | f.write(f"# Line {i}\ndef func_{i}():\n return {i}\n\n") 319 | 320 | # Just verify it runs without error 321 | result = self.run_codeview(["-i", "large_file.py"]) 322 | self.assertEqual(result.returncode, 0) 323 | self.assertIn("large_file.py", result.stdout) 324 | 325 | def test_different_language_highlighting(self): 326 | """Test syntax highlighting for different languages""" 327 | # Create files with different extensions 328 | with open(os.path.join(self.test_dir, "script.js"), "w") as f: 329 | f.write("function hello() { return 'world'; }\n") 330 | with open(os.path.join(self.test_dir, "styles.css"), "w") as f: 331 | f.write("body { color: red; }\n") 332 | 333 | # Check markdown output uses correct language tags 334 | result = self.run_codeview(["-i", "*.js", "-i", "*.css", "-m", "markdown"]) 335 | self.assertIn("```js", result.stdout) 336 | self.assertIn("```css", result.stdout) 337 | 338 | def test_json_output_validity(self): 339 | """Test that JSON output is actually valid JSON""" 340 | result = self.run_codeview(["-m", "json", "-t"]) 341 | 342 | # Try to parse the output as JSON 343 | try: 344 | parsed = json.loads(result.stdout) 345 | self.assertIn("files", parsed) 346 | self.assertTrue(isinstance(parsed["files"], list)) 347 | except json.JSONDecodeError: 348 | self.fail("JSON output is not valid JSON") 349 | 350 | def test_empty_files(self): 351 | """Test handling of empty files""" 352 | # Create an empty file 353 | with open(os.path.join(self.test_dir, "empty.py"), "w") as f: 354 | pass 355 | 356 | result = self.run_codeview(["-i", "empty.py"]) 357 | self.assertIn("empty.py", result.stdout) 358 | 359 | 360 | def test_files_with_special_chars(self): 361 | """Test handling of files with special characters in names""" 362 | # Create a file with special characters in the name 363 | special_filename = "special@#$%^&.py" 364 | with open(os.path.join(self.test_dir, special_filename), "w") as f: 365 | f.write("# File with special characters in name\n") 366 | 367 | result = self.run_codeview(["-i", special_filename]) 368 | self.assertIn(special_filename, result.stdout) 369 | 370 | def test_command_line_args_from_shell(self): 371 | """Test running codeview with shell-style arguments""" 372 | # This simulates how the command would be used in a shell 373 | cmd = CODEVIEW_CMD + " -i '*.py' -e 'tests' -m markdown" 374 | result = subprocess.run(cmd, shell=True, capture_output=True, text=True) 375 | self.assertEqual(result.returncode, 0) 376 | self.assertIn("```py", result.stdout) 377 | 378 | def test_different_language_highlighting(self): 379 | """Test syntax highlighting for different languages""" 380 | # Create files with different extensions 381 | with open(os.path.join(self.test_dir, "script.js"), "w") as f: 382 | f.write("function hello() { return 'world'; }\n") 383 | with open(os.path.join(self.test_dir, "styles.css"), "w") as f: 384 | f.write("body { color: red; }\n") 385 | 386 | # Check markdown output uses correct language tags 387 | result = self.run_codeview(["-i", "*.js", "-i", "*.css", "-m", "markdown"]) 388 | self.assertIn("```js", result.stdout) 389 | self.assertIn("```css", result.stdout) 390 | 391 | def test_no_colors_in_output_if_output_file(self): 392 | """Test that colors are not included in output if writing to a file""" 393 | output_file = os.path.join(self.test_dir, "output.txt") 394 | result = self.run_codeview(["-o", output_file]) 395 | 396 | # Check that the command succeeded 397 | self.assertEqual(result.returncode, 0) 398 | 399 | # Check that the file was created 400 | self.assertTrue(os.path.exists(output_file)) 401 | 402 | # Check file contents 403 | with open(output_file, "r") as f: 404 | content = f.read() 405 | self.assertNotIn("\033[", content) 406 | 407 | if __name__ == "__main__": 408 | unittest.main() 409 | --------------------------------------------------------------------------------