Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f0e2a82
file-org-workflows: add github workflow for testing
ad5665 Oct 29, 2025
afe3a62
file-org-workflows: trigger workflow on all branches
ad5665 Oct 29, 2025
85fa8a8
file-org-workflows: use python 3.13
ad5665 Oct 29, 2025
b097cb7
file-org-workflows: allow lint to fail
ad5665 Oct 29, 2025
69926b9
file-org-workflows: remove main expected return
ad5665 Oct 29, 2025
bd43921
file-org-workflows: add type hint to counts variable
ad5665 Oct 29, 2025
fe0246d
file-org-workflows: fix failing tests by using tmp_path
ad5665 Oct 29, 2025
8ad1193
file-org-workflows: black fixes
ad5665 Oct 29, 2025
7dd91b1
file-org-workflows: add ruff and ruff fixes
ad5665 Oct 29, 2025
05d44b2
file-org-workflows: Templatize GitHub Actions workflows
ad5665 Oct 30, 2025
e8f4490
file-org-workflows: add uses tag
ad5665 Oct 30, 2025
daa324f
file-org-workflows: move action
ad5665 Oct 30, 2025
5c29a81
file-org-workflows: "cannot specify version when calling local workfl…
ad5665 Oct 30, 2025
08d7323
file-org-workflows: move actions and only trigger tests on !main
ad5665 Oct 30, 2025
332523c
file-org-workflows: workflows has to be at top level
ad5665 Oct 30, 2025
1f5b7ee
file-org-workflows: code coverage summary
ad5665 Oct 30, 2025
3c63b77
file-org-workflows: summary
ad5665 Oct 30, 2025
52b8a01
file-org-workflows: summary
ad5665 Oct 30, 2025
33c8016
file-org-workflows: silly ai prompting
ad5665 Oct 30, 2025
2f1c6f9
file-org-workflows: code block
ad5665 Oct 30, 2025
1272871
file-org-workflows: move to a action
ad5665 Oct 30, 2025
ec9c4fb
file-org-workflows: add shell
ad5665 Oct 30, 2025
a4cfb63
file-org-workflows: remove shell
ad5665 Oct 30, 2025
c7a9a5d
file-org-workflows: add file-org path
ad5665 Oct 30, 2025
fb14ba2
file-org-workflows: change logic of recursive
ad5665 Oct 30, 2025
df6c22a
file-org-workflows: set no recurive arg to false
ad5665 Oct 30, 2025
c681b0a
file-org-workflows: additional test and fixes
ad5665 Oct 30, 2025
335edf9
file-org-workflows: ruff fixes
ad5665 Oct 30, 2025
1c07377
file-org-workflows: increase code coverage
ad5665 Oct 30, 2025
beef094
file-org-workflows: coverage
ad5665 Oct 30, 2025
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
55 changes: 55 additions & 0 deletions .github/actions/python-test/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: 'Python Test Action'
description: 'A custom action to run tests, lint, format, and typecheck a Python project.'
inputs:
project-path:
description: 'The path to the Python project.'
required: true
runs:
using: "composite"
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.13'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ${{ inputs.project-path }}/requirements.txt
pip install ruff black mypy pytest pytest-cov coverage
shell: bash

- name: Lint with ruff
continue-on-error: true
run: ruff check ${{ inputs.project-path }}/src ${{ inputs.project-path }}/tests
shell: bash

- name: Format with black
run: black --check ${{ inputs.project-path }}/src ${{ inputs.project-path }}/tests
shell: bash

- name: Typecheck with mypy
run: mypy ${{ inputs.project-path }}/src
shell: bash

- name: Test with pytest
run: pytest --cov=${{ inputs.project-path }}/src ${{ inputs.project-path }}/tests
shell: bash

- name: Combine coverage reports
run: coverage combine
shell: bash
working-directory: ${{ inputs.project-path }}

- name: Generate coverage summary
run: coverage report --show-missing > coverage_summary.md
shell: bash
working-directory: ${{ inputs.project-path }}

- name: Display coverage summary in job summary
run: |
echo "## Code Coverage Summary for $(echo ${{ inputs.project-path }} | cut -d '/' -f 2)" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat ${{ inputs.project-path }}/coverage_summary.md >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
shell: bash
17 changes: 17 additions & 0 deletions .github/workflows/file-organiser-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: file-organiser-test

