Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Project Context for Agents

## Project Overview
This is a Python project that implements an "End Of File Fixer" - a tool that ensures files end with exactly one newline character. The tool scans files in a directory and automatically adds or removes newlines at the end of files to maintain consistent formatting.

Despite the project name, the description in `pyproject.toml` mentions "Implementations of the Circuit Breaker" which appears to be incorrect or outdated.

### Key Technologies
- **Language**: Python 3.10+
- **Dependencies**:
- `pathspec` for gitignore pattern matching
- **Development Tools**:
- `uv` for package management and building
- `ruff` for linting and formatting
- `mypy` for type checking
- `pytest` for testing
- `just` as a command runner

### Architecture
The project follows a simple CLI tool structure:
- Main entry point: `end_of_file_fixer/main.py`
- CLI interface using `argparse`
- File processing logic in `_fix_file()` function
- Integration with `.gitignore` patterns via `pathspec`

## Building and Running

### Setup
```bash
# Install dependencies
just install
```

### Development Commands
```bash
# Run linting and type checking
just lint

# Run tests
just test

# Run tests with arguments
just test -v

# Format code
just lint # Includes auto-formatting
```

### Using the Tool
```bash
# Fix files in a directory (modifies files)
python -m end_of_file_fixer.main /path/to/directory

# Check files in a directory (dry run)
python -m end_of_file_fixer.main /path/to/directory --check
```

Or using the installed script:
```bash
# Fix files
end-of-file-fixer /path/to/directory

# Check files
end-of-file-fixer /path/to/directory --check
```

## Development Conventions

### Code Style
- Line length: 120 characters
- Strict type checking with mypy
- Ruff linting with specific rule exceptions (see pyproject.toml)
- No mandatory docstrings (D1 ignored)

### Testing
- Uses pytest framework
- Tests should be placed in the `tests/` directory
- Follow standard pytest naming conventions (`test_*.py` files, `test_*` functions)

### Project Structure
```
end-of-file-fixer/
├── end_of_file_fixer/ # Main package
│ ├── __init__.py # Package initializer
│ └── main.py # Main CLI implementation
├── tests/ # Test files
│ └── test_dummy.py # Sample test file
├── pyproject.toml # Project configuration
├── Justfile # Command definitions
├── README.md # Project description
└── .gitignore # Git ignore patterns
```

### Dependency Management
- Uses `uv` for fast dependency resolution and installation
- Dependencies defined in `pyproject.toml`
- Development dependencies in `[dependency-groups].dev`

### CI/CD
- Linting and type checking enforced in CI
- Publishing handled via `just publish` command
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 modern-python

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
129 changes: 129 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,130 @@
# End Of File Fixer

A command-line tool that ensures all your text files end with exactly one newline character.
This tool helps maintain consistent file formatting across your codebase by automatically adding or removing trailing newlines as needed.

## Why This Matters

Many POSIX systems expect text files to end with a newline character. Having consistent line endings:
- Prevents spurious diffs in version control
- Ensures proper concatenation of files
- Satisfies POSIX compliance
- Improves readability in terminal environments

## Features

- Automatically adds a newline to files that don't end with one
- Removes excess trailing newlines from files that have too many
- Respects `.gitignore` patterns to avoid processing unwanted files
- Works with all text file types
- Cross-platform compatibility (Windows, macOS, Linux)
- Dry-run mode to preview changes before applying them

## Installation

### Using uv

```bash
uv add end-of-file-fixer
```

### Using pip

```bash
pip install end-of-file-fixer
```

## Usage

### Basic Usage

To fix all files in the current directory and subdirectories:

```bash
end-of-file-fixer .
```

To check which files would be modified without making changes:

```bash
end-of-file-fixer . --check
```

## How It Works

The end-of-file-fixer processes files in the following way:

1. **Files with no trailing newline**: Adds exactly one newline at the end
2. **Files with exactly one trailing newline**: Leaves unchanged
3. **Files with multiple trailing newlines**: Truncates to exactly one newline
4. **Empty files**: Left unchanged

### Examples

| Original File Content | After Processing |
|----------------------|------------------|
| `hello world` | `hello world\n` |
| `hello world\n` | `hello world\n` |
| `hello world\n\n\n` | `hello world\n` |
| `` (empty file) | `` (unchanged) |

