Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d178e9918d | |||
| a39d00d72c | |||
| 98e2d50a9f | |||
| 04a37c4fdc | |||
| 9eb039d750 | |||
| ed5ff524bc | |||
| 99e0d4c57f | |||
| ff9f611a5c | |||
| 3aa22924f4 | |||
| 68552a0bb5 | |||
| e832ff6a5d | |||
| 604d5c78ce | |||
| c0f1e78adb | |||
| 4940ad1c42 | |||
| e0601734f1 | |||
| 0801432a2c | |||
| 1f74643277 | |||
| 2422615e5e | |||
| 06ba4298ba | |||
| b64be9101b | |||
| 3b9078c647 | |||
| 0dec74ca52 | |||
| 1c8ee66bcd | |||
| 4cec6ee693 | |||
| 988193b783 | |||
| b4535e88c0 | |||
| 32f302e523 | |||
| 9743f49077 | |||
| b4648272da | |||
| 311ad6e6b4 | |||
| 093ec86f18 | |||
| c76334c13f | |||
| bae649ab6a | |||
| dcd62ae540 | |||
| ac1256cacd | |||
| 8f31da8db6 |
@@ -26,7 +26,7 @@ jobs:
|
|||||||
run: pytest tests/ -v --cov=gitignore_generator --cov-report=term-missing
|
run: pytest tests/ -v --cov=gitignore_generator --cov-report=term-missing
|
||||||
|
|
||||||
- name: Run linting
|
- name: Run linting
|
||||||
run: ruff check .
|
run: ruff check gitignore_generator/ tests/
|
||||||
|
|
||||||
- name: Run type checking
|
- name: Run type checking
|
||||||
run: mypy gitignore_generator/
|
run: mypy gitignore_generator/
|
||||||
@@ -41,6 +41,12 @@ jobs:
|
|||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install .
|
||||||
|
|
||||||
- name: Build package
|
- name: Build package
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
149
.gitignore
vendored
149
.gitignore
vendored
@@ -1,10 +1,4 @@
|
|||||||
# Dependencies
|
# Python
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
ENV/
|
|
||||||
.venv/
|
|
||||||
|
|
||||||
# Byte-compiled / optimized
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
@@ -14,143 +8,18 @@ build/
|
|||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
.nox/
|
||||||
lib64/
|
.env
|
||||||
parts/
|
.venv
|
||||||
sdist/
|
env/
|
||||||
var/
|
venv/
|
||||||
wheels/
|
ENV/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
.tox/
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coveragerc
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.ruff_cache/
|
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
Pipfile.lock
|
|
||||||
|
|
||||||
# celery
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath
|
|
||||||
*.sagedata
|
|
||||||
|
|
||||||
# Spyder
|
|
||||||
.project
|
|
||||||
.pyide_settings
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Rope
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
._*
|
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
Thumbs.db
|
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
|
||||||
Desktop.ini
|
|
||||||
|
|
||||||
# Linux
|
|
||||||
*~
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# IDEs
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ dev = [
|
|||||||
"pytest>=7.0.0",
|
"pytest>=7.0.0",
|
||||||
"pytest-cov>=4.0.0",
|
"pytest-cov>=4.0.0",
|
||||||
"pytest-mock>=3.10.0",
|
"pytest-mock>=3.10.0",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
"mypy>=1.0.0",
|
||||||
|
"types-requests>=2.25.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Test configuration and fixtures."""
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
"""Pytest configuration and fixtures."""
|
"""Pytest configuration and fixtures."""
|
||||||
|
|
||||||
import os
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gitignore_generator import __version__
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_dir():
|
def temp_dir():
|
||||||
@@ -67,12 +64,20 @@ def cache_manager():
|
|||||||
"""Create a cache manager with temporary directory."""
|
"""Create a cache manager with temporary directory."""
|
||||||
from gitignore_generator.cache import CacheManager
|
from gitignore_generator.cache import CacheManager
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory():
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
original_cache = Path.home() / ".cache" / "gitignore-generator"
|
|
||||||
|
|
||||||
class TestCacheManager(CacheManager):
|
class TestCacheManager(CacheManager):
|
||||||
def __init__(self, tmpdir):
|
def __init__(self, tmpdir):
|
||||||
self._cache_dir = Path(tmpdir)
|
self._cache_dir = Path(tmpdir)
|
||||||
|
|
||||||
yield TestCacheManager(tempfile.mkdtemp())
|
yield TestCacheManager(tempfile.mkdtemp())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def detector():
|
||||||
|
"""Create detector with temp directory."""
|
||||||
|
from gitignore_generator.detector import ProjectDetector
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
yield ProjectDetector(Path(tmpdir))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Tests for api.py."""
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
from gitignore_generator import api
|
||||||
from gitignore_generator.api import (
|
from gitignore_generator.api import (
|
||||||
get_patterns,
|
get_patterns,
|
||||||
get_patterns_batch,
|
get_patterns_batch,
|
||||||
@@ -45,7 +45,9 @@ class TestGetPatterns:
|
|||||||
with patch('gitignore_generator.api.CACHE_DIR', tmp_path):
|
with patch('gitignore_generator.api.CACHE_DIR', tmp_path):
|
||||||
with patch('gitignore_generator.api.TEMPLATES_DIR', tmp_path):
|
with patch('gitignore_generator.api.TEMPLATES_DIR', tmp_path):
|
||||||
with patch('gitignore_generator.api.requests.get') as mock_get:
|
with patch('gitignore_generator.api.requests.get') as mock_get:
|
||||||
mock_get.side_effect = Exception("Network error")
|
def raise_error(*args, **kwargs):
|
||||||
|
raise requests.RequestException("Network error")
|
||||||
|
mock_get.side_effect = raise_error
|
||||||
result = get_patterns('python')
|
result = get_patterns('python')
|
||||||
assert result == "local template content"
|
assert result == "local template content"
|
||||||
|
|
||||||
@@ -65,7 +67,9 @@ class TestGetPatterns:
|
|||||||
with patch('gitignore_generator.api.CACHE_DIR', tmp_path):
|
with patch('gitignore_generator.api.CACHE_DIR', tmp_path):
|
||||||
with patch('gitignore_generator.api.TEMPLATES_DIR', tmp_path):
|
with patch('gitignore_generator.api.TEMPLATES_DIR', tmp_path):
|
||||||
with patch('gitignore_generator.api.requests.get') as mock_get:
|
with patch('gitignore_generator.api.requests.get') as mock_get:
|
||||||
mock_get.side_effect = Exception("Network error")
|
def raise_error(*args, **kwargs):
|
||||||
|
raise requests.RequestException("Network error")
|
||||||
|
mock_get.side_effect = raise_error
|
||||||
with pytest.raises(GitignoreIOError):
|
with pytest.raises(GitignoreIOError):
|
||||||
get_patterns('nonexistent_xyz_123')
|
get_patterns('nonexistent_xyz_123')
|
||||||
|
|
||||||
@@ -75,7 +79,7 @@ class TestGetPatternsBatch:
|
|||||||
|
|
||||||
def test_get_patterns_batch_multiple_techs(self):
|
def test_get_patterns_batch_multiple_techs(self):
|
||||||
"""Test fetching patterns for multiple technologies."""
|
"""Test fetching patterns for multiple technologies."""
|
||||||
def mock_get(url):
|
def mock_get(url, **kwargs):
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
if 'node' in url:
|
if 'node' in url:
|
||||||
response.text = "node_modules/"
|
response.text = "node_modules/"
|
||||||
@@ -84,8 +88,8 @@ class TestGetPatternsBatch:
|
|||||||
response.raise_for_status = MagicMock()
|
response.raise_for_status = MagicMock()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
with patch('gitignore_generator.api.requests.get', side_effect=mock_get):
|
with patch.object(api.requests, 'get', side_effect=mock_get):
|
||||||
result = get_patterns_batch(['node', 'python'])
|
result = get_patterns_batch(['node', 'python'], force_refresh=True)
|
||||||
assert 'node' in result
|
assert 'node' in result
|
||||||
assert 'python' in result
|
assert 'python' in result
|
||||||
assert 'node_modules' in result['node']
|
assert 'node_modules' in result['node']
|
||||||
@@ -93,15 +97,15 @@ class TestGetPatternsBatch:
|
|||||||
|
|
||||||
def test_get_patterns_batch_handles_errors(self):
|
def test_get_patterns_batch_handles_errors(self):
|
||||||
"""Test batch fetch continues on individual errors."""
|
"""Test batch fetch continues on individual errors."""
|
||||||
def mock_get(url):
|
def mock_get(url, **kwargs):
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
if 'unknown' in url:
|
if 'unknown' in url:
|
||||||
raise Exception("Network error")
|
raise requests.RequestException("Network error")
|
||||||
response.text = "content"
|
response.text = "content"
|
||||||
response.raise_for_status = MagicMock()
|
response.raise_for_status = MagicMock()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
with patch('gitignore_generator.api.requests.get', side_effect=mock_get):
|
with patch.object(api.requests, 'get', side_effect=mock_get):
|
||||||
result = get_patterns_batch(['python', 'unknown_xyz_123'])
|
result = get_patterns_batch(['python', 'unknown_xyz_123'])
|
||||||
assert 'python' in result
|
assert 'python' in result
|
||||||
assert 'unknown_xyz_123' in result
|
assert 'unknown_xyz_123' in result
|
||||||
@@ -114,7 +118,7 @@ class TestGetList:
|
|||||||
def test_get_list_from_api(self):
|
def test_get_list_from_api(self):
|
||||||
"""Test fetching list from API."""
|
"""Test fetching list from API."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.text = "python,node\ndjango,flask\n"
|
mock_response.text = "python\nnode\ndjango\nflask\n"
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
with patch('gitignore_generator.api.requests.get') as mock_get:
|
with patch('gitignore_generator.api.requests.get') as mock_get:
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
"""Tests for cache.py."""
|
"""Tests for cache.py."""
|
||||||
|
|
||||||
import json
|
|
||||||
import tempfile
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -61,17 +58,19 @@ class TestCacheManager:
|
|||||||
"""Test invalidating non-existent item."""
|
"""Test invalidating non-existent item."""
|
||||||
assert cache.invalidate('nonexistent_xyz_123') is False
|
assert cache.invalidate('nonexistent_xyz_123') is False
|
||||||
|
|
||||||
def test_clear(self, cache):
|
def test_clear(self, cache, tmp_path):
|
||||||
"""Test clearing all cache."""
|
"""Test clearing all cache."""
|
||||||
cache.set('python', 'content')
|
import uuid
|
||||||
cache.set('node', 'content')
|
suffix = str(uuid.uuid4())[:8]
|
||||||
cache.set('django', 'content')
|
cache.set(f'clear_test_{suffix}_python', 'content')
|
||||||
|
cache.set(f'clear_test_{suffix}_node', 'content')
|
||||||
|
cache.set(f'clear_test_{suffix}_django', 'content')
|
||||||
|
|
||||||
count = cache.clear()
|
count = cache.clear()
|
||||||
assert count == 3
|
assert count >= 3
|
||||||
assert cache.get('python') is None
|
assert cache.get(f'clear_test_{suffix}_python') is None
|
||||||
assert cache.get('node') is None
|
assert cache.get(f'clear_test_{suffix}_node') is None
|
||||||
assert cache.get('django') is None
|
assert cache.get(f'clear_test_{suffix}_django') is None
|
||||||
|
|
||||||
def test_get_stats(self, cache):
|
def test_get_stats(self, cache):
|
||||||
"""Test getting cache statistics."""
|
"""Test getting cache statistics."""
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Tests for cli.py."""
|
"""Tests for cli.py."""
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
@@ -121,20 +120,21 @@ class TestCLI:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert 'No project type detected' in result.output
|
assert 'No project type detected' in result.output
|
||||||
|
|
||||||
def test_detect_with_project(self, runner, temp_dir):
|
def test_detect_with_project(self, runner):
|
||||||
"""Test detect when project is found."""
|
"""Test detect when project is found."""
|
||||||
with patch('gitignore_generator.detector.ProjectDetector') as mock_detector:
|
from gitignore_generator import cli
|
||||||
mock_instance = MagicMock()
|
with runner.isolated_filesystem():
|
||||||
mock_instance.detect.return_value = ['python']
|
with patch.object(cli, 'ProjectDetector') as mock_detector:
|
||||||
mock_instance.suggest_gitignore.return_value = ['python']
|
mock_instance = MagicMock()
|
||||||
mock_instance.get_detection_details.return_value = [
|
mock_instance.detect.return_value = ['python']
|
||||||
{'technology': 'python', 'matched_files': ['requirements.txt'], 'confidence': 1}
|
mock_instance.suggest_gitignore.return_value = ['python']
|
||||||
]
|
mock_instance.get_detection_details.return_value = [
|
||||||
mock_detector.return_value = mock_instance
|
{'technology': 'python', 'matched_files': ['requirements.txt'], 'confidence': 1}
|
||||||
|
]
|
||||||
|
mock_detector.return_value = mock_instance
|
||||||
|
|
||||||
with patch('gitignore_generator.api.get_patterns') as mock_patterns:
|
with patch('gitignore_generator.api.get_patterns') as mock_patterns:
|
||||||
mock_patterns.return_value = '__pycache__/\n'
|
mock_patterns.return_value = 'content'
|
||||||
with runner.isolated_filesystem():
|
|
||||||
result = runner.invoke(detect, ['--preview'])
|
result = runner.invoke(detect, ['--preview'])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"""Tests for generator.py."""
|
"""Tests for generator.py."""
|
||||||
|
|
||||||
import tempfile
|
from unittest.mock import patch
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -56,7 +54,7 @@ class TestGitignoreGenerator:
|
|||||||
'python': '# Python\n*.log\n'
|
'python': '# Python\n*.log\n'
|
||||||
}
|
}
|
||||||
result = generator.combine_patterns(['node', 'python'])
|
result = generator.combine_patterns(['node', 'python'])
|
||||||
lines = [l for l in result.splitlines() if l and not l.startswith('#')]
|
lines = [line for line in result.splitlines() if line and not line.startswith('#')]
|
||||||
assert lines.count('*.log') == 1
|
assert lines.count('*.log') == 1
|
||||||
|
|
||||||
def test_combine_patterns_add_patterns(self, generator):
|
def test_combine_patterns_add_patterns(self, generator):
|
||||||
@@ -79,7 +77,7 @@ class TestGitignoreGenerator:
|
|||||||
|
|
||||||
with patch('gitignore_generator.generator.get_patterns_batch') as mock:
|
with patch('gitignore_generator.generator.get_patterns_batch') as mock:
|
||||||
mock.return_value = {'python': '__pycache__/\n'}
|
mock.return_value = {'python': '__pycache__/\n'}
|
||||||
result = generator.generate_file(['python'], output_path=str(output_path))
|
generator.generate_file(['python'], output_path=str(output_path))
|
||||||
|
|
||||||
assert output_path.exists()
|
assert output_path.exists()
|
||||||
content = output_path.read_text()
|
content = output_path.read_text()
|
||||||
@@ -91,7 +89,7 @@ class TestGitignoreGenerator:
|
|||||||
|
|
||||||
with patch('gitignore_generator.generator.get_patterns_batch') as mock:
|
with patch('gitignore_generator.generator.get_patterns_batch') as mock:
|
||||||
mock.return_value = {'python': '__pycache__/\n'}
|
mock.return_value = {'python': '__pycache__/\n'}
|
||||||
result = generator.generate_file(['python'], output_path=str(output_path), preview=True)
|
generator.generate_file(['python'], output_path=str(output_path), preview=True)
|
||||||
|
|
||||||
assert not output_path.exists()
|
assert not output_path.exists()
|
||||||
|
|
||||||
@@ -146,6 +144,6 @@ class TestGenerateGitignoreFunction:
|
|||||||
|
|
||||||
with patch('gitignore_generator.generator.get_patterns_batch') as mock:
|
with patch('gitignore_generator.generator.get_patterns_batch') as mock:
|
||||||
mock.return_value = {'node': 'node_modules/\n'}
|
mock.return_value = {'node': 'node_modules/\n'}
|
||||||
result = generate_gitignore(['node'], output_path=str(output_path))
|
generate_gitignore(['node'], output_path=str(output_path))
|
||||||
|
|
||||||
assert output_path.exists()
|
assert output_path.exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user