Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints #64

Merged
merged 19 commits into from Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -19,6 +19,9 @@ jobs:
python: "3.12"
tox: py312
coverage: true
- name: "Lint: pyright"
python: "3.12"
tox: pyright
- name: "Lint: ruff lint"
python: "3.12"
tox: ruff-lint
Expand Down
23 changes: 20 additions & 3 deletions pyproject.toml
Expand Up @@ -21,12 +21,17 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Topic :: Multimedia :: Sound/Audio :: Players",
]
dependencies = ["mopidy >= 3.3.0", "pykka >= 4.0", "setuptools >= 66"]
dependencies = [
"mopidy >= 4.0.0a1",
"pygobject >= 3.42",
"pykka >= 4.0",
"setuptools >= 66",
]

[project.optional-dependencies]
lint = ["ruff"]
test = ["pytest", "pytest-cov"]
typing = ["pyright"]
typing = ["pygobject-stubs", "pyright"]
dev = ["mopidy-mpd[lint,test,typing]", "tox"]

[project.urls]
Expand All @@ -37,6 +42,16 @@ Issues = "https://github.com/mopidy/mopidy-mpd/issues"
mpd = "mopidy_mpd:Extension"


[tool.pyright]
pythonVersion = "3.11"
# Use venv from parent directory, to share it with any extensions:
venvPath = "../"
venv = ".venv"
typeCheckingMode = "standard"
# Already covered by flake8-self:
reportPrivateImportUsage = false


[tool.ruff]
target-version = "py311"

