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
16 changes: 14 additions & 2 deletions copier/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

import os
import platform
import stat
import subprocess
import sys
import warnings
from collections.abc import Iterable, Mapping, Sequence
from contextlib import suppress
from contextvars import ContextVar
Expand Down Expand Up @@ -733,7 +735,7 @@ def _render_template(self) -> None:
else:
self._render_file(src_relpath, dst_relpath, extra_context=ctx or {})

def _render_file(
def _render_file( # noqa: C901
self,
src_relpath: Path,
dst_relpath: Path,
Expand Down Expand Up @@ -785,7 +787,17 @@ def _render_file(
# replace a symlink with a file we have to unlink it first
dst_abspath.unlink()
dst_abspath.write_bytes(new_content)
dst_abspath.chmod(src_mode)
if (dst_mode := dst_abspath.stat().st_mode) != src_mode:
try:
dst_abspath.chmod(src_mode)
except PermissionError:
# In some filesystems (e.g., gcsfuse), `chmod` is not allowed,
# so we suppress the `PermissionError` here.
warnings.warn(
f"Path permissions for {dst_abspath} cannot be changed from "
f"{stat.filemode(dst_mode)} to {stat.filemode(src_mode)}",
stacklevel=2,
)

def _render_symlink(self, src_relpath: Path, dst_relpath: Path) -> None:
"""Render one symlink.
Expand Down
42 changes: 42 additions & 0 deletions tests/test_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from textwrap import dedent, indent
from time import sleep
from typing import Any
from unittest import mock

import pytest
import yaml
Expand Down Expand Up @@ -1384,3 +1385,44 @@ def test_skipped_question_with_unset_default_value(
)
copier.run_copy(str(src), dst, defaults=True)
assert (dst / "output.txt").read_text("utf-8") == "<undefined>"


def test_warn_if_chmod_raises_permission_error(
tmp_path_factory: pytest.TempPathFactory,
) -> None:
"""Test that a warning is raised when `chmod` raises a `PermissionError`.

Changing path permissions is not allowed in some filesystems like gcsfuse. In this
case, Copier should not fail but suppress the error and raise a warning.
"""
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
build_file_tree(
{
src / "test.txt": "foo",
dst / "test.txt": "bar",
}
)

# Ensure that the source and destination files have different permissions, so
# Copier will want to change the permissions of the destination file to those of
# the source file.
(src / "test.txt").chmod(0o400)
(dst / "test.txt").chmod(0o600)

dst_file_mode = (dst / "test.txt").stat().st_mode

def mocked_chmod(self: Path, mode: int) -> None:
if self == dst / "test.txt":
raise PermissionError("Operation not permitted")
return chmod(self, mode)

chmod = Path.chmod

with (
mock.patch.object(Path, "chmod", new=mocked_chmod),
pytest.warns(UserWarning, match=r"^Path permissions for .+? cannot be changed"),
):
copier.run_copy(str(src), dst, overwrite=True)

assert (dst / "test.txt").stat().st_mode == dst_file_mode
assert (dst / "test.txt").read_text("utf-8") == "foo"
Loading