Refactor can-prefixed properties

This commit is contained in:
Sumner Evans
2020-05-27 00:08:00 -06:00
parent 097732dba8
commit 1bba10b37c
6 changed files with 90 additions and 120 deletions

View File

@@ -52,29 +52,29 @@ functions and properties first:
* ``__init__``: Used to initialize your adapter. See the
:class:`sublime.adapters.Adapter.__init__` documentation for the function
signature of the ``__init__`` function.
* ``can_service_requests``: This property which will tell the UI whether or not
your adapter can currently service requests. (See the
:class:`sublime.adapters.Adapter.can_service_requests` documentation for
examples of what you may want to check in this property.)
* ``ping_status``: Assuming that your adapter requires connection to the
internet, this property needs to be implemented. (If your adapter doesn't
require connection to the internet, set
:class:`sublime.adapters.Adapter.is_networked` to ``False`` and ignore the
rest of this bullet point.)
This property will tell the UI whether or not the underlying server can be
pinged.
.. warning::
This function is called *a lot* (probably too much?) so it *must* return a
value *instantly*. **Do not** perform a network request in this function.
If your adapter depends on connection to the network use a periodic ping
that updates a state variable that this function returns.
value *instantly*. **Do not** perform the actual network request in this
function. Instead, use a periodic ping that updates a state variable that
this function returns.
.. TODO: these are totally wrong
* ``get_config_parameters``: Specifies the settings which can be configured on
for the adapter. See :ref:`adapter-api:Handling Configuration` for details.
* ``verify_configuration``: Verifies whether or not a given set of configuration
values are valid. See :ref:`adapter-api:Handling Configuration` for details.
.. tip::
While developing the adapter, setting ``can_service_requests`` to ``True``
will indicate to the UI that your adapter is always ready to service
requests. This can be a useful debugging tool.
.. note::
The :class:`sublime.adapters.Adapter` class is an `Abstract Base Class
@@ -118,21 +118,12 @@ to implement the actual adapter data retrieval functions.
For each data retrieval function there is a corresponding ``can_``-prefixed
property (CPP) which will be used by the UI to determine if the data retrieval
function can be called at the given time. If the CPP is ``False``, the UI will
never call the corresponding function (and if it does, it's a UI bug). The CPP
can be dynamic, for example, if your adapter supports many API versions, some of
the CPPs may depend on the API version.
There is a special, global ``can_``-prefixed property which determines whether
the adapter can currently service *any* requests. This should be used for checks
such as making sure that the user is able to access the server. (However, this
must be done in a non-blocking manner since this is called *a lot*.)
.. code:: python
@property
def can_service_requests(self) -> bool:
return self.cached_ping_result_is_ok()
function can be called. If the CPP is ``False``, the UI will never call the
corresponding function (and if it does, it's a UI bug). The CPP can be dynamic,
for example, if your adapter supports many API versions, some of the CPPs may
depend on the API version. However, CPPs should not be dependent on connection
status (there are times where the user may want to force a connection retry,
even if the most recent ping failed).
Here is an example of what a ``get_playlists`` interface for an external server
might look:
@@ -155,7 +146,7 @@ might look:
``True``.*
\* At the moment, this isn't really the case and the UI just kinda explodes
if it doesn't have some of the functions available, but in the future guards
if it doesn't have some of the functions available, but in the future, guards
will be added around all of the function calls.
Usage Parameters

View File

@@ -286,6 +286,10 @@ class Adapter(abc.ABC):
"""
return True
# Network Properties
# These properties determine whether or not the adapter requires connection over a
# network and whether the underlying server can be pinged.
# ==================================================================================
@property
def is_networked(self) -> bool:
"""
@@ -299,68 +303,56 @@ class Adapter(abc.ABC):
@abc.abstractmethod
def ping_status(self) -> bool:
"""
This function should return whether or not the server can be pinged, however it
must do it *instantly*. This function is called *very* often, and even a few
milliseconds delay stacks up quickly and can block the UI thread.
If the adapter :class:`is_networked`, then this function should return whether
or not the server can be pinged. This function must provide an answer
*instantly* (it can't actually ping the server). This function is called *very*
often, and even a few milliseconds delay stacks up quickly and can block the UI
thread.
One option is to ping the server every few seconds and cache the result of the
ping and use that as the result of this function.
"""
# Availability Properties
# These properties determine if what things the adapter can be used to do
# at the current moment.
# These properties determine if what things the adapter can be used to do. These
# properties can be dynamic based on things such as underlying API version, or other
# factors like that. However, these properties should not be dependent on the
# connection state. After the initial sync, these properties are expected to be
# constant.
# ==================================================================================
# TODO: change these to just be "if it has the attribute, then you can try the
# action" and use "ping_status" to determine online/offline status.
@property
@abc.abstractmethod
def can_service_requests(self) -> bool:
"""
Specifies whether or not the adapter can currently service requests. If this is
``False``, none of the other data retrieval functions are expected to work.
This property must be server *instantly*. This function is called *very* often,
and even a few milliseconds delay stacks up quickly and can block the UI thread.
For example, if your adapter requires access to an external service, on option
is to ping the server every few seconds and cache the result of the ping and use
that as the return value of this function.
"""
# Playlists
@property
def can_get_playlists(self) -> bool:
"""
Whether the adapter supports :class:`get_playlist`.
Whether or not the adapter supports :class:`get_playlist`.
"""
return False
@property
def can_get_playlist_details(self) -> bool:
"""
Whether :class:`get_playlist_details` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_playlist_details`.
"""
return False
@property
def can_create_playlist(self) -> bool:
"""
Whether :class:`create_playlist` can be called on the adapter right now.
Whether or not the adapter supports :class:`create_playlist`.
"""
return False
@property
def can_update_playlist(self) -> bool:
"""
Whether :class:`update_playlist` can be called on the adapter right now.
Whether or not the adapter supports :class:`update_playlist`.
"""
return False
@property
def can_delete_playlist(self) -> bool:
"""
Whether :class:`delete_playlist` can be called on the adapter right now.
Whether or not the adapter supports :class:`delete_playlist`.
"""
return False
@@ -380,20 +372,20 @@ class Adapter(abc.ABC):
@property
def can_get_cover_art_uri(self) -> bool:
"""
Whether :class:`get_cover_art_uri` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_cover_art_uri`.
"""
@property
def can_stream(self) -> bool:
"""
Whether or not the adapter can provide a stream URI right now.
Whether or not the adapter can provide a stream URI.
"""
return False
@property
def can_get_song_uri(self) -> bool:
"""
Whether :class:`get_song_uri` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_song_uri`.
"""
return False
@@ -401,14 +393,14 @@ class Adapter(abc.ABC):
@property
def can_get_song_details(self) -> bool:
"""
Whether :class:`get_song_details` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_song_details`.
"""
return False
@property
def can_scrobble_song(self) -> bool:
"""
Whether :class:`scrobble_song` can be called on the adapter right now.
Whether or not the adapter supports :class:`scrobble_song`.
"""
return False
@@ -426,21 +418,21 @@ class Adapter(abc.ABC):
@property
def can_get_artists(self) -> bool:
"""
Whether :class:`get_aritsts` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_aritsts`.
"""
return False
@property
def can_get_artist(self) -> bool:
"""
Whether :class:`get_aritst` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_aritst`.
"""
return False
@property
def can_get_ignored_articles(self) -> bool:
"""
Whether :class:`get_ignored_articles` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_ignored_articles`.
"""
return False
@@ -448,14 +440,14 @@ class Adapter(abc.ABC):
@property
def can_get_albums(self) -> bool:
"""
Whether :class:`get_albums` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_albums`.
"""
return False
@property
def can_get_album(self) -> bool:
"""
Whether :class:`get_album` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_album`.
"""
return False
@@ -463,7 +455,7 @@ class Adapter(abc.ABC):
@property
def can_get_directory(self) -> bool:
"""
Whether :class:`get_directory` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_directory`.
"""
return False
@@ -471,7 +463,7 @@ class Adapter(abc.ABC):
@property
def can_get_genres(self) -> bool:
"""
Whether :class:`get_genres` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_genres`.
"""
return False
@@ -479,14 +471,14 @@ class Adapter(abc.ABC):
@property
def can_get_play_queue(self) -> bool:
"""
Whether :class:`get_play_queue` can be called on the adapter right now.
Whether or not the adapter supports :class:`get_play_queue`.
"""
return False
@property
def can_save_play_queue(self) -> bool:
"""
Whether :class:`save_play_queue` can be called on the adapter right now.
Whether or not the adapter supports :class:`save_play_queue`.
"""
return False
@@ -494,7 +486,7 @@ class Adapter(abc.ABC):
@property
def can_search(self) -> bool:
"""
Whether :class:`search` can be called on the adapter right now.
Whether or not the adapter supports :class:`search`.
"""
return False

View File

@@ -79,7 +79,6 @@ class FilesystemAdapter(CachingAdapter):
# ==================================================================================
can_be_cached = False # Can't be cached (there's no need).
is_networked = False # Doesn't access the network.
can_service_requests = True # Can always be used to service requests.
# TODO (#200) make these dependent on cache state. Need to do this kinda efficiently
can_get_cover_art_uri = True

View File

@@ -275,18 +275,8 @@ class AdapterManager:
TAdapter = TypeVar("TAdapter", bound=Adapter)
@staticmethod
def _adapter_can_do(adapter: Optional[TAdapter], action_name: str) -> bool:
return (
adapter is not None
and adapter.can_service_requests
and getattr(adapter, f"can_{action_name}", False)
)
@staticmethod
def _cache_can_do(action_name: str) -> bool:
return AdapterManager._instance is not None and AdapterManager._adapter_can_do(
AdapterManager._instance.caching_adapter, action_name
)
def _adapter_can_do(adapter: TAdapter, action_name: str) -> bool:
return adapter is not None and getattr(adapter, f"can_{action_name}", False)
@staticmethod
def _ground_truth_can_do(action_name: str) -> bool:
@@ -302,16 +292,13 @@ class AdapterManager:
def _can_use_cache(force: bool, action_name: str) -> bool:
if force:
return False
return AdapterManager._cache_can_do(action_name)
@staticmethod
def _any_adapter_can_do(action_name: str) -> bool:
if AdapterManager._instance is None:
return False
return AdapterManager._ground_truth_can_do(
action_name
) or AdapterManager._cache_can_do(action_name)
return (
AdapterManager._instance is not None
and AdapterManager._instance.caching_adapter is not None
and AdapterManager._adapter_can_do(
AdapterManager._instance.caching_adapter, action_name
)
)
@staticmethod
def _create_ground_truth_result(
@@ -557,27 +544,27 @@ class AdapterManager:
# ==================================================================================
@staticmethod
def can_get_playlists() -> bool:
return AdapterManager._any_adapter_can_do("get_playlists")
return AdapterManager._ground_truth_can_do("get_playlists")
@staticmethod
def can_get_playlist_details() -> bool:
return AdapterManager._any_adapter_can_do("get_playlist_details")
return AdapterManager._ground_truth_can_do("get_playlist_details")
@staticmethod
def can_create_playlist() -> bool:
return AdapterManager._any_adapter_can_do("create_playlist")
return AdapterManager._ground_truth_can_do("create_playlist")
@staticmethod
def can_update_playlist() -> bool:
return AdapterManager._any_adapter_can_do("update_playlist")
return AdapterManager._ground_truth_can_do("update_playlist")
@staticmethod
def can_delete_playlist() -> bool:
return AdapterManager._any_adapter_can_do("delete_playlist")
return AdapterManager._ground_truth_can_do("delete_playlist")
@staticmethod
def can_get_song_filename_or_stream() -> bool:
return AdapterManager._any_adapter_can_do("get_song_uri")
return AdapterManager._ground_truth_can_do("get_song_uri")
@staticmethod
def can_batch_download_songs() -> bool:
@@ -586,23 +573,23 @@ class AdapterManager:
@staticmethod
def can_get_genres() -> bool:
return AdapterManager._any_adapter_can_do("get_genres")
return AdapterManager._ground_truth_can_do("get_genres")
@staticmethod
def can_scrobble_song() -> bool:
return AdapterManager._any_adapter_can_do("scrobble_song")
return AdapterManager._ground_truth_can_do("scrobble_song")
@staticmethod
def can_get_artists() -> bool:
return AdapterManager._any_adapter_can_do("get_artists")
return AdapterManager._ground_truth_can_do("get_artists")
@staticmethod
def can_get_artist() -> bool:
return AdapterManager._any_adapter_can_do("get_artist")
return AdapterManager._ground_truth_can_do("get_artist")
@staticmethod
def can_get_directory() -> bool:
return AdapterManager._any_adapter_can_do("get_directory")
return AdapterManager._ground_truth_can_do("get_directory")
@staticmethod
def can_get_play_queue() -> bool:
@@ -614,7 +601,7 @@ class AdapterManager:
@staticmethod
def can_search() -> bool:
return AdapterManager._any_adapter_can_do("search")
return AdapterManager._ground_truth_can_do("search")
# Data Retrieval Methods
# ==================================================================================
@@ -1017,7 +1004,7 @@ class AdapterManager:
@staticmethod
def _get_ignored_articles(use_ground_truth_adapter: bool) -> Set[str]:
# TODO (#21) get this at first startup.
if not AdapterManager._any_adapter_can_do("get_ignored_articles"):
if not AdapterManager._ground_truth_can_do("get_ignored_articles"):
return set()
try:
ignored_articles: Set[str] = AdapterManager._get_from_cache_or_ground_truth(

View File

@@ -158,6 +158,7 @@ class SubsonicAdapter(Adapter):
def _set_ping_status(self):
now = datetime.now().timestamp()
if now - self._last_ping_timestamp.value < 15:
print("ohea")
return
try:
@@ -172,10 +173,6 @@ class SubsonicAdapter(Adapter):
def ping_status(self) -> bool:
return self._server_available.value
@property
def can_service_requests(self) -> bool:
return self._server_available.value
# TODO (#199) make these way smarter
can_get_playlists = True
can_get_playlist_details = True
@@ -271,11 +268,11 @@ class SubsonicAdapter(Adapter):
if self._is_mock:
logging.info("Using mock data")
return self._get_mock_data()
result = requests.get(
url, params=params, verify=not self.disable_cert_verify, timeout=timeout
)
result = self._get_mock_data()
else:
result = requests.get(
url, params=params, verify=not self.disable_cert_verify, timeout=timeout
)
# TODO (#122): make better
if result.status_code != 200:
@@ -324,6 +321,8 @@ class SubsonicAdapter(Adapter):
def _set_mock_data(self, data: Any):
class MockResult:
status_code = 200
def __init__(self, content: Any):
self._content = content

View File

@@ -3,6 +3,7 @@ import logging
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from time import sleep
from typing import Any, Generator, List, Tuple
import pytest
@@ -87,22 +88,23 @@ def test_request_making_methods(adapter: SubsonicAdapter):
assert adapter._make_url("foo") == "http://subsonic.example.com/rest/foo.view"
def test_can_service_requests(adapter: SubsonicAdapter):
def test_ping_status(adapter: SubsonicAdapter):
# Mock a connection error
adapter._set_mock_data(Exception())
assert not adapter.can_service_requests
assert not adapter.ping_status
# Simulate some sort of ping error
for filename, data in mock_data_files("ping_failed"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
assert not adapter.can_service_requests
assert not adapter.ping_status
# Simulate valid ping
adapter._set_mock_data(mock_json())
adapter._last_ping_timestamp.value = 0.0
adapter._set_ping_status()
assert adapter.can_service_requests
assert adapter.ping_status
def test_get_playlists(adapter: SubsonicAdapter):