Expand Down Expand Up @@ -80,7 +95,9 @@ select = [
"W", # pycodestyle
]
ignore = [
"ANN", # flake8-annotations
"ANN101", # missing-type-self
"ANN102", # missing-type-cls
"ANN401", # any-type
"D", # pydocstyle
"ISC001", # single-line-implicit-string-concatenation
"TRY003", # raise-vanilla-args
Expand Down
6 changes: 3 additions & 3 deletions src/mopidy_mpd/__init__.py
Expand Up @@ -11,10 +11,10 @@ class Extension(ext.Extension):
ext_name = "mpd"
version = __version__

def get_default_config(self):
def get_default_config(self) -> str:
return config.read(pathlib.Path(__file__).parent / "ext.conf")

def get_config_schema(self):
def get_config_schema(self) -> config.ConfigSchema:
schema = super().get_config_schema()
schema["hostname"] = config.Hostname()
schema["port"] = config.Port(optional=True)
Expand All @@ -26,7 +26,7 @@ def get_config_schema(self):
schema["default_playlist_scheme"] = config.String()
return schema

def setup(self, registry):
def setup(self, registry: ext.Registry) -> None:
from .actor import MpdFrontend

registry.add("frontend", MpdFrontend)
32 changes: 15 additions & 17 deletions src/mopidy_mpd/actor.py
@@ -1,10 +1,11 @@
import logging
from typing import Any

import pykka
from mopidy import exceptions, listener, zeroconf
from mopidy.core import CoreListener
from mopidy.core import CoreListener, CoreProxy

from mopidy_mpd import network, session, uri_mapper
from mopidy_mpd import network, session, types, uri_mapper

logger = logging.getLogger(__name__)

Expand All @@ -27,29 +28,26 @@


class MpdFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
def __init__(self, config: types.Config, core: CoreProxy) -> None:
super().__init__()

self.hostname = network.format_hostname(config["mpd"]["hostname"])
self.port = config["mpd"]["port"]
self.uri_map = uri_mapper.MpdUriMapper(core)

self.zeroconf_name = config["mpd"]["zeroconf"]
self.zeroconf_service = None

self.uri_map = uri_mapper.MpdUriMapper(core)
self.server = self._setup_server(config, core)

def _setup_server(self, config, core):
def _setup_server(self, config: types.Config, core: CoreProxy) -> network.Server:
try:
server = network.Server(
self.hostname,
self.port,
config=config,
core=core,
uri_map=self.uri_map,
protocol=session.MpdSession,
protocol_kwargs={
"config": config,
"core": core,
"uri_map": self.uri_map,
},
host=self.hostname,
port=self.port,
max_connections=config["mpd"]["max_connections"],
timeout=config["mpd"]["connection_timeout"],
)
Expand All @@ -60,14 +58,14 @@ def _setup_server(self, config, core):

return server

def on_start(self):
def on_start(self) -> None:
if self.zeroconf_name and not network.is_unix_socket(self.server.server_socket):
self.zeroconf_service = zeroconf.Zeroconf(
name=self.zeroconf_name, stype="_mpd._tcp", port=self.port
)
self.zeroconf_service.publish()

def on_stop(self):
def on_stop(self) -> None:
if self.zeroconf_service:
self.zeroconf_service.unpublish()

Expand All @@ -77,12 +75,12 @@ def on_stop(self):

self.server.stop()

def on_event(self, event, **kwargs):
def on_event(self, event: str, **kwargs: Any) -> None:
if event not in _CORE_EVENTS_TO_IDLE_SUBSYSTEMS:
logger.warning("Got unexpected event: %s(%s)", event, ", ".join(kwargs))
else:
self.send_idle(_CORE_EVENTS_TO_IDLE_SUBSYSTEMS[event])

def send_idle(self, subsystem):
def send_idle(self, subsystem: str | None) -> None:
if subsystem:
listener.send(session.MpdSession, subsystem)
140 changes: 140 additions & 0 deletions src/mopidy_mpd/context.py
@@ -0,0 +1,140 @@
from __future__ import annotations

import logging
import re
from typing import (
TYPE_CHECKING,
Any,
Literal,
overload,
)

from mopidy_mpd import exceptions, types

if TYPE_CHECKING:
from collections.abc import Generator

Check warning on line 15 in src/mopidy_mpd/context.py

View check run for this annotation

Codecov / codecov/patch

src/mopidy_mpd/context.py#L15

Added line #L15 was not covered by tests

import pykka
from mopidy.core import CoreProxy
from mopidy.models import Ref, Track
from mopidy.types import Uri

Check warning on line 20 in src/mopidy_mpd/context.py

View check run for this annotation

Codecov / codecov/patch

src/mopidy_mpd/context.py#L17-L20

Added lines #L17 - L20 were not covered by tests

from mopidy_mpd.dispatcher import MpdDispatcher
from mopidy_mpd.session import MpdSession
from mopidy_mpd.uri_mapper import MpdUriMapper

Check warning on line 24 in src/mopidy_mpd/context.py

View check run for this annotation

Codecov / codecov/patch

src/mopidy_mpd/context.py#L22-L24

Added lines #L22 - L24 were not covered by tests


logger = logging.getLogger(__name__)


class MpdContext:
"""
This object is passed as the first argument to all MPD command handlers to
give the command handlers access to important parts of Mopidy.
"""

#: The Mopidy config.
config: types.Config

#: The Mopidy core API.
core: CoreProxy

#: The current session instance.
session: MpdSession

#: The current dispatcher instance.
dispatcher: MpdDispatcher

#: Mapping of URIs to MPD names.
uri_map: MpdUriMapper

def __init__( # noqa: PLR0913
self,
config: types.Config,
core: CoreProxy,
uri_map: MpdUriMapper,
session: MpdSession,
dispatcher: MpdDispatcher,
) -> None:
self.config = config
self.core = core
self.uri_map = uri_map
self.session = session
self.dispatcher = dispatcher

@overload
def browse(
self, path: str | None, *, recursive: bool, lookup: Literal[True]
) -> Generator[tuple[str, pykka.Future[dict[Uri, list[Track]]] | None], Any, None]:
...

Check warning on line 69 in src/mopidy_mpd/context.py

View check run for this annotation

Codecov / codecov/patch

src/mopidy_mpd/context.py#L69

Added line #L69 was not covered by tests

@overload
def browse(
self, path: str | None, *, recursive: bool, lookup: Literal[False]
) -> Generator[tuple[str, Ref | None], Any, None]:
...

Check warning on line 75 in src/mopidy_mpd/context.py

View check run for this annotation

Codecov / codecov/patch

src/mopidy_mpd/context.py#L75

Added line #L75 was not covered by tests

def browse( # noqa: C901, PLR0912
self,
path: str | None,
*,
recursive: bool = True,
lookup: bool = True,
) -> Generator[Any, Any, None]:
"""
Browse the contents of a given directory path.

Returns a sequence of two-tuples ``(path, data)``.

If ``recursive`` is true, it returns results for all entries in the
given path.

If ``lookup`` is true and the ``path`` is to a track, the returned
``data`` is a future which will contain the results from looking up
the URI with :meth:`mopidy.core.LibraryController.lookup`. If
``lookup`` is false and the ``path`` is to a track, the returned
``data`` will be a :class:`mopidy.models.Ref` for the track.

For all entries that are not tracks, the returned ``data`` will be
:class:`None`.
"""

path_parts: list[str] = re.findall(r"[^/]+", path or "")
root_path: str = "/".join(["", *path_parts])

uri = self.uri_map.uri_from_name(root_path)
if uri is None:
for part in path_parts:
for ref in self.core.library.browse(uri).get():
if ref.type != ref.TRACK and ref.name == part:
uri = ref.uri
break
else:
raise exceptions.MpdNoExistError("Not found")
root_path = self.uri_map.insert(root_path, uri)

if recursive:
yield (root_path, None)

path_and_futures = [(root_path, self.core.library.browse(uri))]
while path_and_futures:
base_path, future = path_and_futures.pop()
for ref in future.get():
if ref.name is None or ref.uri is None:
continue

path = "/".join([base_path, ref.name.replace("/", "")])
path = self.uri_map.insert(path, ref.uri)

if ref.type == ref.TRACK:
if lookup:
# TODO: can we lookup all the refs at once now?
yield (path, self.core.library.lookup(uris=[ref.uri]))
else:
yield (path, ref)
else:
yield (path, None)
if recursive:
path_and_futures.append(
(path, self.core.library.browse(ref.uri))
)