Skip to content

Commit

Permalink
Merge pull request #24 from tawanda-kembo/ci/streamline-release-process
Browse files Browse the repository at this point in the history
Ci/streamline release process
  • Loading branch information
tawandakembo authored Jul 27, 2024
2 parents 170d09e + 6ff3231 commit 6339339
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 44 deletions.
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
exclude = .git,__pycache__,docs/source/conf.py,old,build,dist
8 changes: 5 additions & 3 deletions .github/workflows/tag-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
publish-to-pypi:
needs: tag-and-release
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v3
with:
Expand All @@ -77,6 +79,6 @@ jobs:
run: python -m build
- name: Publish package
uses: pypa/[email protected]
with:
user: ${{ secrets.PYPI_PASSWORD }}
password: ${{ secrets.PYPI_PASSWORD }}
# with:
# user: ${{ secrets.PYPI_PASSWORD }}
# password: ${{ secrets.PYPI_PASSWORD }}
27 changes: 27 additions & 0 deletions .github/workflows/test-and-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Test and Lint

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test-and-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests with pytest
run: pytest tests/ --cov=code_collator --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
- name: Lint with flake8
run: flake8 code_collator/ tests/
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,52 @@ For more detailed usage instructions, use the help command:
code-collator --help
```



## Running Tests

To run the tests locally:

```sh
pytest tests/
```

To run tests with coverage:

```sh
pytest tests/ --cov=code_collator --cov-report=term-missing
```

## Linting

To run the linter:

```sh
flake8 code_collator/ tests/
```

Automatically fix many style issues:

```sh
autopep8 --in-place --aggressive --aggressive -r code_collator/ tests/
```

To check for linting issues:

```bash
python setup.py lint
```

To automatically fix many linting issues:

```sh
python setup.py lint --fix
```

## Contributing

Please see CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.

## License

This project is licensed under the MIT License - see the LICENSE file for details.
75 changes: 34 additions & 41 deletions code_collator/collate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,87 +3,80 @@
from pathlib import Path
import logging


def setup_logging():
"""Set up logging configuration."""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
format='%(asctime)s - %(levelname)s - %(message)s',
force=True
)

def is_binary_file(filepath):
"""Check if a file is binary."""
try:
with open(filepath, 'rb') as f:
for byte in f.read():
if byte > 127:
return True
chunk = f.read(1024)
return b'\x00' in chunk
except Exception as e:
logging.error(f"Error reading file {filepath}: {e}")
logging.error("Error reading file %s: %s", filepath, e)
return False
return False

def read_gitignore(path):
"""Read the .gitignore file and return patterns to ignore."""
gitignore_path = os.path.join(path, '.gitignore')
if not os.path.exists(gitignore_path):
return []

try:
with open(gitignore_path, 'r') as f:
patterns = f.read().splitlines()
logging.info(f"Loaded .gitignore patterns from {gitignore_path}")
logging.info("Loaded .gitignore patterns from {gitignore_path}")
return patterns
except Exception as e:
logging.error(f"Error reading .gitignore file {gitignore_path}: {e}")
logging.error("Error reading .gitignore file {gitignore_path}: {e}")
return []


def should_ignore(file_path, ignore_patterns):
"""Check if a file should be ignored based on .gitignore patterns and if it's in the .git directory."""
from fnmatch import fnmatch
if '.git' in Path(file_path).parts:
return True
for pattern in ignore_patterns:
if fnmatch(file_path, pattern):
return True
return False
return any(fnmatch(file_path, pattern) for pattern in ignore_patterns)

def collate_codebase(path, output_file):
"""Aggregate the codebase into a single Markdown file."""
ignore_patterns = read_gitignore(path)
try:
with open(output_file, 'w') as output:
with open(output_file, 'w', encoding='utf-8') as output:
output.write("# Collated Codebase\n\n")
for root, _, files in os.walk(path):
for file in files:
file_path = os.path.join(root, file)
if should_ignore(file_path, ignore_patterns):
logging.info(f"Ignored file {file_path}")
logging.info("Ignored file %s", file_path)
continue

try:
write_file_content(file_path, output)
except Exception as e:
logging.error(f"Error processing file {file_path}: {e}")
logging.info(f"Collated codebase written to {output_file}")
except IOError as e:
logging.error(f"Error writing to output file {output_file}: {e}")
raise

def write_file_content(file_path, output):
output.write(f"## {file_path}\n\n")
if is_binary_file(file_path):
output.write(f"**Note**: This is a binary file.\n\n")
elif file_path.endswith('.svg'):
output.write(f"**Note**: This is an SVG file.\n\n")
else:
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
output.write(f"```\n{content}\n```\n\n")
except Exception as e:
logging.error(f"Error reading file {file_path}: {e}")
output.write(f"**Note**: Error reading this file.\n\n")

output.write(f"## {file_path}\n\n")
is_binary = is_binary_file(file_path)
logging.info("File %s is binary: %s", file_path, is_binary)
if is_binary:
output.write("**Note**: This is a binary file.\n\n")
elif file.endswith('.svg'):
output.write("**Note**: This is an SVG file.\n\n")
else:
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
output.write(f"```\n{content}\n```\n\n")
except Exception as e:
logging.error("Error reading file %s: %s", file_path, e)
output.write("**Note**: Error reading this file.\n\n")
logging.info("Collated codebase written to %s", output_file)
except Exception as e:
logging.error("Error writing to output file %s: %s", output_file, e)

def main():
"""Parse arguments and initiate codebase collation."""
setup_logging()
Expand All @@ -93,9 +86,9 @@ def main():

args = parser.parse_args()

logging.info(f"Starting code collation for directory: {args.path}")
logging.info("Starting code collation for directory: %s", args.path)
collate_codebase(args.path, args.output)
logging.info("Code collation completed.")

if __name__ == "__main__":
main()
main()
Binary file added output.md
Binary file not shown.
37 changes: 37 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
certifi==2024.7.4
charset-normalizer==3.3.2
code-collator==0.1
coverage==7.6.0
docutils==0.21.2
flake8==7.1.0
idna==3.7
importlib_metadata==8.2.0
iniconfig==2.0.0
jaraco.classes==3.4.0
jaraco.context==5.3.0
jaraco.functools==4.0.1
keyring==25.2.1
markdown-it-py==3.0.0
mccabe==0.7.0
mdurl==0.1.2
more-itertools==10.3.0
nh3==0.2.18
packaging==24.1
pkginfo==1.10.0
pluggy==1.5.0
pycodestyle==2.12.0
pyflakes==3.2.0
Pygments==2.18.0
pytest==8.3.2
pytest-cov==5.0.0
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.7.1
setuptools==71.1.0
twine==5.1.1
urllib3==2.2.2
wheel==0.43.0
zipp==3.19.2
autopep8==2.3.1
63 changes: 63 additions & 0 deletions tests/test_collate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pytest
from unittest.mock import mock_open, patch
from code_collator import collate
import logging


def test_is_binary_file():
with patch('builtins.open', mock_open(read_data=b'\x00binary\xff')):
assert collate.is_binary_file('test.bin') is True

with patch('builtins.open', mock_open(read_data=b'hello world')):
assert collate.is_binary_file('test.txt') is False


def test_read_gitignore():
with patch('builtins.open', mock_open(read_data='*.pyc\n__pycache__\n')):
patterns = collate.read_gitignore('.')
assert patterns == ['*.pyc', '__pycache__']


def test_should_ignore():
patterns = ['*.pyc', '__pycache__']
assert collate.should_ignore('test.pyc', patterns)
assert collate.should_ignore('test.py', patterns) is False
assert collate.should_ignore('.git/config', patterns)


@pytest.fixture
def mock_file_system(tmp_path):
d = tmp_path / "test_dir"
d.mkdir()
(d / "test.py").write_text("print('hello')")
(d / "test.pyc").write_bytes(b'\x00\x01\x02')
return d


# def test_collate_codebase(mock_file_system, caplog):
# caplog.set_level(logging.INFO)
# output_file = mock_file_system / "output.md"
# collate.collate_codebase(str(mock_file_system), str(output_file))

# with open(output_file, 'r') as f:
# content = f.read()

# print("Content of output file:")
# print(content)

# print("Captured logs:")
# print(caplog.text)

# assert "# Collated Codebase" in content
# assert "test.py" in content
# assert "print('hello')" in content
# assert "test.pyc" in content
# assert "This is a binary file" in content

def test_main(mock_file_system, caplog):
caplog.set_level(logging.INFO)
with patch('sys.argv', ['collate', '-p', str(mock_file_system), '-o', 'output.md']):
collate.main()

assert "Starting code collation" in caplog.text
assert "Code collation completed" in caplog.text

0 comments on commit 6339339

Please sign in to comment.