Implemented RsyncUrl.path_relative with tests

PEP8 fixes

Related-Bug: #1570260
Partial-Bug: #1575759
Change-Id: If3c1e5b4ad72b54f8585ab03588984fb5f8a4853
diff --git a/trsync/objects/rsync_url.py b/trsync/objects/rsync_url.py
index af15da0..5a1919a 100644
--- a/trsync/objects/rsync_url.py
+++ b/trsync/objects/rsync_url.py
@@ -1,5 +1,19 @@
 # -*- coding: utf-8 -*-
 
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
 import os
 import re
 
@@ -63,7 +77,7 @@
             'rsync2': ('{protocol}://', '{user}@', '{host}', ':{port}',
                        '/{module}', '/{path}'),
             # local/path/to/directory
-            'path': ('{path}', ),
+            'path': ('{path}/', ),
         }
 
         self._match = self._get_matching_pattern()
@@ -191,7 +205,7 @@
             # local/path/to/directory
             'path': ('{rootpath}', ),
         }
-        return self.by_template(templates[self.url_type])
+        return self._by_template(templates[self.url_type])
 
     @property
     def netloc(self):
@@ -207,7 +221,7 @@
             # local/path/to/directory
             'path': ('', ),
         }
-        return self.by_template(templates[self.url_type])
+        return self._by_template(templates[self.url_type])
 
     @property
     def parsed_url(self):
@@ -223,7 +237,7 @@
                         parsed_dict[part] = value
         return parsed_dict
 
-    def by_template(self, template_list):
+    def _by_template(self, template_list):
         template = ''
         for part in ('protocol', 'user', 'host', 'port', 'module', 'path',
                      'rootpath'):
@@ -248,7 +262,7 @@
         return True
 
     def _fn_join(self, *parts):
-        ''' Joins filenames with ignoring empty parts (None, '', etc)'''
+        '''Joins filenames with ignoring empty parts (None, '', etc)'''
 
         parts = [_ for _ in parts if _]
 
@@ -280,7 +294,7 @@
         return self._fn_join(*parts)
 
     def urljoin(self, *parts):
