Merge branch 'master' into master

This commit is contained in:
Lorenz Diener 2023-04-23 18:54:56 +03:00 committed by GitHub
commit 3b5c4ae6f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 153 additions and 107 deletions

View File

@ -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 version number. Breaking changes will be indicated by a change in the minor
(or major) version number, and will generally be avoided. (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 v1.8.0
------ ------
* Overall: Support level is now 3.5.5 (last before 4.0.0) * Overall: Support level is now 3.5.5 (last before 4.0.0)

17
Pipfile
View File

@ -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 = "*"

View File

@ -143,7 +143,7 @@ class Mastodon(Internals):
def me(self): def me(self):
""" """
Get this user's account. Synonym for `account_verify_credentials()`, does exactly 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. name.
""" """
return self.account_verify_credentials() return self.account_verify_credentials()
@ -244,7 +244,7 @@ class Mastodon(Internals):
params) params)
@api_version("1.0.0", "2.3.0", _DICT_VERSION_ACCOUNT) @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 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 in the username@domain format and not yet in the database. Set `following` to

View File

@ -43,7 +43,7 @@ class Mastodon(Internals):
if role_ids is not None: if role_ids is not None:
if not isinstance(role_ids, list): if not isinstance(role_ids, list):
role_ids = [role_ids] 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: if invited_by is not None:
invited_by = self.__unpack_id(invited_by) invited_by = self.__unpack_id(invited_by)

View File

