Skip to content

Commit

Permalink
#29 wip: sketches Path interface for Files enpoint covering contents …
Browse files Browse the repository at this point in the history
…and delete methods. by Piotr
  • Loading branch information
caseneuve committed Dec 18, 2020
1 parent b3d0835 commit a380e7e
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 7 deletions.
2 changes: 1 addition & 1 deletion pythonanywhere/api/files_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
""" Interface speaking with PythonAnywhere API providing methods for files.
*Don't use* `Files` :class: in helper scripts, use `pythonanywhere.files` classes instead."""
*Don't use* `Files` :class: in helper scripts, use `pythonanywhere.files.Path` class instead."""

import getpass
from os import path
Expand Down
47 changes: 47 additions & 0 deletions pythonanywhere/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""User interface for interacting with PythonAnywhere files.
Provides a class `Path` which should be used by helper scripts
providing features for programmatic handling of user's files."""

import logging

from pythonanywhere.api.files_api import Files
from pythonanywhere.snakesay import snakesay

logger = logging.getLogger(name=__name__)


class Path:
"""Class providing interface for interacting with PythonAnywhere user files.
"""

def __init__(self, path):
self.path = path
self.api = Files()

def __repr__(self):
user_url = self.api.base_url.replace("/api/v0", "")
return f"{user_url}{self.path[1:]}"

def contents(self):
content = self.api.path_get(self.path)
return content if type(content) == dict else content.decode("utf-8")

def delete(self):
try:
self.api.path_delete(self.path)
logger.info(snakesay(f"{self.path} deleted!"))
except Exception as e:
logger.warning(snakesay(str(e)))

def upload(self):
pass

def share(self):
pass

def unshare(self):
pass

def tree(self):
pass

13 changes: 7 additions & 6 deletions tests/test_api_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class TestFiles:
".profile": {"type": "file", "url": f"{base_url}path{home_dir_path}/.profile"},
"README.txt": {"type": "file", "url": f"{base_url}path{home_dir_path}/README.txt"},
}
readme_contents = (
b"# vim: set ft=rst:\n\nSee https://help.pythonanywhere.com/ "
b'(or click the "Help" link at the top\nright) '
b"for help on how to use PythonAnywhere, including tips on copying and\n"
b"pasting from consoles, and writing your own web applications.\n"
)


@pytest.mark.files
Expand All @@ -43,12 +49,7 @@ def test_returns_contents_of_directory_when_path_to_dir_provided(
def test_returns_file_contents_when_file_path_provided(self, api_token, api_responses):
filepath = urljoin(self.home_dir_path, "README.txt")
file_url = urljoin(self.base_url, f"path{filepath}")
body = (
b"# vim: set ft=rst:\n\nSee https://help.pythonanywhere.com/ "
b'(or click the "Help" link at the top\nright) '
b"for help on how to use PythonAnywhere, including tips on copying and\n"
b"pasting from consoles, and writing your own web applications.\n"
)
body = self.readme_contents
api_responses.add(
responses.GET,
url=file_url,
Expand Down
75 changes: 75 additions & 0 deletions tests/test_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import getpass
from urllib.parse import urljoin
from unittest.mock import call

import pytest

from pythonanywhere.api.base import get_api_endpoint
from pythonanywhere.files import Path
from tests.test_api_files import TestFiles


@pytest.mark.files
class TestPathRepr(TestFiles):
def test_contains_correct_pythonanywhere_resource_url_for_instantiated_path(self):
path = self.home_dir_path

user_path = self.base_url.replace('/api/v0', '')
assert Path(path).__repr__() == f"{user_path}{path[1:]}"


@pytest.mark.files
class TestPathContents(TestFiles):
def test_returns_file_contents_as_string_if_path_points_to_a_file(self, mocker):
path = f"{self.home_dir_path}README.txt"
mock_path_get = mocker.patch("pythonanywhere.api.files_api.Files.path_get")
mock_path_get.return_value = self.readme_contents

result = Path(path).contents()

assert mock_path_get.call_args == call(path)
assert result == self.readme_contents.decode()

def test_returns_directory_contents_if_path_points_to_a_directory(self, mocker):
mock_path_get = mocker.patch("pythonanywhere.api.files_api.Files.path_get")
mock_path_get.return_value = self.default_home_dir_files

result = Path(self.home_dir_path).contents()

assert result == self.default_home_dir_files

def test_raises_when_path_unavailable(self, mocker):
mock_path_get = mocker.patch("pythonanywhere.api.files_api.Files.path_get")
mock_path_get.side_effect = Exception("error msg")

with pytest.raises(Exception) as e:
Path('/home/different_user').contents()

assert str(e.value) == "error msg"


@pytest.mark.files
class TestPathDelete(TestFiles):
def test_informes_about_successful_file_deletion(self, mocker):
mock_delete = mocker.patch("pythonanywhere.api.files_api.Files.path_delete")
mock_delete.return_value.status_code = 204
mock_snake = mocker.patch("pythonanywhere.files.snakesay")
mock_info = mocker.patch("pythonanywhere.files.logger.info")
path = "/valid/path"

Path(path).delete()

assert mock_delete.call_args == call(path)
assert mock_snake.call_args == call(f"{path} deleted!")
assert mock_info.call_args == call(mock_snake.return_value)

def test_warns_about_failed_deletion(self, mocker):
mock_delete = mocker.patch("pythonanywhere.api.files_api.Files.path_delete")
mock_delete.side_effect = Exception("error msg")
mock_snake = mocker.patch("pythonanywhere.files.snakesay")
mock_warning = mocker.patch("pythonanywhere.files.logger.warning")
undeletable_path = "/home/"

Path(undeletable_path).delete()

assert mock_snake.call_args == call("error msg")

0 comments on commit a380e7e

Please sign in to comment.