def METHOD_NAME(self) -> str:
    # This function uses md5 hashes because they are fast, and because
    # the hashes only need to be unique per project, so clashes are
    # tremendously unlikely and not a big deal even if they happen.
    hashes: List[str] = list()
    for root, dirs, files in os.walk(self._project_dir):
        # Sort contents, we need this to be stable so it's reproducible
        dirs.sort()
        files.sort()
        for filename in files:
            md5_hash = hashlib.md5()
            # Read files in chunks in case they are big
            with open(os.path.join(root, filename), "rb") as f:
                for block in iter(lambda: f.read(4096), b""):
                    md5_hash.update(block)
            hashes.append(md5_hash.hexdigest())
    # Return final hash of hashes for the directory
    return hashlib.md5("".join(hashes).encode()).hexdigest()