@ -9,7 +9,7 @@ import collections
from .errors import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError from .errors import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError
from .versions import _DICT_VERSION_APPLICATION 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 .utility import parse_version_string, api_version
from .internals import Mastodon as Internals from .internals import Mastodon as Internals
@ -21,7 +21,7 @@ class Mastodon(Internals):
### ###
@staticmethod @staticmethod
def create_app(client_name, scopes=_DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, 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" 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 - more granular scopes are available, please refer to Mastodon documentation for which) on the instance given
@ -37,6 +37,8 @@ class Mastodon(Internals):
Specify `session` with a requests.Session for it to be used instead of the default. This can be 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. 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 Presently, app registration is open by default, but this is not guaranteed to be the case for all
Mastodon instances in the future. Mastodon instances in the future.
@ -51,21 +53,24 @@ class Mastodon(Internals):
'client_name': client_name, 'client_name': client_name,
'scopes': " ".join(scopes) 'scopes': " ".join(scopes)
} }
headers = {
'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: 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: 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() response = ret.json()
else: 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() response = response.json()
except Exception as e: except Exception as e:
raise MastodonNetworkError(f"Could not complete request: {e}") raise MastodonNetworkError(f"Could not complete request: {e}")
@ -84,7 +89,7 @@ class Mastodon(Internals):
### ###
def __init__(self, client_id=None, client_secret=None, access_token=None, api_base_url=None, debug_requests=False, 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, 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 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 instance given by `api_base_url`. If you give a `client_id` and it is not a file, you must

View File

@ -3,6 +3,7 @@
_DEFAULT_TIMEOUT = 300 _DEFAULT_TIMEOUT = 300
_DEFAULT_STREAM_TIMEOUT = 300 _DEFAULT_STREAM_TIMEOUT = 300
_DEFAULT_STREAM_RECONNECT_WAIT_SEC = 5 _DEFAULT_STREAM_RECONNECT_WAIT_SEC = 5
_DEFAULT_USER_AGENT = "mastodonpy"
_DEFAULT_SCOPES = ['read', 'write', 'follow', 'push'] _DEFAULT_SCOPES = ['read', 'write', 'follow', 'push']
_SCOPE_SETS = { _SCOPE_SETS = {
'read': [ 'read': [

View File

@ -15,6 +15,11 @@ class Mastodon(Internals):
""" """
Fetch the logged-in user's favourited statuses. 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() <fetch_next()>`
and :ref:`fetch_previous() <fetch_previous()>` should be used instead.
Returns a list of :ref:`status dicts <status dicts>`. Returns a list of :ref:`status dicts <status dicts>`.
""" """
if max_id is not None: if max_id is not None:
@ -37,6 +42,11 @@ class Mastodon(Internals):
""" """
Get a list of statuses bookmarked by the logged-in user. 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() <fetch_next()>`
and :ref:`fetch_previous() <fetch_previous()>` should be used instead.
Returns a list of :ref:`status dicts <status dicts>`. Returns a list of :ref:`status dicts <status dicts>`.
""" """
if max_id is not None: if max_id is not None:

View File

@ -48,7 +48,7 @@ class Mastodon(Internals):
since_id = self.__unpack_id(since_id, dateconv=True) since_id = self.__unpack_id(since_id, dateconv=True)
params = self.__generate_params(locals(), ['id']) 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 # Writing data: Lists
@ -91,7 +91,7 @@ class Mastodon(Internals):
if not isinstance(account_ids, list): if not isinstance(account_ids, list):
account_ids = [account_ids] 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']) params = self.__generate_params(locals(), ['id'])
self.__api_request('POST', f'/api/v1/lists/{id}/accounts', params) self.__api_request('POST', f'/api/v1/lists/{id}/accounts', params)
@ -105,7 +105,7 @@ class Mastodon(Internals):
if not isinstance(account_ids, list): if not isinstance(account_ids, list):
account_ids = [account_ids] 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']) params = self.__generate_params(locals(), ['id'])
self.__api_request('DELETE', f'/api/v1/lists/{id}/accounts', params) self.__api_request('DELETE', f'/api/v1/lists/{id}/accounts', params)

View File

@ -53,7 +53,7 @@ class Mastodon(Internals):
if status_ids is not None: if status_ids is not None:
if not isinstance(status_ids, list): if not isinstance(status_ids, list):
status_ids = [status_ids] 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() params_initial = locals()
if not forward: if not forward:

View File

@ -321,6 +321,8 @@ class Mastodon(Internals):
the users that are being replied to the status text and retains the users that are being replied to the status text and retains
CW and visibility if not explicitly overridden. CW and visibility if not explicitly overridden.
Note that `to_status` should be a :ref:`status dict <status dict>` and not an ID.
Set `untag` to True if you want the reply to only go to the user you 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 are replying to, removing every other mentioned user from the
conversation. conversation.
@ -334,7 +336,10 @@ class Mastodon(Internals):
# Determine users to mention # Determine users to mention
mentioned_accounts = collections.OrderedDict() 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: if not untag:
for account in to_status.mentions: for account in to_status.mentions:
@ -342,8 +347,7 @@ class Mastodon(Internals):
mentioned_accounts[account.id] = account.acct mentioned_accounts[account.id] = account.acct
# Join into one piece of text. The space is added inside because of self-replies. # Join into one piece of text. The space is added inside because of self-replies.
status = "".join(map(lambda x: "@" + x + " ", status = " ".join(f"@{x}" for x in mentioned_accounts.values()) + " " + status
mentioned_accounts.values())) + status
# Retain visibility / cw # Retain visibility / cw
if visibility is None and 'visibility' in to_status: if visibility is None and 'visibility' in to_status:

View File

@ -1,3 +1,5 @@
from pathlib import Path
from setuptools import setup from setuptools import setup
test_deps = [ test_deps = [
@ -26,15 +28,21 @@ extras = {
"blurhash": blurhash_deps, "blurhash": blurhash_deps,
} }
this_directory = Path(__file__).parent
long_description = (this_directory / "README.rst").read_text()
setup(name='Mastodon.py', setup(name='Mastodon.py',
version='1.8.0', version='1.8.0',
description='Python wrapper for the Mastodon API', description='Python wrapper for the Mastodon API',
long_description=long_description,
long_description_content_type='text/x-rst',
packages=['mastodon'], packages=['mastodon'],
install_requires=[ install_requires=[
'requests>=2.4.2', 'requests>=2.4.2',
'python-dateutil', 'python-dateutil',
'six', 'six',
'python-magic', 'python-magic-bin ; platform_system=="Windows"', # pragma: no cover
'python-magic ; platform_system!="Windows"',
'decorator>=4.0.0', 'decorator>=4.0.0',
] + blurhash_deps, ] + blurhash_deps,
tests_require=test_deps, tests_require=test_deps,

View File

@ -206,14 +206,14 @@ def test_account_pin_unpin(api, api2):
try: try:
assert relationship assert relationship
assert relationship['endorsed'] assert relationship['endorsed']
assert user["id"] in map(lambda x: x["id"], endorsed) assert any(x["id"] == user["id"] for x in endorsed)
finally: finally:
relationship = api.account_unpin(user) relationship = api.account_unpin(user)
endorsed2 = api.endorsements() endorsed2 = api.endorsements()
api.account_unfollow(user) api.account_unfollow(user)
assert relationship assert relationship
assert not relationship['endorsed'] 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() @pytest.mark.vcr()
def test_preferences(api): def test_preferences(api):

View File

@ -173,7 +173,7 @@ def test_admin_domain_blocks(api2):
assert block3.public_comment == "sicko behaviour" assert block3.public_comment == "sicko behaviour"
assert block3.private_comment == "jk ilu <3" assert block3.private_comment == "jk ilu <3"
api2.admin_delete_domain_block(block2) 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']) @pytest.mark.vcr(match_on=['path'])
def test_admin_stats(api2): def test_admin_stats(api2):

View File

@ -1,6 +1,7 @@
from mastodon import Mastodon from mastodon import Mastodon, MastodonNetworkError
import pytest import pytest
import requests import requests
from requests import HTTPError
import time import time
try: try:
@ -8,7 +9,7 @@ try:
except ImportError: except ImportError:
from unittest.mock import Mock 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 # there is no easy way to delete an anonymously created app so
# instead we mock Requests # instead we mock Requests
resp = Mock() resp = Mock()
@ -22,7 +23,8 @@ def test_create_app(mocker, to_file=None, redirect_uris=None, website=None):
api_base_url="example.com", api_base_url="example.com",
to_file=to_file, to_file=to_file,
redirect_uris=redirect_uris, redirect_uris=redirect_uris,
website=website website=website,
user_agent=user_agent
) )
assert app == ('foo', 'bar') assert app == ('foo', 'bar')
@ -38,11 +40,39 @@ def test_create_app_redirect_uris(mocker):
kwargs = requests.post.call_args[1] kwargs = requests.post.call_args[1]
assert kwargs['data']['redirect_uris'] == 'http://example.net' 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): def test_create_app_website(mocker):
test_create_app(mocker, website='http://example.net') test_create_app(mocker, website='http://example.net')
kwargs = requests.post.call_args[1] kwargs = requests.post.call_args[1]
assert kwargs['data']['website'] == 'http://example.net' 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]
assert kwargs['headers']['User-Agent'] == 'pytest'
@pytest.mark.vcr() @pytest.mark.vcr()
def test_app_verify_credentials(api): def test_app_verify_credentials(api):
app = api.app_verify_credentials() app = api.app_verify_credentials()
@ -54,7 +84,7 @@ def test_app_account_create():
# This leaves behind stuff on the test server, which is unfortunate, but eh. # This leaves behind stuff on the test server, which is unfortunate, but eh.
suffix = str(time.time()).replace(".", "")[-5:] suffix = str(time.time()).replace(".", "")[-5:]
test_app = test_app = Mastodon.create_app( test_app = Mastodon.create_app(
"mastodon.py generated test app", "mastodon.py generated test app",
api_base_url="http://localhost:3000/" api_base_url="http://localhost:3000/"
) )
@ -74,7 +104,7 @@ def test_app_account_create():
def test_app_account_create_invalid(): def test_app_account_create_invalid():
suffix = str(time.time()).replace(".", "")[-5:] suffix = str(time.time()).replace(".", "")[-5:]
test_app = test_app = Mastodon.create_app( test_app = Mastodon.create_app(
"mastodon.py generated test app", "mastodon.py generated test app",
api_base_url="http://localhost:3000/" api_base_url="http://localhost:3000/"
) )

View File

@ -58,10 +58,11 @@ def test_filter_serverside(api, api2):
time.sleep(2) time.sleep(2)
tl = api.timeline_home() tl = api.timeline_home()
try: try:
assert not status_1['id'] in map(lambda st: st['id'], tl) st_ids = {st["id"] for st in tl}
assert not status_2['id'] in map(lambda st: st['id'], tl) assert status_1["id"] not in st_ids
assert status_3['id'] in map(lambda st: st['id'], tl) assert status_2["id"] not in st_ids
assert status_4['id'] in map(lambda st: st['id'], tl) assert status_3["id"] in st_ids
assert status_4["id"] in st_ids
finally: finally:
api.filter_delete(keyword_filter_1) api.filter_delete(keyword_filter_1)
api.filter_delete(keyword_filter_2) api.filter_delete(keyword_filter_2)
@ -94,16 +95,18 @@ def test_filter_clientside(api, api2):
tl = api.timeline_home() tl = api.timeline_home()
try: try:
assert status_1['id'] in map(lambda st: st['id'], tl) st_ids = {st["id"] for st in tl}
assert status_2['id'] in map(lambda st: st['id'], tl) assert status_1['id'] in st_ids
assert status_3['id'] in map(lambda st: st['id'], tl) assert status_2['id'] in st_ids
assert status_4['id'] in map(lambda st: st['id'], tl) 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') 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) st_ids = {st["id"] for st in filtered}
assert not status_2['id'] in map(lambda st: st['id'], filtered) assert status_1['id'] not in st_ids
assert status_3['id'] in map(lambda st: st['id'], filtered) assert status_2['id'] not in st_ids
assert status_4['id'] in map(lambda st: st['id'], filtered) assert status_3['id'] in st_ids
assert status_4['id'] in st_ids
finally: finally:
api.filter_delete(keyword_filter_1) api.filter_delete(keyword_filter_1)
api.filter_delete(keyword_filter_2) api.filter_delete(keyword_filter_2)

View File

@ -24,15 +24,15 @@ def test_list_add_remove_account(api, api2, mastodon_list):
api.account_follow(user) api.account_follow(user)
api.list_accounts_add(mastodon_list, 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) api.account_unfollow(user)
assert len(api.list_accounts(mastodon_list)) == 0 assert len(api.list_accounts(mastodon_list)) == 0
api.account_follow(user) api.account_follow(user)
api.list_accounts_add(mastodon_list, 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) api.list_accounts_delete(mastodon_list, user)
assert len(api.list_accounts(mastodon_list)) == 0 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") status = api2.status_post("I have never stolen a ham in my life.", visibility="public")
time.sleep(2) time.sleep(2)
list_tl = list(map(lambda x: x.id, api.timeline_list(mastodon_list))) assert any(x.id == status.id for x in api.timeline_list(mastodon_list))
assert status.id in list_tl
api2.status_delete(status) api2.status_delete(status)
api.account_unfollow(user) api.account_unfollow(user)

View File

@ -172,15 +172,15 @@ def test_scheduled_status(api):
assert scheduled_toot_2.scheduled_at < scheduled_toot.scheduled_at assert scheduled_toot_2.scheduled_at < scheduled_toot.scheduled_at
scheduled_toot_list = api.scheduled_statuses() 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) scheduled_toot_3 = api.scheduled_status(scheduled_toot.id)
assert scheduled_toot_2.id == scheduled_toot_3.id assert scheduled_toot_2.id == scheduled_toot_3.id
api.scheduled_status_delete(scheduled_toot_2) api.scheduled_status_delete(scheduled_toot_2)
scheduled_toot_list_2 = api.scheduled_statuses() 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"): 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'))) the_very_immediate_future = datetime.datetime.fromtimestamp(pickle.load(open("tests/cassettes/test_scheduled_status_datetimeobjects.pkl", 'rb')))
else: else:
@ -190,9 +190,9 @@ def test_scheduled_status(api):
time.sleep(15) time.sleep(15)
statuses = api.timeline_home() statuses = api.timeline_home()
scheduled_toot_list_3 = api.scheduled_statuses() scheduled_toot_list_3 = api.scheduled_statuses()
assert scheduled_toot_4.id in map(lambda x: x.id, statuses) assert any(x.id == scheduled_toot_4.id for x in statuses)
assert not scheduled_toot_4.id in map(lambda x: x.id, scheduled_toot_list_3) 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. # 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. # Sorry, I can't think of a better way to test scheduled statuses actually work as intended.
@pytest.mark.vcr(match_on=['path']) @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')) 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 = 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() 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')) pickle.dump(scheduled_toot.params.text, open("tests/cassettes_special/test_scheduled_status_long_text.pkl", 'wb'))
@pytest.mark.vcr(match_on=['path']) @pytest.mark.vcr(match_on=['path'])

View File

@ -323,17 +323,17 @@ def test_stream_user_direct(api, api2, api3):
conversations = [] conversations = []
edits = [] edits = []
listener = CallbackStreamListener( listener = CallbackStreamListener(
update_handler = lambda x: updates.append(x), update_handler=updates.append,
local_update_handler = lambda x: local_updates.append(x), local_update_handler=local_updates.append,
notification_handler = lambda x: notifications.append(x), notification_handler=notifications.append,
delete_handler = lambda x: deletes.append(x), delete_handler=deletes.append,
conversation_handler = lambda x: conversations.append(x), conversation_handler=conversations.append,
status_update_handler = lambda x: edits.append(x), status_update_handler=edits.append,
filters_changed_handler = lambda x: 0, filters_changed_handler=lambda x: 0,
announcement_handler = lambda x: 0, announcement_handler=lambda x: 0,
announcement_reaction_handler = lambda x: 0, announcement_reaction_handler=lambda x: 0,
announcement_delete_handler = lambda x: 0, announcement_delete_handler=lambda x: 0,
encryted_message_handler = lambda x: 0, encryted_message_handler=lambda x: 0,
) )
posted = [] posted = []
@ -384,7 +384,7 @@ def test_stream_user_local(api, api2):
updates = [] updates = []
listener = CallbackStreamListener( listener = CallbackStreamListener(
local_update_handler = lambda x: updates.append(x), local_update_handler=updates.append,
) )
posted = [] posted = []
@ -412,7 +412,7 @@ def test_stream_direct(api, api2):
conversations = [] conversations = []
listener = CallbackStreamListener( listener = CallbackStreamListener(
conversation_handler = lambda x: conversations.append(x), conversation_handler=conversations.append,
) )
def do_activities(): def do_activities():

View File

@ -9,14 +9,14 @@ import os
def test_public_tl_anonymous(api_anonymous, status3): def test_public_tl_anonymous(api_anonymous, status3):
time.sleep(3) time.sleep(3)
tl = api_anonymous.timeline_public() 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() @pytest.mark.vcr()
def test_public_tl(api, status): def test_public_tl(api, status):
public = api.timeline_public() public = api.timeline_public()
local = api.timeline_local() local = api.timeline_local()
assert status['id'] in map(lambda st: st['id'], public) assert any(st["id"] == status["id"] for st in public)
assert status['id'] in map(lambda st: st['id'], local) assert any(st["id"] == status["id"] for st in local)
@pytest.mark.vcr() @pytest.mark.vcr()
def test_unauthed_home_tl_throws(api_anonymous, status): 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): def test_home_tl(api, status):
time.sleep(3) time.sleep(3)
tl = api.timeline_home() 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() @pytest.mark.vcr()
def test_hashtag_tl(api): def test_hashtag_tl(api):
status = api.status_post('#hoot (hashtag toot)') status = api.status_post('#hoot (hashtag toot)')
tl = api.timeline_hashtag('hoot') tl = api.timeline_hashtag('hoot')
try: try:
assert status['id'] in map(lambda st: st['id'], tl) assert any(st["id"] == status["id"] for st in tl)
finally: finally:
api.status_delete(status['id']) api.status_delete(status['id'])
@ -58,8 +58,8 @@ def test_conversations(api, api2):
conversations2 = api2.conversations() conversations2 = api2.conversations()
api.status_delete(status) api.status_delete(status)
assert conversations assert conversations
assert status.id in map(lambda x: x.last_status.id, conversations) assert any(x.last_status.id == status.id for x in conversations)
assert account.id in map(lambda x: x.accounts[0].id, conversations) assert any(x.accounts[0].id == account.id for x in conversations)
assert conversations[0].unread is True assert conversations[0].unread is True
assert conversations2[0].unread is False assert conversations2[0].unread is False
@ -67,16 +67,16 @@ def test_conversations(api, api2):
def test_min_max_id(api, status): def test_min_max_id(api, status):
time.sleep(3) time.sleep(3)
tl = api.timeline_home(min_id = status.id - 1000, max_id = status.id + 1000) 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) 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) 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) 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() @pytest.mark.vcr()
def test_min_max_id_datetimes(api, status): def test_min_max_id_datetimes(api, status):
@ -99,7 +99,7 @@ def test_min_max_id_datetimes(api, status):
time.sleep(3) time.sleep(3)
tl = api.timeline_home(min_id = the_past, max_id = the_future) 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) 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)

View File

@ -1,11 +1,8 @@
[tox] [tox]
envlist = py36,py37 envlist = py36,py37,py38,py39,py310,py311
skipsdist = true skipsdist = true
[testenv] [testenv]
deps = pipenv==2018.11.14 deps = .[test]
passenv = HOME commands = python setup.py test
commands =
pipenv sync -d
pipenv run pytest