Skip to content
Open
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
22 changes: 18 additions & 4 deletions dploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,32 @@
assert sys.version_info >= (3, 3), "Requires Python 3.3 or Greater"


def stow(sources, dest, is_silent=True, is_dry_run=False, ignore_patterns=None):
def stow(
sources,
dest,
is_silent=True,
is_dry_run=False,
ignore_patterns=None,
dotfiles=False,
):
"""
sub command stow
"""
stowcmd.Stow(sources, dest, is_silent, is_dry_run, ignore_patterns)
stowcmd.Stow(sources, dest, is_silent, is_dry_run, ignore_patterns, dotfiles)


def unstow(sources, dest, is_silent=True, is_dry_run=False, ignore_patterns=None):
def unstow(
sources,
dest,
is_silent=True,
is_dry_run=False,
ignore_patterns=None,
dotfiles=False,
):
"""
sub command unstow
"""
stowcmd.UnStow(sources, dest, is_silent, is_dry_run, ignore_patterns)
stowcmd.UnStow(sources, dest, is_silent, is_dry_run, ignore_patterns, dotfiles)


def clean(sources, dest, is_silent=True, is_dry_run=False, ignore_patterns=None):
Expand Down
38 changes: 30 additions & 8 deletions dploy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,25 @@ def create_parser():
stow_parser = sub_parsers.add_parser("stow")
stow_parser.add_argument("source", nargs="+", help="source directory to stow")
stow_parser.add_argument("dest", help="destination path to stow into")
stow_parser.add_argument(
"--dotfiles",
dest="dotfiles",
action="store_true",
help="Treating a source file or folder named 'dot-prefix-something' is equivalent to a destination file or folder named '.prefix-something'",
)
add_ignore_argument(stow_parser)

unstow_parser = sub_parsers.add_parser("unstow")
unstow_parser.add_argument(
"source", nargs="+", help="source directory to unstow from"
)
unstow_parser.add_argument("dest", help="destination path to unstow")
unstow_parser.add_argument(
"--dotfiles",
dest="dotfiles",
action="store_true",
help="Treating a source file or folder named 'dot-prefix-something' is equivalent to a destination file or folder named '.prefix-something'",
)
add_ignore_argument(unstow_parser)

clean_parser = sub_parsers.add_parser("clean")
Expand Down Expand Up @@ -99,16 +111,26 @@ def run(arguments=None):
sys.exit(0)

try:
subcmd(
args.source,
args.dest,
is_silent=args.is_silent,
is_dry_run=args.is_dry_run,
ignore_patterns=args.ignore_patterns,
)
if args.subcmd in ["stow", "unstow"]:
subcmd(
args.source,
args.dest,
is_silent=args.is_silent,
is_dry_run=args.is_dry_run,
ignore_patterns=args.ignore_patterns,
dotfiles=args.dotfiles,
)
else:
subcmd(
args.source,
args.dest,
is_silent=args.is_silent,
is_dry_run=args.is_dry_run,
ignore_patterns=args.ignore_patterns,
)
except DployError:
sys.exit(1)

except (KeyboardInterrupt) as error:
except KeyboardInterrupt as error:
print(error, file=sys.stderr)
sys.exit(130)
12 changes: 11 additions & 1 deletion dploy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,24 @@ class AbstractBaseSubCommand:
"""

# pylint: disable=too-many-arguments
def __init__(self, subcmd, sources, dest, is_silent, is_dry_run, ignore_patterns):
def __init__(
self,
subcmd,
sources,
dest,
is_silent,
is_dry_run,
ignore_patterns,
dotfiles=False,
):
self.subcmd = subcmd

self.actions = actions.Actions(is_silent, is_dry_run)
self.errors = error.Errors(is_silent)

self.is_silent = is_silent
self.is_dry_run = is_dry_run
self.dotfiles = dotfiles

self.dest_input = pathlib.Path(dest)
source_inputs = [pathlib.Path(source) for source in sources]
Expand Down
54 changes: 46 additions & 8 deletions dploy/stowcmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ class AbstractBaseStow(main.AbstractBaseSubCommand):
"""

# pylint: disable=too-many-arguments
def __init__(self, subcmd, source, dest, is_silent, is_dry_run, ignore_patterns):
def __init__(
self,
subcmd,
source,
dest,
is_silent,
is_dry_run,
ignore_patterns,
dotfiles=False,
):
self.is_unfolding = False
super().__init__(subcmd, source, dest, is_silent, is_dry_run, ignore_patterns)
super().__init__(
subcmd, source, dest, is_silent, is_dry_run, ignore_patterns, dotfiles
)

