diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 7dc04cb..ca26baf 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -51,7 +51,7 @@ def cmd_install(o): totemp=tmpdir, showdiff=o.install_showdiff, backup_suffix=o.install_backup_suffix) - installed = [] + installed = 0 for dotfile in dotfiles: preactions = [] if not o.install_temporary and dotfile.actions \ @@ -73,13 +73,13 @@ def cmd_install(o): if not tmp: continue src = tmp - r = inst.install(t, src, dotfile.dst, actions=preactions, - noempty=dotfile.noempty) + r, err = inst.install(t, src, dotfile.dst, actions=preactions, + noempty=dotfile.noempty) if tmp: tmp = os.path.join(o.dotpath, tmp) if os.path.exists(tmp): remove(tmp) - if len(r) > 0: + if r: if not o.install_temporary and \ Cfg.key_actions_post in dotfile.actions: actions = dotfile.actions[Cfg.key_actions_post] @@ -91,10 +91,12 @@ def cmd_install(o): if o.debug: LOG.dbg('executing post action {}'.format(action)) action.execute() - installed.extend(r) + installed += 1 + elif not r and err: + LOG.err('installing \"{}\" failed: {}'.format(dotfile.key, err)) if o.install_temporary: - LOG.log('\nInstalled to tmp \"{}\".'.format(tmpdir)) - LOG.log('\n{} dotfile(s) installed.'.format(len(installed))) + LOG.log('\ninstalled to tmp \"{}\".'.format(tmpdir)) + LOG.log('\n{} dotfile(s) installed.'.format(installed)) return True diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 4d371a4..2bdb38f 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -50,21 +50,27 @@ class Installer: self.log = Logger() def install(self, templater, src, dst, actions=[], noempty=False): - """install the src to dst using a template""" + """ + install src to dst using a template + return + - True, None: success + - False, error_msg: error + - False, None, ignored + """ if self.debug: self.log.dbg('install {} to {}'.format(src, dst)) self.action_executed = False src = os.path.join(self.base, os.path.expanduser(src)) if not os.path.exists(src): - self.log.err('source dotfile does not exist: {}'.format(src)) - return [] + err = 'source dotfile does not exist: {}'.format(src) + return False, err dst = os.path.expanduser(dst) if self.totemp: dst = self._pivot_path(dst, self.totemp) if utils.samefile(src, dst): # symlink loop - self.log.err('dotfile points to itself: {}'.format(dst)) - return [] + err = 'dotfile points to itself: {}'.format(dst) + return False, err isdir = os.path.isdir(src) if self.debug: self.log.dbg('install {} to {}'.format(src, dst)) @@ -76,15 +82,21 @@ class Installer: actions=actions, noempty=noempty) def link(self, templater, src, dst, actions=[]): - """set src as the link target of dst""" + """ + set src as the link target of dst + return + - True, None: success + - False, error_msg: error + - False, None, ignored + """ if self.debug: self.log.dbg('link {} to {}'.format(src, dst)) self.action_executed = False src = os.path.normpath(os.path.join(self.base, os.path.expanduser(src))) if not os.path.exists(src): - self.log.err('source dotfile does not exist: {}'.format(src)) - return [] + err = 'source dotfile does not exist: {}'.format(src) + return False, err dst = os.path.normpath(os.path.expanduser(dst)) if self.totemp: # ignore actions @@ -95,14 +107,20 @@ class Installer: self.log.dbg('dotfile is a template') self.log.dbg('install to {} and symlink'.format(self.workdir)) tmp = self._pivot_path(dst, self.workdir, striphome=True) - i = self.install(templater, src, tmp, actions=actions) + i, err = self.install(templater, src, tmp, actions=actions) if not i and not os.path.exists(tmp): - return [] + return i, err src = tmp return self._link(src, dst, actions=actions) def link_children(self, templater, src, dst, actions=[]): - """link all dotfiles in a given directory""" + """ + link all dotfiles in a given directory + return + - True, None: success + - False, error_msg: error + - False, None, ignored + """ if self.debug: self.log.dbg('link_children {} to {}'.format(src, dst)) self.action_executed = False @@ -110,17 +128,16 @@ class Installer: # Fail if source doesn't exist if not os.path.exists(parent): - self.log.err('source dotfile does not exist: {}'.format(parent)) - return [] + err = 'source dotfile does not exist: {}'.format(parent) + return False, err # Fail if source not a directory if not os.path.isdir(parent): if self.debug: self.log.dbg('symlink children of {} to {}'.format(src, dst)) - self.log.err('source dotfile is not a directory: {}' - .format(parent)) - return [] + err = 'source dotfile is not a directory: {}'.format(parent) + return False, err dst = os.path.normpath(os.path.expanduser(dst)) if not os.path.lexists(dst): @@ -134,9 +151,8 @@ class Installer: ]).format(dst) if self.safe and not self.log.ask(msg): - msg = 'ignoring "{}", nothing installed' - self.log.warn(msg.format(dst)) - return [] + err = 'ignoring "{}", nothing installed'.format(dst) + return False, err os.unlink(dst) os.mkdir(dst) @@ -171,54 +187,52 @@ class Installer: if len(result): actions = [] - return (src, dst) + return True, None def _link(self, src, dst, actions=[]): """set src as a link target of dst""" overwrite = not self.safe if os.path.lexists(dst): if os.path.realpath(dst) == os.path.realpath(src): - if self.debug: - self.log.dbg('ignoring "{}", link exists'.format(dst)) - return [] + err = 'ignoring "{}", link exists'.format(dst) + return False, err if self.dry: self.log.dry('would remove {} and link to {}'.format(dst, src)) - return [] + return True, None msg = 'Remove "{}" for link creation?'.format(dst) if self.safe and not self.log.ask(msg): - msg = 'ignoring "{}", link was not created' - self.log.warn(msg.format(dst)) - return [] + err = 'ignoring "{}", link was not created'.format(dst) + return False, err overwrite = True try: utils.remove(dst) except OSError as e: - self.log.err('something went wrong with {}: {}'.format(src, e)) - return [] + err = 'something went wrong with {}: {}'.format(src, e) + return False, err if self.dry: self.log.dry('would link {} to {}'.format(dst, src)) - return [] + return True, None base = os.path.dirname(dst) if not self._create_dirs(base): - self.log.err('creating directory for {}'.format(dst)) - return [] - if not self._exec_pre_actions(actions): - return [] + err = 'creating directory for {}'.format(dst) + return False, err + r, e = self._exec_pre_actions(actions) + if not r: + return False, e # re-check in case action created the file if os.path.lexists(dst): msg = 'Remove "{}" for link creation?'.format(dst) if self.safe and not overwrite and not self.log.ask(msg): - msg = 'ignoring "{}", link was not created' - self.log.warn(msg.format(dst)) - return [] + err = 'ignoring "{}", link was not created'.format(dst) + return False, err try: utils.remove(dst) except OSError as e: - self.log.err('something went wrong with {}: {}'.format(src, e)) - return [] + err = 'something went wrong with {}: {}'.format(src, e) + return False, err os.symlink(src, dst) self.log.sub('linked {} to {}'.format(dst, src)) - return [(src, dst)] + return True, None def _handle_file(self, templater, src, dst, actions=[], noempty=False): """install src to dst when is a file""" @@ -227,52 +241,61 @@ class Installer: self.log.dbg('ignore empty: {}'.format(noempty)) if utils.samefile(src, dst): # symlink loop - self.log.err('dotfile points to itself: {}'.format(dst)) - return [] + err = 'dotfile points to itself: {}'.format(dst) + return False, err content = templater.generate(src) if noempty and utils.content_empty(content): - self.log.warn('ignoring empty template: {}'.format(src)) - return [] + self.log.dbg('ignoring empty template: {}'.format(src)) + return False, None if content is None: - self.log.err('generate from template {}'.format(src)) - return [] + err = 'empty template {}'.format(src) + return False, err if not os.path.exists(src): - self.log.err('source dotfile does not exist: {}'.format(src)) - return [] + err = 'source dotfile does not exist: {}'.format(src) + return False, err st = os.stat(src) - ret = self._write(src, dst, content, st.st_mode, actions=actions) + ret, err = self._write(src, dst, content, st.st_mode, actions=actions) if ret < 0: - self.log.err('installing {} to {}'.format(src, dst)) - return [] + return False, err if ret > 0: if self.debug: self.log.dbg('ignoring {}'.format(dst)) - return [] + return False, None if ret == 0: if not self.dry and not self.comparing: self.log.sub('copied {} to {}'.format(src, dst)) - return [(src, dst)] - return [] + return True, None + err = 'installing {} to {}'.format(src, dst) + return False, err def _handle_dir(self, templater, src, dst, actions=[], noempty=False): """install src to dst when is a directory""" if self.debug: self.log.dbg('install dir {}'.format(src)) self.log.dbg('ignore empty: {}'.format(noempty)) - ret = [] + ret = True, None if not self._create_dirs(dst): - return [] + err = 'creating directory for {}'.format(dst) + return False, err # handle all files in dir for entry in os.listdir(src): f = os.path.join(src, entry) if not os.path.isdir(f): - res = self._handle_file(templater, f, os.path.join(dst, entry), - actions=actions, noempty=noempty) - ret.extend(res) + res, err = self._handle_file(templater, f, + os.path.join(dst, entry), + actions=actions, + noempty=noempty) + if not res: + ret = res, err + break else: - res = self._handle_dir(templater, f, os.path.join(dst, entry), - actions=actions, noempty=noempty) - ret.extend(res) + res, err = self._handle_dir(templater, f, + os.path.join(dst, entry), + actions=actions, + noempty=noempty) + if not res: + ret = res, err + break return ret def _fake_diff(self, dst, content): @@ -284,13 +307,13 @@ class Installer: def _write(self, src, dst, content, rights, actions=[]): """write content to file - return 0 for success, - 1 when already exists - -1 when error""" + return 0, None: for success, + 1, None: when already exists + -1, err: when error""" overwrite = not self.safe if self.dry: self.log.dry('would install {}'.format(dst)) - return 0 + return 0, None if os.path.lexists(dst): samerights = False try: @@ -298,12 +321,12 @@ class Installer: except OSError as e: if e.errno == errno.ENOENT: # broken symlink - self.log.err('broken symlink {}'.format(dst)) - return -1 + err = 'broken symlink {}'.format(dst) + return -1, err if self.diff and self._fake_diff(dst, content) and samerights: if self.debug: self.log.dbg('{} is the same'.format(dst)) - return 1 + return 1, None if self.safe: if self.debug: self.log.dbg('change detected for {}'.format(dst)) @@ -311,32 +334,33 @@ class Installer: self._diff_before_write(src, dst, content) if not self.log.ask('Overwrite \"{}\"'.format(dst)): self.log.warn('ignoring {}'.format(dst)) - return 1 + return 1, None overwrite = True if self.backup and os.path.lexists(dst): self._backup(dst) base = os.path.dirname(dst) if not self._create_dirs(base): - self.log.err('creating directory for {}'.format(dst)) - return -1 - if not self._exec_pre_actions(actions): - return -1 + err = 'creating directory for {}'.format(dst) + return -1, err + r, e = self._exec_pre_actions(actions) + if not r: + return -1, e if self.debug: self.log.dbg('write content to {}'.format(dst)) # re-check in case action created the file if self.safe and not overwrite and os.path.lexists(dst): if not self.log.ask('Overwrite \"{}\"'.format(dst)): self.log.warn('ignoring {}'.format(dst)) - return 1 + return 1, None # write the file try: with open(dst, 'wb') as f: f.write(content) except NotADirectoryError as e: - self.log.err('opening dest file: {}'.format(e)) - return -1 + err = 'opening dest file: {}'.format(e) + return -1, err os.chmod(dst, rights) - return 0 + return 0, None def _diff_before_write(self, src, dst, src_content): """diff before writing when using --showdiff - not efficient""" @@ -355,7 +379,7 @@ class Installer: def _create_dirs(self, directory): """mkdir -p """ if not self.create and not os.path.exists(directory): - return False + return False, if os.path.exists(directory): return True if self.dry: @@ -390,7 +414,7 @@ class Installer: def _exec_pre_actions(self, actions): """execute pre-actions if any""" if self.action_executed: - return True + return True, None for action in actions: if self.dry: self.log.dry('would execute action: {}'.format(action)) @@ -398,10 +422,10 @@ class Installer: if self.debug: self.log.dbg('executing pre action {}'.format(action)) if not action.execute(): - self.log.err('pre-action {} failed'.format(action.key)) - return False + err = 'pre-action \"{}\" failed'.format(action.key) + return False, err self.action_executed = True - return True + return True, None def _install_to_temp(self, templater, src, dst, tmpdir): """install a dotfile to a tempdir""" diff --git a/tests/test_install.py b/tests/test_install.py index d844322..0eb2972 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -266,12 +266,12 @@ exec bspwm logger = MagicMock() installer.log.err = logger - res = installer.link_children(templater=MagicMock(), src=src, - dst='/dev/null', actions=[]) + res, err = installer.link_children(templater=MagicMock(), src=src, + dst='/dev/null', actions=[]) - self.assertEqual(res, []) - logger.assert_called_with('source dotfile does not exist: {}' - .format(src)) + self.assertFalse(res) + e = 'source dotfile does not exist: {}'.format(src) + self.assertEqual(err, e) def test_fails_when_src_file(self): """test fails when src file""" @@ -288,14 +288,13 @@ exec bspwm installer.log.err = logger # pass src file not src dir - res = installer.link_children(templater=templater, src=src, - dst='/dev/null', actions=[]) + res, err = installer.link_children(templater=templater, src=src, + dst='/dev/null', actions=[]) # ensure nothing performed - self.assertEqual(res, []) - # ensure logger logged error - logger.assert_called_with('source dotfile is not a directory: {}' - .format(src)) + self.assertFalse(res) + e = 'source dotfile is not a directory: {}'.format(src) + self.assertEqual(err, e) def test_creates_dst(self): """test creates dst"""