From d7701bd2e6beb3a303d4133f685ac685080cf1dd Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Sun, 16 Apr 2017 14:06:16 +0200 Subject: [PATCH] Replace deprecated optparse with argparse --- .gitignore | 4 +- Makefile | 5 +- requirements-dev.txt | 7 +-- tests/test_console.py | 109 +++++++++++++++++++++++++++++++++--------- toot/console.py | 103 +++++++++++++++++++++------------------ 5 files changed, 154 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 2ed9d2c..5d6bb89 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ build/ dist/ tmp/ .pypirc -/.env \ No newline at end of file +/.env +/.coverage +/htmlcov diff --git a/Makefile b/Makefile index 1440700..ab9a33e 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,10 @@ dist : @echo "\nDone." clean : - rm -rf build dist *.egg-info MANIFEST + rm -rf build dist *.egg-info MANIFEST htmlcov publish : twine upload dist/* + +coverage: + py.test --cov=toot --cov-report html tests/ diff --git a/requirements-dev.txt b/requirements-dev.txt index 65e2b15..6e0deda 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ -pytest>=3.0.0 -twine>=1.8.1 -wheel>=0.29.0 \ No newline at end of file +pytest-cov~=2.4.0 +pytest~=3.0.0 +twine~=1.8.1 +wheel~=0.29.0 diff --git a/tests/test_console.py b/tests/test_console.py index 2b1249a..5d38e6f 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -1,9 +1,9 @@ +# -*- coding: utf-8 -*- import pytest import requests -import sys from toot import User, App -from toot.console import cmd_post_status, ConsoleError +from toot.console import print_usage, cmd_post_status, cmd_timeline, cmd_upload from tests.utils import MockResponse @@ -11,12 +11,19 @@ app = App('https://habunek.com', 'foo', 'bar') user = User('ivan@habunek.com', 'xxx') -def test_post_status_defaults(monkeypatch): +def test_print_usagecap(capsys): + print_usage() + out, err = capsys.readouterr() + assert "toot - interact with Mastodon from the command line" in out + + +def test_post_status_defaults(monkeypatch, capsys): def mock_prepare(request): assert request.method == 'POST' assert request.url == 'https://habunek.com/api/v1/statuses' + assert request.headers == {'Authorization': 'Bearer xxx'} assert request.data == { - 'status': '"Hello world"', + 'status': 'Hello world', 'visibility': 'public', 'media_ids[]': None, } @@ -29,14 +36,17 @@ def test_post_status_defaults(monkeypatch): monkeypatch.setattr(requests.Request, 'prepare', mock_prepare) monkeypatch.setattr(requests.Session, 'send', mock_send) - sys.argv = ['toot', 'post', '"Hello world"'] - cmd_post_status(app, user) + cmd_post_status(app, user, ['Hello world']) + + out, err = capsys.readouterr() + assert "Toot posted" in out -def test_post_status_with_options(monkeypatch): +def test_post_status_with_options(monkeypatch, capsys): def mock_prepare(request): assert request.method == 'POST' assert request.url == 'https://habunek.com/api/v1/statuses' + assert request.headers == {'Authorization': 'Bearer xxx'} assert request.data == { 'status': '"Hello world"', 'visibility': 'unlisted', @@ -51,25 +61,80 @@ def test_post_status_with_options(monkeypatch): monkeypatch.setattr(requests.Request, 'prepare', mock_prepare) monkeypatch.setattr(requests.Session, 'send', mock_send) - sys.argv = ['toot', 'post', '"Hello world"', - '--visibility', 'unlisted'] + args = ['"Hello world"', '--visibility', 'unlisted'] - cmd_post_status(app, user) + cmd_post_status(app, user, args) + + out, err = capsys.readouterr() + assert "Toot posted" in out -def test_post_status_invalid_visibility(monkeypatch): - sys.argv = ['toot', 'post', '"Hello world"', - '--visibility', 'foo'] +def test_post_status_invalid_visibility(monkeypatch, capsys): + args = ['Hello world', '--visibility', 'foo'] - with pytest.raises(ConsoleError) as ex: - cmd_post_status(app, user) - assert str(ex.value) == "Invalid visibility value given: 'foo'" + with pytest.raises(SystemExit): + cmd_post_status(app, user, args) + + out, err = capsys.readouterr() + assert "invalid visibility value: 'foo'" in err -def test_post_status_invalid_media(monkeypatch): - sys.argv = ['toot', 'post', '"Hello world"', - '--media', 'does_not_exist.jpg'] +def test_post_status_invalid_media(monkeypatch, capsys): + args = ['Hello world', '--media', 'does_not_exist.jpg'] - with pytest.raises(ConsoleError) as ex: - cmd_post_status(app, user) - assert str(ex.value) == "File does not exist: does_not_exist.jpg" + with pytest.raises(SystemExit): + cmd_post_status(app, user, args) + + out, err = capsys.readouterr() + assert "can't open 'does_not_exist.jpg'" in err + + +def test_timeline(monkeypatch, capsys): + def mock_get(url, params, headers=None): + assert url == 'https://habunek.com/api/v1/timelines/home' + assert headers == {'Authorization': 'Bearer xxx'} + assert params is None + + return MockResponse([{ + 'account': { + 'display_name': 'Frank Zappa', + 'username': 'fz' + }, + 'created_at': '2017-04-12T15:53:18.174Z', + 'content': "

The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.

", + 'reblog': None, + }]) + + monkeypatch.setattr(requests, 'get', mock_get) + + cmd_timeline(app, user, []) + + out, err = capsys.readouterr() + assert "The computer can't tell you the emotional story." in out + assert "Frank Zappa @fz" in out + + +def test_upload(monkeypatch, capsys): + def mock_prepare(request): + assert request.method == 'POST' + assert request.url == 'https://habunek.com/api/v1/media' + assert request.headers == {'Authorization': 'Bearer xxx'} + assert request.files.get('file') is not None + + def mock_send(*args): + return MockResponse({ + 'id': 123, + 'url': 'https://bigfish.software/123/456', + 'preview_url': 'https://bigfish.software/789/012', + 'text_url': 'https://bigfish.software/345/678', + 'type': 'image', + }) + + monkeypatch.setattr(requests.Request, 'prepare', mock_prepare) + monkeypatch.setattr(requests.Session, 'send', mock_send) + + cmd_upload(app, user, [__file__]) + + out, err = capsys.readouterr() + assert "Uploading media" in out + assert __file__ in out diff --git a/toot/console.py b/toot/console.py index dfb5855..a0c3e2b 100644 --- a/toot/console.py +++ b/toot/console.py @@ -12,7 +12,7 @@ from datetime import datetime from future.moves.itertools import zip_longest from getpass import getpass from itertools import chain -from optparse import OptionParser +from argparse import ArgumentParser, FileType from textwrap import TextWrapper from .config import save_user, load_user, load_app, save_app, CONFIG_APP_FILE, CONFIG_USER_FILE @@ -131,7 +131,13 @@ def parse_timeline(item): } -def cmd_timeline(app, user): +def cmd_timeline(app, user, args): + parser = ArgumentParser(prog="toot timeline", + description="Show recent items in your public timeline", + epilog="https://github.com/ihabunek/toot") + + args = parser.parse_args(args) + items = timeline_home(app, user) parsed_items = [parse_timeline(t) for t in items] @@ -141,39 +147,41 @@ def cmd_timeline(app, user): print("─" * 31 + "┼" + "─" * 88) -def cmd_post_status(app, user): - parser = OptionParser(usage="toot post [options] TEXT") +def visibility(value): + if value not in ['public', 'unlisted', 'private', 'direct']: + raise ValueError("Invalid visibility value") - parser.add_option("-m", "--media", dest="media", type="string", - help="path to the media file to attach") + return value - parser.add_option("-v", "--visibility", dest="visibility", type="string", default="public", - help='post visibility, either "public" (default), "direct", "private", or "unlisted"') - (options, args) = parser.parse_args() +def cmd_post_status(app, user, args): + parser = ArgumentParser(prog="toot post", + description="Post a status text to the timeline", + epilog="https://github.com/ihabunek/toot") + parser.add_argument("text", help="The status text to post.") + parser.add_argument("-m", "--media", type=FileType('rb'), + help="path to the media file to attach") + parser.add_argument("-v", "--visibility", type=visibility, default="public", + help='post visibility, either "public" (default), "direct", "private", or "unlisted"') - if len(args) < 2: - parser.print_help() - raise ConsoleError("No text given") + args = parser.parse_args(args) - if options.visibility not in ['public', 'unlisted', 'private', 'direct']: - raise ConsoleError("Invalid visibility value given: '{}'".format(options.visibility)) - - if options.media: - media = do_upload(app, user, options.media) + if args.media: + media = do_upload(app, user, args.media) media_ids = [media['id']] else: media_ids = None - response = post_status( - app, user, args[1], media_ids=media_ids, visibility=options.visibility) + response = post_status(app, user, args.text, media_ids=media_ids, visibility=args.visibility) print("Toot posted: " + green(response.get('url'))) -def cmd_auth(app, user): - parser = OptionParser(usage='%prog auth') - parser.parse_args() +def cmd_auth(app, user, args): + parser = ArgumentParser(prog="toot auth", + description="Show login details", + epilog="https://github.com/ihabunek/toot") + parser.parse_args(args) if app and user: print("You are logged in to " + green(app.base_url)) @@ -185,7 +193,9 @@ def cmd_auth(app, user): def cmd_login(): - parser = OptionParser(usage='%prog login') + parser = ArgumentParser(prog="toot login", + description="Log into a Mastodon instance", + epilog="https://github.com/ihabunek/toot") parser.parse_args() app = create_app_interactive() @@ -194,24 +204,26 @@ def cmd_login(): return app, user -def cmd_logout(app, user): - parser = OptionParser(usage='%prog logout') - parser.parse_args() +def cmd_logout(app, user, args): + parser = ArgumentParser(prog="toot logout", + description="Log out, delete stored access keys", + epilog="https://github.com/ihabunek/toot") + parser.parse_args(args) os.unlink(CONFIG_APP_FILE) os.unlink(CONFIG_USER_FILE) print("You are now logged out") -def cmd_upload(app, user): - parser = OptionParser(usage='%prog upload ') - parser.parse_args() +def cmd_upload(app, user, args): + parser = ArgumentParser(prog="toot upload", + description="Upload an image or video file", + epilog="https://github.com/ihabunek/toot") + parser.add_argument("file", help="Path to the file to upload", type=FileType('rb')) - if len(sys.argv) < 3: - print_error("No status text given") - return + args = parser.parse_args(args) - response = do_upload(sys.argv[2]) + response = do_upload(app, user, args.file) print("\nSuccessfully uploaded media ID {}, type '{}'".format( yellow(response['id']), yellow(response['type']))) @@ -220,16 +232,12 @@ def cmd_upload(app, user): print("Text URL: " + green(response['text_url'])) -def do_upload(app, user, path): - if not os.path.exists(path): - raise ConsoleError("File does not exist: " + path) - - with open(path, 'rb') as f: - print("Uploading media: {}".format(green(f.name))) - return upload_media(app, user, f) +def do_upload(app, user, file): + print("Uploading media: {}".format(green(file.name))) + return upload_media(app, user, file) -def run_command(command): +def run_command(command, args): app = load_app() user = load_user() @@ -238,7 +246,7 @@ def run_command(command): return cmd_login() if command == 'auth': - return cmd_auth(app, user) + return cmd_auth(app, user, args) # Commands which require user to be logged in if not app or not user: @@ -247,16 +255,16 @@ def run_command(command): return if command == 'logout': - return cmd_logout(app, user) + return cmd_logout(app, user, args) if command == 'post': - return cmd_post_status(app, user) + return cmd_post_status(app, user, args) if command == 'timeline': - return cmd_timeline(app, user) + return cmd_timeline(app, user, args) if command == 'upload': - return cmd_upload(app, user) + return cmd_upload(app, user, args) print(red("Unknown command '{}'\n".format(command))) print_usage() @@ -267,11 +275,12 @@ def main(): logging.basicConfig(level=logging.DEBUG) command = sys.argv[1] if len(sys.argv) > 1 else None + args = sys.argv[2:] if not command: return print_usage() try: - run_command(command) + run_command(command, args) except ConsoleError as e: print_error(str(e))