Repository: a2i2/surround

Issue:
Should be able to use surround from any subdir of a project (like git)
```
$ cd tests
$ surround list remotename
error: project name not present in config
```

It should be able to walk toward the root of the drive looking for the .surround folder before executing the command

Discussion:

Patch:
diff --git a/surround/cli.py b/surround/cli.py
index 62996a6..5090537 100644
--- a/surround/cli.py
+++ b/surround/cli.py
@@ -175,30 +175,16 @@ def parse_lint_args(args):
 def parse_run_args(args):
     logging.getLogger().setLevel(logging.INFO)
 
-    deploy = {
-        "new": {
-            "dirs": [
-                "models",
-                "{project_name}",
-            ],
-            "files" : [
-                ("{project_name}/config.yaml", "output:\n  text: Hello World"),
-                ("dodo.py", "")
-            ],
-            "templates" : []
-        }
-    }
-
-    errors, warnings = Linter().check_project(deploy, args.path)
-    if errors:
-        print("Invalid Surround project")
-    for e in errors + warnings:
-        print(e)
-    if not errors:
+    if remote_cli.is_surround_project():
+        actual_current_dir = os.getcwd()
+        os.chdir(remote_cli.get_project_root_from_current_dir())
         if args.web:
             run_as_web()
         else:
             run_locally(args)
+        os.chdir(actual_current_dir)
+    else:
+        print("error: not a surround project")
 
 def run_locally(args):
     if args.task:
@@ -207,13 +193,13 @@ def run_locally(args):
         task = 'list'
 
     print("Project tasks:")
-    run_process = subprocess.Popen(['python3', '-m', 'doit', task], cwd=args.path)
+    run_process = subprocess.Popen(['python3', '-m', 'doit', task])
     run_process.wait()
 
 def run_as_web():
     obj = None
     loaded_class = None
-    project_root = get_project_root(os.getcwd())
+    project_root = remote_cli.get_project_root_from_current_dir()
     if project_root is not None:
         path_to_modules = os.path.join(project_root, os.path.basename(project_root))
         path_to_config = os.path.join(path_to_modules, "config.yaml")
@@ -244,6 +230,8 @@ def run_as_web():
         if obj is None:
             print("error: cannot load " + class_name + " from " + module_name)
             return
+    else:
+        print("error: not a surround project")
 
     api.make_app(obj).listen(8888)
     print(os.path.basename(os.getcwd()) + " is running on http://localhost:8888")
@@ -297,19 +285,6 @@ def parse_tool_args(parsed_args, remote_parser, tool):
     else:
         parse_init_args(parsed_args)
 
-def get_project_root(current_directory):
-    home = str(Path.home())
-
-    while True:
-        list_ = os.listdir(current_directory)
-        parent_directory = os.path.dirname(current_directory)
-        if current_directory in (home, parent_directory):
-            print("Not a surround project")
-            break
-        elif ".surround" in list_:
-            return current_directory
-        current_directory = parent_directory
-
 def main():
 
     parser = argparse.ArgumentParser(prog='surround', description="The Surround Command Line Interface")
@@ -322,7 +297,6 @@ def main():
 
     run_parser = sub_parser.add_parser('run', help="Run a Surround project task, witout an argument all tasks will be shown")
     run_parser.add_argument('task', help="Task defined in a Surround project dodo.py file.", nargs='?')
-    run_parser.add_argument('path', type=lambda x: is_valid_dir(parser, x), help="Path to a Surround project", nargs='?', default="./")
     run_parser.add_argument('-w', '--web', help="Name of the class inherited from Wrapper", action='store_true')
 
     linter_parser = sub_parser.add_parser('lint', help="Run the Surround linter")
diff --git a/surround/remote/cli.py b/surround/remote/cli.py
index 88e1858..90ec86b 100644
--- a/surround/remote/cli.py
+++ b/surround/remote/cli.py
@@ -11,11 +11,28 @@ BASE_REMOTE = base.BaseRemote()
 LOCAL = local.Local()
 
 def is_surround_project():