## Configuration

The tool automatically respects patterns in your `.gitignore` file, so it won't process files that are ignored by Git. Additionally, it always ignores:
- `.git` directories
- `.cache` directories (used by uv)

## Exit Codes

- `0`: No files needed fixing or all files were successfully fixed
- `1`: Some files needed fixing (when using `--check` mode)

## Development

### Prerequisites

- [uv](https://docs.astral.sh/uv/) for dependency management

### Setup

```bash
# Clone the repository
git clone https://github.com/community-of-python/end-of-file-fixer.git
cd end-of-file-fixer

# Install dependencies
just install
```

### Running Tests

```bash
# Run tests
just test
```

### Linting

```bash
# Run linting and formatting
just lint
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Related Projects

- [pre-commit](https://pre-commit.com/) - A framework for managing and maintaining multi-language pre-commit hooks
- [editorconfig](https://editorconfig.org/) - Helps maintain consistent coding styles across different editors and IDEs
30 changes: 18 additions & 12 deletions end_of_file_fixer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pathspec


def _fix_file(file_obj: IO[bytes]) -> int:
def _fix_file(file_obj: IO[bytes], check: bool) -> int: # noqa: C901
# Test for newline at end of file
# Empty files will throw IOError here
try:
Expand All @@ -18,18 +18,20 @@ def _fix_file(file_obj: IO[bytes]) -> int:
last_character = file_obj.read(1)
# last_character will be '' for an empty file
if last_character not in {b"\n", b"\r"} and last_character != b"":
# Needs this seek for windows, otherwise IOError
file_obj.seek(0, os.SEEK_END)
file_obj.write(b"\n")
if not check:
# Needs this seek for windows, otherwise IOError
file_obj.seek(0, os.SEEK_END)
file_obj.write(b"\n")
return 1

while last_character in {b"\n", b"\r"}:
# Deal with the beginning of the file
if file_obj.tell() == 1:
# If we've reached the beginning of the file and it is all
# linebreaks then we can make this file empty
file_obj.seek(0)
file_obj.truncate()
if not check:
# If we've reached the beginning of the file and it is all
# linebreaks then we can make this file empty
file_obj.seek(0)
file_obj.truncate()
return 1

# Go back two bytes and read a character
Expand All @@ -43,20 +45,24 @@ def _fix_file(file_obj: IO[bytes]) -> int:
for sequence in (b"\n", b"\r\n", b"\r"):
if remaining == sequence:
return 0

if remaining.startswith(sequence):
file_obj.seek(position + len(sequence))
file_obj.truncate()
if not check:
file_obj.seek(position + len(sequence))
file_obj.truncate()
return 1

return 0
return 0 # pragma: no cover


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("path", help="path to directory", type=pathlib.Path)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()

path: pathlib.Path = args.path
check: bool = args.check
gitignore_path = path / ".gitignore"

ignore_patterns = [
Expand All @@ -72,7 +78,7 @@ def main() -> int:
retv = 0
for filename in gitignore_spec.match_tree(path, negate=True):
with pathlib.Path(filename).open("rb+") as f:
ret_for_file = _fix_file(f)
ret_for_file = _fix_file(f, check=check)
if ret_for_file:
sys.stdout.write(f"Fixing {filename}")
retv |= ret_for_file
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "end-of-file-fixer"
description = "Implementations of the Circuit Breaker"
description = "A command-line tool that ensures all your text files end with exactly one newline character"
readme = "README.md"
requires-python = ">=3.10,<4"
dependencies = [
Expand Down
Empty file added tests/fixtures/empty.txt
Empty file.
2 changes: 2 additions & 0 deletions tests/fixtures/multiple_newlines.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
This file has multiple newlines at the end

3 changes: 3 additions & 0 deletions tests/fixtures/newlines_only.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@



1 change: 1 addition & 0 deletions tests/fixtures/no_newline.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has no newline at the end
1 change: 1 addition & 0 deletions tests/fixtures/one_newline.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has exactly one newline at the end
1 change: 1 addition & 0 deletions tests/fixtures/perfect.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file already has exactly one newline at the end
1 change: 1 addition & 0 deletions tests/fixtures/temp_file.tmp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a temporary file that should be ignored
2 changes: 0 additions & 2 deletions tests/test_dummy.py

This file was deleted.

Loading