Skip to content

macOS: case-only rename produces spurious added event instead of deleted + added pair #368

Description

@monque

Description

On macOS with the default case-insensitive APFS filesystem, renaming a file with only a case change (e.g., hello.txtHello.txt) produces two added events instead of the expected deleted(old) + added(new) pair.

This happens because the fallback rename handling in src/lib.rs uses Path::new(&path).exists() to determine whether a rename event corresponds to the source or destination. On a case-insensitive filesystem, both hello.txt and Hello.txt resolve to the same inode, so exists() returns true for both paths, causing both to be classified as added.

Example Code

import os
import tempfile
import time
from pathlib import Path
from threading import Thread

from watchfiles import watch, Change


def main():
    with tempfile.TemporaryDirectory() as tmp_dir:
        watch_dir = Path(tmp_dir)
        original = watch_dir / "hello.txt"
        renamed = watch_dir / "Hello.txt"

        original.write_text("content")
        time.sleep(0.5)

        def do_rename():
            time.sleep(1.5)
            os.rename(original, renamed)
            time.sleep(3)
            (watch_dir / "stop.txt").write_text("stop")

        Thread(target=do_rename, daemon=True).start()

        for changes in watch(watch_dir, debounce=400, step=100):
            print(changes)
            if any(Path(p).name == "stop.txt" for _, p in changes):
                break


if __name__ == "__main__":
    main()

Watchfiles Output

{(Change.added, '/tmp/xxx/Hello.txt'), (Change.added, '/tmp/xxx/hello.txt')}
{(Change.added, '/tmp/xxx/stop.txt')}

Expected Output

{(Change.deleted, '/tmp/xxx/hello.txt'), (Change.added, '/tmp/xxx/Hello.txt')}
{(Change.added, '/tmp/xxx/stop.txt')}

Operating System & Architecture

macOS-26.4.1-arm64-arm-64bit
Darwin Kernel Version 25.4.0: Thu Mar 19 19:31:09 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T8132

Environment

No response

Python & Watchfiles Version

python: 3.13.13 (main, Apr 14 2026, 14:32:41) [Clang 22.1.3 ], watchfiles: 1.1.1

Rust & Cargo Version

No response

Root Cause

In src/lib.rs L154-162:

EventKind::Modify(ModifyKind::Name(_)) => {
    if Path::new(&path).exists() {
        CHANGE_ADDED
    } else {
        CHANGE_DELETED
    }
}

On case-insensitive filesystems, Path::new("hello.txt").exists() returns true even after the file has been renamed to Hello.txt, because the OS treats them as the same path. Both events get classified as CHANGE_ADDED.

A possible fix would be to compare the actual on-disk filename (e.g., via std::fs::canonicalize or reading the directory entry) against the event path to determine if the casing matches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions