Skip to content

Commit

Permalink
#29 wip: register path command with get and tree commands drafted
Browse files Browse the repository at this point in the history
  • Loading branch information
caseneuve committed Mar 13, 2021
1 parent 48afd5f commit e43a12d
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cli/pa
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import typer
from cli import django
from cli import schedule
from cli import webapp
from cli import path

help = """This is a new experimental PythonAnywhere cli client.
Expand All @@ -15,6 +16,7 @@ app = typer.Typer(help=help)
app.add_typer(django.app, name="django", help="Makes Django Girls tutorial projects deployment easy")
app.add_typer(schedule.app, name="schedule", help="Manage scheduled tasks")
app.add_typer(webapp.app, name="webapp", help="Everything for web apps")
app.add_typer(path.app, name="path", help="Perform some operations on files")


if __name__ == "__main__":
Expand Down
117 changes: 117 additions & 0 deletions cli/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import getpass
import re
import sys

from collections import namedtuple
from pprint import pprint

import typer

from pythonanywhere.files import PAPath

app = typer.Typer()


def standarize_path(path):
return path.replace("~", f"/home/{getpass.getuser()}") if path.startswith("~") else path


@app.command()
def get(
path: str = typer.Argument(..., help="Path to PythonAnywhere file or directory"),
only_files: bool = typer.Option(False, "-f", "--files", help="List only files"),
only_dirs: bool = typer.Option(False, "-d", "--dirs", help="List only directories"),
sort_by_type: bool = typer.Option(False, "-t", "--type", help="Sort by type"),
sort_reverse: bool = typer.Option(False, "-r", "--reverse", help="Sort in reverse order"),
raw: bool = typer.Option(False, "-a", "--raw", help="Print API response (if PATH is file that's the only option)"),
):
"""
Get contents of PATH.
If PATH points to a directory, show list of it's contents.
If PATH points to a file, print it's contents.
"""
path = standarize_path(path)
contents = PAPath(path).contents

if contents is None:
sys.exit(1)

if raw or type(contents) == str:
{dict: pprint, str: print}[type(contents)](contents)
sys.exit()

NameToType = namedtuple("NameToType", ["name", "type"])
item = "file" if only_files else "directory" if only_dirs else "every"
data = [NameToType(k, v["type"]) for k, v in contents.items()]

if sort_reverse or sort_by_type:
data.sort(key=lambda x: x.type if sort_by_type else x.name, reverse=sort_reverse)

print(f"{path}:")
for name, type_ in data:
if item == "every":
print(f"{type_[0].upper()} {name}")
elif type_ == item:
print(f" {name}")


def _format_tree(data, current):
last_child = "└── "
next_child = "├── "
connector = "│ "
filler = " "

formatted = []
following = []

for idx, entry in enumerate(reversed(data)):
entry = re.sub(r"/$", "\0", entry.replace(current, ""))
chunks = [cc for cc in entry.split('/') if cc]
item = chunks[-1].replace("\0", "/")

level = len(chunks) - 1
following = [ll for ll in following if ll <= level]

indent = ""
for lvl in range(level):
indent += connector if lvl in following else filler
indent += last_child if level not in following else next_child

if level not in following:
following.append(level)

formatted.append(indent + item)

return "\n".join(reversed(formatted))


@app.command()
def tree(path: str = typer.Argument(..., help="Path to PythonAnywhere file or directory")):
path = sanitize_path(path)
tree = PAPath(path).tree

if tree is not None:
formatted_tree = _format_tree(tree, path)
print(f"{path}:")
print(".")
print(formatted_tree)


@app.command()
def upload(path: str = typer.Argument(..., help="Path to PythonAnywhere file or directory")):
pass


@app.command()
def delete(path: str = typer.Argument(..., help="Path to PythonAnywhere file or directory")):
pass


@app.command()
def share(path: str = typer.Argument(..., help="Path to PythonAnywhere file or directory")):
pass


@app.command()
def unshare(path: str = typer.Argument(..., help="Path to PythonAnywhere file or directory")):
pass
176 changes: 176 additions & 0 deletions tests/test_cli_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import getpass
from textwrap import dedent

import pytest
from typer.testing import CliRunner

from cli.path import app

runner = CliRunner()


@pytest.fixture
def home_dir():
return f"/home/{getpass.getuser()}"


@pytest.fixture
def mock_path(mocker):
return mocker.patch("cli.path.PAPath", autospec=True)


@pytest.fixture
def mock_homedir_path(mock_path):
contents = {
'.bashrc': {'type': 'file', 'url': 'bashrc_file_url'},
'A_file': {'type': 'file', 'url': 'A_file_url'},
'a_dir': {'type': 'directory', 'url': 'dir_one_url'},
'a_file': {'type': 'file', 'url': 'a_file_url'},
'b_file': {'type': 'file', 'url': 'b_file_url'},
'dir_two': {'type': 'directory', 'url': 'dir_two_url'},
}

mock_path.return_value.contents = contents
return mock_path


@pytest.fixture
def mock_file_path(mock_path):
mock_path.return_value.contents = "file contents"
return mock_path


class TestGet:
def test_replaces_tilde_in_path(self, mock_path, home_dir):
runner.invoke(app, ["get", '~'])

mock_path.assert_called_once_with(home_dir)

def test_exits_early_when_no_contents_for_given_path(self, mock_path, mocker):
mock_exit = mocker.patch("cli.path.sys.exit")
mock_path.return_value.contents = None

runner.invoke(app, ["get", '~/nonexistent.file'])

mock_exit.assert_called_once_with(1)

def test_prints_file_contents_and_exits_when_path_is_file(self, mock_file_path, home_dir):
result = runner.invoke(app, ["get", "~/some-file"])

mock_file_path.assert_called_once_with(f"{home_dir}/some-file")
assert "file contents\n" == result.stdout

def test_prints_api_contents_and_exits_when_raw_option_set(self, mock_homedir_path):
result = runner.invoke(app, ["get", "~", "--raw"])

assert "'.bashrc': {'type': 'file', 'url': 'bashrc_file_url'}" in result.stdout

def test_lists_only_directories_when_dirs_option_set(self, mock_homedir_path, home_dir):
result = runner.invoke(app, ["get", "~", "--dirs"])

assert result.stdout.startswith(home_dir)
for item, value in mock_homedir_path.return_value.contents.items():
if value['type'] == 'file':
assert item not in result.stdout
elif value['type'] == 'directory':
assert item in result.stdout

def test_lists_only_files_when_files_option_set(self, mock_homedir_path, home_dir):
result = runner.invoke(app, ["get", "~", "--files"])

assert result.stdout.startswith(home_dir)
for item, value in mock_homedir_path.return_value.contents.items():
if value['type'] == 'file':
assert item in result.stdout
elif value['type'] == 'directory':
assert item not in result.stdout

def test_reverses_directory_content_list_when_reverse_option_set(self, mock_homedir_path):
result = runner.invoke(app, ["get", "~", "--reverse"])

expected = dedent(
"""\
/home/piotr:
D dir_two
F b_file
F a_file
D a_dir
F A_file
F .bashrc
"""
)

assert expected == result.stdout

def test_sorts_directory_content_list_by_type_when_type_option_set(self, mock_homedir_path):
result = runner.invoke(app, ["get", "~", "--type"])

expected = dedent(
"""\
/home/piotr:
D a_dir
D dir_two
F .bashrc
F A_file
F a_file
F b_file
"""
)

assert expected == result.stdout

def test_ignores_options_when_path_is_file(self, mock_file_path):
result = runner.invoke(app, ["get", "~/some-file", "--type", "--reverse"])

assert "file contents\n" == result.stdout


class TestTree:
def test_prints_formatted_tree_when_successfull_api_call(self, mock_path, home_dir):
mock_path.return_value.tree = [
f'{home_dir}/README.txt',
f'{home_dir}/dir_one/',
f'{home_dir}/dir_one/bar.txt',
f'{home_dir}/dir_one/foo.txt',
f'{home_dir}/dir_one/nested_one/',
f'{home_dir}/dir_one/nested_one/foo.txt',
f'{home_dir}/dir_one/nested_two/',
f'{home_dir}/dir_two/',
f'{home_dir}/file.py'
]

result = runner.invoke(app, ["tree", "~"])

expected = dedent(f"""\
{home_dir}:
.
├── README.txt
├── dir_one/
│ ├── bar.txt
│ ├── foo.txt
│ ├── nested_one/
│ │ └── foo.txt
│ └── nested_two/
├── dir_two/
└── file.py
""")
assert result.stdout == expected

def test_does_not_print_tree_when_path_is_incorrect(self, mock_path):
mock_path.return_value.tree = None

result = runner.invoke(app, ["tree", "/wrong/path"])

assert result.stdout == ""

def test_prints_tree_for_empty_directory(self, mock_path, home_dir):
mock_path.return_value.tree = []

result = runner.invoke(app, ["tree", "~/empty_dir"])

expected = dedent(f"""\
{home_dir}/empty_dir:
.
""")
assert result.stdout == expected

0 comments on commit e43a12d

Please sign in to comment.