def _is_valid_input(self, sources, dest):
"""
Expand Down Expand Up @@ -105,7 +116,10 @@ def _collect_actions(self, source, dest):
self.ignore.ignore(subsources)
continue

dest_path = dest / pathlib.Path(subsources.name)
dest_name = subsources.name
if self.dotfiles and dest_name.startswith("dot-"):
dest_name = dest_name.replace("dot-", ".", 1)
dest_path = dest / pathlib.Path(dest_name)

does_dest_path_exist = False
try:
Expand Down Expand Up @@ -134,9 +148,17 @@ class Stow(AbstractBaseStow):

# pylint: disable=too-many-arguments
def __init__(
self, source, dest, is_silent=True, is_dry_run=False, ignore_patterns=None
self,
source,
dest,
is_silent=True,
is_dry_run=False,
ignore_patterns=None,
dotfiles=False,
):
super().__init__("stow", source, dest, is_silent, is_dry_run, ignore_patterns)
super().__init__(
"stow", source, dest, is_silent, is_dry_run, ignore_patterns, dotfiles
)

def _unfold(self, source, dest):
"""
Expand Down Expand Up @@ -221,9 +243,17 @@ class UnStow(AbstractBaseStow):

# pylint: disable=too-many-arguments
def __init__(
self, source, dest, is_silent=True, is_dry_run=False, ignore_patterns=None
self,
source,
dest,
is_silent=True,
is_dry_run=False,
ignore_patterns=None,
dotfiles=False,
):
super().__init__("unstow", source, dest, is_silent, is_dry_run, ignore_patterns)
super().__init__(
"unstow", source, dest, is_silent, is_dry_run, ignore_patterns, dotfiles
)

def _are_same_file(self, source, dest):
"""
Expand Down Expand Up @@ -379,7 +409,15 @@ def __init__(self, source, dest, is_silent, is_dry_run, ignore_patterns):
self.source = [pathlib.Path(s) for s in source]
self.dest = pathlib.Path(dest)
self.ignore_patterns = ignore_patterns
super().__init__("clean", source, dest, is_silent, is_dry_run, ignore_patterns)
super().__init__(
"clean",
source,
dest,
is_silent,
is_dry_run,
ignore_patterns,
dotfiles=False,
)

def _is_valid_input(self, sources, dest):
"""
Expand Down
41 changes: 41 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,44 @@ def file_dploystowignore(tmpdir):
name = str(tmpdir.join(".dploystowignore"))
utils.create_file(name)
return name


@pytest.fixture(scope="function")
def source_with_dotfiles(tmpdir):
"""
a source directory to stow and unstow that contains files and folders named with prefix 'dot-'
"""
name = str(tmpdir.join("source_with_dotfiles"))
tree = [
{
name: [
{
"aaa": [
"dot-aaa",
"bbb",
{
"dot-ccc": [
"dot-aaa",
"bbb",
],
},
],
},
"dot-bbb",
],
},
]
utils.create_tree(tree)
yield name
utils.remove_tree(name)


