-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#29 wip: register path command with get and tree commands drafted
- Loading branch information
Showing
3 changed files
with
295 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |