diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..42df28d --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,20 @@ +name: CI Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - main + release: + types: + - published + +jobs: + ci: + uses: community-of-python/community-workflow/.github/workflows/preset.yml@main + with: + python-version: '["3.10","3.11","3.12","3.13","3.14"]' + os: '["ubuntu-latest"]' + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5fdece --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Generic things +*.pyc +*~ +__pycache__/* +*.swp +*.sqlite3 +*.map +.vscode +.idea +.DS_Store +.env +.mypy_cache +.pytest_cache +.ruff_cache +.coverage +htmlcov/ +coverage.xml +pytest.xml +dist/ +.python-version +.venv +uv.lock +/*.egg-info/ +.zed diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..af54993 --- /dev/null +++ b/Justfile @@ -0,0 +1,24 @@ +default: install lint test + +install: + uv lock --upgrade + uv sync --all-extras --frozen + +lint: + uv run ruff format + uv run ruff check --fix + uv run mypy . + +lint-ci: + uv run ruff format --check + uv run ruff check --no-fix + uv run mypy . + +test *args: + uv run --no-sync pytest {{ args }} + +publish: + rm -rf dist + uv version $GITHUB_REF_NAME + uv build + uv publish --token $PYPI_TOKEN diff --git a/end_of_file_fixer/__init__.py b/end_of_file_fixer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/end_of_file_fixer/main.py b/end_of_file_fixer/main.py new file mode 100644 index 0000000..c1a6fde --- /dev/null +++ b/end_of_file_fixer/main.py @@ -0,0 +1,77 @@ +import argparse +import os +import pathlib +import sys +from typing import IO + +import pathspec + + +def _fix_file(file_obj: IO[bytes]) -> int: + # Test for newline at end of file + # Empty files will throw IOError here + try: + file_obj.seek(-1, os.SEEK_END) + except OSError: + return 0 + + 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") + 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() + return 1 + + # Go back two bytes and read a character + file_obj.seek(-2, os.SEEK_CUR) + last_character = file_obj.read(1) + + # Our current position is at the end of the file just before any amount of + # newlines. If we find extraneous newlines, then backtrack and trim them. + position = file_obj.tell() + remaining = file_obj.read() + 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() + return 1 + + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("path", help="path to directory", type=pathlib.Path) + args = parser.parse_args() + + path: pathlib.Path = args.path + gitignore_path = path / ".gitignore" + + ignore_patterns = [".git"] + if gitignore_path.exists(): + with gitignore_path.open("r") as f: + ignore_patterns.extend(f.readlines()) + + gitignore_spec = pathspec.GitIgnoreSpec.from_lines(ignore_patterns) + + 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) + if ret_for_file: + sys.stdout.write(f"Fixing {filename}") + retv |= ret_for_file + + return retv diff --git a/end_of_file_fixer/py.typed b/end_of_file_fixer/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3a2d655 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,77 @@ +[project] +name = "end-of-file-fixer" +description = "Implementations of the Circuit Breaker" +readme = "README.md" +requires-python = ">=3.10,<4" +dependencies = [ + "pathspec", +] +version = "0" +authors = [{ name = "community-of-python" }] +keywords = [ + "python", +] +classifiers = [ + "Typing :: Typed", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] + +[project.urls] +Repository = "https://github.com/community-of-python/end-of-file-fixer" +Issues = "https://github.com/community-of-python/end-of-file-fixer/issues" +Changelogs = "https://github.com/community-of-python/end-of-file-fixer/releases" + +[project.scripts] +end-of-file-fixer = "end_of_file_fixer.main:main" + +[dependency-groups] +dev = [ + "ruff", + "mypy", + "pytest", + "pytest-cov", +] + +[build-system] +requires = ["uv_build"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "end_of_file_fixer" +module-root = "" + +[tool.mypy] +python_version = "3.10" +strict = true + +[tool.ruff] +fix = true +unsafe-fixes = true +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "D1", # allow missing docstrings + "S101", # allow asserts + "TCH", # ignore flake8-type-checking + "FBT", # allow boolean args + "D203", # "one-blank-line-before-class" conflicting with D211 + "D213", # "multi-line-summary-second-line" conflicting with D212 + "COM812", # flake8-commas "Trailing comma missing" + "ISC001", # flake8-implicit-str-concat +] +isort.lines-after-imports = 2 +isort.no-lines-before = ["standard-library", "local-folder"] + +[tool.pytest.ini_options] +addopts = "--cov=. --cov-report term-missing" + +[tool.coverage] +run.concurrency = ["thread"] +report.exclude_also = ["if typing.TYPE_CHECKING:"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 0000000..3fb8a97 --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,2 @@ +def test_dummy() -> None: + assert True