@pytest.fixture(scope="function")
def dest_with_dotfiles(tmpdir):
"""
a destination directory to stow into or unstow from with dotfiles
"""
name = str(tmpdir.join("dest_with_dotfiles"))
utils.create_directory(name)
yield name
utils.remove_tree(name)
56 changes: 56 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for the CLI interface
"""

# pylint: disable=missing-docstring
# disable lint errors for function names longer that 30 characters
# pylint: disable=invalid-name
Expand Down Expand Up @@ -88,3 +89,58 @@ def test_cli_with_version_option(capsys):
dploy.cli.run(args)
out, _ = capsys.readouterr()
assert re.match(r"dploy \d+.\d+\.\d+(-\w+)?\n", out) is not None


def test_cli_stow_with_dotfiles_option(
source_with_dotfiles, dest_with_dotfiles, capsys
):
args = ["stow", "--dotfiles", source_with_dotfiles, dest_with_dotfiles]
dploy.cli.run(args)
assert os.readlink(os.path.join(dest_with_dotfiles, "aaa")) == os.path.join(
"..", "source_with_dotfiles", "aaa"
)
assert os.readlink(os.path.join(dest_with_dotfiles, ".bbb")) == os.path.join(
"..", "source_with_dotfiles", "dot-bbb"
)

out, _ = capsys.readouterr()
d = os.path.join(dest_with_dotfiles, "aaa")
s = os.path.relpath(os.path.join(source_with_dotfiles, "aaa"), dest_with_dotfiles)
d2 = os.path.join(dest_with_dotfiles, ".bbb")
s2 = os.path.relpath(
os.path.join(source_with_dotfiles, "dot-bbb"), dest_with_dotfiles
)
assert (
out
== "dploy stow: link {dest} => {source}\ndploy stow: link {dest2} => {source2}\n".format(
source=s, dest=d, source2=s2, dest2=d2
)
)


def test_cli_unstow_with_dotfiles_option(
source_with_dotfiles, dest_with_dotfiles, capsys
):
args = ["stow", "--dotfiles", source_with_dotfiles, dest_with_dotfiles]
dploy.cli.run(args)
args_unstow = ["unstow", "--dotfiles", source_with_dotfiles, dest_with_dotfiles]
dploy.cli.run(args_unstow)
assert not os.path.exists(os.path.join(dest_with_dotfiles, "aaa"))
assert not os.path.exists(os.path.join(dest_with_dotfiles, ".bbb"))

out, _ = capsys.readouterr()
d = os.path.join(dest_with_dotfiles, "aaa")
s = os.path.relpath(os.path.join(source_with_dotfiles, "aaa"), dest_with_dotfiles)
d2 = os.path.join(dest_with_dotfiles, ".bbb")
s2 = os.path.relpath(
os.path.join(source_with_dotfiles, "dot-bbb"), dest_with_dotfiles
)
expected_output = (
"dploy stow: link {dest} => {source}\n"
"dploy stow: link {dest2} => {source2}\n"
"dploy unstow: unlink {dest} => {source}\n"
"dploy unstow: unlink {dest2} => {source2}\n".format(
source=s, dest=d, source2=s2, dest2=d2
)
)
assert out == (expected_output)
47 changes: 47 additions & 0 deletions tests/test_stow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for the stow stub command
"""

# pylint: disable=missing-docstring
# disable lint errors for function names longer that 30 characters
# pylint: disable=invalid-name
Expand Down Expand Up @@ -238,3 +239,49 @@ def test_stow_unfolding_with_write_only_source_file(source_a, source_b, dest):
)
with pytest.raises(error.InsufficientPermissionsToSubcmdFrom):
dploy.stow([source_a, source_b], dest)


def test_stow_with_dotfiles(source_with_dotfiles, dest_with_dotfiles):
dploy.stow([source_with_dotfiles], dest_with_dotfiles, dotfiles=True)

assert os.readlink(os.path.join(dest_with_dotfiles, ".bbb")) == os.path.join(
"..", "source_with_dotfiles", "dot-bbb"
)
assert os.path.islink(os.path.join(dest_with_dotfiles, "aaa"))
assert os.readlink(os.path.join(dest_with_dotfiles, "aaa")) == os.path.join(
"..", "source_with_dotfiles", "aaa"
)
assert not os.path.islink(os.path.join(dest_with_dotfiles, "aaa", "dot-aaa"))


def test_stow_with_dot_in_exist_fold_with_dotfiles(
source_with_dotfiles, dest_with_dotfiles
):
utils.create_directory(os.path.join(dest_with_dotfiles, "aaa"))
dploy.stow([source_with_dotfiles], dest_with_dotfiles, dotfiles=True)

assert not os.path.islink(os.path.join(dest_with_dotfiles, "aaa"))
assert os.readlink(os.path.join(dest_with_dotfiles, "aaa", ".aaa")) == os.path.join(
"..", "..", "source_with_dotfiles", "aaa", "dot-aaa"
)


def test_stow_with_dot_in_exist_fold_exist_other_with_dotfiles(
source_with_dotfiles, dest_with_dotfiles
):
utils.create_directory(os.path.join(dest_with_dotfiles, "aaa"))
utils.create_file(os.path.join(dest_with_dotfiles, "aaa", ".keep"))
dploy.stow([source_with_dotfiles], dest_with_dotfiles, dotfiles=True)

assert os.readlink(os.path.join(dest_with_dotfiles, "aaa", ".aaa")) == os.path.join(
"..", "..", "source_with_dotfiles", "aaa", "dot-aaa"
)

assert os.readlink(os.path.join(dest_with_dotfiles, "aaa", ".ccc")) == os.path.join(
"..", "..", "source_with_dotfiles", "aaa", "dot-ccc"
)

assert not os.path.islink(
os.path.join(dest_with_dotfiles, "aaa", ".ccc", "dot-aaa")
)
assert not os.path.islink(os.path.join(dest_with_dotfiles, "aaa", ".ccc", "bbb"))
Loading