Browse files

Merge branch '860-int' into 1.6

  • Loading branch information...
2 parents 434fa9e + b974507 commit f95581147480ced374292516ee123b2225d8b011 @bitprophet bitprophet committed Mar 19, 2013
Showing with 97 additions and 21 deletions.
  1. +10 −1 .travis.yml
  2. +4 −0 docs/changelog.rst
  3. +11 −7 fabric/contrib/files.py
  4. +10 −3 fabric/contrib/project.py
  5. +10 −9 fabric/tasks.py
  6. +51 −0 integration/test_contrib.py
  7. +1 −1 tests/test_contrib.py
View
11 .travis.yml
@@ -12,7 +12,16 @@ install:
- "if [[ $TRAVIS_PYTHON_VERSION == '2.5' ]]; then pip install multiprocessing; fi"
# Deal with issue on Travis builders re: multiprocessing.Queue :(
- "sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm"
-script: fab test
+before_script:
+ # Allow us to SSH passwordless to localhost
+ - ssh-keygen -f ~/.ssh/id_rsa -N ""
+ - cp ~/.ssh/{id_rsa.pub,authorized_keys}
+ - "netstat -tan | grep LISTEN"
+script:
+ # Normal tests
+ #- fab test
+ # Integration tests
+ - fab test:"--tests\=integration"
notifications:
irc:
channels: "irc.freenode.org#fabric"
View
4 docs/changelog.rst
@@ -25,6 +25,10 @@ would have also been included in the 1.2 line.
Changelog
=========
+* :bug:`367` Expand paths with tilde inside (``contrib.files``).
+* :feature:`845` Downstream synchronization option implemented for
+ `~fabric.contrib.project.rsync_project`. Thanks to Antonio Barrero for the
+ patch.
* :release:`1.6.0 <2013-03-01>`
* :release:`1.5.4 <2013-03-01>`
* :bug:`844` Account for SSH config overhaul in Paramiko 1.10 by e.g. updating
View
18 fabric/contrib/files.py
@@ -25,7 +25,7 @@ def exists(path, use_sudo=False, verbose=False):
behavior.
"""
func = use_sudo and sudo or run
- cmd = 'test -e "$(echo %s)"' % path
+ cmd = 'test -e %s' % _expand_path(path)
# If verbose, run normally
if verbose:
with settings(warn_only=True):
@@ -81,7 +81,7 @@ def upload_template(filename, destination, context=None, use_jinja=False,
func = use_sudo and sudo or run
# Normalize destination to be an actual filename, due to using StringIO
with settings(hide('everything'), warn_only=True):
- if func('test -d %s' % destination).succeeded:
+ if func('test -d %s' % _expand_path(destination)).succeeded:
sep = "" if destination.endswith('/') else "/"
destination += sep + os.path.basename(filename)
@@ -105,14 +105,14 @@ def upload_template(filename, destination, context=None, use_jinja=False,
tb = traceback.format_exc()
abort(tb + "\nUnable to import Jinja2 -- see above.")
else:
- with open(filename) as inputfile:
+ with open(os.path.expanduser(filename)) as inputfile:
text = inputfile.read()
if context:
text = text % context
# Back up original file
if backup and exists(destination):
- func("cp %s{,.bak}" % destination)
+ func("cp %s{,.bak}" % _expand_path(destination))
# Upload the file.
return put(
@@ -178,6 +178,7 @@ def sed(filename, before, after, limit='', use_sudo=False, backup='.bak',
hasher.update(env.host_string)
hasher.update(filename)
tmp = "/tmp/%s" % hasher.hexdigest()
+ filename = _expand_path(filename)
# Use temp file to work around lack of -i
expr = r"""cp -p %(filename)s %(tmp)s \
&& sed -r -e '%(limit)ss/%(before)s/%(after)s/%(flags)sg' %(filename)s > %(tmp)s \
@@ -186,7 +187,7 @@ def sed(filename, before, after, limit='', use_sudo=False, backup='.bak',
command = expr % locals()
else:
expr = r"sed -i%s -r -e '%ss/%s/%s/%sg' %s"
- command = expr % (backup, limit, before, after, flags, filename)
+ command = expr % (backup, limit, before, after, flags, _expand_path(filename))
return func(command, shell=shell)
@@ -314,7 +315,7 @@ def contains(filename, text, exact=False, use_sudo=False, escape=True,
if exact:
text = "^%s$" % text
with settings(hide('everything'), warn_only=True):
- egrep_cmd = 'egrep "%s" "%s"' % (text, filename)
+ egrep_cmd = 'egrep "%s" %s' % (text, _expand_path(filename))
return func(egrep_cmd, shell=shell).succeeded
@@ -367,7 +368,7 @@ def append(filename, text, use_sudo=False, partial=False, escape=True,
shell=shell)):
continue
line = line.replace("'", r"'\\''") if escape else line
- func("echo '%s' >> %s" % (line, filename))
+ func("echo '%s' >> %s" % (line, _expand_path(filename)))
def _escape_for_regex(text):
"""Escape ``text`` to allow literal matching using egrep"""
@@ -379,3 +380,6 @@ def _escape_for_regex(text):
# Whereas single quotes should not be escaped
regex = regex.replace(r"\'", "'")
return regex
+
+def _expand_path(path):
+ return '"$(echo %s)"' % path
View
13 fabric/contrib/project.py
@@ -17,7 +17,7 @@
@needs_host
def rsync_project(remote_dir, local_dir=None, exclude=(), delete=False,
- extra_opts='', ssh_opts='', capture=False):
+ extra_opts='', ssh_opts='', capture=False, upload=True):
"""
Synchronize a remote directory with the current project directory via rsync.
@@ -68,6 +68,8 @@ def rsync_project(remote_dir, local_dir=None, exclude=(), delete=False,
* ``ssh_opts``: Like ``extra_opts`` but specifically for the SSH options
string (rsync's ``--rsh`` flag.)
* ``capture``: Sent directly into an inner `~fabric.operations.local` call.
+ * ``upload``: a boolean controlling whether file synchronization is
+ performed up or downstream. Upstream by default.
Furthermore, this function transparently honors Fabric's port and SSH key
settings. Calling this function when the current host string contains a
@@ -120,9 +122,14 @@ def rsync_project(remote_dir, local_dir=None, exclude=(), delete=False,
if host.count(':') > 1:
# Square brackets are mandatory for IPv6 rsync address,
# even if port number is not specified
- cmd = "rsync %s %s [%s@%s]:%s" % (options, local_dir, user, host, remote_dir)
+ remote_prefix = "[%s@%s]" % (user, host)
else:
- cmd = "rsync %s %s %s@%s:%s" % (options, local_dir, user, host, remote_dir)
+ remote_prefix = "%s@%s" % (user, host)
+ if upload:
+ cmd = "rsync %s %s %s:%s" % (options, local_dir, remote_prefix, remote_dir)
+ else:
+ cmd = "rsync %s %s:%s %s" % (options, remote_prefix, remote_dir, local_dir)
+
if output.running:
print("[%s] rsync_project: %s" % (env.host_string, cmd))
return local(cmd, capture=capture)
View
19 fabric/tasks.py
@@ -240,15 +240,16 @@ def execute(task, *args, **kwargs):
kwarg1='value')`` will (once per host) invoke ``mytask('arg1',
kwarg1='value')``.
- This function returns a dictionary mapping host strings to the given task's
- return value for that host's execution run. For example, ``execute(foo,
- hosts=['a', 'b'])`` might return ``{'a': None, 'b': 'bar'}`` if ``foo``
- returned nothing on host `a` but returned ``'bar'`` on host `b`.
-
- In situations where a task execution fails for a given host but overall
- progress does not abort (such as when :ref:`env.skip_bad_hosts
- <skip-bad-hosts>` is True) the return value for that host will be the error
- object or message.
+ :returns:
+ a dictionary mapping host strings to the given task's return value for
+ that host's execution run. For example, ``execute(foo, hosts=['a',
+ 'b'])`` might return ``{'a': None, 'b': 'bar'}`` if ``foo`` returned
+ nothing on host `a` but returned ``'bar'`` on host `b`.
+
+ In situations where a task execution fails for a given host but overall
+ progress does not abort (such as when :ref:`env.skip_bad_hosts
+ <skip-bad-hosts>` is True) the return value for that host will be the
+ error object or message.
.. seealso::
:ref:`The execute usage docs <execute>`, for an expanded explanation
View
51 integration/test_contrib.py
@@ -0,0 +1,51 @@
+import types
+
+from fabric.api import env, run, local
+from fabric.contrib import files
+
+
+class Integration(object):
+ def setup(self):
+ env.host_string = "127.0.0.1"
+
+
+def tildify(path):
+ home = run("echo ~", quiet=True).stdout.strip()
+ return path.replace('~', home)
+
+def expect(path):
+ assert files.exists(tildify(path))
+
+def expect_contains(path, value):
+ assert files.contains(tildify(path), value)
+
+def escape(path):
+ return path.replace(' ', r'\ ')
+
+
+class TestTildeExpansion(Integration):
+ def test_append(self):
+ for target in ('~/append_test', '~/append_test with spaces'):
+ files.append(target, ['line'])
+ expect(target)
+
+ def test_exists(self):
+ for target in ('~/exists_test', '~/exists test with space'):
+ run("touch %s" % escape(target))
+ expect(target)
+
+ def test_sed(self):
+ for target in ('~/sed_test', '~/sed test with space'):
+ run("echo 'before' > %s" % escape(target))
+ files.sed(target, 'before', 'after')
+ expect_contains(target, 'after')
+
+ def test_upload_template(self):
+ for i, target in enumerate((
+ '~/upload_template_test',
+ '~/upload template test with space'
+ )):
+ src = "source%s" % i
+ local("touch %s" % src)
+ files.upload_template(src, target)
+ expect(target)
View
2 tests/test_contrib.py
@@ -11,7 +11,7 @@ class TestContrib(FabricTest):
# Make sure it knows / is a directory.
# This is in lieu of starting down the "actual honest to god fake operating
# system" road...:(
- @server(responses={'test -d /': ""})
+ @server(responses={'test -d "$(echo /)"': ""})
def test_upload_template_uses_correct_remote_filename(self):
"""
upload_template() shouldn't munge final remote filename

0 comments on commit f955811

Please sign in to comment.