on:
push:
branches-ignore:
- main
paths:
- "Beginner/file_organiser/**"

jobs:
file_organiser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/python-test
with:
project-path: Beginner/file_organiser
6 changes: 6 additions & 0 deletions Beginner/file_organiser/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[run]
source = src
parallel = true

[report]
show_missing = true
3 changes: 2 additions & 1 deletion Beginner/file_organiser/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ dependencies = [
"pytest>=8.4.2",
"python-magic>=0.4.27",
"rich>=14.2.0",
]
"ruff>=0.14.2",
]
12 changes: 6 additions & 6 deletions Beginner/file_organiser/src/file_organiser.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def get_type(file_path: Path | str) -> str:


# Using a path, return all files, using full Path and filename from Path.Walk()
def get_files(path: Path | str, recursive=False):
def get_files(path: Path | str, recursive=True):
p = Path(path)

if not p.exists(): # If the path doesn't exist, throw a error
Expand All @@ -28,13 +28,13 @@ def get_files(path: Path | str, recursive=False):
for dirpath, subdirs, files in p.walk():
for f in files:
yield dirpath / f
if recursive:
if not recursive:
break


# This fuction calls the above 2 fuctions, to grab a list of files, and then fetching the file types, saving within a Counter
def process_files(path: Path | str, recursive=False):
counts = Counter()
def process_files(path: Path | str, recursive=True):
counts: Counter[str] = Counter()
for f in track(get_files(path, recursive), description="Working...."):
try:
file_type = get_type(f)
Expand All @@ -60,14 +60,14 @@ def parse_args(argv=None):
ap.add_argument(
"-n",
"--no-recursive",
action="store_true",
action="store_false",
help="Disable recursion if you add non-recursive mode",
)
ap.add_argument("--list", action="store_true", help="(future) list files with detected MIME")
return ap.parse_args(argv)


def main(argv=None) -> int:
def main(argv=None):
args = parse_args(argv)
if args.file:
file_type = get_type(args.file)
Expand Down
133 changes: 109 additions & 24 deletions Beginner/file_organiser/tests/unit/test_file_organiser.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,68 @@
#!/usr/bin/env python3

import subprocess
from collections import Counter
from pathlib import Path

import pytest

from file_organiser import main, get_files, get_type, process_files
from file_organiser import get_files, get_type, main, process_files


# Test main with args
def test_path():
amount = main(["/mnt/c/Users/Adam/Desktop/adhd/"])
assert amount > 0
# Test main
def test_path(tmp_path):
"""Test main with a directory containing one file, expecting 1 file to be processed."""
(tmp_path / "file1.txt").touch()
amount = main([str(tmp_path)])

assert amount == 1

def test_no_path():
amount = main([])
assert amount > 0

def test_main_empty_dir(tmp_path):
"""Test main with an empty directory, should process 0 files."""
amount = main([str(tmp_path)])
assert amount == 0

def test_no_path_recursive():
amount = main(["--no-recursive"])
assert amount > 0

def test_main_no_path(tmp_path, monkeypatch):
"""Test main with no path, which should default to the current directory."""
monkeypatch.chdir(tmp_path)
(tmp_path / "file1.txt").touch()
amount = main([])
assert amount == 1

def test_file_not_found():
with pytest.raises(FileNotFoundError, match="does not exist"):
list(get_files("/tmp/file-org/not-here"))

def test_main_file_not_found():
"""Test main with a non-existent file path."""
with pytest.raises(FileNotFoundError):
main(["/non/existent/path"])


def test_not_dir():
def test_not_dir(tmp_path):
"""Test main with a non-directory path."""
file = tmp_path / "text.txt"
file.touch()
with pytest.raises(NotADirectoryError, match="is not a directory"):
list(main(["/tmp/file-org/text.txt"]))
list(main([str(file)]))


def test_with_file():
file_type = main(["--file", "/mnt/c/Users/Adam/Desktop/adhd/20250820_160657.jpg"])
def test_with_file(tmp_path):
"""Test main with a file path."""
fake_jpeg = tmp_path / "test.jpg"
fake_jpeg.write_bytes(b"\xff\xd8\xff\xe0")
file_type = main(["--file", str(fake_jpeg)])
assert file_type == "image/jpeg"


