From 640476592145a40924294ce6ae40fb8e997431be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sat, 3 Dec 2022 22:04:26 +0100 Subject: [PATCH 01/15] refactor: replace lambdas with python --- mastodon/admin.py | 2 +- mastodon/lists.py | 4 ++-- mastodon/reports.py | 2 +- mastodon/statuses.py | 3 +-- tests/test_account.py | 4 ++-- tests/test_admin.py | 2 +- tests/test_filters.py | 27 +++++++++++++++------------ tests/test_lists.py | 11 +++++------ tests/test_status.py | 14 +++++++------- tests/test_streaming.py | 26 +++++++++++++------------- tests/test_timeline.py | 26 +++++++++++++------------- 11 files changed, 61 insertions(+), 60 deletions(-) diff --git a/mastodon/admin.py b/mastodon/admin.py index 405b30b..8ce30df 100644 --- a/mastodon/admin.py +++ b/mastodon/admin.py @@ -42,7 +42,7 @@ class Mastodon(Internals): if role_ids is not None: if not isinstance(role_ids, list): role_ids = [role_ids] - role_ids = list(map(self.__unpack_id, role_ids)) + role_ids = [self.__unpack_id(x) for x in role_ids] if invited_by is not None: invited_by = self.__unpack_id(invited_by) diff --git a/mastodon/lists.py b/mastodon/lists.py index 57ced46..ce7944b 100644 --- a/mastodon/lists.py +++ b/mastodon/lists.py @@ -90,7 +90,7 @@ class Mastodon(Internals): if not isinstance(account_ids, list): account_ids = [account_ids] - account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) + account_ids = [self.__unpack_id(x) for x in account_ids] params = self.__generate_params(locals(), ['id']) self.__api_request('POST', f'/api/v1/lists/{id}/accounts', params) @@ -104,7 +104,7 @@ class Mastodon(Internals): if not isinstance(account_ids, list): account_ids = [account_ids] - account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) + account_ids = [self.__unpack_id(x) for x in account_ids] params = self.__generate_params(locals(), ['id']) self.__api_request('DELETE', f'/api/v1/lists/{id}/accounts', params) \ No newline at end of file diff --git a/mastodon/reports.py b/mastodon/reports.py index 3d6380e..0485e12 100644 --- a/mastodon/reports.py +++ b/mastodon/reports.py @@ -52,7 +52,7 @@ class Mastodon(Internals): if status_ids is not None: if not isinstance(status_ids, list): status_ids = [status_ids] - status_ids = list(map(lambda x: self.__unpack_id(x), status_ids)) + status_ids = [self.__unpack_id(x) for x in status_ids] params_initial = locals() if not forward: diff --git a/mastodon/statuses.py b/mastodon/statuses.py index eb372d8..3c64cae 100644 --- a/mastodon/statuses.py +++ b/mastodon/statuses.py @@ -341,8 +341,7 @@ class Mastodon(Internals): mentioned_accounts[account.id] = account.acct # Join into one piece of text. The space is added inside because of self-replies. - status = "".join(map(lambda x: "@" + x + " ", - mentioned_accounts.values())) + status + status = " ".join(f"@{x}" for x in mentioned_accounts.values()) + " " + status # Retain visibility / cw if visibility is None and 'visibility' in to_status: diff --git a/tests/test_account.py b/tests/test_account.py index afc2122..5cfad98 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -206,14 +206,14 @@ def test_account_pin_unpin(api, api2): try: assert relationship assert relationship['endorsed'] - assert user["id"] in map(lambda x: x["id"], endorsed) + assert any(x["id"] == user["id"] for x in endorsed) finally: relationship = api.account_unpin(user) endorsed2 = api.endorsements() api.account_unfollow(user) assert relationship assert not relationship['endorsed'] - assert not user["id"] in map(lambda x: x["id"], endorsed2) + assert not any(x["id"] == user["id"] for x in endorsed2) @pytest.mark.vcr() def test_preferences(api): diff --git a/tests/test_admin.py b/tests/test_admin.py index b0716e5..87402c1 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -173,7 +173,7 @@ def test_admin_domain_blocks(api2): assert block3.public_comment == "sicko behaviour" assert block3.private_comment == "jk ilu <3" api2.admin_delete_domain_block(block2) - assert not block3.id in map(lambda x: x.id, api2.admin_domain_blocks()) + assert not any(x.id == block3.id for x in api2.admin_domain_blocks()) @pytest.mark.vcr(match_on=['path']) def test_admin_stats(api2): diff --git a/tests/test_filters.py b/tests/test_filters.py index d3dab8d..e89a9d0 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -58,10 +58,11 @@ def test_filter_serverside(api, api2): time.sleep(2) tl = api.timeline_home() try: - assert not status_1['id'] in map(lambda st: st['id'], tl) - assert not status_2['id'] in map(lambda st: st['id'], tl) - assert status_3['id'] in map(lambda st: st['id'], tl) - assert status_4['id'] in map(lambda st: st['id'], tl) + st_ids = {st["id"] for st in tl} + assert status_1["id"] not in st_ids + assert status_2["id"] not in st_ids + assert status_3["id"] in st_ids + assert status_4["id"] in st_ids finally: api.filter_delete(keyword_filter_1) api.filter_delete(keyword_filter_2) @@ -94,16 +95,18 @@ def test_filter_clientside(api, api2): tl = api.timeline_home() try: - assert status_1['id'] in map(lambda st: st['id'], tl) - assert status_2['id'] in map(lambda st: st['id'], tl) - assert status_3['id'] in map(lambda st: st['id'], tl) - assert status_4['id'] in map(lambda st: st['id'], tl) + st_ids = {st["id"] for st in tl} + assert status_1['id'] in st_ids + assert status_2['id'] in st_ids + assert status_3['id'] in st_ids + assert status_4['id'] in st_ids filtered = api.filters_apply(tl, [keyword_filter_1, keyword_filter_2, keyword_filter_3], 'home') - assert not status_1['id'] in map(lambda st: st['id'], filtered) - assert not status_2['id'] in map(lambda st: st['id'], filtered) - assert status_3['id'] in map(lambda st: st['id'], filtered) - assert status_4['id'] in map(lambda st: st['id'], filtered) + st_ids = {st["id"] for st in filtered} + assert status_1['id'] not in st_ids + assert status_2['id'] not in st_ids + assert status_3['id'] in st_ids + assert status_4['id'] in st_ids finally: api.filter_delete(keyword_filter_1) api.filter_delete(keyword_filter_2) diff --git a/tests/test_lists.py b/tests/test_lists.py index b3321c1..decaf92 100644 --- a/tests/test_lists.py +++ b/tests/test_lists.py @@ -24,15 +24,15 @@ def test_list_add_remove_account(api, api2, mastodon_list): api.account_follow(user) api.list_accounts_add(mastodon_list, user) - assert user.id in map(lambda x: x.id, api.list_accounts(mastodon_list)) + assert any(x.id == user.id for x in api.list_accounts(mastodon_list)) api.account_unfollow(user) assert len(api.list_accounts(mastodon_list)) == 0 api.account_follow(user) api.list_accounts_add(mastodon_list, user) - assert user.id in map(lambda x: x.id, api.list_accounts(mastodon_list)) - + assert any(x.id == user.id for x in api.list_accounts(mastodon_list)) + api.list_accounts_delete(mastodon_list, user) assert len(api.list_accounts(mastodon_list)) == 0 @@ -56,9 +56,8 @@ def test_list_timeline(api, api2, mastodon_list): status = api2.status_post("I have never stolen a ham in my life.", visibility="public") time.sleep(2) - list_tl = list(map(lambda x: x.id, api.timeline_list(mastodon_list))) - assert status.id in list_tl - + assert any(x.id == status.id for x in api.timeline_list(mastodon_list)) + api2.status_delete(status) api.account_unfollow(user) \ No newline at end of file diff --git a/tests/test_status.py b/tests/test_status.py index 56e7397..9551fb1 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -172,15 +172,15 @@ def test_scheduled_status(api): assert scheduled_toot_2.scheduled_at < scheduled_toot.scheduled_at scheduled_toot_list = api.scheduled_statuses() - assert scheduled_toot_2.id in map(lambda x: x.id, scheduled_toot_list) + assert any(x.id == scheduled_toot_2.id for x in scheduled_toot_list) scheduled_toot_3 = api.scheduled_status(scheduled_toot.id) assert scheduled_toot_2.id == scheduled_toot_3.id api.scheduled_status_delete(scheduled_toot_2) scheduled_toot_list_2 = api.scheduled_statuses() - assert not scheduled_toot_2.id in map(lambda x: x.id, scheduled_toot_list_2) - + assert not any(x.id == scheduled_toot_2.id for x in scheduled_toot_list_2) + if os.path.exists("tests/cassettes/test_scheduled_status_datetimeobjects.pkl"): the_very_immediate_future = datetime.datetime.fromtimestamp(pickle.load(open("tests/cassettes/test_scheduled_status_datetimeobjects.pkl", 'rb'))) else: @@ -190,9 +190,9 @@ def test_scheduled_status(api): time.sleep(15) statuses = api.timeline_home() scheduled_toot_list_3 = api.scheduled_statuses() - assert scheduled_toot_4.id in map(lambda x: x.id, statuses) - assert not scheduled_toot_4.id in map(lambda x: x.id, scheduled_toot_list_3) - + assert any(x.id == scheduled_toot_4.id for x in statuses) + assert not any(x.id == scheduled_toot_4.id for x in scheduled_toot_list_3) + # The following two tests need to be manually (!) ran 10 minutes apart when recording. # Sorry, I can't think of a better way to test scheduled statuses actually work as intended. @pytest.mark.vcr(match_on=['path']) @@ -205,7 +205,7 @@ def test_scheduled_status_long_part1(api): pickle.dump(the_medium_term_future.timestamp(), open("tests/cassettes_special/test_scheduled_status_long_datetimeobjects.pkl", 'wb')) scheduled_toot = api.status_post(f"please ensure maximum headroom at {the_medium_term_future}", scheduled_at=the_medium_term_future) scheduled_toot_list = api.scheduled_statuses() - assert scheduled_toot.id in map(lambda x: x.id, scheduled_toot_list) + assert any(x.id == scheduled_toot.id for x in scheduled_toot_list) pickle.dump(scheduled_toot.params.text, open("tests/cassettes_special/test_scheduled_status_long_text.pkl", 'wb')) @pytest.mark.vcr(match_on=['path']) diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 9b96ab8..afa9350 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -323,17 +323,17 @@ def test_stream_user_direct(api, api2, api3): conversations = [] edits = [] listener = CallbackStreamListener( - update_handler = lambda x: updates.append(x), - local_update_handler = lambda x: local_updates.append(x), - notification_handler = lambda x: notifications.append(x), - delete_handler = lambda x: deletes.append(x), - conversation_handler = lambda x: conversations.append(x), - status_update_handler = lambda x: edits.append(x), - filters_changed_handler = lambda x: 0, - announcement_handler = lambda x: 0, - announcement_reaction_handler = lambda x: 0, - announcement_delete_handler = lambda x: 0, - encryted_message_handler = lambda x: 0, + update_handler=updates.append, + local_update_handler=local_updates.append, + notification_handler=notifications.append, + delete_handler=deletes.append, + conversation_handler=conversations.append, + status_update_handler=edits.append, + filters_changed_handler=lambda x: 0, + announcement_handler=lambda x: 0, + announcement_reaction_handler=lambda x: 0, + announcement_delete_handler=lambda x: 0, + encryted_message_handler=lambda x: 0, ) posted = [] @@ -384,7 +384,7 @@ def test_stream_user_local(api, api2): updates = [] listener = CallbackStreamListener( - local_update_handler = lambda x: updates.append(x), + local_update_handler=updates.append, ) posted = [] @@ -412,7 +412,7 @@ def test_stream_direct(api, api2): conversations = [] listener = CallbackStreamListener( - conversation_handler = lambda x: conversations.append(x), + conversation_handler=conversations.append, ) def do_activities(): diff --git a/tests/test_timeline.py b/tests/test_timeline.py index 239fac3..c36f1e0 100644 --- a/tests/test_timeline.py +++ b/tests/test_timeline.py @@ -9,14 +9,14 @@ import os def test_public_tl_anonymous(api_anonymous, status3): time.sleep(3) tl = api_anonymous.timeline_public() - assert status3['id'] in list(map(lambda st: st['id'], tl)) + assert any(st["id"] == status3["id"] for st in tl) @pytest.mark.vcr() def test_public_tl(api, status): public = api.timeline_public() local = api.timeline_local() - assert status['id'] in map(lambda st: st['id'], public) - assert status['id'] in map(lambda st: st['id'], local) + assert any(st["id"] == status["id"] for st in public) + assert any(st["id"] == status["id"] for st in local) @pytest.mark.vcr() def test_unauthed_home_tl_throws(api_anonymous, status): @@ -27,14 +27,14 @@ def test_unauthed_home_tl_throws(api_anonymous, status): def test_home_tl(api, status): time.sleep(3) tl = api.timeline_home() - assert status['id'] in map(lambda st: st['id'], tl) + assert any(st["id"] == status["id"] for st in tl) @pytest.mark.vcr() def test_hashtag_tl(api): status = api.status_post('#hoot (hashtag toot)') tl = api.timeline_hashtag('hoot') try: - assert status['id'] in map(lambda st: st['id'], tl) + assert any(st["id"] == status["id"] for st in tl) finally: api.status_delete(status['id']) @@ -58,8 +58,8 @@ def test_conversations(api, api2): conversations2 = api2.conversations() api.status_delete(status) assert conversations - assert status.id in map(lambda x: x.last_status.id, conversations) - assert account.id in map(lambda x: x.accounts[0].id, conversations) + assert any(x.last_status.id == status.id for x in conversations) + assert any(x.accounts[0].id == account.id for x in conversations) assert conversations[0].unread is True assert conversations2[0].unread is False @@ -67,16 +67,16 @@ def test_conversations(api, api2): def test_min_max_id(api, status): time.sleep(3) tl = api.timeline_home(min_id = status.id - 1000, max_id = status.id + 1000) - assert status['id'] in map(lambda st: st['id'], tl) + assert any(st["id"] == status["id"] for st in tl) tl = api.timeline_home(min_id = status.id - 2000, max_id = status.id - 1000) - assert not status['id'] in map(lambda st: st['id'], tl) + assert not any(st["id"] == status["id"] for st in tl) tl = api.timeline_home(min_id = status.id + 1000, max_id = status.id + 2000) - assert not status['id'] in map(lambda st: st['id'], tl) + assert not any(st["id"] == status["id"] for st in tl) tl = api.timeline_home(since_id = status.id - 1000) - assert status['id'] in map(lambda st: st['id'], tl) + assert any(st["id"] == status["id"] for st in tl) @pytest.mark.vcr() def test_min_max_id_datetimes(api, status): @@ -99,7 +99,7 @@ def test_min_max_id_datetimes(api, status): time.sleep(3) tl = api.timeline_home(min_id = the_past, max_id = the_future) - assert status['id'] in map(lambda st: st['id'], tl) + assert any(st["id"] == status["id"] for st in tl) tl = api.timeline_home(min_id = the_future, max_id = the_far_future) - assert not status['id'] in map(lambda st: st['id'], tl) + assert not any(st["id"] == status["id"] for st in tl) From 802a352ad8ec1b917d092864c64d60d79b6ba3ea Mon Sep 17 00:00:00 2001 From: zevaryx Date: Wed, 7 Dec 2022 20:30:56 -0700 Subject: [PATCH 02/15] Add resolve keyword to account_search --- mastodon/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mastodon/accounts.py b/mastodon/accounts.py index cd43c57..af717eb 100644 --- a/mastodon/accounts.py +++ b/mastodon/accounts.py @@ -243,7 +243,7 @@ class Mastodon(Internals): params) @api_version("1.0.0", "2.3.0", _DICT_VERSION_ACCOUNT) - def account_search(self, q, limit=None, following=False): + def account_search(self, q, limit=None, following=False, resolve=False): """ Fetch matching accounts. Will lookup an account remotely if the search term is in the username@domain format and not yet in the database. Set `following` to From fa1c6c97d22f07a54e57a50cf3e9fcd9d288aa0a Mon Sep 17 00:00:00 2001 From: Junpei Kawamoto Date: Fri, 30 Dec 2022 22:31:38 -0600 Subject: [PATCH 03/15] Supports User-Agent header in create_app method Following #240, it's helpful if `create_app` also supports the User-Agent header. This commit fixes https://github.com/halcy/Mastodon.py/issues/213#issuecomment-1276999193. --- mastodon/authentication.py | 15 +++++++++++---- tests/test_create_app.py | 10 ++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/mastodon/authentication.py b/mastodon/authentication.py index 9f42587..03eaf12 100644 --- a/mastodon/authentication.py +++ b/mastodon/authentication.py @@ -14,13 +14,15 @@ from .utility import parse_version_string, api_version from .internals import Mastodon as Internals +_DEFAULT_USER_AGENT = "mastodonpy" + class Mastodon(Internals): ### # Registering apps ### @staticmethod def create_app(client_name, scopes=_DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, - api_base_url=None, request_timeout=_DEFAULT_TIMEOUT, session=None): + api_base_url=None, request_timeout=_DEFAULT_TIMEOUT, session=None, user_agent=_DEFAULT_USER_AGENT): """ Create a new app with given `client_name` and `scopes` (The basic scopes are "read", "write", "follow" and "push" - more granular scopes are available, please refer to Mastodon documentation for which) on the instance given @@ -36,6 +38,8 @@ class Mastodon(Internals): Specify `session` with a requests.Session for it to be used instead of the default. This can be used to, amongst other things, adjust proxy or SSL certificate settings. + Specify `user_agent` if you want to use a specific name as `User-Agent` header, otherwise "mastodonpy" will be used. + Presently, app registration is open by default, but this is not guaranteed to be the case for all Mastodon instances in the future. @@ -50,6 +54,9 @@ class Mastodon(Internals): 'client_name': client_name, 'scopes': " ".join(scopes) } + headers = { + 'User-Agent': user_agent + } try: if redirect_uris is not None: @@ -61,10 +68,10 @@ class Mastodon(Internals): if website is not None: request_data['website'] = website if session: - ret = session.post(f"{api_base_url}/api/v1/apps", data=request_data, timeout=request_timeout) + ret = session.post(f"{api_base_url}/api/v1/apps", data=request_data, headers=headers, timeout=request_timeout) response = ret.json() else: - response = requests.post(f"{api_base_url}/api/v1/apps", data=request_data, timeout=request_timeout) + response = requests.post(f"{api_base_url}/api/v1/apps", data=request_data, headers=headers, timeout=request_timeout) response = response.json() except Exception as e: raise MastodonNetworkError(f"Could not complete request: {e}") @@ -83,7 +90,7 @@ class Mastodon(Internals): ### def __init__(self, client_id=None, client_secret=None, access_token=None, api_base_url=None, debug_requests=False, ratelimit_method="wait", ratelimit_pacefactor=1.1, request_timeout=_DEFAULT_TIMEOUT, mastodon_version=None, - version_check_mode="created", session=None, feature_set="mainline", user_agent="mastodonpy", lang=None): + version_check_mode="created", session=None, feature_set="mainline", user_agent=_DEFAULT_USER_AGENT, lang=None): """ Create a new API wrapper instance based on the given `client_secret` and `client_id` on the instance given by `api_base_url`. If you give a `client_id` and it is not a file, you must diff --git a/tests/test_create_app.py b/tests/test_create_app.py index 97556c6..c7d2205 100644 --- a/tests/test_create_app.py +++ b/tests/test_create_app.py @@ -8,7 +8,7 @@ try: except ImportError: from unittest.mock import Mock -def test_create_app(mocker, to_file=None, redirect_uris=None, website=None): +def test_create_app(mocker, to_file=None, redirect_uris=None, website=None, user_agent="mastodonpy"): # there is no easy way to delete an anonymously created app so # instead we mock Requests resp = Mock() @@ -22,7 +22,8 @@ def test_create_app(mocker, to_file=None, redirect_uris=None, website=None): api_base_url="example.com", to_file=to_file, redirect_uris=redirect_uris, - website=website + website=website, + user_agent=user_agent ) assert app == ('foo', 'bar') @@ -43,6 +44,11 @@ def test_create_app_website(mocker): kwargs = requests.post.call_args[1] assert kwargs['data']['website'] == 'http://example.net' +def test_create_app_user_agent(mocker): + test_create_app(mocker, user_agent="pytest") + kwargs = requests.post.call_args[1] + assert kwargs['headers']['User-Agent'] == 'pytest' + @pytest.mark.vcr() def test_app_verify_credentials(api): app = api.app_verify_credentials() From 5542d58c36d8d34d7f655ae175d6bf3c70b033f4 Mon Sep 17 00:00:00 2001 From: Junpei Kawamoto Date: Mon, 2 Jan 2023 14:58:52 -0600 Subject: [PATCH 04/15] Improve test coverage --- mastodon/authentication.py | 16 ++++++++-------- tests/test_create_app.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/mastodon/authentication.py b/mastodon/authentication.py index 03eaf12..4403770 100644 --- a/mastodon/authentication.py +++ b/mastodon/authentication.py @@ -58,15 +58,15 @@ class Mastodon(Internals): 'User-Agent': user_agent } + if redirect_uris is not None: + if isinstance(redirect_uris, (list, tuple)): + redirect_uris = "\n".join(list(redirect_uris)) + request_data['redirect_uris'] = redirect_uris + else: + request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob' + if website is not None: + request_data['website'] = website try: - if redirect_uris is not None: - if isinstance(redirect_uris, (list, tuple)): - redirect_uris = "\n".join(list(redirect_uris)) - request_data['redirect_uris'] = redirect_uris - else: - request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob' - if website is not None: - request_data['website'] = website if session: ret = session.post(f"{api_base_url}/api/v1/apps", data=request_data, headers=headers, timeout=request_timeout) response = ret.json() diff --git a/tests/test_create_app.py b/tests/test_create_app.py index c7d2205..2f7c3fc 100644 --- a/tests/test_create_app.py +++ b/tests/test_create_app.py @@ -1,6 +1,7 @@ -from mastodon import Mastodon +from mastodon import Mastodon, MastodonNetworkError import pytest import requests +from requests import HTTPError import time try: @@ -39,11 +40,34 @@ def test_create_app_redirect_uris(mocker): kwargs = requests.post.call_args[1] assert kwargs['data']['redirect_uris'] == 'http://example.net' +def test_create_app_multiple_redirect_uris(mocker): + test_create_app(mocker, redirect_uris=['http://example.net', 'https://example.net']) + kwargs = requests.post.call_args[1] + assert kwargs['data']['redirect_uris'] == 'http://example.net\nhttps://example.net' + def test_create_app_website(mocker): test_create_app(mocker, website='http://example.net') kwargs = requests.post.call_args[1] assert kwargs['data']['website'] == 'http://example.net' +def test_create_app_session(): + resp = Mock(**{'json.return_value': {'client_id': 'foo', 'client_secret': 'bar'}}) + sess = Mock(**{'post.return_value': resp}) + + app = Mastodon.create_app("Mastodon.py test suite", api_base_url="example.com", session=sess) + + assert app == ('foo', 'bar') + sess.post.assert_called() + +def test_create_app_error(mocker): + def post(_url, **_kwargs): + raise HTTPError("Unauthorized") + + mocker.patch('requests.post', side_effect=post) + + with pytest.raises(MastodonNetworkError): + Mastodon.create_app("Mastodon.py test suite", api_base_url="example.com") + def test_create_app_user_agent(mocker): test_create_app(mocker, user_agent="pytest") kwargs = requests.post.call_args[1] @@ -60,7 +84,7 @@ def test_app_account_create(): # This leaves behind stuff on the test server, which is unfortunate, but eh. suffix = str(time.time()).replace(".", "")[-5:] - test_app = test_app = Mastodon.create_app( + test_app = Mastodon.create_app( "mastodon.py generated test app", api_base_url="http://localhost:3000/" ) @@ -80,7 +104,7 @@ def test_app_account_create(): def test_app_account_create_invalid(): suffix = str(time.time()).replace(".", "")[-5:] - test_app = test_app = Mastodon.create_app( + test_app = Mastodon.create_app( "mastodon.py generated test app", api_base_url="http://localhost:3000/" ) From e6e1e1519f5102bd25300c765491b2a5c2d359b9 Mon Sep 17 00:00:00 2001 From: Junpei Kawamoto Date: Mon, 2 Jan 2023 23:31:51 -0600 Subject: [PATCH 05/15] Add newer python versions to tox.ini --- tox.ini | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 2392345..69cd684 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,8 @@ [tox] -envlist = py36,py37 +envlist = py36,py37,py38,py39,py310,py311 skipsdist = true [testenv] -deps = pipenv==2018.11.14 -passenv = HOME -commands = - pipenv sync -d - pipenv run pytest +deps = .[test] +commands = python setup.py test From c58ce36c8e798812418fb82cf8d4d0ab129713de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Notin?= Date: Tue, 10 Jan 2023 00:42:45 +0100 Subject: [PATCH 06/15] Fix small typo --- mastodon/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mastodon/accounts.py b/mastodon/accounts.py index cd43c57..79e4364 100644 --- a/mastodon/accounts.py +++ b/mastodon/accounts.py @@ -142,7 +142,7 @@ class Mastodon(Internals): def me(self): """ Get this user's account. Synonym for `account_verify_credentials()`, does exactly - the same thing, just exists becase `account_verify_credentials()` has a confusing + the same thing, just exists because `account_verify_credentials()` has a confusing name. """ return self.account_verify_credentials() From 9dac13edc2830b5838f3fd56f01486f8216efa29 Mon Sep 17 00:00:00 2001 From: Junpei Kawamoto Date: Wed, 11 Jan 2023 14:33:54 -0600 Subject: [PATCH 07/15] Remove Pipfile Since it was only used in tox, we can remove it. --- Pipfile | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 Pipfile diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d5d7ad5..0000000 --- a/Pipfile +++ /dev/null @@ -1,17 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] -mastodon-py = {editable = true, extras = ["tests"], path = "."} -pytest = "<4" -pytest-runner = "*" -pytest-cov = "*" -vcrpy = "*" -pytest-vcr = "<1" -pytest-mock = "*" -requests-mock = "*" - From 32d5e385e60cc1f3ba1d0fdcb1a265bb4de56114 Mon Sep 17 00:00:00 2001 From: xloem <0xloem@gmail.com> Date: Sun, 5 Feb 2023 21:50:07 -0500 Subject: [PATCH 08/15] Use python-magic-bin on windows --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 735b582..0f4f58c 100644 --- a/setup.py +++ b/setup.py @@ -33,8 +33,9 @@ setup(name='Mastodon.py', install_requires=[ 'requests>=2.4.2', 'python-dateutil', - 'six', - 'python-magic', + 'six', + 'python-magic-bin ; platform_system=="Windows"', + 'python-magic ; platform_system!="Windows"', 'decorator>=4.0.0', ] + blurhash_deps, tests_require=test_deps, From 9ba8781a1e57dfcec86c3dcbe5010cca43e99ba7 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 6 Feb 2023 08:23:34 -0500 Subject: [PATCH 09/15] exclude windows deps from coverage, which is not calculated on windows. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0f4f58c..cd30258 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setup(name='Mastodon.py', 'requests>=2.4.2', 'python-dateutil', 'six', - 'python-magic-bin ; platform_system=="Windows"', + 'python-magic-bin ; platform_system=="Windows"', # pragma: no cover 'python-magic ; platform_system!="Windows"', 'decorator>=4.0.0', ] + blurhash_deps, From 4bd8afa7aa5a11c6881fd834ba8c592a25e46b79 Mon Sep 17 00:00:00 2001 From: Leon Cowle Date: Tue, 7 Feb 2023 15:50:23 -0600 Subject: [PATCH 10/15] Add missing "params" arg to lists.py:list_accounts() __api_request call --- mastodon/lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mastodon/lists.py b/mastodon/lists.py index 57ced46..037b4a3 100644 --- a/mastodon/lists.py +++ b/mastodon/lists.py @@ -47,7 +47,7 @@ class Mastodon(Internals): since_id = self.__unpack_id(since_id, dateconv=True) params = self.__generate_params(locals(), ['id']) - return self.__api_request('GET', f'/api/v1/lists/{id}/accounts') + return self.__api_request('GET', f'/api/v1/lists/{id}/accounts', params) ### # Writing data: Lists @@ -107,4 +107,4 @@ class Mastodon(Internals): account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) params = self.__generate_params(locals(), ['id']) - self.__api_request('DELETE', f'/api/v1/lists/{id}/accounts', params) \ No newline at end of file + self.__api_request('DELETE', f'/api/v1/lists/{id}/accounts', params) From 92a7cbac35444d6ec9f6a705e8d431b886c38962 Mon Sep 17 00:00:00 2001 From: alex martin shepherd <110127860+alexmshepherd@users.noreply.github.com> Date: Fri, 3 Mar 2023 12:43:40 +0100 Subject: [PATCH 11/15] Clarify status_reply ( #310 ) --- mastodon/statuses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mastodon/statuses.py b/mastodon/statuses.py index 36a7d2b..7368961 100644 --- a/mastodon/statuses.py +++ b/mastodon/statuses.py @@ -326,6 +326,8 @@ class Mastodon(Internals): the users that are being replied to to the status text and retains CW and visibility if not explicitly overridden. + Note that `to_status` should be a :ref:`status dict ` and not an ID. + Set `untag` to True if you want the reply to only go to the user you are replying to, removing every other mentioned user from the conversation. From 2905c07863ab7ebc237707dd8eeee3c3ebd46999 Mon Sep 17 00:00:00 2001 From: alex martin shepherd <110127860+alexmshepherd@users.noreply.github.com> Date: Fri, 3 Mar 2023 12:47:43 +0100 Subject: [PATCH 12/15] Add custom exception --- mastodon/statuses.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mastodon/statuses.py b/mastodon/statuses.py index 7368961..d2c8628 100644 --- a/mastodon/statuses.py +++ b/mastodon/statuses.py @@ -341,7 +341,10 @@ class Mastodon(Internals): # Determine users to mention mentioned_accounts = collections.OrderedDict() - mentioned_accounts[to_status.account.id] = to_status.account.acct + try: + mentioned_accounts[to_status.account.id] = to_status.account.acct + except AttributeError as e: + raise TypeError("to_status must specify a status dict!") from e if not untag: for account in to_status.mentions: From 0a1cb46ba24945335c64b2303ceac8dcbddf4a47 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 5 Mar 2023 10:18:17 +0200 Subject: [PATCH 13/15] Add README to PyPI page --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 735b582..a18e3b6 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +from pathlib import Path + from setuptools import setup test_deps = [ @@ -26,9 +28,14 @@ extras = { "blurhash": blurhash_deps, } +this_directory = Path(__file__).parent +long_description = (this_directory / "README.rst").read_text() + setup(name='Mastodon.py', version='1.8.0', description='Python wrapper for the Mastodon API', + long_description=long_description, + long_description_content_type='text/x-rst', packages=['mastodon'], install_requires=[ 'requests>=2.4.2', From a6d974b69be103fa7e863ce4a348b3b0b9fe6ed1 Mon Sep 17 00:00:00 2001 From: codl Date: Sun, 16 Apr 2023 21:12:06 +0200 Subject: [PATCH 14/15] doc: warn against using max_id in favourites/bookmarks see #335 --- mastodon/favourites.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mastodon/favourites.py b/mastodon/favourites.py index 9d86424..35d7782 100644 --- a/mastodon/favourites.py +++ b/mastodon/favourites.py @@ -14,6 +14,11 @@ class Mastodon(Internals): """ Fetch the logged-in user's favourited statuses. + This endpoint uses internal ids for pagination, passing status ids to + `max_id`, `min_id`, or `since_id` will not work. Pagination functions + :ref:`fetch_next() ` + and :ref:`fetch_previous() ` should be used instead. + Returns a list of :ref:`status dicts `. """ if max_id is not None: @@ -36,6 +41,11 @@ class Mastodon(Internals): """ Get a list of statuses bookmarked by the logged-in user. + This endpoint uses internal ids for pagination, passing status ids to + `max_id`, `min_id`, or `since_id` will not work. Pagination functions + :ref:`fetch_next() ` + and :ref:`fetch_previous() ` should be used instead. + Returns a list of :ref:`status dicts `. """ if max_id is not None: @@ -49,4 +59,4 @@ class Mastodon(Internals): params = self.__generate_params(locals()) return self.__api_request('GET', '/api/v1/bookmarks', params) - \ No newline at end of file + From 43ccb72275e7401167d30529c100d8454832c8fd Mon Sep 17 00:00:00 2001 From: halcy Date: Sun, 23 Apr 2023 18:44:47 +0300 Subject: [PATCH 15/15] Move user agent default to where it belongs --- CHANGELOG.rst | 6 ++++++ mastodon/authentication.py | 4 +--- mastodon/defaults.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f146ee5..e0320af 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ A note on versioning: This librarys major version will grow with the APIs version number. Breaking changes will be indicated by a change in the minor (or major) version number, and will generally be avoided. +v1.8.0 (in progress) +-------------------- +* Replace some lambdas with list comprenehsions (thanks eumiro) +* Add `resolve` keyword to `account_search` (thanks zevaryx) +* Add support for user agent header in `create_app` (thanks jkawamoto) + v1.8.0 ------ * Overall: Support level is now 3.5.5 (last before 4.0.0) diff --git a/mastodon/authentication.py b/mastodon/authentication.py index 4403770..6cb2aad 100644 --- a/mastodon/authentication.py +++ b/mastodon/authentication.py @@ -9,13 +9,11 @@ import collections from .errors import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError from .versions import _DICT_VERSION_APPLICATION -from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT +from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT, _DEFAULT_USER_AGENT from .utility import parse_version_string, api_version from .internals import Mastodon as Internals -_DEFAULT_USER_AGENT = "mastodonpy" - class Mastodon(Internals): ### # Registering apps diff --git a/mastodon/defaults.py b/mastodon/defaults.py index 950ffa2..401b358 100644 --- a/mastodon/defaults.py +++ b/mastodon/defaults.py @@ -3,6 +3,7 @@ _DEFAULT_TIMEOUT = 300 _DEFAULT_STREAM_TIMEOUT = 300 _DEFAULT_STREAM_RECONNECT_WAIT_SEC = 5 +_DEFAULT_USER_AGENT = "mastodonpy" _DEFAULT_SCOPES = ['read', 'write', 'follow', 'push'] _SCOPE_SETS = { 'read': [