-    """Whether inside surround project root directory
+    """Whether inside surround project
     Check for the .surround folder
     """
-    file_ = Path(".surround/config.yaml")
-    return file_.exists()
+    dir_ = get_project_root_from_current_dir()
+    if dir_ is None:
+        return False
+    return True
+
+def get_project_root_from_current_dir():
+    return get_project_root(os.getcwd())
+
+def get_project_root(current_directory):
+    home = str(Path.home())
+
+    while True:
+        list_ = os.listdir(current_directory)
+        parent_directory = os.path.dirname(current_directory)
+        if current_directory in (home, parent_directory):
+            break
+        elif ".surround" in list_:
+            return current_directory
+        current_directory = parent_directory
 
 def add_remote_parser(sub_parser):
     remote_parser = sub_parser.add_parser('remote', help="Initialise a new remote")
@@ -74,10 +91,12 @@ def add_remote(remote_parser, parsed_args, type_):
             write_remote_config(parsed_args, remote_parser, os.path.join(home, ".surround/config.yaml"), type_)
         else:
             if is_surround_project():
+                actual_current_dir = os.getcwd()
+                os.chdir(get_project_root_from_current_dir())
                 write_remote_config(parsed_args, remote_parser, ".surround/config.yaml", type_)
+                os.chdir(actual_current_dir)
             else:
                 print("error: not a surround project")
-                print("error: goto project root directory")
 
 def print_remote_info(parsed_args, remotes):
     verbose = parsed_args.verbose
@@ -112,24 +131,30 @@ def parse_remote_args(remote_parser, parsed_args):
             print_remote_info(parsed_args, remotes)
         else:
             if is_surround_project():
+                actual_current_dir = os.getcwd()
+                os.chdir(get_project_root_from_current_dir())
                 remotes = BASE_REMOTE.read_all_from_local_config("remote")
                 print_remote_info(parsed_args, remotes)
+                os.chdir(actual_current_dir)
             else:
                 print("error: not a surround project")
-                print("error: goto project root directory")
 
 def parse_add_args(parsed_args):
     if is_surround_project():
+        actual_current_dir = os.getcwd()
+        os.chdir(get_project_root_from_current_dir())
         remote = parsed_args.remote
         file_to_add = parsed_args.key
         message = BASE_REMOTE.add(remote, file_to_add)
         print(message)
+        os.chdir(actual_current_dir)
     else:
         print("error: not a surround project")
-        print("error: goto project root directory")
 
 def parse_pull_args(parsed_args):
     if is_surround_project():
+        actual_current_dir = os.getcwd()
+        os.chdir(get_project_root_from_current_dir())
         path_to_remote = BASE_REMOTE.get_path_to_remote(parsed_args.remote)
         if path_to_remote is None:
             print(BASE_REMOTE.message)
@@ -144,12 +169,14 @@ def parse_pull_args(parsed_args):
             messages = current_remote.pull(parsed_args.remote, key)
             for message in messages:
                 print(message)
+        os.chdir(actual_current_dir)
     else:
         print("error: not a surround project")
-        print("error: goto project root directory")
 
 def parse_push_args(parsed_args):
     if is_surround_project():
+        actual_current_dir = os.getcwd()
+        os.chdir(get_project_root_from_current_dir())
         path_to_remote = BASE_REMOTE.get_path_to_remote(parsed_args.remote)
         if path_to_remote is None:
             print(BASE_REMOTE.message)
@@ -164,12 +191,14 @@ def parse_push_args(parsed_args):
             messages = current_remote.push(parsed_args.remote, key)
             for message in messages:
                 print(message)
+        os.chdir(actual_current_dir)
     else:
         print("error: not a surround project")
-        print("error: goto project root directory")
 
 def parse_list_args(parsed_args):
     if is_surround_project():
+        actual_current_dir = os.getcwd()
+        os.chdir(get_project_root_from_current_dir())
         path_to_remote = BASE_REMOTE.get_path_to_remote(parsed_args.remote)
         if path_to_remote is None:
             print(BASE_REMOTE.message)
@@ -182,9 +211,9 @@ def parse_list_args(parsed_args):
                 print(remote_file)
         else:
             print(response)
+        os.chdir(actual_current_dir)
     else:
         print("error: not a surround project")
-        print("error: goto project root directory")
 
 def get_corresponding_remote(remote):
     return LOCAL


Test patch:
diff --git a/surround/tests/cli/project_cli/generated_project_test.py b/surround/tests/cli/project_cli/generated_project_test.py
new file mode 100644
index 0000000..1f05745
--- /dev/null
+++ b/surround/tests/cli/project_cli/generated_project_test.py
@@ -0,0 +1,134 @@
+import os
+import shutil
+from pathlib import Path
+import unittest
+import subprocess
+
+class InitTest(unittest.TestCase):
+
+    def setUp(self):
+        subprocess.run(['surround', 'init', './', '-p', 'temp', '-d', 'temp'], encoding='utf-8', stdout=subprocess.PIPE)
+
+        os.makedirs('remote')
+        os.makedirs('temp/test_remote')
+        Path('temp/test_remote/a.txt').touch()
+
+    def test_run_from_subdir(self):
+        process = subprocess.run(['surround', 'run'], encoding='utf-8', stdout=subprocess.PIPE)
+        self.assertEqual(process.stdout, "error: not a surround project\n")
+
+        process = subprocess.run(['surround', 'run'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout[:55], "build    Build the Docker image for the current project")
+
+        process = subprocess.run(['surround', 'run'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout[:55], "build    Build the Docker image for the current project")
+
+    def test_remote_from_subdir(self):
+        process = subprocess.run(['surround', 'remote'], encoding='utf-8', stdout=subprocess.PIPE)
+        self.assertEqual(process.stdout, "error: not a surround project\n")
+
+        process = subprocess.run(['surround', 'remote'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "info: no remote found\n")
+
+        process = subprocess.run(['surround', 'remote'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "info: no remote found\n")
+
+        process = subprocess.run(['surround', 'remote', '-a', '-n', 'test_remote', '-u', '~', '-t', 'data'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        process = subprocess.run(['surround', 'remote'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "test_remote\n")
+
+    def test_add_from_subdir(self):
+        process = subprocess.run(['surround', 'add', 'test_remote', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE)
+        self.assertEqual(process.stdout, "error: not a surround project\n")
+
+        process = subprocess.run(['surround', 'add', 'test_remote', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'add', 'test_remote', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'remote', '-a', '-n', 'test_remote', '-u', 'remote', '-t', 'data'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+
+        process = subprocess.run(['surround', 'add', 'test_remote', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "error: a.jpg not found.\n")
+
+        process = subprocess.run(['surround', 'add', 'test_remote', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "error: a.jpg not found.\n")
+
+        process = subprocess.run(['surround', 'add', 'test_remote', 'a.txt'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "info: file added successfully\n")
+
+    def test_pull_from_subdir(self):
+        process = subprocess.run(['surround', 'pull', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE)
+        self.assertEqual(process.stdout, "error: not a surround project\n")
+
+        process = subprocess.run(['surround', 'pull', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'pull', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'remote', '-a', '-n', 'test_remote', '-u', 'remote', '-t', 'data'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+
+        process = subprocess.run(['surround', 'pull', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "error: file does not exist\n")
+
+        process = subprocess.run(['surround', 'pull', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "error: file does not exist\n")
+
+        process = subprocess.run(['surround', 'pull', 'test_remote', '-k', 'a.txt'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "info: test_remote/a.txt already exists\n")
+
+    def test_push_from_subdir(self):
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE)
+        self.assertEqual(process.stdout, "error: not a surround project\n")
+
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'remote', '-a', '-n', 'test_remote', '-u', 'remote', '-t', 'data'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "error: file does not exist\n")
+
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.jpg'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "error: file does not exist\n")
+
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.txt'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "info: a.txt pushed successfully\n")
+
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.txt'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "info: remote/temp/a.txt already exists\n")
+
+        process = subprocess.run(['rm', 'test_remote/a.txt'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        process = subprocess.run(['surround', 'pull', 'test_remote', '-k', 'a.txt'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "info: a.txt pulled successfully\n")
+
+    def test_list_from_subdir(self):
+        process = subprocess.run(['surround', 'list', 'test_remote'], encoding='utf-8', stdout=subprocess.PIPE)
+        self.assertEqual(process.stdout, "error: not a surround project\n")
+
+        process = subprocess.run(['surround', 'list', 'test_remote'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'list', 'test_remote'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "error: no remote named test_remote\n")
+
+        process = subprocess.run(['surround', 'remote', '-a', '-n', 'test_remote', '-u', 'remote', '-t', 'data'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+
+        process = subprocess.run(['surround', 'push', 'test_remote', '-k', 'a.txt'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "info: a.txt pushed successfully\n")
+
+        process = subprocess.run(['surround', 'list', 'test_remote'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp')
+        self.assertEqual(process.stdout, "a.txt\n")
+
+        process = subprocess.run(['surround', 'list', 'test_remote'], encoding='utf-8', stdout=subprocess.PIPE, cwd='temp/temp')
+        self.assertEqual(process.stdout, "a.txt\n")
+
+    def tearDown(self):
+        # Remove residual directories and files
+        shutil.rmtree('temp')
+        shutil.rmtree('remote')
diff --git a/surround/tests/cli/project_cli/init_test.py b/surround/tests/cli/project_cli/init_test.py
index 1bbce4e..2e68c45 100644
--- a/surround/tests/cli/project_cli/init_test.py
+++ b/surround/tests/cli/project_cli/init_test.py
@@ -1,4 +1,5 @@
 import os
+import shutil
 import unittest
 import subprocess
 
@@ -19,4 +20,4 @@ class InitTest(unittest.TestCase):
 
     def tearDown(self):
         # Remove residual files
-        subprocess.run(['rm', '-r', './temp'], encoding='utf-8', stdout=subprocess.PIPE)
+        shutil.rmtree('temp')
diff --git a/surround/tests/cli/remote_cli/add_test.py b/surround/tests/cli/remote_cli/add_test.py
index 537a8d8..8d98ed7 100644
--- a/surround/tests/cli/remote_cli/add_test.py
+++ b/surround/tests/cli/remote_cli/add_test.py
@@ -1,4 +1,5 @@
 import os
+import shutil
 import unittest
 import subprocess
 
@@ -36,4 +37,4 @@ class AddTest(unittest.TestCase):
 
     def tearDown(self):
         # Remove residual files
-        subprocess.run(['rm', '-r', './temp'], encoding='utf-8', stdout=subprocess.PIPE)
+        shutil.rmtree('temp')
diff --git a/surround/tests/cli/remote_cli/list_test.py b/surround/tests/cli/remote_cli/list_test.py
index d993284..623fbc3 100644
--- a/surround/tests/cli/remote_cli/list_test.py
+++ b/surround/tests/cli/remote_cli/list_test.py
@@ -1,4 +1,5 @@
 import os
+import shutil
 import unittest
 import subprocess
 
@@ -35,4 +36,4 @@ class ListTest(unittest.TestCase):
 
     def tearDown(self):
         # Remove residual files
-        subprocess.run(['rm', '-r', './temp'], encoding='utf-8', stdout=subprocess.PIPE)
+        shutil.rmtree('temp')
diff --git a/surround/tests/cli/remote_cli/remote_test.py b/surround/tests/cli/remote_cli/remote_test.py
index 7236530..9cbe781 100644
--- a/surround/tests/cli/remote_cli/remote_test.py
+++ b/surround/tests/cli/remote_cli/remote_test.py
@@ -1,4 +1,5 @@
 import os
+import shutil
 import unittest
 import subprocess
 
@@ -9,7 +10,7 @@ class RemoteTest(unittest.TestCase):
 
     def test_remote(self):
         process = subprocess.run(['surround', 'remote'], encoding='utf-8', stdout=subprocess.PIPE)
-        self.assertEqual(process.stdout, "error: not a surround project\nerror: goto project root directory\n")
+        self.assertEqual(process.stdout, "error: not a surround project\n")
 
         process = subprocess.run(['surround', 'init', './', '-p', 'temp', '-d', 'temp'], encoding='utf-8', stdout=subprocess.PIPE)
         self.assertEqual(process.stdout, "info: project created at " + os.getcwd() + "/temp\n")
@@ -37,4 +38,4 @@ class RemoteTest(unittest.TestCase):
 
     def tearDown(self):
         # Remove residual files
-        subprocess.run(['rm', '-r', './temp'], encoding='utf-8', stdout=subprocess.PIPE)
+        shutil.rmtree('temp')