# Test get_files
def test_file_not_found(tmp_path):
"""Test get_files with a non-existent file path."""
with pytest.raises(FileNotFoundError, match="does not exist"):
list(get_files(tmp_path / "not-here"))


def test_get_files_recursive(tmp_path):
"""Test get_files with recursive mode enabled."""
# Create a dummy directory structure
(tmp_path / "subdir1").mkdir()
(tmp_path / "subdir2").mkdir()
Expand All @@ -54,20 +79,28 @@ def test_get_files_recursive(tmp_path):


def test_get_files_non_recursive(tmp_path):
# Create a dummy directory structure
"""Test get_files with non-recursive mode enabled."""
(tmp_path / "subdir1").mkdir()
(tmp_path / "subdir2").mkdir()
(tmp_path / "file1.txt").touch()
(tmp_path / "subdir1" / "file2.jpg").touch()
(tmp_path / "subdir2" / "file3.pdf").touch()

# Test non-recursive behavior
files = list(get_files(tmp_path, recursive=True))
files = list(get_files(tmp_path, recursive=False))
assert len(files) == 1
assert tmp_path / "file1.txt" in files


def test_get_files_empty_dir(tmp_path):
"""Test get_files with an empty directory."""
files = list(get_files(tmp_path))
assert len(files) == 0


# Test process_files
def test_process_files_recursive(tmp_path):
"""Test process_files with recursive mode enabled."""
# Create dummy files with different types
(tmp_path / "file1.txt").write_text("hello")
(tmp_path / "file2.jpg").touch() # python-magic will likely identify this as empty data
Expand All @@ -79,18 +112,70 @@ def test_process_files_recursive(tmp_path):
assert counts["text/plain"] == 1


def test_process_files_non_recursive(tmp_path):
"""Test process_files with non-recursive mode enabled."""
# Create dummy files
(tmp_path / "file1.txt").write_text("hello")
(tmp_path / "subdir").mkdir()
(tmp_path / "subdir" / "file2.txt").write_text("world")

counts = process_files(tmp_path, recursive=False)
assert counts == Counter({"text/plain": 1})


def test_process_files_exception(tmp_path, monkeypatch):
"""Test process_files with an exception during file type retrieval."""
(tmp_path / "file1.txt").touch()

def mock_get_type(path):
raise Exception("Test exception")

monkeypatch.setattr("file_organiser.get_type", mock_get_type)

counts = process_files(tmp_path)
assert counts.total() == 0


# Test get_type
def test_get_type_image():
type = get_type("/mnt/c/Users/Adam/Desktop/adhd/20250820_160657.jpg")
def test_get_type_image(tmp_path):
"""Test get_type with an image file."""
fake_jpeg = tmp_path / "test.jpg"
fake_jpeg.write_bytes(b"\xff\xd8\xff\xe0")
type = get_type(str(fake_jpeg))
assert type == "image/jpeg"


def test_get_type_with_temp_file():
def test_get_type_with_temp_file(tmp_path):
"""Test get_type with a temporary file."""
# Create a dummy txt file
txt_content = b"This is a dummy text file."
txt_file = "/tmp/dummy.txt"
txt_file = tmp_path / "dummy.txt"
with open(txt_file, "wb") as f:
f.write(txt_content)
# test with get_type
type = get_type(txt_file)
assert type == "text/plain"


def test_get_type_empty_file(tmp_path):
"""Test get_type with an empty file."""
empty_file = tmp_path / "empty.dat"
empty_file.touch()
file_type = get_type(empty_file)
assert file_type == "inode/x-empty"


def test_main_as_script(tmp_path):
"""Test running the script as the main program."""
(tmp_path / "file1.txt").touch()

# Get the path to the Beginner/file_organiser directory
file_organiser_dir = Path(__file__).parent.parent.parent

result = subprocess.run(
["coverage", "run", "--append", "src/file_organiser.py", str(tmp_path)],
capture_output=True,
text=True,
cwd=str(file_organiser_dir),
)
assert result.returncode == 1
28 changes: 28 additions & 0 deletions Beginner/file_organiser/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading