Skip to content

Commit

Permalink
Merge pull request #64 from jodal/type-hints
Browse files Browse the repository at this point in the history
Add type hints
  • Loading branch information
jodal committed Feb 29, 2024
2 parents e4f4e19 + d524bbb commit 133f381
Show file tree
Hide file tree
Showing 40 changed files with 1,562 additions and 1,018 deletions.
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

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

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


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]:
...

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

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))
)

0 comments on commit 133f381

Please sign in to comment.