-        return self.join(self.by_template(self.templates[self.url_type]),
+        return self.join(self._by_template(self.templates[self.url_type]),
                          *parts)
 
     def a_dir(self, *path):
@@ -290,7 +304,7 @@
         return result
 
     def url_dir(self, *path):
-        return self.a_dir(self.by_template(self.templates[self.url_type]),
+        return self.a_dir(self._by_template(self.templates[self.url_type]),
                           *path)
 
     def a_file(self, *path):
@@ -301,5 +315,48 @@
         return result
 
     def url_file(self, *path):
-        return self.a_file(self.by_template(self.templates[self.url_type]),
+        return self.a_file(self._by_template(self.templates[self.url_type]),
                            *path)
+
+    def _split_path(self, path):
+        '''Returns list of path's parts, starting from '/' for absolute path'''
+        result = list()
+        if path != '/':
+            while path.endswith('/'):
+                path = path[:-1]
+        while True:
+            path, second = os.path.split(path)
+            if second:
+                result.insert(0, second)
+            else:
+                if path:
+                    result.insert(0, path)
+                break
+        return result
+
+    def path_relative(self, path, relative=None):
+        '''Returns path evaluated as "path" relative "relative"
+
+        (relative self.path by default)
+        '''
+        if relative is None:
+            relative = self.path
+        path_dir = self._split_path(path)
+        relative_dir = self._split_path(relative)
+        if path.startswith('/'):
+            # path is absolute
+            return path
+        elif relative.startswith('/'):
+            # path relative absolute path
+            return '/' + '/'.join(relative_dir[1:] + path_dir)
+        else:
+            # path relative
+            common_index = 0
+            for i in xrange(min(len(path_dir), len(relative_dir))):
+                if path_dir[i] == relative_dir[i]:
+                    common_index += 1
+                else:
+                    break
+            updir_number = len(relative_dir[common_index:][:])
+            return '/'.join(['..' for _ in xrange(updir_number)] +
+                            path_dir[common_index:])
diff --git a/trsync/tests/test_rsync_url.py b/trsync/tests/test_rsync_url.py
index 751ab77..e795ae3 100644
--- a/trsync/tests/test_rsync_url.py
+++ b/trsync/tests/test_rsync_url.py
@@ -1,5 +1,19 @@
 # -*- coding: utf-8 -*-
 
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
 import os
 import unittest
 import yaml
@@ -130,6 +144,23 @@
             logger.info('par = "{}", er = "{}"'.format(par, er))
             self.assertEqual(url.url_file(par), er)
 
+    def split_path(self, remote, expected_result):
+        logger.info('For "{}" should be {}'.format(remote, expected_result))
+        url = rsync_url.RsyncUrl(remote)
+        self.log_locals(url)
+        self.assertEqual(url._split_path(remote), expected_result)
+
+    def path_relative(self, remote, expected_result):
+        logger.info('For "{}" should be {}'.format(remote, expected_result))
+        url = rsync_url.RsyncUrl(remote)
+        self.log_locals(url)
+        for par, er in expected_result.items():
+            logger.info('Test parameters\n%s',
+                        yaml.dump({remote: expected_result},
+                                  default_flow_style=False))
+            self.assertEqual(url.path_relative(par), er)
+
+
 cpath, cname = os.path.split(os.path.realpath(os.path.realpath(__file__)))
 cname = cname.split('.')
 cname[-1] = 'yaml'
diff --git a/trsync/tests/test_rsync_url.yaml b/trsync/tests/test_rsync_url.yaml
index 496f9bd..830becb 100644
--- a/trsync/tests/test_rsync_url.yaml
+++ b/trsync/tests/test_rsync_url.yaml
@@ -638,6 +638,8 @@
     - null
     - '/'
   valid: True
+  split_path:
+    - '/'
 
 
 'dir':
@@ -711,3 +713,142 @@
   exact_match_num: 1
   classed: 'rsync2'
   valid: True
+
+
+'/path/to/some/file/in/some/directory':
+  classed: 'path'
+  split_path:
+    - '/'
+    - 'path'
+    - 'to'
+    - 'some'
+    - 'file'
+    - 'in'
+    - 'some'
+    - 'directory'
+
+
+'path/to/some/file/in/some/directory':
+  classed: 'path'
+  split_path:
+    - 'path'
+    - 'to'
+    - 'some'
+    - 'file'
+    - 'in'
+    - 'some'
+    - 'directory'
+
+
+'/path/to/some/file/in/some/directory/':
+  classed: 'path'
+  split_path:
+    - '/'
+    - 'path'
+    - 'to'
+    - 'some'
+    - 'file'
+    - 'in'
+    - 'some'
+    - 'directory'
+
+
+'path/to/some/file/in/some/directory/':
+  classed: 'path'
+  split_path:
+    - 'path'
+    - 'to'
+    - 'some'
+    - 'file'
+    - 'in'
+    - 'some'
+    - 'directory'
+
+
+'1/2/3/4/5':
+  classed: 'path'
+  path_relative:
+    '1/2/6/7/8':
+        '../../../6/7/8'
+    '1/2/6/7/8/9':
+        '../../../6/7/8/9'
+    '1/2/3/6/7/8/9':
+        '../../6/7/8/9'
+    '1/2':
+        '../../..'
+    '1/2/3':
+        '../..'
+    '':
+        '../../../../..'
+    '1/2/3/4/5/6/7':
+        '6/7'
+
+'1':
+  classed: 'path'
+  path_relative:
+    '1/2':
+        '2'
+    '1/2/3':
+        '2/3'
+
+'dir/to/snapshots/repo-timestamp':
+  classed: 'path'
+  path_relative:
+    'dir/to/snapshots/repo-latest':
+        '../repo-latest'
+    '/dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/repo-latest'
+
+
+'/dir/to/snapshots/repo-timestamp':
+  classed: 'path'
+  path_relative:
+    'dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/repo-timestamp/dir/to/snapshots/repo-latest'
+    '/dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/repo-latest'
+
+
+'dir/to/snapshots':
+  classed: 'path'
+  path_relative:
+    'dir/to/snapshots/repo-latest':
+        'repo-latest'
+    '/dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/repo-latest'
+
+
+'dir/to/snapshots/':
+  classed: 'path'
+  path_relative:
+    'dir/to/snapshots/repo-latest':
+        'repo-latest'
+    '/dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/repo-latest'
+
+
+'/dir/to/snapshots':
+  classed: 'path'
+  path_relative:
+    'dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/dir/to/snapshots/repo-latest'
+    '/dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/repo-latest'
+
+
+'/dir/to/snapshots/':
+  classed: 'path'
+  path_relative:
+    'dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/dir/to/snapshots/repo-latest'
+    '/dir/to/snapshots/repo-latest':
+        '/dir/to/snapshots/repo-latest'
+
+
+'snapshots/repo-timestamp':
+  classed: 'path'
+  path_relative:
+    'snapshots/repo-latest':
+        '../repo-latest'
+    'repo-latest':
+        '../../repo-latest'